[WebGL] WebGL #2 – 기초 2

개요top

WebGL 기초 2번째 글입니다. 지난 글에서 WebGL에 대해 간단하게 소개했습니다. 이번에는 직접 코드를 작성할 것입니다.
첫 프로그램 작성 때 다들 Hello World를 찍어본다고 하네요. WebGL은 Hello World를 찍는게 더 어렵습니다. ㅎㅎ 대신, 삼각형을 하나를 그려볼 생각입니다.

(참고로 이 글은 webglfundamentals.org의 글을 나름대로 해석하고 공부하면서 정리한 글입니다.)

HTML 기본 구조 잡기top

WebGL은 잘 아시다시피 Web에서 동작합니다. 그래서 HTML 환경에서 구동되고 JavaScript를 사용하게 됩니다.

일단 실습을 위해 다음과 같은 HTML 구조를 만들어 봅니다.

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebGL #2 - 기초 2</title>
</head>
<body>
<!-- 1. 캔버스 정의. WebGL로 렌더링한 결과가 표시됩니다.-->
<canvas id="c" width="500" height="500"></canvas>

<!-- 2. 버텍스 셰이더를 작성합니다. -->
<script id="2d-vertex-shader" type="notjs">
</script>

<!-- 3. 프래그먼트 셰이더를 작성합니다. -->
<script id="2d-fragment-shader" type="notjs">
</script> 

<!-- 4. WegGL 자바스크립트 코드를 작성합니다. -->
<script>
</script>

</body>
</html>

1~4까지 주석부분에 작성될 코드에 대해서 간단하게 소개하자면,

1번 캔버스는 최종적으로 WebGL이 그린 결과를 여기에 표시해주게 됩니다.

2,3번은 각각 버텍스 셰이더와 프래그먼트 셰이더가 작성될 겁니다. type=”notjs”에 주목해보시면 “notejs”가 무엇을 의미한다기 보다는 브라우저에게 여기에서 작성된 코드는 javascript가 아닌 그 무엇도 아닌 코드다라고 알려줍니다. 그래서 브라우저는 <script>..</script>내에 작성된 문자열은 따로 해석하지 않고 화면 출력도 없이 넘어갑니다. 우린 script에 지정된 id로 window.getElementById()함수로 여기에 작성된 셰이더 코드 문자열을 읽어올 수 있습니다. 여기서는 <script>..<script>내에 작성되지만 셰이더 코드는 그저 텍스트일 뿐이므로 ajax로 가져와도 되고 javascript의 하나의 변수에 작성해도 무방합니다.

4번엔 일반 JavaScript와 셰이더를 컴파일하고 셰이더에서 쓰일 데이타를 공급 및 실행할 WebGL API로 작성된 JavaScript 코드들이 작성될 겁니다.

시작이 반이니… 반은 했네요. ^^

그림 그리기 준비 – 셰이더 작성, 컴파일해서 프로그램 생성, GPU에 데이타 공급top

셰이더 작성하기

WebGL은 단지 클립 좌표계(clip coordinates)와 색상을 도출하는데 목적이 있습니다. 개발자는 이 목적을 달성하기 위해 WebGL에 2개의 셰이더(shader)를 제공해야 합니다. 2개중 하나인 버텍스 셰이더는 클립 좌표계의 값을 제공하고 프래그먼트 셰이더는 색상을 제공합니다. 셰이더는 GPU가 어떻게 그림을 그려야 하는지 알려주는 명령 코드이며 GLSL(GL Shader Language)라는 특수 언어로 작성할 수 있다고 이전 글에서 이미 언급했습니다. 기억하시죠? ^^

클립 좌표계는 canvas의 크기에 상관없이 항상 -1 ~ +1의 범위를 가집니다. 우리는 기본이 되는 매우 단순한 셰이더를 작성할 것입니다. 결국 우리는 이 셰이더를 이용해 3개의 정점을 가진 2차원 삼각형을 그릴겁니다.

버텍스 셰이더 작성하기

자, 그럼 본격적으로 GLSL로 작성된 클립 좌표값을 도출할 버텍스 셰이더 코드를 보겠습니다.

// attribute는 buffer로부터 데이타를 받을 것입니다.
attribute vec4 a_position;

// 모든 shader는 main 함수를 가집니다.
void main() {
	// gl_Position은 버텍스 셰이더에서 
	// 설정을 담당하는 특수한 변수입니다.
	gl_Position = a_position;
}

GLSL로 작성된 위 코드가 어떻게 호출되고 실행되는지 이해하기 쉽게 JavaScript로 작성해 본다면 다음과 비슷할 겁니다.

// *** PSUEDO CODE!! ***
 
var positionBuffer = [
  0, 0, 0, 0,
  0, 0.5, 0, 0,
  0.7, 0, 0, 0,
];
var attributes = {};
var gl_Position;
 
drawArrays(..., offset, count) {
  var stride = 4;
  var size = 4;
  for (var i = 0; i < count; ++i) {
     // positionBuffer에서 4개의 값을 a_position에 복사합니다.
     attributes.a_position = 
        positionBuffer.slice((offset + i) * size, size);
     runVertexShader();
     ...
     doSomethingWith_gl_Position();
}

버퍼(Buffer)로 지정된 positionBuffer변수에 x, y, z, alpha 값을 묶어 3개 넣었습니다(GLSL에서 vec4가 4개의 값을 의미합니다). 그리고 애트리뷰트(attributes)가 정의되었습니다. 앞서 글에서 버퍼와 애트리뷰트는 셰이더에 데이타를 공급하는 하나의 방법이라고도 언급했습니다. GLSL에 내장된 gl_Position은 특수한 변수로 여기에 값을 할당하면 계산된 정점 결과가 설정됩니다.

WebGL의 drawArray함수가 호출되면 이 함수 내부에선 인자로 넘어온 offset과 size를 사용해 positionBuffer의 일부 데이타를 반복해서 읽어옵니다. 그리고 그 값을 애트리뷰트로 지정된 attributes.a_position에 할당하죠. 그리고 runVertextShader()가 호출되면서 버텍스 셰이더가 호출되고 attributes.a_position을 가공한 다음 gl_Position에 대입할 겁니다(물론 위 GLSL코드에선 어떤 가공도 없이 그냥 대입했죠). doSomethingWith_gl_Position()에서 gl_Position에 지정된 값을 가지고 무엇인가를 할겁니다.

버텍스 셰이더를 정의하면 대략, 이런 식으로 동작할 것이다라고 인지하면 좋을 것 같습니다.
(오히려 어려웠을지도… ^^)

프래그먼트 셰이더 작성하기

이제 색상을 담당하는 프래그먼트 셰이더를 작성해 봅니다.

// 프래그먼트 셰이더는 기본 정밀도가 필요합니다.
// 그래서 기본적으로 하나를 선택해서 설정하는데,
// 여기서는 mediump을 선택했습니다. 
// mediump는 중간 정밀도를 의미합니다. 
precision mediump float;
 
void main() {
 	// gl_FragColor는 프래그먼트 셰이더에서 
	// 설정을 담당하는 특수한 변수입니다.
	gl_FragColor = vec4(1, 0, 0.5, 1); // 붉은 보라색을 반환합니다.
}

2개의 셰이더 코드(함수)가 준비되었으므로 본격적인 WebGL 코드를 작성할 수 있게 되었습니다.

WebGL 렌더링 컨텍스트 생성하기

WebGL 렌더링 컨텍스트(WebGL Rendering Context)는 초반에 작성한 canvas 요소로 부터 얻어올 수 있습니다.
방법을 살펴봅시다.

아래 코드는 위에서 작성한 canvas HTML 요소입니다.

<!-- 1. 캔버스 정의. WebGL로 렌더링한 결과가 표시됩니다.-->
<canvas id="c" width="500" height="500"></canvas>

그럼 다음처럼 JavaScript를 통해 canvas 요소를 참조할 수 있습니다.

var canvas = document.getElementById("c");

그리고 다음처럼 WebGL 렌더링 컨텍스트를 canvas요소에 이미 정의되어 있는 getContext() 메서드를 통해 얻어올 수 있습니다.
WebGL이 정의되어 있지 않는 브라우저라면 if(!gl) 문 내부가 실행될 것입니다.

var gl = canvas.getContext("webgl");
if (!gl) {
    console.log('no webgl for you!');
}

우린 여기 생성한 WebGL 렌더링 컨텍스트를 참조한 gl 변수를 계속 사용할 것입니다. 기억해주세요~

셰이더 컴파일 및 링크 : 프로그램 만들기

작성한 셰이더를 GPU에 동작하도록 올려주기 위해서 다음 단계를 진행합니다.

1단계 – 셰이더 코드를 문자열로 가져오기

작성한 셰이더를 GPU에 올리기 위해서는 작성된 셰이더 코드 문자열을 가져와야 합니다. 여기서는 <script>..</script> 내에 작성해서 가져옵니다.

<!-- 2. 버텍스 셰이더를 작성합니다. -->
<script id="2d-vertex-shader" type="notjs">
// attribute는 buffer로부터 데이타를 받을 것입니다.
attribute vec4 a_position;

// 모든 shader는 main 함수를 가집니다.
void main() {
	// gl_Position은 버텍스 셰이더에서 
	// 설정을 담당하는 특수한 변수입니다.
	gl_Position = a_position;
}
</script>

<!-- 3. 프래그먼트 셰이더를 작성합니다. -->
<script id="2d-fragment-shader" type="notjs">
// 프래그먼트 셰이더는 기본 정밀도가 필요합니다.
// 그래서 기본적으로 하나를 선택해서 설정하는데,
// 여기서는 mediump을 선택했습니다. 
// mediump는 중간 정밀도를 의미합니다. 
precision mediump float;
 
void main() {
 	// gl_FragColor는 프래그먼트 셰이더에서 
	// 설정을 담당하는 특수한 변수입니다.
	gl_FragColor = vec4(1, 0, 0.5, 1); // 붉은 보라색을 반환합니다.
}
</script> 

이제 아래 JavaScript 코드에서 이 두개의 셰이더 문자열을 가져옵니다.

 
var vertexShaderSource = 
    document.getElementById("2d-vertex-shader").text;
var fragmentShaderSource = 
    document.getElementById("2d-fragment-shader").text;

2단계 – 셰이더 생성 및 컴파일

가져온 2개의 셰이더 코드를 이용해 컴파일을 합니다.

function createShader(gl, type, source) {
	var 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);
}
var vertexShader = createShader(
	gl, gl.VERTEX_SHADER, vertexShaderSource
);
var fragmentShader = createShader(
	gl, gl.FRAGMENT_SHADER, fragmentShaderSource
);

함수 내부를 보면 알겠지만, 앞서 만든 gl에 정의된 createShader()함수를 이용해 셰이더를 생성하고 인자로 준 셰이더 문자열 코드를 준비한 뒤, 최종적으로 compileShader()로 컴파일 합니다. 참고로 getShaderParameter()함수로 컴파일 결과를 얻어올 수 있습니다.

3단계 – 2개의 셰이더를 1개의 프로그램으로 링크하기

2개의 셰이더는 순차적으로 실행됩니다. 순차적으로 실행한다를 어떻게 지정할 수 있을까요?
그 과정을 정의하는 부분이 아래 코드인데요, 컴파일 된 셰이더 2개를 링크(Link)시켜서 GLSL 프로그램(Program)을 만듭니다.

function createProgram(gl, vertexShader, fragmentShader) {
	var program = gl.createProgram();
	gl.attachShader(program, vertexShader);
	gl.attachShader(program, fragmentShader);
	gl.linkProgram(program);
	var success = gl.getProgramParameter(program, gl.LINK_STATUS);
	if (success) {
		return program;
	}
	console.log(gl.getProgramInfoLog(program));
	gl.deleteProgram(program);
}
var program = createProgram(
	gl, vertexShader, fragmentShader
);

지금까지 WebGL API로 GLSL로 만들어진 2개의 셰이더 코드를 컴파일하고 링크해서 하나의 GLSL 프로그램을 만드는 과정까지 살펴봤습니다. 이제부터는 이 프로그램에게 일을 시키기 위한 준비작업을 설명할 겁니다.

셰이더에서 참조할 데이터를 GPU에 제공하기

지금까지 GPU에서 동작하는 GLSL 프로그램을 만들었습니다. WebGL API를 사용해서 GLSL 프로그램에서 사용할 데이타를 GPU에 공급할 것입니다. 참고로, 대부분의 WebGL API는 GLSL 프로그램에 데이타를 제공하는 것입니다. 이는 정말 세밀하게 데이타를 공급할 수 있다는 의미겠지요.

우리가 작성한 GLSL 프로그램에서 쓰일 유일한 입력 애트리뷰트(attribute)는 a_position 였습니다. 이 애트리뷰트에서 소비할 데이타를 제공할 버퍼(buffer)를 만들겠습니다.

var positionBuffer = gl.createBuffer();

위 코드로 위치 데이타를 담을 위치 버퍼를 생성했습니다.

약간 어려운 이야기를 하자면, WebGL은 내부에 이런 버퍼를 전역 영역에서 여러개 생성하고 관리할 수 있습니다.
그래서 현 시점에 어떤 버퍼를 사용하고 있는지 알려줘야만 합니다. 이때 어떤 버퍼를 사용하고 있다고 가리키는 것이 바로 바인드 포인트(bind point)라고 합니다.
아래 코드처럼 bindBuffer() 함수를 실행하면 바인드 포인트는 positionBuffer를 가리키게 되므로 이후부터 공급되는 데이타는 이 바인드 포인트가 가리키는 버퍼에 입력되게 됩니다.
(그래서 결국 WebGL은 상태 머신인 셈입니다.)

gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

위 함수는 gl.ARRAY_BUFFER의 바인트 포인트로 positionBuffer를 지정했습니다. 이제, 다음처럼 바인드 포인트로 참조하고 있는 버퍼에 데이타를 입력할 수 있습니다.

var positions = [
  0, 0,
  0, 0.5,
  0.7, 0,
];
gl.bufferData(
	gl.ARRAY_BUFFER, 
	new Float32Array(positions), 
	gl.STATIC_DRAW
);

이 코드는 많은 일을 하는데요, 첫번째로 배열로 6개의 실수를 가지는 가지는 positions 변수를 정의했습니다. 삼각형의 좌표가 될 x, y좌표 쌍을 총 3개를 가진다는 의미로 일단 해석하시면 되겠고요.
WebGL은 더욱 강력한 형의 값이 요구하기 때문에 positions 데이타를 32bits 부동 소수점 형식의 값을 배열로 만들기 위해 new Float32Array()를 사용했습니다. 그런 다음, gl.bufferData()함수를 통해 GPU에 이 데이타를 올리도록 합니다. 이 때 잘 보시면 아까 정의한 positionBuffer를 인자로 넘기지 않습니다. 이미 gl.ARRAY_BUFFER의 바인드 포인트가 positionBuffer를 가리키고 있기 때문입니다(상태 머신 맞네요 ^^).

gl.bufferData() 함수의 마지막 인자로 gl.STATIC_DRAW는 최적화 힌트입니다. 이는 WebGL에 데이타를 거의 변경하지 않을 것임을 알려줍니다. 그럼 WebGL은 그에 맞게 최적화를 시도하겠지요(뭘 어떻게 최적화 하는지는 잘 모르겠어요).

애트리뷰트 참고하기

우리가 작성한 GLSL 프로그램에서 쓰일 유일한 입력 애트리뷰트(attribute)는 a_position 입니다. WebGL API에서 프로그램에 정의된 애트리뷰트의 위치를 얻는 방법은 다음과 같습니다.

var positionAttributeLocation =
   gl.getAttribLocation(program, "a_position");

위 코드로 부터 gl의 getAttribLocation()함수를 이용해 프로그램의 a_position 애트리뷰트의 위치를 참조했습니다.

지금까지 셰이더를 컴파일 링크하고, 데이터를 버퍼에 제공한 뒤 애트리뷰트를 참고하는 과정을 설명했습니다.
이 과정은 초기화 과정으로 전체코드 동작중에 딱 한번만 실행하도록 해야합니다. 즉, 매번 렌더링을 위한 반복 실행 과정이 아닌 초기화 과정시 수행하는 것이 좋습니다.

다음부터는 반복과정에서 쓰이는 렌더링 과정을 설명합니다.

그림 그리기 시작 – 뷰포트 설정, 버퍼와 애트리뷰트 관계 설정, 프로그램 실행 실시top

데이타도 준비 되었으니 실제 그림을 그려봅시다. 전문용어로 렌더링(rendering) 한다고 해야 하나요? ^^

GLSL에서 gl_Position은 클립 좌표계의 값을 다룬다고 했습니다. 이 좌표계를 다루는 공간을 클립 공간(clip space)라고 부릅니다. 그래서 최종적으로는 화면 공간(screen space)에 픽셀(pixel) 단위로 변환하는 방법을 알려줘야 합니다. 이때 사용하는 WebGL 함수가 gl.viewport()입니다.

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

위 코드의 의미를 살펴볼께요. 클립공간은 -1 ~ +1의 범위를 가집니다. 위 뷰포트 설정은 이 클립공간에 대해 좌표공간은 x축으로 0 ~ gl.canvas.width y축으로 0 ~ gl.canvas.height에 대응함을 의미합니다.
대략, 이해가 될까요? 다음에 또 설명할겁니다. 넘어가시죠~

이제 캔버스를 clear 합니다. clearColor()함수의 인자 값, 0, 0, 0, 1는 각각 red, green, blue, alpha를 의미합니다. 그래서 아래 코드는 clear()함수로 호출 시 불투명 검은색으로 전체 canvas를 덮는다는 것이겠지요.

// canvas를 clear처리 
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

WebGL에 어느 셰이더 프로그램을 실행할지 알려줍니다.

// 어느 셰이더 프로그램을 사용할지 지정
gl.useProgram(program);

다음으로 앞서 설정한 버퍼에서 데이터를 가져와 셰이더의 애트리뷰트에 전달할 방법을 WebGL에게 알려줘야 합니다. 그래서 먼저 앞서 참조한 위치 애트리뷰트를 사용할 것임을 알려야 합니다.

gl.enableVertexAttribArray(positionAttributeLocation);

다음으로 이 애트리뷰트에 버퍼의 데이타를 가져오는 방법을 알려줍니다.

// 위치 버퍼를 바인드 합니다. 
// 어느 버퍼를 사용할지 바인드 포인트로 지정하는 셈입니다.
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

// positionBuffer (ARRAY_BUFFER)에서 데이터를 가져오는 
// 방법을 위치 애트리뷰트에게 알려 줌
var size = 2;          // 각 반복마다 2개씩 버퍼 데이타 참조 
var type = gl.FLOAT;   // 32bit 부동 소수점 값
var normalize = false; // 데이터를 노말라이즈 하지 않는다.
var stride = 0;        // 0 = move forward size * sizeof(type) 각 반복마다 다음 위치 
var offset = 0;        // 버퍼 시작 위치 
gl.vertexAttribPointer(
	positionAttributeLocation, 
	size, 
	type, 
	normalize, 
	stride, 
	offset
);

gl.vertexAttribPointer() 함수에서도 어떤 버퍼를 참조하는지 지정하지 않습니다. 그건 그 앞에 gl.bindBuffer()로 현재의 바인드 포인터가 가리키는 버퍼가 positionBuffer임을 알려줬기 때문입니다. 그래서 gl.vertexAttribPointer() 함수를 호출하는 순간 위치 애트리뷰트가 바라봐야 하는 버퍼를 이미 아는 셈이죠.

GLSL 버텍스 셰이더에서 a_position 애트리뷰터의 타입은 vec4였습니다.

attribute vec4 a_position;

vec4는 4개의 부동 소수점 값을 가집니다. JavaScript로 표현하자면 a_position = {x:0, y:0, z:0, w:1} 같이 생각해 볼 수 있을 겁니다. 위에서 size = 2라고 했기 때문에 GLSL의 vec4 애트리뷰트의 기본 값은 0, 0, 0, 1이므로 버퍼에서 2개의 값(x, y)를 가져와 첫번째 값과 두번째 값만 설정되고 나머지 뒤에 2개 값(z, w)는 그대로 기본값이 0, 1이 될 겁니다.

이제 드디어 우리는 마지막으로 WebGL 에게 만든 GLSL 프로그램을 실행하라고 요청할 수 있습니다.

//GLSL 프로그램을 실행하도록 요청 
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);

위 코드에서 count = 3은 버텍스 셰이더가 3번 실행됨을 의미합니다. 첫번째 실행에서 위치 셰이더 애트리뷰트인 a_position.x와 a_position.y에 positionBuffer로 부터 첫 2개의 값을 가져와 설정합니다. 비슷하게 두번째 실행에서의 a_postion.xy는 다음 2개의 positionBuffer값으로 설정되겠지요.

primitiveType이 gl.TRIANGLES이므로 버텍스 셰이더가 3번 실행할 때 WebGL은 gl_Position에 설정한 3개의 값을 따라 삼각형을 그리게 됩니다. 캔버스 크기와 상관없이 각 값은 클립 공간의 좌표값이며 그 범위는 -1 ~ 1입니다.

우리가 만든 버텍스 셰이더는 positionBuffer값을 gl_Position에 그대로 복사하기 때문에 삼각형은 그 값대로 클립 공간 좌표에 쓰여집니다.

  0, 0,
  0, 0.5,
  0.7, 0,

WebGL은 클립 공간에서 화면 공간으로 좌표를 변환할 겁니다. 우리는 캔버스의 크기를 500 x 500으로 했으므로… 다음처럼 변환 되어 표시될 것입니다.

 클립 공간         화면 공간
   0, 0       ->   250, 250
   0, 0.5     ->   250, 375
 0.7, 0       ->   425, 250

이로써 WebGL은 삼각형을 그릴 것입니다. WebGL은 그릴 예정인 삼각형에 들어가는 면에 속한 픽셀에 대해서 프래그먼트 셰이더를 호출합니다. 우리가 작성한 프래그먼트 셰이더는 gl_FragColor로 단일 색인 1, 0, 0.5, 1을 지정했습니다. Canvas는 채널당 8bit를 다루므로 WebGL은 255,0,127,255 값을 캔버스에 그립니다.

결국 다음과 같은 화면을 볼 수 있을 겁니다.

우리는 WebGL로 이 삼각형을 그리기 위해 최소한 이 정도의 코드가 필요함을 알게 되었습니다.

전체코드top

지금까지 작성한 코드를 전부 붙여보면 다음과 같습니다.

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebGL #2 - 기초 2</title>
</head>
<body>
<!-- 1. 캔버스 정의. WebGL로 렌더링한 결과가 표시됩니다.-->
<canvas id="c" width="500" height="500"></canvas>

<!-- 2. 버텍스 쉐이더를 작성합니다. -->
<script id="2d-vertex-shader" type="notjs">
// attribute는 buffer로부터 데이타를 받을 것입니다.
attribute vec4 a_position;

// 모든 shader는 main 함수를 가집니다.
void main() {
	// gl_Position은 버텍스 쉐이더에서 
	// 설정을 담당하는 특수한 변수입니다.
	gl_Position = a_position;
}
</script>

<!-- 3. 프래그먼트 쉐이더를 작성합니다. -->
<script id="2d-fragment-shader" type="notjs">
// 프래그먼트 쉐이더는 기본 정밀도가 필요합니다.
// 그래서 기본적으로 하나를 선택해서 설정하는데,
// 여기서는 mediump을 선택했습니다. 
// mediump는 중간 정밀도를 의미합니다. 
precision mediump float;
 
void main() {
 	// gl_FragColor는 프래그먼트 쉐이더에서 
	// 설정을 담당하는 특수한 변수입니다.
	gl_FragColor = vec4(1, 0, 0.5, 1); // 붉은 보라색을 반환합니다.
}
</script> 

<!-- 4. WegGL 자바스크립트 코드를 작성합니다. -->
<script>
//WebGL Rendering Context 생성 
var canvas = document.getElementById("c");
var gl = canvas.getContext("webgl");
if (!gl) {
    console.log('no webgl for you!');
}

//쉐이더 컴파일 및 링크 - 1단계: 쉐이더 문자열 가져오기 
var vertexShaderSource = document.getElementById("2d-vertex-shader").text;
var fragmentShaderSource = document.getElementById("2d-fragment-shader").text;

//쉐이더 컴파일 및 링크 - 2단계 : 쉐이더 생성 및 컴파일 
function createShader(gl, type, source) {
	var 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);
}
var vertexShader = createShader(
	gl, gl.VERTEX_SHADER, vertexShaderSource
);
var fragmentShader = createShader(
	gl, gl.FRAGMENT_SHADER, fragmentShaderSource
);

//쉐이더 컴파일 및 링크 -  
// 3단계 : 컴파일된 2개의 쉐이더를 1개의 프로그램으로 링크하기
function createProgram(gl, vertexShader, fragmentShader) {
	var program = gl.createProgram();
	gl.attachShader(program, vertexShader);
	gl.attachShader(program, fragmentShader);
	gl.linkProgram(program);
	var success = gl.getProgramParameter(
		program, gl.LINK_STATUS
	);
	if (success) {
		return program;
	}
	console.log(gl.getProgramInfoLog(program));
	gl.deleteProgram(program);
}
var program = createProgram(
	gl, vertexShader, fragmentShader
);

//버퍼에 데이타 공급 
var positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var positions = [
  0, 0,
  0, 0.5,
  0.7, 0,
];
gl.bufferData(
	gl.ARRAY_BUFFER, 
	new Float32Array(positions), 
	gl.STATIC_DRAW
);

//애트리뷰트의 위치 참조 
var positionAttributeLocation = 
   gl.getAttribLocation(program, "a_position");
   
   
//뷰포트 설정 
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

// canvas를 clear처리 
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);


// 어느 셰이더 프로그램을 사용할지 지정
gl.useProgram(program);

//사용할 애트리뷰트 지정 
gl.enableVertexAttribArray(positionAttributeLocation);

// 위치 버퍼를 바인드 합니다.
// 이 코드 전체를 보면 2번 바인드할 필요는 없지만,
// 의미상...
// 앞서 바인드는 데이터 공급을 위한 것이지만 
// 이번 바인드는 애트리뷰트에서 사용할 버퍼를 지정한 것임.
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
 
// positionBuffer (ARRAY_BUFFER)에서 데이터를 가져오는 
// 방법을 위치 애트리뷰트에게 알려 줌
var size = 2;          // 각 반복마다 2개씩 버퍼 데이타 참조 
var type = gl.FLOAT;   // 32bit 부동 소수점 값
var normalize = false; // 데이터를 노말라이즈 하지 않는다.
var stride = 0;        // 0 = move forward size * sizeof(type) 각 반복마다 다음 위치 
var offset = 0;        // 버퍼 시작 위치 
gl.vertexAttribPointer(
	positionAttributeLocation, 
	size, 
	type, 
	normalize, 
	stride, 
	offset
);

//GLSL 프로그램을 실행하도록 요청 
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);
</script>

</body>
</html>

결론top

지금까지 WebGL로 2D 단색 삼각형을 하나 그려봤습니다. 그런데, WebGL로 직접 그림을 그려보니 어떤가요? 아마도 여기까지만 보면 미친 짓이 아닌가 싶습니다. 겨우 삼각형을 그리기 위해 GLSL도 알아야 하고 GPU에 데이타 공급하는 방법도 알아야 하고 도통 잘 알아먹기 힘든 셰이더, 바인딩 포인터, 클립공간 등의 생소한 용어도 익혀야 하니깐요. 하지만 항상 프로그래밍을 통해 우리가 원하는 무엇가를 얻기 위해선 기초가 중요함은 여러번 강조해도 지나치지 않다고 봅니다. 이런 지식들이 하나하나 쌓여서 멋진 3D 결과물을 만들 수 있다는 것을 상상해 보세요. 근사할 겁니다. 암튼 긴 글 읽어주셔서 감사합니다(webglfundamentals.org을 만드신 분에게 감사해야할 것 같네요).

다음 WebGL #3 – 기초 3에서는 이번 글에서 작성한 코드를 바탕으로 응용 해보겠습니다.

이전글 : WebGL #2 – 기초 1
다음글 : WebGL #3 – 기초 3