[WebGL] WebGL #5 – WebGL 작동 방법의 이해 2

개요top

지난 글에서 셰이더를 중심으로 WebGL 작동 방법에 대해서 이해할 수 있었습니다. 배어링을 이용해 각 셰이더간 어떻게 데이타가 전달되는지도 확인했습니다.

이번 글에서는 애트리뷰트와 버퍼를 이용해 직접 색상을 지정하는 방법에 대해서 고찰해 보고자 합니다. 또한 버퍼와 애트리뷰트의 관계를 더욱 명확히 공부해 보겠습니다.

직접 정점의 색을 버퍼에 제공하기top

지난 글, 작성한 코드의 버텍스 셰이더에서 색상 정보를 받을 수 있도록 다음처럼 수정합니다.

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;
}

기존에 gl_Position을 이용해 v_color를 계산하는 것을 없애고, vec4 타입의 a_color 애트리뷰트의 값이 바로 v_color 배어링으로 전달되도록 바꿨습니다.

이 예제에서는 삼각형 2개를 합친 사각형을 그릴 겁니다. 그러므로 위치 데이타를 전달하는 버퍼 셋팅 부분도 약간 수정되어야 합니다.

var positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var positions = [
  -0.7,  0.7, //첫번째 삼각형의 정점 1
  -0.7, -0.7, //첫번째 삼각형의 정점 2
   0.7, -0.7, //첫번째 삼각형의 정점 3
   0.7,  0.7, //두번째 삼각형의 정점 1
  -0.7,  0.7, //두번째 삼각형의 정점 2
   0.7, -0.7  //두번째 삼각형의 정점 3
];
gl.bufferData(
	gl.ARRAY_BUFFER, 
	new Float32Array(positions), 
	gl.STATIC_DRAW
);

이제 각 삼각형 2개를 이루는 총 6개 정점의 색을 지정해 봅시다. 먼저 색상 정보를 제공할 버퍼를 만든 후, 이 버퍼에 6개 색상 정보를 제공합니다. 색상 1개는 총 4개의 R, G, B, A 값으로 구성됩니다.

var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var r1 = Math.random(); //첫번째 삼각형의 Red색
var b1 = Math.random(); //첫번째 삼각형의 Blud색
var g1 = Math.random(); //첫번째 삼각형의 Green색
var r2 = Math.random(); //두번째 삼각형의 Red색
var b2 = Math.random(); //두번째 삼각형의 Blud색
var g2 = Math.random(); //두번째 삼각형의 Green색
gl.bufferData(
	gl.ARRAY_BUFFER,
	new Float32Array([ 
		r1, b1, g1, 1,
		r1, b1, g1, 1,
		r1, b1, g1, 1,
		r2, b2, g2, 1,
		r2, b2, g2, 1,
		r2, b2, g2, 1
	]),
	gl.STATIC_DRAW
);

그리고, 버퍼의 색상을 참조할 애트리뷰트인 a_color의 위치를 아래 코드 처럼 가져옵니다.

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

다음으로 랜더링 시에 색상 애트리뷰트가 색상 버퍼의 데이타를 어떻게 가져올지 알려줘야 합니다. 아래 코드에서 R,G,B,A 총 4개를 하나의 색상으로 버퍼에 보냈기 때문에 size가 4인 점에 주의하세요. 버텍스 셰이더에서 a_color의 타입도 vec4입니다.

//색상 애트리뷰트가 어떻게 색상 버퍼의 데이타를 참조할지 지정함 
gl.enableVertexAttribArray(colorAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var size = 4;           
var type = gl.FLOAT;  
var normalize = false;
var stride = 0; 
var offset = 0;
gl.vertexAttribPointer(
	colorAttributeLocation, 
	size, 
	type, 
	normalize, 
	stride, 
	offset
);

이제 그림을 그립니다. 단, 2개의 삼각형을 구성하는 6개의 정점을 그릴 수 있도록 해야합니다. 그래서 count는 6으로 조정되어야 합니다. 아래 코드에서 이미 postions.length / 2는 6을 계산할 것입니다. 이로써 버텍스 셰이더는 6번 실행되겠지요.

//렌더링 실시 
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = positions.length / 2; 
gl.drawArrays(primitiveType, offset, count);

결과는 다음과 같습니다. 2개의 단색 삼각형이 그려졌습니다. 색상은 무작위 이므로 여러분의 결과와 다른 색으로 보일 겁니다.

단색인 이유는 각 삼각형의 정점 색이 모두 같기 때문이겠지요. 아래 코드처럼 모든 정점의 색을 다르게 하면 삼각형 전체에서 다양한 보간 색이 적용될 것입니다.

var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var colors = [ 
	Math.random(), Math.random(), Math.random(), 1,
	Math.random(), Math.random(), Math.random(), 1,
	Math.random(), Math.random(), Math.random(), 1,
	Math.random(), Math.random(), Math.random(), 1,
	Math.random(), Math.random(), Math.random(), 1,
	Math.random(), Math.random(), Math.random(), 1
];
gl.bufferData(
	gl.ARRAY_BUFFER,
	new Float32Array(colors),
	gl.STATIC_DRAW
);

결과는 다음과 같습니다.

이 예제를 통해 처음으로 애트리뷰트 2개(위치,색상)를 사용했다는 점과 색상 정보가 배어링을 통해 버텍스 셰이더에서 프래그먼트 셰이더로 보간해서 전달됨을 재확인 할 수 있었습니다.

버퍼와 애트리뷰트 관계의 이해top

버퍼와 애트리뷰트와의 관계를 심도있게 공부해보겠습니다.

버퍼는 정점 및 기타 정점 관련 데이터를 GPU에 전달하는 방법입니다. 이는 WebGL의 API로 수행할 수 있었지요. gl.createBuffer() 함수는 버퍼를 생성하고, gl.bindBuffer()는 생성된 버퍼를 작업할 버퍼로 지정합니다. 마지막으로 gl.bufferData()는 지정된 버퍼에 데이터를 전달합니다. 이 작업은 일반적으로 초기화 때 수행합니다. 이 과정을 정확히 이해하는게 중요합니다.

그리고 버텍스 셰이더의 애트리뷰트가 버퍼의 데이타를 읽어오는 방법을 WebGL API를 통해 알려줄 수 있습니다. 이를 위해 먼저 아래 코드처럼 WebGL에 애트리뷰트의 위치를 얻어옵니다.

var positionLocation = gl.getAttribLocation(program, "a_position");
var colorLocation = gl.getAttribLocation(program, "a_color");

이 작업은 보통 초기화 때 수행합니다. 애트리뷰트의 위치를 알게 되면 렌더링 단계에서 그림을 그리기 직전에 아래 3개의 명령을 실행합니다.

//첫번째
gl.enableVertexAttribArray(location); 

//두번째
gl.bindBuffer(gl.ARRAY_BUFFER, someBuffer); 

//세번째
gl.vertexAttribPointer(  
    location,
    numComponents,
    typeOfData,
    normalizeFlag,
    strideToNextPieceOfData,
    offsetIntoBuffer);

첫번째 gl.enableVertexAttribArray()는 해당 애트리뷰트 위치를 사용할 수 있도록 설정합니다.

두번째로 gl.bindBuffer()에서 지금부터 애트리뷰트가 참조할 버퍼를 알려줍니다. 이 명령에서 첫번째 인자로 ARRAY_BUFFER를 주었으므로 두번째 인자로 전달한 버퍼(someBuffer)를 ARRAY_BUFFER의 바인드 포인트(bind point)로 바인드 한다는 의미이며, 이 바인드 포인트는 WebGL 내부의 전역변수가 됩니다.

마지막 세번째로 gl.vertexAttribPointer()는 애트리뷰트가 현재의 ARRAY_BUFFER의 바인드 포인트에서 데이터를 어떻게 가져올지 지시하는 역할을 합니다. 이 함수의 첫번째 인자는 애트리뷰트의 위치이며, 두번째 인자인 numComponents는 정점당 얼마나 많은 컴포넌트를 가져오는지 알려줍니다(항상 1~4이어야 함, 셰이더에서 vec1 ~ vec4이므로). 세번째 인자인 typeOfData에서 데이타의 타입은 무엇인지(BYTE, FLOAT, INT, UNSIGNED_SHORT등)를 알려줍니다. 네번째 인자인 normalizeFlag는 조금 있다가 자세히 언급하기로 하고요. 그리고 다섯번째 인자인 strideToNextPieceOfData에서 하나의 데이터 조각에서 다음 데이타 조각을 가져오기 위해 건너 뛸 바이트 수를 알려줍니다. 여섯번째 인자인 offsetIntoBuffer는 버퍼의 어디서 부터 읽기 시작할지 설정하지요.

테이터의 타입에 대해서 1개의 버퍼를 사용하는 경우에는 다섯번째 인자인 strideToNextPieceOfData와 여섯번째 인자인 offsetIntoBuffer는 보통 0이면 됩니다. strideToNextPieceOfData가 0이면 데이터 타입과 크기에 맞게 자동으로 데이터를 가져옴을 뜻합니다. offsetIntoBuffer가 0이면 버퍼의 처음 데이타부터 가져옴을 뜻하죠. 이들을 0이 아닌 다른 값으로 설정하는 것은 복잡하고 성능 면에서 별다른 이득이 없기 때문에 되도록 0을 지정하는 방향으로 쓰는 것이 좋겠습니다.

아직 언급하지 않는 gl.vertexAttribPointer()의 네번째 인자인 normalizeFlag에 대해서 설명하겠습니다. normalizeFlag는 입력되는 데이터를 정규화를 할 것인가 결정하는 건데요. 만약 이 값이 false이면 정규화를 하지 않는다는 의미로써, 데이터 타입이 BYTE의 경우는 -128 ~ 127, UNSIGNED_BYTE는 0 ~ 255, SHORT는 -32768 ~ 32767 데이타 범위를 그대도 애트리뷰트가 가져오게 됩니다. 하지만 이 값을 true로 설정하면 BYTE(-128 ~ 127 범위)의 값이 -1.0 ~ +1.0 으로 정규화됩니다. 비슷하게 UNSIGNED_BYTE(0 ~ 255)의 경우는 0.0 ~ +1.0으로 정규화되고, SHORT는 -1.0 ~ +1.0 이 됩니다. 단, SHORT의 경우 BYTE 보다는 훨씬 큰 값의 범위를 가지므로 더 높은 해상도의 값으로 표현되겠지요.

정규화된 데이타를 사용하는 가장 일반적인 경우는 색상 입니다. 색상은 0.0 ~ 1.0의 범위만 적용됩니다. 이전 코드에서 FLOAT로 지정했으므로 RGBA를 사용한다면 색상당 16바이트의 정점이 사용됩니다. 복잡한 지오메트리(geometry, 기하구조)를 가지는 데이타는 큰 용량을 가지는 FLOAT 데이터 타입이 필요할 수 있지만, 색상의 경우 FLOAT가 아닌 UNSIGNED_BYTE를 사용한다면 0 ~ 255 범위만 사용해도 충분합니다. 이때 GLSL 내에서 색의 범위는 0.0 ~ 1.0이므로 별도의 변환 없이 정규화 처리하면 됩니다. 이렇게 되면 각 정점의 색상 정보는 4바이트만 필요하게 되므로 75%의 용량을 절약할 수 있게 됩니다.

정규화를 하는 이유도 알았고 색상 데이타의 용량을 줄일 수 있게 되었으니 기존 코드를 수정해 보겠습니다.

버퍼에 색상 데이타를 아래처럼 넣습니다. Float32Array가 아닌 Uint8Array로 데이타를 전달함을 확인하세요.

//색상 데이타를 GPU 버퍼에 공급 (삼각형 2개임) 
var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var r1 = Math.random() * 256; // 0 ~ 255.99999
var b1 = Math.random() * 256; // 이 값들은 
var g1 = Math.random() * 256; // Uint8Array로 
var r2 = Math.random() * 256; // 저장될 때 
var b2 = Math.random() * 256; // 소수값은 버려질 겁니다.
var g2 = Math.random() * 256;
var colors = [ 
	r1, b1, g1, 255,
	r1, b1, g1, 255,
	r1, b1, g1, 255,
	r2, b2, g2, 255,
	r2, b2, g2, 255,
	r2, b2, g2, 255
];
gl.bufferData(
	gl.ARRAY_BUFFER,
	new Uint8Array(colors),
	gl.STATIC_DRAW
);

그리고 아래처럼 gl.vertexAttribPointer()함수에서 UNSIGNED_BYTE로 데이터 타입을 지정하고 정규화를 처리를 위해 normalize를 true로 합니다.

gl.enableVertexAttribArray(colorAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var size = 4;           
var type = gl.UNSIGNED_BYTE;  
var normalize = true;
var stride = 0; 
var offset = 0;
gl.vertexAttribPointer(
	colorAttributeLocation, 
	size, 
	type, 
	normalize, 
	stride, 
	offset
);

결과는 기존과 같습니다. 적절한 데이터 타입을 사용하는 방법을 공부할 수 있다는 점이 중요합니다.

전체 코드top

지금까지 실습한 코드입니다. 꼭 한번 실행해 보시고 한 줄씩 그 의미를 되새김 하시면 도움이 되겠습니다.

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebGL #4 - WebGL 작동 방법의 이해 2</title>
</head>
<body>
<canvas id="c" width="500" height="500"></canvas>
<script id="2d-vertex-shader" type="notjs">
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="2d-fragment-shader" type="notjs">
precision mediump float;

varying vec4 v_color;

void main() {
	gl_FragColor = v_color;
}
</script> 
<script>
//WebGL 컨텍스트 생성 
var canvas = document.getElementById("c");
var gl = canvas.getContext("webgl");
if (!gl) {
    console.log('no webgl for you!');
}

//셰이더 생성(버텍스, 프래그먼트)
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 vertexShaderSource = document.getElementById("2d-vertex-shader").text;
var fragmentShaderSource = document.getElementById("2d-fragment-shader").text;
var vertexShader = createShader(
	gl, gl.VERTEX_SHADER, vertexShaderSource
);
var fragmentShader = createShader(
	gl, gl.FRAGMENT_SHADER, fragmentShaderSource
);

//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
);

//위치 데이타를 GPU 버퍼에 공급 (삼각형 2개임) 
var positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var positions = [
  -0.7,  0.7,
  -0.7, -0.7,
   0.7, -0.7,
   0.7,  0.7,
  -0.7,  0.7,
   0.7, -0.7
];
gl.bufferData(
	gl.ARRAY_BUFFER, 
	new Float32Array(positions), 
	gl.STATIC_DRAW
);

//색상 데이타를 GPU 버퍼에 공급 (삼각형 2개임) 
var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var r1 = Math.random() * 256; // 0 ~ 255.99999
var b1 = Math.random() * 256; // 이 값들은 
var g1 = Math.random() * 256; // Uint8Array로 
var r2 = Math.random() * 256; // 저장될 때 
var b2 = Math.random() * 256; // 소수값은 버려질 겁니다.
var g2 = Math.random() * 256;
var colors = [ 
	r1, b1, g1, 255,
	r1, b1, g1, 255,
	r1, b1, g1, 255,
	r2, b2, g2, 255,
	r2, b2, g2, 255,
	r2, b2, g2, 255
];
gl.bufferData(
	gl.ARRAY_BUFFER,
	new Uint8Array(colors),
	gl.STATIC_DRAW
);

//애트리뷰드와 유니폼 위치 가져오기 
var positionAttributeLocation = 
   gl.getAttribLocation(program, "a_position");
var colorAttributeLocation = 
   gl.getAttribLocation(program, "a_color");
var matrixUniformLocation = 
   gl.getUniformLocation(program, "u_matrix");   

//뷰포트 설정 
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

//화면 clear
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);

//프로그램 사용 실시 
gl.useProgram(program);

//GPU에 등록된 버퍼의 위치 데이타를 
//어떻게 위치 애트리뷰트 참조하는지 셋팅 
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var size = 2;           
var type = gl.FLOAT;  
var normalize = false;
var stride = 0; 
var offset = 0;
gl.vertexAttribPointer(
	positionAttributeLocation, 
	size, 
	type, 
	normalize, 
	stride, 
	offset
);

//GPU에 등록된 버퍼의 색상 데이타를 
//어떻게 색상 애트리뷰트 참조하는지 셋팅 
gl.enableVertexAttribArray(colorAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var size = 4;           
var type = gl.UNSIGNED_BYTE;  
var normalize = true;
var stride = 0; 
var offset = 0;
gl.vertexAttribPointer(
	colorAttributeLocation, 
	size, 
	type, 
	normalize, 
	stride, 
	offset
);

//프로그램에 변환행렬 설정(아래는 단위 행렬로 아무 변환 안함)
gl.uniformMatrix4fv(matrixUniformLocation, false, [
	1, 0, 0, 0,
	0, 1, 0, 0,
	0, 0, 1, 0,
	0, 0, 0, 1
]);

//렌더링 실시 
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = positions.length / 2; 
gl.drawArrays(primitiveType, offset, count);
</script>
</body>
</html>

결론top

우리는 2편의 글을 통해 WebGL이 그림을 그리기 위해 어떻게 작동하는지에 대해서 구체적으로 이해해 보는 시간을 가졌습니다.
이번 글은 버퍼와 애트리뷰트를 이용해 외부에서 색상을 공급하는 방법과 버퍼와 애트리뷰트와의 관계를 더욱 자세히 공부했습니다.

다음 글에서는 셰이더와 GLSL에 대해서 조금 더 자세히 공부하는 시간을 가져보겠습니다.

이전글 : [WebGL] WebGL #4 – WebGL 작동 방법의 이해 1
다음글 : (작성중)[WebGL] WebGL #6 – WebGL 셰이더와 GLSL