Scene, Camera, Renderer, render loop
Three.js의 기본 흐름은 Scene, Camera, Renderer입니다. 이 세 가지를 editor frame lifecycle로 다시 읽으면 앞에서 만든 render loop와 거의 같은 구조가 됩니다.
최소 렌더링 구조
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
function frame() {
syncSceneFromEditorState(scene);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
여기서 중요한 것은 scene을 source of truth로 보지 않는 것입니다. source of truth는 editor state이고, Three.js scene은 그 상태를 그리기 위한 projection입니다.
render loop의 책임
input
editor state update
scene sync
renderer.render(scene, camera)
Figma-like editor에서는 매 프레임마다 모든 것을 새로 만들면 안 됩니다. node id별로 Object3D를 재사용하고, 바뀐 속성만 갱신해야 합니다.
dirty할 때만 scene을 갱신한다
const objectsByNodeId = new Map();
function syncSceneFromEditorState(scene, document) {
const liveIds = new Set();
for (const node of document.nodes) {
liveIds.add(node.id);
let object = objectsByNodeId.get(node.id);
if (!object) {
object = createObjectForNode(node);
object.userData.nodeId = node.id;
objectsByNodeId.set(node.id, object);
scene.add(object);
}
if (node.dirtyTransform) {
object.position.set(node.x, node.y, node.z ?? 0);
object.rotation.z = node.rotation ?? 0;
object.scale.set(node.scaleX ?? 1, node.scaleY ?? 1, 1);
}
if (node.dirtyStyle) {
syncMaterial(object.material, node.fill, node.opacity);
}
}
for (const [id, object] of objectsByNodeId) {
if (liveIds.has(id)) continue;
scene.remove(object);
disposeObject(object);
objectsByNodeId.delete(id);
}
}
이 코드는 Three.js scene을 editor state의 cache로 다룹니다. React/Vue 컴포넌트처럼 매번 새로 만드는 것이 아니라, node id를 기준으로 재사용하고 dirty flag가 있는 속성만 갱신합니다.
오늘의 핵심
Three.js render loop도 editor loop입니다. 입력, 상태, scene sync, render의 순서를 분리해야 디버깅할 수 있습니다.