3D Transform
앞선 글에서는 2차원, 그러니 좌표평면에서 물체에 대해 tranlation, scaling, rotation을 하는 방법을 알아봤어요. 이번에는 이 내용들을 3차원에서 사용할 수 있도록 확장할 거예요. 전반적인 내용은 달라지지 않지만 조금 더 복잡해지는 내용들이 있을 거예요!
2차원에서 시작하기
2차원에서 한 점의 좌표는 2개의 수로 표현되며, 이것을 행렬을 이용해서 표현할 때 1행 3열 행렬을 사용했어요. 그럼 공간에서는 3개의 수가 하나의 점을 지정하므로 1행 4열 행렬을 이용하면 되겠지요.
$\begin{pmatrix}x&y&z&1\end{pmatrix}$
4번째 열의 값은 변환 행렬을 곱할 때 효과가 제대로 적용되기 위한 수로 고정하고 나머지 $x$, $y$, $z$에 점의 좌표를 넣는 거죠. 그리고 기존의 3x3 행렬이었던 변환 행렬을 하나씩 늘려서 4x4 행렬을 사용하면 돼요. 1x4 행렬과 4x4 행렬은 다음과 같은 방법으로 곱해요.
어쨌든 이런 방법이고 중요한 건 이게 아니라 4x4 변환 행렬을 2차원에서 했던 고찰을 그대로 가져와서 확장하는 거예요.
1. Translation
Translation은 간단합니다. 각 변수에 변환하고 싶은 만큼을 더하는 것이지요. 각 축으로 다음만큼 평행 이동한다면
$x$: $dx$
$y$: $dy$
$z$: $dz$
변환 행렬 $T$는 다음과 같습니다.
$T=\begin{pmatrix}1&0&0&0\\0&1&0&0\\0&0&1&0\\dx&dy&dz&1\\\end{pmatrix}$
2. Scaling
스케일링도 2차원에서 했던 것 그대로 하면 돼요. 어떤 축으로 $k$배 스케일링하려면 그 축의 모든 점에 $k$배를 하면 돼요. 이때 변환의 기준점은 원점이 되는데, 2D에서는 식이 비교적 간단하기 때문에 기준점이 일반적인 점인 경우에도 논의했지만 3차원의 경우에서는 식이 복잡하기도 하고 굳이 기준점을 바꿀 필요도 없기 때문에 논의하지는 않을게요.
변환 행렬 $S$는 다음과 같습니다.
$S=\begin{pmatrix}sx&0&0&0\\0&sx&0&0\\0&0&sz&0\\0&0&0&1\\\end{pmatrix}$
3. Rotation
Rotation은 조금 복잡합니다. 2차원에서 우리는 한 평면에 대해서 회전하는 점을 고려했어요. 그런데 3차원에서 회전이 이루어지는 평면은 하나가 아니죠. 무수히 많은 평면에서 회전이 일어날 수 있어요. 이렇게 생각하면 2차원의 아이디어를 그대로 가져오기는 힘들어 보이죠.
하지만 우리가 필요로 하는 것이 무엇인지 생각하면 꼭 3D에서 회전에 대한 식을 다시 만들 필요는 없어요. 3차원을 구성하는 세 개의 축 x, y, z로 xy평면, yz 평면, zx 평면으로 경우를 나누고 공간에서의 회전을 세 평면에서의 회전으로 분해하는 것이지요. 이렇게 하면 3차원 공간에서 회전을 온전히 표현한 것이 아니기 때문에 일부 제약이 생기긴 하지만(가령 이 방법으로 원점과 (1, 1, 1)을 지나는 직선을 축으로 어떤 점을 회전시키는 식을 구하기는 힘듭니다) 우리가 필요로 하는 범위 내에서는 충분해요!
이제 3개의 평면에서 회전할 때 사용되는 변환 행렬을 구해보죠.
아래에서 $s$는 $\sin\theta$, $c$는 $\cos\theta$이고 $R_X$는 X축을 축으로 하는 회전(YZ 평면상의 회전)을 뜻합니다. 회전은 반시계 방향을 +로 정합니다.
$R_X=\begin{pmatrix}1&0&0&0\\0&c&s&0\\0&-s&c&0\\0&0&0&1\end{pmatrix}$
$R_Y=\begin{pmatrix}c&0&s&0\\0&1&0&0\\-s&0&c&0\\0&0&0&1\end{pmatrix}$
$R_Z=\begin{pmatrix}c&s&0&0\\-s&c&0&0\\0&0&1&0\\0&0&0&1\end{pmatrix}$
복잡해 보이지만 이것도 앞서 1x4 행렬과 4x4 행렬을 어떻게 곱했는지 생각해보면 2차원에서 논의한 내용과 똑같아요.
구현하기
우선 전체 코드를 올릴게요.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<p>WebGL</p>
<canvas id="canvas" width="800" height="600"></canvas>
</body>
<script>
const pi = Math.PI;
var dx=0, dy=0, dz=0, sx=1, sy=1, sz=1, angleX=-pi/5, angleY=-pi/6, angleZ=0;
var g;
function initWebGL() {
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
g = gl;
if(!gl) {
console.log("WebGL을 사용할 수 없습니다.");
}
const vertexShaderSource = document.getElementById("vertex-shader").text;
const fragmentShaderSource = document.getElementById("fragment-shader").text;
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vertexShader, fragmentShader);
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
const colorAttributeLocation = gl.getAttribLocation(program, "a_color");
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
const transformUniformLocation = gl.getUniformLocation(program, "transform");
const positionBuffer = gl.createBuffer();
const colorBuffer = gl.createBuffer();
var positions = [
// left column front
0, 0, 0,
30, 0, 0,
0, 150, 0,
0, 150, 0,
30, 0, 0,
30, 150, 0,
// top rung front
30, 0, 0,
100, 0, 0,
30, 30, 0,
30, 30, 0,
100, 0, 0,
100, 30, 0,
// middle rung front
30, 60, 0,
67, 60, 0,
30, 90, 0,
30, 90, 0,
67, 60, 0,
67, 90, 0,
// left column back
0, 0, 0,
30, 0, 30,
0, 150, 30,
0, 150, 30,
30, 0, 30,
30, 150, 30,
// top rung back
30, 0, 30,
100, 0, 30,
30, 30, 30,
30, 30, 30,
100, 0, 30,
100, 30, 30,
// middle rung back
30, 60, 30,
67, 60, 30,
30, 90, 30,
30, 90, 30,
67, 60, 30,
67, 90, 30,
// top
0, 0, 0,
100, 0, 0,
100, 0, 30,
0, 0, 0,
100, 0, 30,
0, 0, 30,
// top rung right
100, 0, 0,
100, 30, 0,
100, 30, 30,
100, 0, 0,
100, 30, 30,
100, 0, 30,
// under top rung
30, 30, 0,
30, 30, 30,
100, 30, 30,
30, 30, 0,
100, 30, 30,
100, 30, 0,
// between top rung and middle
30, 30, 0,
30, 30, 30,
30, 60, 30,
30, 30, 0,
30, 60, 30,
30, 60, 0,
// top of middle rung
30, 60, 0,
30, 60, 30,
67, 60, 30,
30, 60, 0,
67, 60, 30,
67, 60, 0,
// right of middle rung
67, 60, 0,
67, 60, 30,
67, 90, 30,
67, 60, 0,
67, 90, 30,
67, 90, 0,
// bottom of middle rung.
30, 90, 0,
30, 90, 30,
67, 90, 30,
30, 90, 0,
67, 90, 30,
67, 90, 0,
// right of bottom
30, 90, 0,
30, 90, 30,
30, 150, 30,
30, 90, 0,
30, 150, 30,
30, 150, 0,
// bottom
0, 150, 0,
0, 150, 30,
30, 150, 30,
0, 150, 0,
30, 150, 30,
30, 150, 0,
// left side
0, 0, 0,
0, 0, 30,
0, 150, 30,
0, 0, 0,
0, 150, 30,
0, 150, 0
];
var colors = [
// 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,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// middle rung front
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// left column back
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
// top rung back
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
// middle rung back
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
// top
70, 200, 210,
70, 200, 210,
70, 200, 210,
70, 200, 210,
70, 200, 210,
70, 200, 210,
// top rung right
200, 200, 70,
200, 200, 70,
200, 200, 70,
200, 200, 70,
200, 200, 70,
200, 200, 70,
// under top rung
210, 100, 70,
210, 100, 70,
210, 100, 70,
210, 100, 70,
210, 100, 70,
210, 100, 70,
// between top rung and middle
210, 160, 70,
210, 160, 70,
210, 160, 70,
210, 160, 70,
210, 160, 70,
210, 160, 70,
// top of middle rung
70, 180, 210,
70, 180, 210,
70, 180, 210,
70, 180, 210,
70, 180, 210,
70, 180, 210,
// right of middle rung
100, 70, 210,
100, 70, 210,
100, 70, 210,
100, 70, 210,
100, 70, 210,
100, 70, 210,
// bottom of middle rung.
76, 210, 100,
76, 210, 100,
76, 210, 100,
76, 210, 100,
76, 210, 100,
76, 210, 100,
// right of bottom
140, 210, 80,
140, 210, 80,
140, 210, 80,
140, 210, 80,
140, 210, 80,
140, 210, 80,
// bottom
90, 130, 110,
90, 130, 110,
90, 130, 110,
90, 130, 110,
90, 130, 110,
90, 130, 110,
// left side
160, 160, 220,
160, 160, 220,
160, 160, 220,
160, 160, 220,
160, 160, 220,
160, 160, 220
];
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(colors), gl.STATIC_DRAW);
render = () => {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Clear the canvas
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var size = 3;
var type = gl.FLOAT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);
gl.enableVertexAttribArray(colorAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var size = 3;
var type = gl.UNSIGNED_BYTE;
var normalize = true;
var stride = 0;
var offset = 0;
gl.vertexAttribPointer(colorAttributeLocation, size, type, normalize, stride, offset);
gl.uniform3f(resolutionLocation, gl.canvas.width, gl.canvas.height, 400);
const translateMatrix = transform.translate(dx, dy, dz);
const scaleMatrix = transform.scale(sx, sy, sz);
const xRotateMatrix = transform.rotateX(angleX);
const yRotateMatrix = transform.rotateY(angleY);
const zRotateMatrix = transform.rotateZ(angleZ);
var transformMatrix = translateMatrix;
transformMatrix = m4.multiply(transformMatrix, xRotateMatrix);
transformMatrix = m4.multiply(transformMatrix, yRotateMatrix);
transformMatrix = m4.multiply(transformMatrix, zRotateMatrix);
transformMatrix = m4.multiply(transformMatrix, scaleMatrix);
gl.uniformMatrix4fv(transformUniformLocation, false, transformMatrix);
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 6 * 16;
gl.drawArrays(primitiveType, offset, count);
}
render();
}
window.onload = initWebGL;
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if(success) return shader;
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
</script>
<script id="vertex-shader" type="glsl">
attribute vec4 a_position;
attribute vec4 a_color;
uniform vec3 u_resolution;
uniform mat4 transform;
varying vec4 v_color;
void main() {
vec4 position = transform * a_position;
vec4 clipSpace = position / vec4(u_resolution, 1) * vec4(2, 2, 2, 1);
gl_Position = clipSpace;
v_color = a_color;
}
</script>
<script id="fragment-shader" type="glsl">
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
</script>
<style>
canvas {
width: 800px;
height: 600px;
border: 1px solid #000;
}
</style>
<script>
/**
* 평행 이동 행렬을 만듭니다.
* @param{number} dx x축 평형 이동
* @param{number} dy y축 평행 이동
* @param{number} dz z축 평행 이동
* @return{Array.number} 평행 이동 행렬
*/
translate = function(dx, dy, dz) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
dx, dy, dz, 1,
];
};
/**
* 크기 조정 행렬을 만듭니다.
* @param{number} sx x축 크기 배율
* @param{number} sy y축 크기 배율
* @param{number} sz z축 크기 배율
* @return{Array.number} 크기 조정 행렬
*/
scale = function(sx, sy, sz) {
return [
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1
]
};
/**
* X축을 축으로 하는 회전 변환 행렬을 만듭니다.
* @param{number} angle 반시계방향을 +로 하는 회전 각도(radian)
* @return{Array.number} 회전 변환 행렬
*/
xRotate = function(angle) {
const s = Math.sin(angle), c = Math.cos(angle);
return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};
/**
* Y축을 축으로 하는 회전 변환 행렬을 만듭니다.
* @param{number} angle 반시계방향을 +로 하는 회전 각도(radian)
* @return{Array.number} 회전 변환 행렬
*/
yRotate = function(angle) {
const s = Math.sin(angle), c = Math.cos(angle);
return [
c, 0, s, 0,
0, 1, 0, 0,
-s, 0, c, 0,
0, 0, 0, 1
];
};
/**
* Z축을 축으로 하는 회전 변환 행렬을 만듭니다.
* @param{number} angle 반시계방향을 +로 하는 회전 각도(radian)
* @return{Array.number} 회전 변환 행렬
*/
zRotate = function(angle) {
const s = Math.sin(angle), c = Math.cos(angle);
return [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
};
const transform = {
translate: translate,
scale: scale,
rotateX: xRotate,
rotateY: yRotate,
rotateZ: zRotate
};
</script>
<script>
const m4 = {
multiply: function(a, b) {
var b00 = b[0 * 4 + 0];
var b01 = b[0 * 4 + 1];
var b02 = b[0 * 4 + 2];
var b03 = b[0 * 4 + 3];
var b10 = b[1 * 4 + 0];
var b11 = b[1 * 4 + 1];
var b12 = b[1 * 4 + 2];
var b13 = b[1 * 4 + 3];
var b20 = b[2 * 4 + 0];
var b21 = b[2 * 4 + 1];
var b22 = b[2 * 4 + 2];
var b23 = b[2 * 4 + 3];
var b30 = b[3 * 4 + 0];
var b31 = b[3 * 4 + 1];
var b32 = b[3 * 4 + 2];
var b33 = b[3 * 4 + 3];
var a00 = a[0 * 4 + 0];
var a01 = a[0 * 4 + 1];
var a02 = a[0 * 4 + 2];
var a03 = a[0 * 4 + 3];
var a10 = a[1 * 4 + 0];
var a11 = a[1 * 4 + 1];
var a12 = a[1 * 4 + 2];
var a13 = a[1 * 4 + 3];
var a20 = a[2 * 4 + 0];
var a21 = a[2 * 4 + 1];
var a22 = a[2 * 4 + 2];
var a23 = a[2 * 4 + 3];
var a30 = a[3 * 4 + 0];
var a31 = a[3 * 4 + 1];
var a32 = a[3 * 4 + 2];
var a33 = a[3 * 4 + 3];
return [
b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30,
b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31,
b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32,
b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33,
b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30,
b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31,
b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32,
b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33,
b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30,
b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31,
b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32,
b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33,
b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30,
b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31,
b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32,
b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33,
];
}
}
</script>
</html>
전체적인 흐름은 맨 처음 글에서 다룬 것과 비슷하지만 일부 바뀐 부분이 있어요.
1. 사용하는 행렬을 4차원으로 확장하기
우선 vertex shader가 3차원에서 사용될 수 있도록 a_position으로 1x4 행렬을 받고 거기에 4x4 변환 행렬을 곱해서 position을 얻게 했어요. 이것은 기존의 vec2를 vec4로, mat3를 mat4로 바꾸어서 이루어졌어요. 물론 각 uniform을 설정해주는 함수도 uniform2f 대신 uniform3f를, uniformMatrix3fv 대신 uniformMatrix4fv를 사용해야 하죠.
2. 공간을 clipspace로 바꾸는 과정
변환된 position을 canvas의 (가로/2)와 (세로/2)로 나누되 z좌표도 100으로 나누어서 실제로는
(-canvas.width/2, -canvas.height/2, -200) ~ (canvas.width/2, canvas.height/2, 200)
의 공간에 입체가 그려진 것처럼 했어요. 이는 매 렌더링마다 u_resolution을 설정해주고 gl_Position을 position / u_resolution으로 나누어서 구현되었죠.
3. 3차원 변형 함수와 4x4 행렬곱 함수 만들기
여기에 4x4 행렬곱을 해주는 함수인 m4.multiply와 변환을 해주는 함수들인 transform을 만들었어요.
4. 도형 바꾸기
도형을 평면의 F가 아닌 공간의 F로 바꾸었어요. 즉, 공간도형으로 만들었다는 것이지요.
참고로 위의 코드를 그대로 돌려보면 F가 원점 대칭돼서 보일 거예요. 사실 이 도형은 여기에서 가져왔는데 여기서는 F를 원점 대칭으로 입력하고 렌더링 할 때 모든 점을 좌우, 위아래 대칭시켜서 F를 제대로 뒤집더라고요. 저는 이 과정을 제거해서 F가 원점 대칭되어서 보이는 거예요.
5. 각 변마다 색깔 다르게 칠하기
도형의 모든 변이 같은 색이면 보기 힘들 거예요. 그래서 각 변을 다른 색으로 칠했어요. 이는 varying울 통해 구현했지요. 앞서서 varying은 vertex shader가 fragment shader와 연락하는 방법이라고 했어요. 그래서 vertex shader에 attribute로 색을 공급해주면 varying을 통해 fragment shader로 전달되고, fragment shader가 색을 전달받은 색으로 칠해는 것이지요.
6. 기본 회전 각도 조정
3D 도형도 앞에서 보면 평면처럼 보일 거예요. 3D처럼 보이게 초기 회전 각도를 조정했어요.
그러면 이걸 얻게 됩니다.
그런데 뭔가 이상하죠. 이건 도형 F보다는 현대미술에 가까운 것 같아요. 이 현상이 발생한 이유는 WebGL이 나중에 그려진 삼각형을 더 위에 그리기 때문이에요. 그려진 도형의 가장 앞면은 분홍색인데 분홍색은 없고 뒷면의 색인 보라색이 보이지요. 이건 분홍색이 가장 먼저 그려졌기 때문에 나중에 그려진 보라색에 덮여버린 거예요. 아래 애니메이션을 보면 더 직관적으로 이해될 거예요.
WebGL에는 두 가지 종류의 삼각형이 있어요. 앞을 보고 있는 삼각형과 뒤를 보고 있는 삼각형이지요.
삼각형의 앞뒤는 꼭짓점의 방향으로 알 수 있어요. 꼭짓점을 찍은 순서가 반시계 방향이 될 때 보고 있는 면이 삼각형의 정면이에요. 그 반대는 후면이지요. 여기서 시계방향, 반시계 방향은 변환을 마친 후의 점의 위치예요.
WebGL에는 모든 삼각형이 한 가지 면만 보여주도록 그릴 수 있는 기능이 있어요. 렌더링 할 때 이 기능을 켜주면 특별히 지정하지 않은 한 우리는 항상 삼각형의 앞면만 보게 됩니다. 그러니 회전 또는 크기 조정으로 뒤를 보게 된 삼각형은 WebGL이 그리지 않을 겁니다.
gl.enable(gl.CULL_FACE);
그리고 이걸 얻게 되지요.
아직 뭔가 이상하죠. 앞면을 보는 삼각형만 그려지게 했는데 몇 개 면이 사라지긴 했지만 여전히 뒷면이 앞면을 가리고 있어요. 심지어 이 도형을 뒤에서 보게 되면
대부분의 면이 보이지도 않습니다. 이건 처음에 삼각형을 지정할 때 모든 면이 도형의 바깥쪽을 바라보게 설정하지 않았기 때문이에요. 예를 들어 첫 번째 사진에서 진한 보라색 면은 뒤를 보고 있기 때문에 그리면 안 되지만 positions 배열에서 꼭짓점을 지정한 순서에 의하면 사실 앞면을 보고 있기 때문에 렌더링이 되는 거예요. 이를 해결하기 위해 positions 배열에서 꼭짓점을 지정해주는 순서를 바꿔서 모든 면이 공간도형의 바깥쪽을 바라보게 지정해야 해요. 다음과 같이 positions 배열을 바꾸어주죠.
var positions = [
// left column front
0, 0, 0,
0, 150, 0,
30, 0, 0,
0, 150, 0,
30, 150, 0,
30, 0, 0,
// top rung front
30, 0, 0,
30, 30, 0,
100, 0, 0,
30, 30, 0,
100, 30, 0,
100, 0, 0,
// middle rung front
30, 60, 0,
30, 90, 0,
67, 60, 0,
30, 90, 0,
67, 90, 0,
67, 60, 0,
// left column back
0, 0, 30,
30, 0, 30,
0, 150, 30,
0, 150, 30,
30, 0, 30,
30, 150, 30,
// top rung back
30, 0, 30,
100, 0, 30,
30, 30, 30,
30, 30, 30,
100, 0, 30,
100, 30, 30,
// middle rung back
30, 60, 30,
67, 60, 30,
30, 90, 30,
30, 90, 30,
67, 60, 30,
67, 90, 30,
// top
0, 0, 0,
100, 0, 0,
100, 0, 30,
0, 0, 0,
100, 0, 30,
0, 0, 30,
// top rung right
100, 0, 0,
100, 30, 0,
100, 30, 30,
100, 0, 0,
100, 30, 30,
100, 0, 30,
// under top rung
30, 30, 0,
30, 30, 30,
100, 30, 30,
30, 30, 0,
100, 30, 30,
100, 30, 0,
// between top rung and middle
30, 30, 0,
30, 60, 30,
30, 30, 30,
30, 30, 0,
30, 60, 0,
30, 60, 30,
// top of middle rung
30, 60, 0,
67, 60, 30,
30, 60, 30,
30, 60, 0,
67, 60, 0,
67, 60, 30,
// right of middle rung
67, 60, 0,
67, 90, 30,
67, 60, 30,
67, 60, 0,
67, 90, 0,
67, 90, 30,
// bottom of middle rung.
30, 90, 0,
30, 90, 30,
67, 90, 30,
30, 90, 0,
67, 90, 30,
67, 90, 0,
// right of bottom
30, 90, 0,
30, 150, 30,
30, 90, 30,
30, 90, 0,
30, 150, 0,
30, 150, 30,
// bottom
0, 150, 0,
0, 150, 30,
30, 150, 30,
0, 150, 0,
30, 150, 30,
30, 150, 0,
// left side
0, 0, 0,
0, 0, 30,
0, 150, 30,
0, 0, 0,
0, 150, 30,
0, 150, 0
];
그러면 다음과 같은 결과를 얻게 돼요.
여전히 몇 개 면이 다른 면을 덮어버려서 실제로는 더 위에 있어야 하는 면이 뒤로 가버리고 있어요. 이를 해결하려면 Z-Buffer(Depth Buffer)를 사용해야 해요.
Depth-Buffer는 각 면의 Z 좌표를 보고 Z 좌표가 더 큰 면을 뒤로 보내요. 즉, 각 면을 그리기 전에 겹쳐지는 도형의 Z 좌표를 확인해서 더 크면 그리지 않는 거예요.
이 기능은 다음 명령어로 켤 수 있어요.
gl.enable(gl.DEPTH_TEST);
그리고 매 렌더링마다 화면의 Depth Buffer를 초기화해야 해요.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
그리고 앞서 말했듯 사용한 F의 Geometry는 여기에서 가져왔는데 여긴 우리의 경우에서 하지 않는 변형을 하나 더 한다고 했어요. 그래서 일반적인 경우와는 다르게 삼각형의 앞면이 아니라 뒷면을 보게 해야 해요.
gl.cullFace(g.FRONT);
그리고 결과를 확인해보면?
의도했던 대로예요!
angle 값을 바꿔서 render 함수를 다시 실행하면 다음과 같이 회전된 F를 만들 수도 있지요.
Full Code:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<p>WebGL</p>
<canvas id="canvas" width="800" height="600"></canvas>
</body>
<script>
const pi = Math.PI;
var dx=0, dy=0, dz=0, sx=1, sy=1, sz=1, angleX=-pi/5, angleY=-pi/6, angleZ=0;
var g;
function initWebGL() {
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
g = gl;
if(!gl) {
console.log("WebGL을 사용할 수 없습니다.");
}
const vertexShaderSource = document.getElementById("vertex-shader").text;
const fragmentShaderSource = document.getElementById("fragment-shader").text;
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vertexShader, fragmentShader);
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
const colorAttributeLocation = gl.getAttribLocation(program, "a_color");
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
const transformUniformLocation = gl.getUniformLocation(program, "transform");
const positionBuffer = gl.createBuffer();
const colorBuffer = gl.createBuffer();
var positions = [
// left column front
0, 0, 0,
0, 150, 0,
30, 0, 0,
0, 150, 0,
30, 150, 0,
30, 0, 0,
// top rung front
30, 0, 0,
30, 30, 0,
100, 0, 0,
30, 30, 0,
100, 30, 0,
100, 0, 0,
// middle rung front
30, 60, 0,
30, 90, 0,
67, 60, 0,
30, 90, 0,
67, 90, 0,
67, 60, 0,
// left column back
0, 0, 30,
30, 0, 30,
0, 150, 30,
0, 150, 30,
30, 0, 30,
30, 150, 30,
// top rung back
30, 0, 30,
100, 0, 30,
30, 30, 30,
30, 30, 30,
100, 0, 30,
100, 30, 30,
// middle rung back
30, 60, 30,
67, 60, 30,
30, 90, 30,
30, 90, 30,
67, 60, 30,
67, 90, 30,
// top
0, 0, 0,
100, 0, 0,
100, 0, 30,
0, 0, 0,
100, 0, 30,
0, 0, 30,
// top rung right
100, 0, 0,
100, 30, 0,
100, 30, 30,
100, 0, 0,
100, 30, 30,
100, 0, 30,
// under top rung
30, 30, 0,
30, 30, 30,
100, 30, 30,
30, 30, 0,
100, 30, 30,
100, 30, 0,
// between top rung and middle
30, 30, 0,
30, 60, 30,
30, 30, 30,
30, 30, 0,
30, 60, 0,
30, 60, 30,
// top of middle rung
30, 60, 0,
67, 60, 30,
30, 60, 30,
30, 60, 0,
67, 60, 0,
67, 60, 30,
// right of middle rung
67, 60, 0,
67, 90, 30,
67, 60, 30,
67, 60, 0,
67, 90, 0,
67, 90, 30,
// bottom of middle rung.
30, 90, 0,
30, 90, 30,
67, 90, 30,
30, 90, 0,
67, 90, 30,
67, 90, 0,
// right of bottom
30, 90, 0,
30, 150, 30,
30, 90, 30,
30, 90, 0,
30, 150, 0,
30, 150, 30,
// bottom
0, 150, 0,
0, 150, 30,
30, 150, 30,
0, 150, 0,
30, 150, 30,
30, 150, 0,
// left side
0, 0, 0,
0, 0, 30,
0, 150, 30,
0, 0, 0,
0, 150, 30,
0, 150, 0
];
var colors = [
// 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,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// middle rung front
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// left column back
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
// top rung back
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
// middle rung back
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
// top
70, 200, 210,
70, 200, 210,
70, 200, 210,
70, 200, 210,
70, 200, 210,
70, 200, 210,
// top rung right
200, 200, 70,
200, 200, 70,
200, 200, 70,
200, 200, 70,
200, 200, 70,
200, 200, 70,
// under top rung
210, 100, 70,
210, 100, 70,
210, 100, 70,
210, 100, 70,
210, 100, 70,
210, 100, 70,
// between top rung and middle
210, 160, 70,
210, 160, 70,
210, 160, 70,
210, 160, 70,
210, 160, 70,
210, 160, 70,
// top of middle rung
70, 180, 210,
70, 180, 210,
70, 180, 210,
70, 180, 210,
70, 180, 210,
70, 180, 210,
// right of middle rung
100, 70, 210,
100, 70, 210,
100, 70, 210,
100, 70, 210,
100, 70, 210,
100, 70, 210,
// bottom of middle rung.
76, 210, 100,
76, 210, 100,
76, 210, 100,
76, 210, 100,
76, 210, 100,
76, 210, 100,
// right of bottom
140, 210, 80,
140, 210, 80,
140, 210, 80,
140, 210, 80,
140, 210, 80,
140, 210, 80,
// bottom
90, 130, 110,
90, 130, 110,
90, 130, 110,
90, 130, 110,
90, 130, 110,
90, 130, 110,
// left side
160, 160, 220,
160, 160, 220,
160, 160, 220,
160, 160, 220,
160, 160, 220,
160, 160, 220
];
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(colors), gl.STATIC_DRAW);
render = () => {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Clear the canvas
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.CULL_FACE);
gl.enable(gl.DEPTH_TEST);
gl.cullFace(g.FRONT);
gl.useProgram(program);
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var size = 3;
var type = gl.FLOAT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);
gl.enableVertexAttribArray(colorAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var size = 3;
var type = gl.UNSIGNED_BYTE;
var normalize = true;
var stride = 0;
var offset = 0;
gl.vertexAttribPointer(colorAttributeLocation, size, type, normalize, stride, offset);
gl.uniform3f(resolutionLocation, gl.canvas.width, gl.canvas.height, 400);
const translateMatrix = transform.translate(dx, dy, dz);
const scaleMatrix = transform.scale(sx, sy, sz);
const xRotateMatrix = transform.rotateX(angleX);
const yRotateMatrix = transform.rotateY(angleY);
const zRotateMatrix = transform.rotateZ(angleZ);
var transformMatrix = translateMatrix;
transformMatrix = m4.multiply(transformMatrix, xRotateMatrix);
transformMatrix = m4.multiply(transformMatrix, yRotateMatrix);
transformMatrix = m4.multiply(transformMatrix, zRotateMatrix);
transformMatrix = m4.multiply(transformMatrix, scaleMatrix);
gl.uniformMatrix4fv(transformUniformLocation, false, transformMatrix);
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 6 * 16;
gl.drawArrays(primitiveType, offset, count);
}
render();
}
window.onload = initWebGL;
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if(success) return shader;
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
</script>
<script id="vertex-shader" type="glsl">
attribute vec4 a_position;
attribute vec4 a_color;
uniform vec3 u_resolution;
uniform mat4 transform;
varying vec4 v_color;
void main() {
vec4 position = transform * a_position;
vec4 clipSpace = position / vec4(u_resolution, 1) * vec4(2, 2, 2, 1);
gl_Position = clipSpace;
v_color = a_color;
}
</script>
<script id="fragment-shader" type="glsl">
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
</script>
<style>
canvas {
width: 800px;
height: 600px;
border: 1px solid #000;
}
</style>
<script>
/**
* 평행 이동 행렬을 만듭니다.
* @param{number} dx x축 평형 이동
* @param{number} dy y축 평행 이동
* @param{number} dz z축 평행 이동
* @return{Array.number} 평행 이동 행렬
*/
translate = function(dx, dy, dz) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
dx, dy, dz, 1,
];
};
/**
* 크기 조정 행렬을 만듭니다.
* @param{number} sx x축 크기 배율
* @param{number} sy y축 크기 배율
* @param{number} sz z축 크기 배율
* @return{Array.number} 크기 조정 행렬
*/
scale = function(sx, sy, sz) {
return [
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1
]
};
/**
* X축을 축으로 하는 회전 변환 행렬을 만듭니다.
* @param{number} angle 반시계방향을 +로 하는 회전 각도(radian)
* @return{Array.number} 회전 변환 행렬
*/
xRotate = function(angle) {
const s = Math.sin(angle), c = Math.cos(angle);
return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};
/**
* Y축을 축으로 하는 회전 변환 행렬을 만듭니다.
* @param{number} angle 반시계방향을 +로 하는 회전 각도(radian)
* @return{Array.number} 회전 변환 행렬
*/
yRotate = function(angle) {
const s = Math.sin(angle), c = Math.cos(angle);
return [
c, 0, s, 0,
0, 1, 0, 0,
-s, 0, c, 0,
0, 0, 0, 1
];
};
/**
* Z축을 축으로 하는 회전 변환 행렬을 만듭니다.
* @param{number} angle 반시계방향을 +로 하는 회전 각도(radian)
* @return{Array.number} 회전 변환 행렬
*/
zRotate = function(angle) {
const s = Math.sin(angle), c = Math.cos(angle);
return [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
};
const transform = {
translate: translate,
scale: scale,
rotateX: xRotate,
rotateY: yRotate,
rotateZ: zRotate
};
</script>
<script>
const m4 = {
multiply: function(a, b) {
var b00 = b[0 * 4 + 0];
var b01 = b[0 * 4 + 1];
var b02 = b[0 * 4 + 2];
var b03 = b[0 * 4 + 3];
var b10 = b[1 * 4 + 0];
var b11 = b[1 * 4 + 1];
var b12 = b[1 * 4 + 2];
var b13 = b[1 * 4 + 3];
var b20 = b[2 * 4 + 0];
var b21 = b[2 * 4 + 1];
var b22 = b[2 * 4 + 2];
var b23 = b[2 * 4 + 3];
var b30 = b[3 * 4 + 0];
var b31 = b[3 * 4 + 1];
var b32 = b[3 * 4 + 2];
var b33 = b[3 * 4 + 3];
var a00 = a[0 * 4 + 0];
var a01 = a[0 * 4 + 1];
var a02 = a[0 * 4 + 2];
var a03 = a[0 * 4 + 3];
var a10 = a[1 * 4 + 0];
var a11 = a[1 * 4 + 1];
var a12 = a[1 * 4 + 2];
var a13 = a[1 * 4 + 3];
var a20 = a[2 * 4 + 0];
var a21 = a[2 * 4 + 1];
var a22 = a[2 * 4 + 2];
var a23 = a[2 * 4 + 3];
var a30 = a[3 * 4 + 0];
var a31 = a[3 * 4 + 1];
var a32 = a[3 * 4 + 2];
var a33 = a[3 * 4 + 3];
return [
b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30,
b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31,
b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32,
b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33,
b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30,
b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31,
b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32,
b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33,
b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30,
b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31,
b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32,
b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33,
b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30,
b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31,
b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32,
b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33,
];
}
}
</script>
</html>
'이것저것 > WebGL' 카테고리의 다른 글
2D 변환 - WebGL (0) | 2022.09.08 |
---|---|
WebGL의 기본 (0) | 2022.09.06 |