WebGL 정사도법 3D
이 글은 WebGL에 관한 글 시리즈의 연속입니다. 첫 번째는 기본사항으로 시작했고 이전에는 2D 행렬에 관한 내용이었습니다. 아직 읽지 않았다면 먼저 확인하세요. 지난 게시물에서는 2D 행렬의 작동 방식을 살펴보았습니다. 변환, 회전, 크기 조절, 픽셀에서 클립 공간으로의 투영까지 모두 하나의 행렬과 몇 가지 마법 같은 행렬 수학으로 수행할 수 있다고 말씀드렸습니다. 3D를 만드는 것은 거기서 한 걸음만 더 나아가면 됩니다. 이전 2D 예시에서는 2D 점 (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개의 회전 함수가 있습니다. 2D에서는 Z축을 중심으로만 회전하므로 하나만 필요했습니다. 이제 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과 유사한 값을 전달하므로 공간은 너비 픽셀까지, 높이 픽셀까지이지만 깊이는 -depth / 2에서 +depth / 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,
마지막 두 꼭짓점을 뒤집어 앞으로 향하게 합니다.
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인 이유
세부적인 부분에 관심이 있는 분이라면 두 속성을 다음과 같이 정의했다는 것을 눈치채셨을 것입니다.
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이며, 이는 행렬 수학이 작동하는 데 필요한 값입니다.