WebGL 직각 3D

Gregg Tavares
Gregg Tavares

WebGL 직각 3D

이 게시물은 WebGL에 대한 게시물 시리즈의 연속입니다. 첫 번째는 기초로 시작했고 이전은 약 2차원 행렬에 관한 약 2차원 행렬이었습니다. 아직 읽지 않았다면 먼저 읽어 보세요. 지난 게시물에서는 2차원 행렬의 작동 방식을 살펴보았습니다. 변환, 회전, 크기 조정은 물론 픽셀에서 클립 공간으로 프로젝션하는 작업까지 모두 하나의 행렬과 일부 매직 행렬 계산으로 수행할 수 있다고 이야기했습니다. 3D 작업은 조금만 더 하면 되겠네요. 이전 2차원 예에서는 2차원 점 (x, y)에 3x3 행렬을 곱했습니다. 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);

첫 번째 문제는 도형이 3D를 보기 어렵게 만드는 평면 F라는 것입니다. 이 문제를 해결하기 위해 도형을 3D로 확장해 보겠습니다. 현재 F는 각각 2개의 삼각형과 3개의 직사각형으로 구성됩니다. 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,

더 가까워졌지만 여전히 문제가 하나 더 있습니다. 모든 삼각형이 올바른 방향을 향하고 뒷면을 향하는 삼각형이 추려진 경우에도 뒤에 있어야 하는 삼각형이 앞에 있어야 하는 삼각형 위에 그려집니다. DEPTH BUFFER를 입력합니다. 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이며 행렬 수학을 수행하는 데 필요합니다.