WebGL 직각 3D

Gregg Tavares
Gregg Tavares

WebGL 직사각형 3D

이 게시물은 WebGL에 관한 일련의 게시물의 연속입니다. 첫 번째는 기초부터 시작했고 이전에는 2D 행렬에 관한 2D 행렬에 관한 내용이었습니다. 아직 읽지 못하셨다면 먼저 확인하세요. 지난 게시물에서는 2D 행렬의 작동 방식을 살펴봤습니다. 변환, 회전, 크기 조정, 심지어 픽셀에서 클립 공간으로의 투영까지 모두 1개의 매트릭스와 몇 가지 매트릭스 수학으로 실행할 수 있다고 설명했습니다. 3D를 실행하는 것은 그다음 단계입니다. 이전 2D 예에서는 3x3 행렬을 곱한 2D 점 (x, y)이 있었습니다. 3D를 실행하려면 3D 점 (x, y, z)과 4x4 행렬이 필요합니다. 마지막 예를 3D로 변경해 보겠습니다. F를 다시 사용하지만 이번에는 3D 'F'를 사용합니다. 먼저 3D를 처리하도록 버텍스 셰이더를 변경해야 합니다. 다음은 이전 셰이더입니다.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform mat3 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>

새로운 템플릿은 다음과 같습니다.

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;

uniform mat4 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
}
</script>

더욱 간편해졌습니다. 그런 다음 3D 데이터를 제공해야 합니다.

...

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

...

// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        // left column
        0,   0,  0,
        30,   0,  0,
        0, 150,  0,
        0, 150,  0,
        30,   0,  0,
        30, 150,  0,

        // top rung
        30,   0,  0,
        100,   0,  0,
        30,  30,  0,
        30,  30,  0,
        100,   0,  0,
        100,  30,  0,

        // middle rung
        30,  60,  0,
        67,  60,  0,
        30,  90,  0,
        30,  90,  0,
        67,  60,  0,
        67,  90,  0]),
    gl.STATIC_DRAW);
}

다음으로 모든 매트릭스 함수를 2D에서 3D로 변경해야 합니다. 다음은 makeTranslation, makeRotation, makeScale의 2D (이전) 버전입니다.

function makeTranslation(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}

function makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c,-s, 0,
s, c, 0,
0, 0, 1
];
}

function makeScale(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}

업데이트된 3D 버전은 다음과 같습니다.

function makeTranslation(tx, ty, tz) {
return [
    1,  0,  0,  0,
    0,  1,  0,  0,
    0,  0,  1,  0,
    tx, ty, tz, 1
];
}

function makeXRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};

function makeYRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
};

function makeZRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
    c, s, 0, 0,
-s, c, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1,
];
}

function makeScale(sx, sy, sz) {
return [
sx, 0,  0,  0,
0, sy,  0,  0,
0,  0, sz,  0,
0,  0,  0,  1,
];
}

이제 회전 함수가 3개 있습니다. Z축을 중심으로만 회전하므로 2D에서는 하나만 있으면 되었습니다. 이제 3D를 실행하려면 x축과 y축을 중심으로 회전할 수도 있어야 합니다. 모두 매우 유사해 보입니다. 풀어보면 이전과 마찬가지로 간단해집니다.

Z 회전

newX = x * c + y * s;
newY = x * -s + y * c;

Y 회전


newX = x * c + z * s;
newZ = x * -s + z * c;

X 회전

newY = y * c + z * s;
newZ = y * -s + z * c;

투영 함수도 업데이트해야 합니다. 이전 버전은 다음과 같습니다.

function make2DProjection(width, height) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}

픽셀에서 클립 공간으로 변환됩니다. 3D로 확장하는 첫 번째 시도에서는

function make2DProjection(width, height, depth) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
    2 / width, 0, 0, 0,
    0, -2 / height, 0, 0,
    0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
}

x와 y의 경우 픽셀에서 클립 공간으로 변환해야 했던 것처럼 z의 경우에도 동일한 작업을 수행해야 합니다. 이 경우 Z 공간 픽셀 단위도 만듭니다. 깊이에 width와 유사한 값을 전달하여 공간의 너비가 0~너비픽셀, 높이가 0~높이픽셀이지만 깊이는 -깊이 / 2~+깊이 / 2가 됩니다. 마지막으로 행렬을 계산하는 코드를 업데이트해야 합니다.

// Compute the matrices
var projectionMatrix =
    make2DProjection(canvas.width, canvas.height, canvas.width);
var translationMatrix =
    makeTranslation(translation[0], translation[1], translation[2]);
var rotationXMatrix = makeXRotation(rotation[0]);
var rotationYMatrix = makeYRotation(rotation[1]);
var rotationZMatrix = makeZRotation(rotation[2]);
var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);

// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);

// Set the matrix.
gl.uniformMatrix4fv(matrixLocation, false, matrix);

첫 번째 문제는 도형이 평면 F이므로 3D를 보기 어렵다는 점입니다. 이 문제를 해결하려면 도형을 3D로 확장해 보겠습니다. 현재 F는 직사각형 3개와 삼각형 2개로 구성되어 있습니다. 3D로 만들려면 총 16개의 직사각형이 필요합니다. 여기에 나열하기에는 너무 많습니다. 직사각형 16개 x 직사각형당 삼각형 2개 x 삼각형당 정점 3개 = 96개 정점 모두 보려면 샘플에서 소스를 확인하세요. 더 많은 정점을 그려야 하므로

// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);

슬라이더를 움직여도 3D인지 알기 어렵습니다. 각 직사각형에 다른 색상을 지정해 보겠습니다. 이를 위해 정점 셰이더에 다른 속성을 추가하고 정점 셰이더에서 프래그먼트 셰이더로 전달하기 위한 변형을 추가합니다. 다음은 새 꼭짓점 셰이더입니다.

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;

uniform mat4 u_matrix;

varying vec4 v_color;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;

// Pass the color to the fragment shader.
v_color = a_color;
}
</script>

그리고 프래그먼트 셰이더에서 이 색상을 사용해야 합니다.

<script id="3d-vertex-shader" type="x-shader/x-fragment">
precision mediump float;

// Passed in from the vertex shader.
varying vec4 v_color;

void main() {
gl_FragColor = v_color;
}
</script>

색상을 제공할 위치를 조회한 다음 색상을 지정할 다른 버퍼와 속성을 설정해야 합니다.

...
var colorLocation = gl.getAttribLocation(program, "a_color");

...
// Create a buffer for colors.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(colorLocation);

// We'll supply RGB as bytes.
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

// Set Colors.
setColors(gl);

...
// Fill the buffer with colors for the 'F'.

function setColors(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Uint8Array([
        // left column front
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,

        // top rung front
    200,  70, 120,
    200,  70, 120,
    ...
    ...
    gl.STATIC_DRAW);
}

어라, 무슨 일이야? 3D 'F'의 앞면, 뒷면, 측면 등 다양한 부분이 도형에 표시되는 순서대로 그려지는 것으로 나타났습니다. 하지만 이렇게 하면 원하는 결과를 얻을 수 없습니다. 뒤에 있는 요소가 앞에 있는 요소보다 나중에 그려지는 경우가 있기 때문입니다. WebGL의 삼각형에는 앞면과 뒷면의 개념이 있습니다. 앞면을 향한 삼각형의 꼭짓점은 시계 방향으로 이동합니다. 뒤를 향한 삼각형의 꼭짓점은 시계 반대 방향으로 이동합니다.

삼각형 와인딩

WebGL은 앞면 또는 뒷면을 향한 삼각형만 그릴 수 있습니다. 다음과 같이 이 기능을 사용 설정할 수 있습니다.

gl.enable(gl.CULL_FACE);

프로그램 시작 시 한 번만 진행됩니다. 이 기능을 사용 설정하면 WebGL은 기본적으로 뒤를 향한 삼각형을 '제거'합니다. 여기서 '선택'은 '그리지 않음'을 의미하는 전문 용어입니다. WebGL에 관해서는 삼각형이 시계 방향으로 이동하는 것으로 간주되는지 또는 시계 반대 방향으로 이동하는 것으로 간주되는지는 클립 공간에서 삼각형의 정점에 따라 다릅니다. 즉, WebGL은 꼭짓점 셰이더의 꼭짓점에 수학을 적용한 후에 삼각형이 앞면인지 뒷면인지 파악합니다. 즉, 예를 들어 X축에서 -1로 크기가 조정된 시계 방향 삼각형은 시계 반대 방향 삼각형이 되며, X축 또는 Y축을 중심으로 180도 회전된 시계 방향 삼각형은 시계 반대 방향 삼각형이 됩니다. CULL_FACE를 사용 중지했기 때문에 시계 방향(앞) 및 시계 반대 방향(뒤) 삼각형을 모두 볼 수 있습니다. 이제 이 기능을 사용 설정했으므로 확장이나 회전 또는 기타 이유로 전면을 향한 삼각형이 뒤집힐 때마다 WebGL이 삼각형을 그리지 않습니다. 이는 3D에서 물체를 회전할 때 일반적으로 나를 향한 삼각형이 전면을 향하는 것으로 간주되기를 원하기 때문에 좋은 점입니다.

안녕? 삼각형은 모두 어디로 갔나요? 하지만 많은 리더가 잘못된 방향으로 나아가고 있습니다. 회전하면 다른 면을 볼 때 텍스트가 표시됩니다. 다행히 이 문제는 쉽게 해결할 수 있습니다. 뒤로 된 꼭짓점을 확인하고 꼭짓점 2개를 교환합니다. 예를 들어 뒤쪽 삼각형이 하나인 경우

1,   2,   3,
40,  50,  60,
700, 800, 900,

마지막 2개의 정점을 뒤집어 앞으로 만듭니다.

1,   2,   3,
700, 800, 900,
40,  50,  60,

이제 거의 해결되었지만 아직 한 가지 문제가 있습니다. 모든 삼각형이 올바른 방향을 향하고 있고 뒤쪽을 향한 삼각형이 제외되었는데도 뒤에 있어야 할 삼각형이 앞에 있어야 할 삼각형 위에 그려지는 부분이 있습니다. 깊이 버퍼를 입력합니다. Z 버퍼라고도 하는 깊이 버퍼는 이미지를 만드는 데 사용되는 각 색상 픽셀에 대해 하나의 깊이 픽셀인 depth 픽셀의 직사각형입니다. WebGL은 각 색상 픽셀을 그리므로 깊이 픽셀도 그릴 수 있습니다. Z의 정점 셰이더에서 반환된 값을 기반으로 합니다. X와 Y의 경우 클립 공간으로 변환해야 했던 것처럼 Z도 클립 공간 또는 (-1~+1)에 있습니다. 그런 다음 이 값이 깊이 공간 값 (0~+1)으로 변환됩니다. WebGL은 색상 픽셀을 그리기 전에 상응하는 깊이 픽셀을 확인합니다. 그릴 예정인 픽셀의 깊이 값이 해당 깊이 픽셀의 값보다 크면 WebGL은 새 색상 픽셀을 그리지 않습니다. 그렇지 않으면 프래그먼트 셰이더의 색상으로 새 색상 픽셀을 그리고 새 깊이 값으로 깊이 픽셀을 그립니다. 즉, 다른 픽셀 뒤에 있는 픽셀은 그려지지 않습니다. 이 기능은 삭제를 사용 설정한 것과 거의 동일한 방식으로 사용 설정할 수 있습니다.

gl.enable(gl.DEPTH_TEST);

또한 그리기를 시작하기 전에 깊이 버퍼를 1.0으로 다시 지워야 합니다.

// Draw the scene.
function drawScene() {
// Clear the canvas AND the depth buffer.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...

다음 게시물에서는 원근감을 만드는 방법을 설명하겠습니다.

속성이 vec4이지만 gl.vertexAttribPointer 크기가 3인 이유는 무엇인가요?

세부사항에 관심이 있는 사용자라면 2가지 속성을 다음과 같이 정의했음을 눈치채셨을 것입니다.

attribute vec4 a_position;
attribute vec4 a_color;

둘 다 'vec4'이지만 WebGL에 버퍼에서 데이터를 가져오는 방법을 알려줄 때는

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

각 속성의 '3'은 속성당 3개의 값만 가져오라는 의미입니다. 이는 꼭지점 셰이더에서 WebGL이 제공하지 않는 값에 기본값을 제공하기 때문에 작동합니다. 기본값은 0, 0, 0, 1이며 여기서 x = 0, y = 0, z = 0, w = 1입니다. 이 때문에 이전 2D 정점 셰이더에서는 1을 명시적으로 제공해야 했습니다. x와 y를 전달하고 있었으며 z에 1이 필요했지만 z의 기본값이 0이므로 1을 명시적으로 제공해야 했습니다. 하지만 3D의 경우 'w'를 제공하지 않더라도 행렬 수학이 작동하는 데 필요한 기본값 1로 설정됩니다.