이건 WebGL의 안티 패턴 목록입니다. 안티 패턴은 피해야 할 항목들입니다.
WebGLRenderingContext
에 viewportWidth
및 viewportHeight
배치
일부 코드는 viewport 너비와 높이에 대한 속성을 추가하고 WebGLRenderingContext
에 다음과 같이 고정하는데
gl = canvas.getContext("webgl"); gl.viewportWidth = canvas.width; // 나쁨!!! gl.viewportHeight = canvas.height; // 나쁨!!!
그런 다음 나중에 이렇게 할 수 있습니다.
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
나쁜 이유:
이제 캔버스의 크기를 바꿀 때마다 2개의 속성을 업데이트해줘야 하기 때문에 객관적으로 나쁩니다.
예를 들어 사용자가 창의 크기를 조정할 때 캔버스의 크기를 바꾼다면 gl.viewportWidth
와 gl.viewportHeight
는 다시 설정해주지 않는 한 잘못된 값이 됩니다.
어느 새로운 프로그래머가 코드를 훑어보고 gl.viewportWidth
와 gl.viewportHeight
를 WebGL 명세서의 일부라고 생각해 수개월동안 혼란스럽게 할 수 있기 때문에 주관적으로도 나쁘다고 생각합니다.
대신 할 일:
왜 스스로 더 많은 일을 만드나요? WebGL context는 사용 가능한 캔버스가 있으며 캔버스는 크기를 가집니다.
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
컨텍스트도 너비와 높이를 가지고 있습니다.
// 캔버스 드로잉 버퍼의 크기와 크기가 일치하도록 뷰포트를 설정하면 이는 항상 정확합니다 gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
극단적인 경우를 처리하는 것이 더 좋은 반면 gl.canvas.width
와 gl.canvas.height
를 사용하는 건 그렇지 않습니다.
이유는 여기를 봐주세요.
종횡비에 canvas.width
및 canvas.height
사용
종종 코드에 이런식으로 종횡비에 대해 canvas.width
와 canvas.height
를 사용하는데
const aspect = canvas.width / canvas.height; perspective(fieldOfView, aspect, zNear, zFar);
나쁜 이유:
캔버스의 너비와 높이는 캔버스가 표시되는 크기와 관련이 없습니다. CSS로 캔버스가 표시되는 크기를 제어합니다.
대신 할 일:
canvas.clientWidth
와 canvas.clientHeight
를 사용하세요.
이 값들은 캔버스가 실제로 화면에 표시되는 크기를 알려줍니다.
이 값들을 사용하여 CSS 설정에 관계없이 항상 올바른 종횡비를 얻을 수 있습니다.
const aspect = canvas.clientWidth / canvas.clientHeight; perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
다음은 드로잉 버퍼가 동일한 크기(width="400" height="300"
)지만 CSS를 사용하여 다른 크기로 캔버스를 표시하도록 브라우저에 지시하는 예제입니다.
두 샘플 모두 올바른 종횡비로 'F'를 표시한다는 점에 주목하세요.
canvas.width
와 canvas.height
를 사용했다면 그렇지 않을 겁니다.
window.innerWidth
와 window.innerHeight
를 사용하여 무엇이든 계산
많은 WebGL 프로그램이 여러 곳에서 window.innerWidth
와 window.innerHeight
를 사용합니다.
예를 들어:
canvas.width = window.innerWidth; // 나쁨!! canvas.height = window.innerHeight; // 나쁨!!
나쁜 이유:
포터블하지 않습니다. 네, 캔버스가 화면을 채우도록 만들고 싶은 WebGL 페이지에서 작동할 수 있습니다. 그렇지 않을 때 문제가 발생하는데요. 이 튜토리얼과 같은 글을 만들기로 한 경우에 캔버스는 더 큰 페이지 안에 있는 작은 다이어그램일 뿐입니다. 또는 측면에 속성 편집기나 게임을 위한 점수가 필수할 수도 있습니다. 물론 이런 경우를 처리하도록 코드를 수정할 수 있지만 처음부터 이러한 경우에 대해 작동하도록 작성하면 어떨까요? 그러면 새로운 프로젝트에 코드를 복사하거나 새로운 방식으로 기존 프로젝트를 사용할 때 코드를 변경하지 않아도 될 겁니다.
대신 할 일:
웹 플랫폼과 싸우는 대신, 설계된 대로 웹 플랫폼을 사용하세요.
CSS 그리고 clientWidth
와 clientHeight
를 사용하세요.
var width = gl.canvas.clientWidth; var height = gl.canvas.clientHeight; gl.canvas.width = width; gl.canvas.height = height;
여기 9가지 경우가 있습니다.
모두 똑같은 코드를 사용하는데요.
아무도 window.innerWidth
나 window.innerHeight
를 참조하지 않는다는 점에 주목하세요.
전체 화면으로 만들기 위해 CSS를 사용하는 캔버스만 있는 페이지
캔버스의 너비로 70%를 쓰도록 설정하여 편집기 제어를 위한 공간이 있는 페이지
box-sizing: border-box;
를 사용한 단락에 캔버스가 포함된 페이지
box-sizing: border-box;
는 테두리와 패딩이 외부가 아닌 정의된 요소에서 공간을 차지하도록 만듭니다.
다시 말해 box-sizing: normal
모드에서 15px 테두리를 가진 400x300픽셀 요소는 15픽셀 테두리에 둘러싸인 400x300픽셀 컨텐츠 공간 가지며 전체 크기는 430x330픽셀이 됩니다.
box-sizing: border-box
모드에서는 테두리가 내부로 들어가므로, 동일한 요소가 400x300픽셀을 유지하며 컨텐츠 공간은 370x270이 됩니다.
이게 clientWidth
와 clientHeight
를 사용하는 것이 중요한 또 다른 이유입니다.
테두리를 1em
으로 설정하면 캔버스의 크기를 알아낼 방법이 없습니다.
다른 컴퓨터 혹은 다른 브라우저의 다른 글꼴에 따라 달라질 수도 있습니다.
전체 화면으로 만들기 위해 CSS를 사용하는 캔버스를 삽입할 컨테이너만 있는 페이지
캔버스를 삽입할 컨테이너의 너비로 70%를 쓰도록 설정하여 편집기 제어를 위한 공간이 있는 페이지
box-sizing: border-box;
를 사용한 단락에 캔버스를 삽입할 컨테이너가 포함된 페이지
전체 화면으로 만들기 위해 CSS를 사용하는 캔버스를 삽입할 요소가 없는 페이지
다시 말하지만, 요점은, 웹을 받아들이고 위의 기술들을 사용하여 코드를 작성한다면 다른 사용 사례를 만났을 때 어떤 코드도 바꿀 필요가 없습니다.
일부 앱은 캔버스의 크기 조정을 위해 다음과 같은 window 'resize'
이벤트를 확인합니다.
window.addEventListener('resize', resizeTheCanvas);
또는 이렇게
window.onresize = resizeTheCanvas;
나쁜 이유:
그 자체로 나쁘진 않지만, 대부분의 WebGL program의 경우 사용 사례가 적습니다.
특히 'resize'
는 창의 크기가 바뀔 때만 작동합니다.
다른 이유로 캔버스의 크기가 바뀌면 작동하지 않습니다.
예를 들어 3D 에디터를 만들고 있다고 가정해 해봅시다.
캔버스는 왼쪽에 있고 설정은 오른쪽에 있습니다.
두 부분을 분리하는 드래그 가능한 바가 있도록 만들었고 바를 드래그하여 설정 공간을 크거나 작게 만들 수 있습니다.
이 경우 'resize'
이벤트가 발생하지 않습니다.
마찬가지로 다른 컨텐츠가 추가되거나 제거되는 페이지가 있고 브라우저가 페이지를 다시 레이아웃함에 따라 캔버스의 크기가 변한다면 resize 이벤트가 발생하지 않습니다.
대신 할 일:
안티 패턴에 대한 위의 여러 해결법과 마찬가지로 대부분의 경우에 작동하도록 코드를 작성할 수 있는데요. 모든 프레임을 끊임없이 그리는 WebGL 앱의 경우 해결법은 다음과 같이 매번 크기 조정이 필요한지 확인하는 겁니다.
function resizeCanvasToDisplaySize() { var width = gl.canvas.clientWidth; var height = gl.canvas.clientHeight; if (gl.canvas.width != width || gl.canvas.height != height) { gl.canvas.width = width; gl.canvas.height = height; } } function render() { resizeCanvasToDisplaySize(); drawStuff(); requestAnimationFrame(render); } render();
이제 이런 경우에 캔버스가 올바른 크기로 조정됩니다. 다른 경우를 위해 어떤 코드도 바꿀 필요가 없습니다. 예시로 위의 #3에서 동일한 코드를 사용하여 크기 변경이 가능한 편집 영역을 가진 에디터입니다.
이런 경우나 페이지에 있는 다른 동적 요소의 크기에 따라 캔버스의 크기가 바뀌는 다른 경우에도 resize 이벤트는 없습니다.
모든 프레임을 다시 그리지 않는 WebGL 앱의 경우에도 위 코드는 여전히 유효하며, 캔버스의 크기가 바뀔 수 있는 모든 경우에 다시 그리기를 트리거해야 합니다.
한 가지 쉬운 방법은 ResizeObserver
를 사용하는 것인데
const resizeObserver = new ResizeObserver(render); resizeObserver.observe(gl.canvas, {box: 'content-box'});
WebGLObject
는 WebGLBuffer
나 WebGLTexture
처럼 WebGL에 있는 다양한 유형의 리소스입니다.
일부 앱들은 이런 객체에 속성을 추가하는데요.
예시 코드:
var buffer = gl.createBuffer(); buffer.itemSize = 3; // 나쁨!! buffer.numComponents = 75; // 나쁨!! var program = gl.createProgram(); ... program.u_matrixLoc = gl.getUniformLocation(program, "u_matrix"); // 나쁨!!
나쁜 이유:
이게 나쁜 이유는 WebGL이 "컨텍스트"를 잃을 수 있기 때문입니다.
어떤 이유로든 발생할 수 있지만 가장 일반적인 원인으로 브라우저가 너무 많은 GPU 리소스를 사용하고 있다고 판단하면 여유 공간 확보를 위해 일부 WebGLRenderingContext
의 컨텍스트를 의도적으로 없앨 수 있습니다.
항상 작동하길 바라는 WebGL 프로그램은 이를 처리해야 하는데요.
이를 처리하는 예시로는 구글 지도가 있습니다.
위 코드의 문제점은 컨텍스트가 없어졌을 때 위의 gl.createBuffer()
같은 WebGL 생성 함수는 null
을 반환한다는 겁니다.
그러면 사실상 코드가 이렇게 됩니다.
var buffer = null; buffer.itemSize = 3; // 오류! buffer.numComponents = 75; // 오류!
다음과 같은 오류로 앱을 죽일 수 있는데
TypeError: Cannot set property 'itemSize' of null
많은 앱들이 컨텍스트가 없어졌을 때 죽어도 상관하지 않는데, 개발자가 컨텍스트 손실 이벤트를 처리하기 위해 앱을 업데이트하기로 결정했다면, 나중에 수정해야 할 코드를 작성하는 건 안 좋은 생각으로 보입니다.
대신 할 일:
WebGLObjects
와 이에 관한 정보를 함께 유지하고 싶다면 한 가지 방법은 자바스크립트 객체를 사용하는 겁니다.
예시:
var bufferInfo = { id: gl.createBuffer(), itemSize: 3, numComponents: 75, }; var programInfo = { id: program, u_matrixLoc: gl.getUniformLocation(program, "u_matrix"), };
개인적으로 WebGL 작성을 훨씬 간단히 만들어주는 몇 가지 도우미를 사용하는 걸 추천합니다.
이게 인터넷에서 본 코드에서 WebGL 안티 패턴이라고 생각하는 것들입니다. 피해야 하는 이유를 설명하고 쉽고 유용한 해결책을 드렸기를 바랍니다.