three.js의 행렬 변환

Three.js는 이동(position), 회전, 크기변환과 같은 3차원 변환을 위해 행렬을 사용함. Object3D의 파생 클래스의 모든 객체는 matrix 속성을 가지고 있고 이 속성은 객체의 위치, 회전, 크기 변환값이 저장되어 있음. 이 글은 객체의 변환을 어떻게 업데이트 하는지 기술함.

객체의 변환을 업데이트하기 위해서는 2가지 방법이 있음.

1. 객체의 position, quaternion, scale 속성을 변경하고 이 속성들로부터 객체의 행렬을 다시 계산시킴.

object.position.copy( start_position );
object.quaternion.copy( quaternion );

기본적으로 matrixAutoUpdate 속성이 true이므로 행렬은 자동적으로 재계산됨. 만약 객체가 정적이거나 행렬 재계산을 수동으로 제어하고자 한다면 matrixAutoUpdate를 false로 지정하여 더 나은 퍼포먼스를 얻을 수 있음. 이때 다음처럼 수동으로 행렬을 재계산할 수 있음.

object.updateMatrix();

2. 객체의 행렬을 직접 변경해도 됨. Matrix4 클래스는 행렬 변경을 위한 다양한 매서드를 제공함.

object.matrix.setRotationFromQuaternion( quaternion );
object.matrix.setPosition( start_position );
object.matrixAutoUpdate = false;

윗 코드의 경우 matrixAutoUpdate의 값은 false로 지정했고 updateMatrix 매서드가 호출되지 않도록 보장해야 함. updateMatrix 매서드를 호출하면 position, scale 등의 속성값으로 행렬이 재계산되어 버림.

객체의 행렬에는 부모 객체와 연관된 객체의 변환값이 저장되어 있음. 월드 좌표계에 대한 객체의 변환을 얻기 위해서는 객체의 Object3D.matrixWorld에 접근해야 함.

부모 또는 자식 객체의 변환이 변경될 때, 자식 객체의 matrixWorld를 업데이트되도록 updateMatrixWorld 매서드를 호출할 수 있음.

Three.js는 3차원 회전을 위해 2가지 방식을 제공: 오일러(Euler) 각도와 쿼터니안(Quaternions). 이 2가지 방식은 상호간의 변환이 가능함. 오일러 각도 방식은 짐벌락(Gimbal Lock)이라고 불리우는 문제가 있음으로 객체의 회전은 반드시 쿼터니안 방식을 위한 quaternion 속성을 이용해야 함.

이 글의 원문

WebRTC 코드 정리

WebRTC는 P2P이며 웹에서 1:1로 연결하여 서로 데이터를 실시간으로 주고 받는 웹 표준기술입니다. 추후 적용을 위해 관련 코드를 정리합니다.

먼저 개념도는 다음과 같습니다. (아래 그림은 WebRTC에 대한 이해를 돕기 위해 제가 작성한 것으로 잘못 표기된 부분이 있을 수 있습니다.)

일단 쭉 말로 설명하면.. 먼저 데이터를 주고 받을 채널을 생성하고 채널을 생성한 Peer가 제안(Offer) 객체를 만들어 다른 Peer에게 전달합니다. 그럼 피어는 제안과 함께 받은 채널을 확인하고 응답(Answer) 객체를 생성해 전달합니다. 그러면 이 두 Peer들은 icecandidate라는 이벤트가 발생하게되고 Candidate 객체를 서로 주고 받아 설정함으로써 두 Peer 간의 연결이 이루어 집니다. 이렇게 연결이 이루어지면 앞서 언급한 채널을 통해 메세지를 실시간으로 주고 받을 수 있습니다. 위의 그림을 보면 Peer간의 연결을 위해 Offer, Answer, Candidate 정보를 교환하기 위해 별도의 서버가 필요한 것을 볼 수 있습니다. 하지만 WebRTC의 핵심은 서버없이 Peer 간의 데이터 전송이므로 일단 연결이 되면 더 이상 서버는 필요치 않습니다.

웹 표준 기술이 지향하는 매우 심플한 API 구조에 맞게 코드가 단순한데요. 서버, 클라이언트 코드가 매우 짧으므로 전체 코드를 남깁니다. 먼저 클라이언트에 대한 코드는 다음과 같습니다.

const socket = io();
const roomName = "room1";

let peerConnection;
let dataChannel;

document.querySelector("form").addEventListener("submit", (event) => {
    event.preventDefault();

    if(dataChannel) {
        const value = document.querySelector("form input").value
        dataChannel.send(value);
    }
});

function onReady() {
    peerConnection = new RTCPeerConnection({
        iceServers: [{ urls: [
            "stun:stun.l.google.com:19302",
            "stun:stun1.l.google.com:19302",
            "stun:stun2.l.google.com:19302",
            "stun:stun3.l.google.com:19302",
            "stun:stun4.l.google.com:19302",
        ]
    }]});

    peerConnection.addEventListener("icecandidate", (event) => {
        socket.emit("ice", event.candidate, roomName);
    });
}

window.onload = () => {
    socket.emit("join", roomName, onReady);
}

function onMessage(msg) {
    const ul = document.querySelector("ul");
    const li = document.createElement("li");
    li.innerText = msg;
    ul.append(li);
}

socket.on("welcome", async () => {
    dataChannel = peerConnection.createDataChannel("chat");
    dataChannel.addEventListener("message", (event) => {
        onMessage(event.data);
    });

    const offer = await peerConnection.createOffer();
    peerConnection.setLocalDescription(offer);
    socket.emit("offer", offer, roomName);
});

socket.on("offer", async (offer) => { 
    peerConnection.addEventListener("datachannel", (event) => {
        dataChannel = event.channel;
        dataChannel.addEventListener("message", (event)=> {
            onMessage(event.data);
        });
    })

    peerConnection.setRemoteDescription(offer);
    const answer = await peerConnection.createAnswer();
    peerConnection.setLocalDescription(answer);
    socket.emit("answer", answer, roomName);
});

socket.on("answer", (answer) => {
    peerConnection.setRemoteDescription(answer);
});

socket.on("ice", (ice) => {
    peerConnection.addIceCandidate(ice);
});

서버단의 코드는 다음과 같습니다.

import http from "http"
import express from "express";
import SocketIO from "socket.io";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));
app.get("/", (_, res) => res.render("home"));

const httpServer = http.createServer(app);
const wsServer = SocketIO(httpServer);

const port = 3000;
const handleListen = () => console.log(`Listening on http://localhost:${port}`)
httpServer.listen(port, handleListen);

wsServer.on("connection", socket => {
    socket.on("join", (roomName, done) => {
        socket.join(roomName);
        done();
        socket.to(roomName).emit("welcome");
    });

    socket.on("offer", (offer, roomName) => {
        socket.to(roomName).emit("offer", offer);
    });

    socket.on("answer", (answer, roomName) => {
        socket.to(roomName).emit("answer", answer);
    });

    socket.on("ice", (ice, roomName) => {
        socket.to(roomName).emit("ice", ice);
    })
});

이용한 기술은 JS, Node.JS, Express, Socket.IO, Pug입니다.

three.js로 체스, 볼링 게임 웹 만들기

three.js를 이용해 웹에서 어떠한 플러그인 없이도 멋진 3차원 그래픽 웹페이지를 개발할 수 있는데요. 게임 느낌의 3D 웹 개발에 대한 컨텐츠입니다. 내용이 다소 길지만 차근 차근 따라하시다보면 금새 완성하실 수 있습니다.


node.js로 server 개발 프로젝트 생성 절차

프로젝트 폴더를 만들어 VS.Code에서 오픈 후 package.json 파일 생성을 위해 터미널에서 다음 명령을 실행한 뒤에 생성된 파일을 편집함

npm init -y

{
  "name": "tstServer",
  "version": "1.0.0",
  "description": "",
  "keywords": [],
  "author": "",
  "license": "ISC"
}

nodemon 설치를 위해 다음 명령 실행

npm i nodemon -D

babel.config.json 파일과 nodemon.json 파일을 생성하고 각각 다음 내용으로 입력

{
    "presets": [ "@babel/preset-env" ]
}
{
    "exec": "babel-node src/index.js"
}

babel 설치를 위해 다음 명령 실행

npm i @babel/core @babel/cli @babel/node @babel/preset-env -D

express 설치를 위해 다음 명령 실행

npm i express

필요할 경우 pug 설치를 위해 다음 명령 실행

npm i pug

package.json에 설치된 항목에 대한 종속성(Dependency)가 존재하는지 확인하고 다음처럼 scripts 속성을 추가(또는 변경)

{
  "name": "tstServer",

  ..

  "scripts": {
    "dev": "nodemon"
  },

  "devDependencies": {
    "@babel/cli": "^7.20.7",
    "@babel/core": "^7.20.7",
    "@babel/node": "^7.20.7",
    "@babel/preset-env": "^7.20.2",
    "nodemon": "^2.0.20"
  },
  "dependencies": {
    "express": "^4.18.2",
    "pug": "^3.0.2"
  }
}

src 폴더 만들어 주고 index.js 파일 생성하고 다음처럼 입력

import express from "express";

const app = express();


const port = 7777;
const handleListen = () => console.log(`Listening on http://localhost:${port}`)
app.listen(port, handleListen);

다음 명령을 실행하여 서버 실행하고 웹브라우저에서 http://localhost:7777/로 접속해서 확인

npm run dev