3.0 캔버스 리사이징
지금까지 우리의 모든 샘플은 고정된 크기의 캔버스에 렌더링되었습니다. 그러나 실제 애플리케이션에서는 캔버스가 전체 페이지 또는 해당 컨테이너를 완전히 차지하도록 하는 경우가 많습니다. 이를 위해 캔버스 크기를 조정하는 것은 예상보다 까다로울 수 있습니다. 이 튜토리얼에서는 이를 달성하는 방법을 알아보겠습니다.
플레이그라운드 실행 - 3_00_canvas_resizing캔버스에는 프레임버퍼 크기와 디스플레이 크기라는 두 가지 주요 치수가 있습니다. 프레임버퍼 크기는 렌더링 크기라고도 하며 렌더링 첨부 파일의 크기를 결정합니다. 디스플레이 크기는 웹 페이지에 캔버스를 표시하는 데 사용됩니다. 렌더링은 프레임버퍼에 적용되며, 렌더링이 완료되면 이를 렌더링 크기의 이미지로 취급합니다. 이 이미지는 디스플레이 크기에 맞춰 웹 페이지에 표시됩니다. 렌더링 크기와 디스플레이 크기가 일치하지 않으면 렌더링된 결과는 디스플레이 크기에 맞게 늘어납니다.
렌더링 크기는 HTML 태그의 `width` 및 `height` 속성을 지정하여 정의할 수 있습니다:
<canvas width="640" height="480"></canvas>또는 JavaScript에서 구성할 수 있습니다:
canvas.width = 640;
canvas.height = 480;디스플레이 크기는 CSS로 제어할 수 있습니다:
<canvas style="width: 400px; height: 300px;"> </canvas>캔버스 크기를 부모 컨테이너의 100%로 지정하여 크기를 최대로 늘릴 수도 있습니다. 그러나 이 조정은 디스플레이 크기에만 영향을 미치고 렌더링 크기에는 영향을 미치지 않습니다. 이 두 가지 크기를 동기화하는 것은 개발자의 몫입니다. 렌더링 크기를 업데이트하려면 먼저 캔버스 리사이징 이벤트를 포착해야 합니다. 이는 `ResizeObserver`를 사용하여 달성할 수 있습니다.
let timeId = null;
const resizeObserver = new ResizeObserver((entries) => {
if (timeId) {
clearTimeout(timeId);
}
timeId = setTimeout(() => {
requestAnimationFrame(render);
}, 100);
});
requestAnimationFrame(render);
resizeObserver.observe(canvas);
`ResizeObserver`를 사용하면 캔버스 크기가 조정될 때마다 트리거되는 콜백 함수를 지정할 수 있습니다. 이 콜백은 새 프레임의 렌더링을 시작해야 합니다. 이 새 프레임에서 렌더링 크기를 조정하고 그에 따라 프레임버퍼 및 깊이 버퍼를 업데이트해야 합니다.
간단한 접근 방식은 `requestAnimationFrame`을 직접 호출하는 것일 수 있습니다. 그러나 이 경우 크기 조정 이벤트가 자주 발생하면 과도한 호출이 발생할 수 있습니다. 예를 들어, 창 크기를 조정하면 일련의 이벤트가 트리거되며, 최종 창 크기가 확정될 때까지 즉시 장면을 다시 렌더링하는 것은 불필요할 수 있습니다. 크기 조정 중에 자주 렌더링하면 프로세스가 느리게 느껴질 수도 있습니다.
위 코드 스니펫에서는 타이머를 사용하여 다시 렌더링을 제한합니다. `ResizeObserver` 콜백은 `requestAnimationFrame`에 100ms 지연을 적용합니다. 타임아웃이 만료되기 전에 다른 크기 조정 이벤트가 발생하면 타임아웃이 재설정됩니다. 이 접근 방식은 다시 렌더링의 실행을 일괄 처리하여 크기 조정 중 성능을 향상시키는 데 도움이 됩니다.
const devicePixelRatio = window.devicePixelRatio || 1;
let currentCanvasWidth = canvas.clientWidth * devicePixelRatio;
let currentCanvasHeight = canvas.clientHeight * devicePixelRatio;
let projectionMatrixUniformBufferUpdate = null;
if (depthTexture === null || currentCanvasWidth != canvas.width || currentCanvasHeight != canvas.height) {
canvas.width = currentCanvasWidth;
canvas.height = currentCanvasHeight;
const depthTextureDesc = {
size: [canvas.width, canvas.height, 1],
dimension: '2d',
format: 'depth24plus-stencil8',
usage: GPUTextureUsage.RENDER_ATTACHMENT
};
if (depthTexture !== null) {
depthTexture.destroy();
}
depthTexture = device.createTexture(depthTextureDesc);
let depthTextureView = depthTexture.createView();
depthAttachment = {
view: depthTextureView,
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'store',
stencilClearValue: 0,
stencilLoadOp: 'clear',
stencilStoreOp: 'store'
};
let projectionMatrix = glMatrix.mat4.perspective(glMatrix.mat4.create(),
1.4, canvas.width / canvas.height, 0.1, 1000.0);
projectionMatrixUniformBufferUpdate = createGPUBuffer(device, projectionMatrix, GPUBufferUsage.COPY_SRC);
}
렌더링 함수에서는 먼저 `window.devicePixelRatio`를 사용하여 픽셀 밀도를 얻습니다. MacBook과 같은 고해상도(High-DPI) 화면에서는 이 값이 2일 수 있으며, 이는 표준 픽셀에 비해 동일한 물리적 영역에 4배 더 많은 픽셀을 채울 수 있음을 나타냅니다. 더 정교한 렌더링을 위해 캔버스 크기의 4배에 해당하는 프레임버퍼를 생성할 수 있습니다. 대부분의 일반 모니터에서는 이 값이 단순히 1.0이며, 이 경우 프레임버퍼 크기를 캔버스 크기와 일치시킵니다.
캔버스 크기가 변경될 때(예를 들어, 이전 `canvas.width`가 새 크기와 일치하지 않을 때) 여러 리소스를 업데이트해야 합니다.
첫째, 깊이 텍스처를 업데이트해야 합니다. 캔버스는 색상 렌더링 대상을 자동으로 유지하지만, 깊이 버퍼는 수동으로 관리됩니다. 따라서 캔버스 크기가 변경되면 이전 깊이 맵을 해제하고 업데이트된 치수로 새 깊이 맵을 생성해야 합니다.
둘째, 투영 행렬을 업데이트해야 합니다. 투영 행렬은 캔버스의 종횡비에 따라 달라지므로 캔버스 크기가 변경될 때 조정되어야 합니다. 유니폼 버퍼는 동시에 `MAP_WRITE` 및 `MAP_READ`일 수 없으므로 이 과정에서 스테이징 버퍼를 사용해야 합니다. 먼저 업데이트된 행렬을 스테이징 버퍼에 로드한 다음 유니폼 버퍼로 복사합니다.