render loop와 frame lifecycle
DOM 에디터에서는 상태를 바꾸면 브라우저가 적당한 시점에 화면을 갱신합니다. Canvas/WebGL/WebGPU 에디터에서는 우리가 프레임을 구성합니다.
read editor state
clear
draw scene
draw overlays
present
이 반복이 render loop입니다.
requestAnimationFrame에서 시작한다
브라우저에서 화면 갱신과 맞춰 무언가를 그릴 때는 requestAnimationFrame을 사용합니다.
function startRenderLoop(editor, renderer) {
let rafId = 0;
function frame(time) {
const snapshot = editor.getRendererSnapshot();
renderer.resizeToDisplaySize();
renderer.render(snapshot, time);
rafId = requestAnimationFrame(frame);
}
rafId = requestAnimationFrame(frame);
return () => cancelAnimationFrame(rafId);
}
처음에는 매 프레임 전체를 다시 그립니다. 성능 최적화는 나중입니다. 초반에는 “한 프레임이 어떤 단계로 만들어지는가”를 코드에 명확히 드러내는 것이 더 중요합니다.
Canvas backing store를 매 프레임 확인한다
Canvas는 CSS 크기와 실제 drawing buffer 크기가 다릅니다. render loop 초반에 크기를 맞춰야 grid, text, WebGL viewport가 뿌옇게 보이지 않습니다.
function resizeCanvasToDisplaySize(canvas) {
const rect = canvas.getBoundingClientRect();
const dpr = Math.max(1, window.devicePixelRatio || 1);
const width = Math.round(rect.width * dpr);
const height = Math.round(rect.height * dpr);
if (canvas.width === width && canvas.height === height) {
return false;
}
canvas.width = width;
canvas.height = height;
return true;
}
function resizeToDisplaySize(gl, canvas) {
resizeCanvasToDisplaySize(canvas);
gl.viewport(0, 0, canvas.width, canvas.height);
}
2D Canvas라면 context transform까지 같이 맞춥니다.
function getHiDpiContext(canvas) {
resizeCanvasToDisplaySize(canvas);
const rect = canvas.getBoundingClientRect();
const dpr = canvas.width / rect.width;
const ctx = canvas.getContext("2d");
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
return ctx;
}
프레임은 편집기 상태의 스냅샷을 그린다
renderer는 현재 editor state를 읽어서 한 장의 화면을 만듭니다.
function renderFrame(ctx, snapshot) {
clearCanvas(ctx);
drawGrid(ctx, snapshot.camera, snapshot.viewport);
drawScene(ctx, snapshot.drawList, snapshot.camera);
drawOverlay(ctx, snapshot.selection, snapshot.camera);
}
여기서 renderer가 editor state의 소유자가 되면 안 됩니다. renderer는 읽고 그립니다. 사용자의 입력, selection 변경, command 실행은 editor core가 처리합니다.
dirty flag는 나중에 붙인다
편집기에서는 매 프레임 모든 것을 다시 그려도 처음에는 충분합니다. 나중에 도형이 많아지고 texture가 늘면 dirty flag와 cache를 붙입니다.
scene dirty -> rebuild node buffer
camera dirty -> update uniform
selection dirty -> rebuild overlay
하지만 이 최적화는 구조가 잡힌 뒤에 들어가야 합니다. 너무 일찍 최적화를 넣으면 렌더링 캐시와 문서 모델이 섞이기 쉽습니다.
오늘의 핵심
render loop는 GPU 에디터의 심장박동입니다. 하지만 편집기의 진실은 render loop 안에 있지 않습니다.
editor core owns state
render loop samples state
renderer draws pixels
이 선을 지키면 나중에 WebGL에서 WebGPU로 넘어가도 editor tool 코드는 대부분 유지됩니다.