https://webglfundamentals.org/webgl/lessons/webgl-3d-orthographic.html
https://github.com/Myoungmin/WebGL_Fundamentals
◈ 이번 실습에서 복습할 포인트
- 3D에서 vertex shader 사용
- 3D 버전 변환 행렬 (스케일, x,y,z축 회전, 이동)
- 3D에 적용하는 투영 함수 (클립 공간의 depth 개념 반영, 3D 수학 라이브러리에서 일반적으로 투영함수를 대신하는 orthographic 함수)
- Culling (기본적으로 삼각형 뒷면을 컬링으로 설정하고 그리지 않음을 뜻함, 앞면은 반시계 방향, 뒷면은 시계 방향, 정점 셰이더에서 정점에 수식을 적용한 후에 삼각형이 앞면인지 뒷면인지 파악)
- DEPTH BUFFER (Depth를 적용하게 되면 다른 픽셀 뒤에 있는 픽셀은 그려지지 않는다.)
WebGL 3D : Orthographic
2D 예제에서는 3x3 행렬을 곱한 2D 포인트(x, y)를 가졌었다.
3D를 수행하기 위해서는 3D 포인트(x, y, z)와 4x4 행렬이 필요하다.
◈ 3D를 다루기 위해 정점 셰이더를 수정, 데이터 제공, 4x4 행렬 적용
2D에서 사용한 기존 정점 셰이더
<script id="vertex-shader-2d" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
// 위치에 행렬 곱하기
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>
3D를 적용하기 위한 셰이더
<script id="vertex-shader-3d" type="x-shader/x-vertex">
attribute vec4 a_position;
uniform mat4 u_matrix;
void main() {
// 위치에 행렬 곱하기
gl_Position = u_matrix * a_position;
}
</script>
2D에서 x와 y를 제공한 뒤 z를 1로 설정한 것처럼,
3D에서는 x, y, z를 제공하고 w가 1이어야 하지만,
w의 기본값이 1이기 때문에 따로 설정할 필요가 없다.
3D 데이터를 자바스크립트로 제공
iteration마다 2개를 사용하던 것을 3개의 컴포넌트를 사용하도록 변경하였다.
//...
// positionBuffer(ARRAY_BUFFER)에서 데이터 가져오는 방법을 속성에 지시
// 3D라 3개 컴포넌트로 변경되었다.
var size = 3; // 반복마다 3개의 컴포넌트
var type = gl.FLOAT; // 데이터는 32비트 부동 소수점
var normalize = false; // 데이터 정규화 안 함
var stride = 0; // 0 = 다음 위치를 가져오기 위해 반복마다 size * sizeof(type) 만큼 앞으로 이동
var offset = 0; // 버퍼의 처음부터 시작
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);
//...
// 현재 ARRAY_BUFFER 버퍼 채우기
// 문자 'F'를 정의하는 값으로 버퍼 채우기
function setGeometry(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
// 왼쪽 열
0, 0, 0,
30, 0, 0,
0, 150, 0,
0, 150, 0,
30, 0, 0,
30, 150, 0,
// 상단 가로 획
30, 0, 0,
100, 0, 0,
30, 30, 0,
30, 30, 0,
100, 0, 0,
100, 30, 0,
// 중간 가로 획
30, 60, 0,
67, 60, 0,
30, 90, 0,
30, 90, 0,
67, 60, 0,
67, 90, 0
]),
gl.STATIC_DRAW
);
}
m3.translation, m3.rotation, m3.scaling의 3D 버전
var m4 = {
translation: function(tx, ty, tz) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
tx, ty, tz, 1,
];
},
xRotation: function(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,
];
},
yRotation: function(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,
];
},
zRotation: function(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,
];
},
scaling: function(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축을 중심으로도 회전되어야 한다.
◈ 픽셀에서 클립 공간으로 변환하는 투영 함수를 2D에서 3D로 변경한다.
기존 2D 투영 함수
projection: function (width, height) {
// 참고: 이 행렬은 Y축을 뒤집기 때문에 0이 상단입니다.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
},
}
3D에 적용하는 투영 함수
projection: function(width, height, depth) {
// 참고: 이 행렬은 Y축을 뒤집기 때문에 0이 상단입니다.
return [
2 / width, 0, 0, 0,
0, -2 / height, 0, 0,
0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
},
X와 Y를 픽셀 공간에서 클립 공간으로 변환해야 했던 것처럼 Z도 동일한 작업을 수행해야 한다.
Z축 픽셀 단위인 depth는 width와 height와 다르게, -depth / 2에서 +depth / 2가 된다.
3D에서 행렬을 계산하는 코드
// 행렬 계산
var matrix = m4.projection(gl.canvas.clientWidth, gl.canvas.clientHeight, 400);
matrix = m4.translate(matrix, translation[0], translation[1], translation[2]);
matrix = m4.xRotate(matrix, rotation[0]);
matrix = m4.yRotate(matrix, rotation[1]);
matrix = m4.zRotate(matrix, rotation[2]);
matrix = m4.scale(matrix, scale[0], scale[1], scale[2]);
// 행렬 설정
gl.uniformMatrix4fv(matrixLocation, false, matrix);
◈ 3D 입체감 주기
면에 해당하는 각 사각형에 다른 색상을 칠해서 입체감을 준다.
정점 셰이더에 또 다른 속성과 이걸 정점 셰이더에서 프래그먼트 셰이더로 전달하기 위한 베링을 추가한다.
<script id="vertex-shader-3d" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;
uniform mat4 u_matrix;
varying vec4 v_color;
void main() {
// 위치에 행렬 곱하기
gl_Position = u_matrix * a_position;
// 프래그먼트 셰이더로 색상 전달
v_color = a_color;
}
</script>
프래그먼트 셰이더에서 해당 색상을 사용한다.
<script id="fragment-shader-3d" type="x-shader/x-fragment">
precision mediump float;
// 정점 셰이더에서 전달됩니다.
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
</script>
색상을 제공하기 위해 속성의 위치를 찾고, 색상을 지정하기 위해 또 다른 버퍼와 속성을 설정한다.
//...
var colorLocation = gl.getAttribLocation(program, "a_color");
//...
// 색상용 버퍼 생성
var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
// 버퍼에 색상 넣기
setColors(gl);
//...
// 'F'의 색상으로 버퍼 채우기
function setColors(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Uint8Array([
// 왼쪽 열 앞쪽
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// 상단 획 앞쪽
200, 70, 120,
200, 70, 120,
...
...
gl.STATIC_DRAW);
}
렌더링할 때 색상 버퍼에서 색상 가져오는 방법을 색상 속성에 알려준다.
// 색상 속성 활성화
gl.enableVertexAttribArray(colorLocation);
// 색상 버퍼 할당
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
// colorBuffer(ARRAY_BUFFER)에서 데이터 가져오는 방법을 속성에 지시
var size = 3; // 반복마다 3개의 컴포넌트
var type = gl.UNSIGNED_BYTE; // 데이터는 부호없는 8비트 값
var normalize = true; // 데이터 정규화 (0-255에서 0-1로 전환)
var stride = 0; // 0 = 다음 위치를 가져오기 위해 반복마다 size * sizeof(type) 만큼 앞으로 이동
var offset = 0; // 버퍼의 처음부터 시작
gl.vertexAttribPointer(colorLocation, size, type, normalize, stride, offset);
◈ Culling
WebGL의 삼각형은 앞면과 뒷면의 개념을 가지고 있다.
기본적으로 삼각형 앞면은 반시계 방향으로 진행하는 정점을 가진다.
삼각형 뒷면은 시계 방향으로 진행하는 정점을 가진다.
WebGL은 삼각형의 앞면 혹은 뒷면만 그릴 수 있고, 아래 코드로 해당 기능을 켤 수 있다.
gl.enable(gl.CULL_FACE);
drawScene 함수에 넣어서 해당 기능을 켜면, WebGL은 기본적으로 삼각형 뒷면을 "컬링"으로 설정한다.
"컬링"은 "그리지 않음"을 의미하는 단어이다.
WebGL에서 삼각형이 시계 혹은 반시계 방향으로 진행되는지는 클립 공간에 있는 해당 삼각형의 정점에 따라 달라진다.
즉 WebGL은 정점 셰이더에서 정점에 수식을 적용한 후에 삼각형이 앞면인지 뒷면인지 파악한다.
CULL_FACE 켜고 해당 정점을 계산할 때 스케일이나 회전 등의 이유로 삼각형이 뒤집혔다고 판단하면 WebGL은 그리지 않는다.
◈ Depth
뒤에 있어야 하는 삼각형이 앞에 있어야 하는 삼각형 위에 그려지는 문제 해결하기
DEPTH BUFFER를 입력
Z-버퍼라고도 불리는 깊이 버퍼는 깊이 픽셀로,
이미지를 만드는 데 사용되는 색상 픽셀마다 깊이 픽셀이 하나씩 존재한다.
이건 Z축에 대해 정점 셰이더에서 반환한 값을 기반으로 한다.
X와 Y를 클립 공간으로 변환해야 했던 것처럼 Z도 클립 공간(-1에서 +1)에 있다.
해당 값은 depth space value(0에서 +1)로 변환된다.
WebGL은 색상 픽셀을 그리기 전에 대응하는 깊이 픽셀을 검사한다.
그릴 픽셀의 깊이 값이 대응하는 깊이 픽셀의 값보다 클 경우 WebGL은 새로운 색상 픽셀을 그리지 않는다.
이는 다른 픽셀 뒤에 있는 픽셀은 그려지지 않는다는 걸 의미한다.
컬링을 적용했던 것처럼 아래의 코드로 간단하게 해당 기능을 사용할 수 있다.
gl.enable(gl.DEPTH_TEST);
그리기를 시작하기 전에 깊이 버퍼를 1.0으로 초기화해야 한다.
// 장면 그리기
function drawScene() {
//...
// 캔버스와 깊이 버퍼 초기화
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
//...
◈ 대부분의 3D 수학 라이브러리에는 클립 공간에서 픽셀 공간으로 변환하는 projection 함수가 없고, 보통 ortho나 orthographic이라 불리는 함수가 있다.
var m4 = {
orthographic: function(left, right, bottom, top, near, far) {
return [
2 / (right - left), 0, 0, 0,
0, 2 / (top - bottom), 0, 0,
0, 0, 2 / (near - far), 0,
(left + right) / (left - right),
(bottom + top) / (bottom - top),
(near + far) / (near - far),
1,
];
}
width, height, depth 등의 매개변수를 가지는 단순한 projection 함수와 달리,
좀 더 일반적인 직교 투영 함수는 더 많은 유연성을 제공하는 left, right, bottom, top, near, far 등을 전달할 수 있다.
원래 쓰던 투영 함수와 동일하게 쓰기 위해서 아래와 같이 호출하면 된다.
var left = 0;
var right = gl.canvas.clientWidth;
var bottom = gl.canvas.clientHeight;
var top = 0;
var near = 400;
var far = -400;
var matrix = m4.orthographic(left, right, bottom, top, near, far);
https://myoungmin.github.io/WebGL_Fundamentals/
'WebGL' 카테고리의 다른 글
WebGL Fundamentals > WebGL 3D : Cameras (0) | 2022.07.03 |
---|---|
WebGL Fundamentals > WebGL 3D : Perspective (0) | 2022.07.02 |
WebGL Fundamentals > WebGL 2D : Matrices (0) | 2022.07.01 |
WebGL Fundamentals > WebGL 2D : Scale (0) | 2022.06.30 |
WebGL Fundamentals > WebGL 2D : Rotation (0) | 2022.06.30 |