WebGL
WebGL 하면 보통 멋있는 3D 엔진, 뭔가 멋진 결과물을 만들 수 있는 마법의 프로그램이라 생각하는 경우가 있어요. 그럴 법도 하죠. 웹브라우저에서 3D로 뭔가를 하면 WebGL로 구현되어 있는 경우가 대부분이니까요. 우선 WebGL로 만들어진 몇 가지 프로그램을 소개할게요.
멋있죠. 하지만 사실 WebGL은 웹에서 3D 작업을 마법처럼 처리해주는 도구는 아니에요. 대신 래스터라이제이션(Rasterization) 도구에 가깝지요. WebGL에 도형(Geometry)을 입력으로 넣어주면 그것을 평면에 그린 이미지를 그리는 게 WebGL의 역할이에요. 그래서 앞으로 WebGL을 하면서 다룰 내용은 아마 여러분이 생각하는 화려한 무언가와는 좀 거리가 있을 수도 있어요.
Vertex shader, Fragment shader
WebGL은 입력을 셰이더(shader)의 형태로 받아요. 셰이더는 C/C++과 비슷한 문법을 가진 GLSL(GL Shader Language)로 작성되는 일종의 코드에요. 용도에 따라 Vertex shader와 Fragment shader로 나뉘지요. 이 둘은 프로그램(program)으로 하나로 묶어서 WebGL로 들어가요.
Vertex shader는 그려야 할 점, 선, 삼각형에 대한 정보를 제공하는 셰이더이고 Fragment shader는 각 픽셀이 그려져야 하는 색상에 대한 정보를 제공하는 셰이더예요. 우리는 각 셰이더에 대한 상태(state)를 설정할 수 있고 이걸 실제로 화면에 보여줄 때는 WebGL에서 제공하는 명령어를 통해 컴퓨터의 GPU에서 작성한 셰이더를 실행하는 방식이죠.
셰이더에 데이터를 제공하는 방법은 4가지가 있어요.
- Attributes와 Buffers
Buffers는 GPU에 올려서 실행할 이진데이터에요. 보통 점, 노말 데이터, 꼭짓점 색 등을 포함하지요. 물론 원하는 모든 데이터를 Buffers에 넣을 수 있어요.
Attributes는 어떻게 buffers에서 데이터를 뽑아서 Vertex shader에 제공할지 알려줘요. 예를 들어 좌표를 3개의 부동소수점 형태로 버퍼에 넣어놓았다면 Attributes에 어느 버퍼에, 어느 타입으로, Buffers의 어디부터, 얼마나 많은 바이트를 읽어야 하는지 지정하지요.
이렇듯 Buffers는 랜덤엑세스 방식이 아니에요. 대신 Attributes에 의해 Buffers의 정해진 값이 액세스 되어 Vertex Shader로 들어가지요. - Uniforms
Uniforms는 셰이더를 실행하기 전 설정할 수 있는 일종의 전역 변수입니다. - Textures
Textures는 셰이더에서 무작위로 접근할 수 있는 데이터 배열이에요. 보통 이미지나 색 정보를 담지요. - Varyings
Varyings는 Vertex shader가 Fragment shader로 데이터를 제공하는 경로예요. Vertex shader에 의해 Varyings의 값이 설정되고 Fragment shader가 이를 사용하지요.
간단한 WebGL 개발하기
다음과 같이 Vertex shader를 작성해요.
// attribute는 buffer에서 데이터를 받아옵니다
attribute vec4 a_position;
// 모든 셰이더는 main 함수가 있습니다.
void main() {
// "gl_Position"은 vertex shader가 설정해야 하는 특별한 변수입니다.
gl_Position = a_position;
}
그리고 Fragment shader를 작성해요.
// fragment shader는 기본 정확도가 없기 때문에 직접 설정해야 합니다.
// 일반적으로는 "mediump"(medium precision)를 사용합니다.
precision mediump float;
void main() {
// "gl_FragColor"는 fragment shader가 설정해야 하는 특별한 변수입니다.
gl_FragColor = vec4(0.65, 1.00, 0.91, 1.00); // 민트색
}
gl_FragColor에 제공한 (0.65, 1.00, 0.91, 1.00)는 RGBA 형식으로 0 ~ 1의 값을 가집니다.
이제 HTML과 스크립트를 작성해보죠.
HTML은
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<p>WebGL</p>
<canvas id="canvas"></canvas>
</body>
<script id="vertex-shader" type="glsl">
attribute vec4 a_position;
void main() {
gl_Position = a_position;
}
</script>
<script id="fragment-shader" type="glsl">
precision mediump float;
void main() {
gl_FragColor = vec4(0.65, 1.00, 0.91, 1.00);
}
</script>
</html>
HTML에 script 태그를 사용해서 셰이더를 넣었는데 이때 셰이더 코드가 Javascript 코드로 받아들여지면 안 되므로 type을 glsl로 지정해줘야 해요. 사실 셰이더는 어떤 방법으로든 코드가 담긴 문자열을 변수로 받으면 되므로 꼭 HTML에 입력해놓을 필요는 없어요. AJAX 등을 사용해서 받아올 수도 있지요.
상용으로 WebGL을 사용하는 많은 경우에서는 셰이더를 템플릿을 활용하여 실시간으로 생성해서 사용해요. 하지만 이번 예제에서는 그 정도로 복잡한 작업이 필요하지는 않아요.
WebGL을 로딩하는 Javascript 코드는
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
if(!gl) {
console.log("WebGL을 사용할 수 없습니다.");
}
이에요. 간단히 canvas에 webgl 요소를 받아오면 돼요.
이제 셰이더를 만드는 함수를 작성할 거예요.
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);
}
셰이더를 생성하고 > 소스를 설정하고 > 컴파일하고 > 성공했으면 반환하기
이 함수로 vertex shader와 fragment shader를 GLSL로부터 컴파일할 수 있어요.
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);
HTML에 정의해놓은 script로부터 GLSL을 가져와서 vertex shader와 fragment 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);
}
프로그램을 생성하고 > vertex shader와 fragment shader를 붙이고 > 프로그램을 링크해서 > 성공하면 반환하기
이제 프로그램을 만들어요.
const program = createProgram(gl, vertexShader, fragmentShader);
자, 이제 GPU에 제공할 셰이더와 이를 실행할 수 있게 해주는 프로그램을 만들었으니 여기에 데이터를 공급해야 해요. 이번 예제에는 a_position만 데이터를 제공하면 돼요. 우선 a_position이 뜻하는 attribute를 찾아요. attribute나 다른 요소들을 찾는 작업은 반드시 초기화 단계에서 진행해야 해요. 렌더링 루프에서 진행되서는 안 되죠.
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
앞서 attribute는 buffer에서 데이터를 가져온다고 했어요. attribute는 있으니 buffer가 필요해요. 그리고 그 버퍼를 바인딩(binding)해요. WebGL은 많은 리소스를 전역적으로 접근할 수 있는 바인딩 포인트(binding point)에 묶어둘 수 있게 해 줘요. Binding point는 간단하게 WebGL 내부에 선언되어 있는 전역 변수라고 생각하면 돼요.
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
이제 버퍼가 바인드 되어 있는 바인딩 포인트에 접근하여 버퍼에 데이터를 넣을 수 있어요.
var positions = [
0.0, 0.0,
0.0, 0.5,
0.5, 0.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
여기서 positions는 일반 Javascript 배열이에요. 2차원 상의 점의 좌표를 가지고 있는 배열이지요.
bufferData로 ARRAY_BUFFER 바인드 포인트에 묶인 버퍼에 값을 대입하도록 지시해요. 이때 이 함수는 타입을 분명히 정해줘야 하기 때문에 Float32Array로 배열을 복사해서 전달하고 있지요. 이러면 bufferData는 다시 GPU에 있는 positionBuffer로 이 배열을 복사해요.
마지막으로 gl.STATIC_DRAW는 WebGL에 이 데이터가 어떤 의미를 가지는지 알려줘요. WebGL은 이를 바탕으로 작업을 최적화합니다. 가령 STATIC_DRAW 힌트는 이 데이터가 자주 변경되지는 않을 거라고 알려주지요.
여기까지의 코드는 초기화 코드(Initialization code)에요. WebGL을 로딩할 때 딱 한번 실행돼야 하지요. 아래로는 렌더링 코드(Rendering code)를 다룰 거예요. 이들은 한 프레임마다 반복적으로 실행돼야 해요.
먼저 WebGL에게 Clip space의 좌표를 변환하도록 지시해야 해요. 기본적으로 WebGL의 결과물이 그려지는 Clip space는 $-1 \leq x \leq 1$, $-1 \leq y \leq 1$인 정사각형이에요. 하지만 canvas는 직사각형일 수 있으니 Clip space의 좌표를 필요에 따라 변환해서 사용해야 해요.
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
$0\leq x \leq \mathrm{gl.canvas.width}$, $0\leq y \leq \mathrm{gl.canvas.height}$ 이도록 바꿨어요.
다음으로는 canvas에 이미 그려진 것을 모두 지워야 해요.
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
clearColor에도 RGBA가 들어가요. 위의 투명도가 0이니 투명색으로 canvas를 색칠하라고 지시하는 거예요.
이제 WebGL에 무슨 프로그램을 실행해야 하는지 말해줘요.
gl.useProgram(program);
그리고 attribute를 통해 buffer에서 데이터를 받을 수 있게 해요. 이는 attribute를 활성화하여 진행됩니다.
gl.enableVertexAttribArray(positionAttributeLocation);
attribute를 통해 셰이더에 buffer의 정보를 공급하는 코드를 작성하면
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var size = 2; // 각 반복마다 2개의 원소 읽기
var type = gl.FLOAT; // 데이터의 타입은 32bit float이다
var normalize = false; // 데이터를 노말라이즈 하지 마라
var stride = 0; // 0 = 전진할 사이즈 * sizeof(타입) each iteration to get the next position
var offset = 0; // 읽기 시작할 버퍼의 인덱스
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);
이 과정에서 ARRAY_BUFFER를 attribute에 바인드 해요. 즉, 이제 이 attribute는 buffer에 바인드 되었고 다른 무언가를 ARRAY_BUFFER에 바인드 해도 좋다는 뜻이에요. 기존의 attribute는 여전히 같은 buffer를 바라보지요.
셰이더에서 a_position을 4차원 벡터로 잡았기 때문에 다음과 같은 형태를 가져요.
$\mathrm{a\_position}=(x=0,y=0,z=0,w=0)$
하지만 앞서서 size=2로 설정했기 때문에 x와 y만 buffer에서 대입되고 나머지는 다음 기본값으로 설정돼요.
$(x=0,y=0,z=0,w=1)$
끝으로 WebGL에 지금까지의 데이터를 그리라고 명령해요.
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);
position 배열에 3개의 점이 있었기 때문에 count=3이고 vertex shader는 3개의 점에 대해 총 3번 실행돼요.
primitiveType이 TRIANGLES이기 때문에 WebGL은 세 점을 가지고 삼각형을 만드려고 하지요.
<결과>
민트색 삼각형이 하나 그려져 있습니다. 여기까지 전체 코드는 가장 아랫부분에 첨부할게요.
이번 예제에서 vertex shader는 단순히 buffer로부터 입력받은 점을 아무 처리 없이 넘겼어요. 하지만 사실 점을 복잡한 처리를 통해 clip space의 점으로 바꿀 수 있으며, 가장 큰 예시로 3D 렌더링을 WebGL로 처리하려면 공간상의 점을 회전, 스케일, 평행이동한 좌표로 바꾸어주는 수식을 직접 셰이더에 작성해야 해요. 이는 앞서 언급했듯 WebGL은 단순히 레스터라이제이션 도구이기 때문이에요. 나머지는 모두 개발자의 몫이지요.
간단한 예시로 clip space의 원점을 바꾸는 작업을 해 볼게요. clip space는 다음과 같은 좌표를 가지고 있어요.
좌측 하단이 -, 우측 상단이 +이에요. 앞선 예시에서 중심이 아닌 곳에 삼각형이 그려진 것도 이것 때문이지요. 대신 원점(0,0)이 왼쪽 하단 꼭짓점이 오게 해볼게요. vertex shader를 다음과 같이 바꿔요.
attribute vec2 a_position;
void main() {
vec2 position = (a_position - 1.0);
gl_Position = vec4(position, 0, 1);
}
우선 a_position을 2차원 벡터로 바꾸어요. 어차피 2차원을 다루는데 4차원 벡터까지는 필요 없어요. 대신 gl_Position으로 넘겨줄 때 고정된 z, w값을 전달해주지요.
그리고 a_position에 -1을 한 값을 넘겨줘요. a_position은 벡터이니 각 원소에 -1 한 값이 새로운 위치가 되는 거죠. 따라서 좌측 하단으로 1만큼 평행 이동하는 거예요.
<결과>
한편 primitiveType을 바꿀 수도 있는데 지금은 TRIANGLES로 되어 있지만 가능한 값들은 다음과 같아요.
- Points
- Lines
- Line loop
- Triangles
- Triangle strip
- Triangle fan
예를 들어 primitiveType을 points, lines, line loop로 설정하고 렌더링 하면 다음과 같아요.
<결과>
Points는 점을 찍는데 점은 보이지 않지요.
Lines는 선을 잇는데 (0,0)에서 (0, 0.5)를 잇고 다음으로 점 두 개가 주어지지 않았기 때문에 렌더링을 그만둔 모습이에요.
Line loop는 선을 순환하도록 잇기 때문에 주어진 세 점을 잇는 삼각형이 만들어지지요.
<Full Code>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<p>WebGL</p>
<canvas id="canvas"></canvas>
</body>
<script>
function initWebGL() {
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
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 positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var positions = [
0, 0,
0, 0.5,
0.5, 0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
render = () => {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Clear the canvas
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var size = 2; // 각 반복마다 2개의 원소 읽기
var type = gl.FLOAT; // 데이터의 타입은 32bit float이다
var normalize = false; // 데이터를 노말라이즈 하지 마라
var stride = 0; // 0 = 전진할 사이즈 * sizeof(타입) each iteration to get the next position
var offset = 0; // 읽기 시작할 버퍼의 인덱스
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
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;
void main() {
gl_Position = a_position;
}
</script>
<script id="fragment-shader" type="glsl">
precision mediump float;
void main() {
gl_FragColor = vec4(0.65, 1.00, 0.91, 1.00);
}
</script>
<style>
canvas {
width: 400px;
height: 300px;
}
</style>
</html>
'이것저것 > WebGL' 카테고리의 다른 글
3D 변환 (1) | 2022.09.25 |
---|---|
2D 변환 - WebGL (0) | 2022.09.08 |