셰이더 프로그램
GLSL(openGL Shading Language)은 각 정점의 색상 속성을 병렬로 계산하여 기술하는 간단한 프로그램, 즉 셰이더(Shader)를 위한 프로그래밍 언어다. 따라서 모든 정점은 주변의 다른 정점의 상태를 알지 못한다. 즉, 정점들은 서로에 대해 눈이 멀어 있다. 그러나
uniform변수를 사용하면 모든 정점이 동일한 값을 공유할 수 있다. 또한varying변수를 사용하면 한 정점의 값을 해당 프래그먼트 셰이더로 전달할 수 있다. 이 글에서는 기본적인 셰이더 프로그램을 구현하는 방법을 정리한다. 아래 그림은 셰이더 프로그램을 만드는 과정을 보여준다.

정점 셰이더와 프래그먼트 셰이더 만들기
정점 셰이더나 프래그먼트 셰이더를 만들려면 WebGL 컨텍스트 gl과 셰이더 소스가 필요하다. 소스를 작성하는 방법에 대한 자세한 내용은 다음 글에서 다룬다. 이 글에서는 간단한 셰이더 소스를 사용한다. 아래 함수가 반환하는 shader 변수는 프로그램을 만드는 데 사용된다. gl.shaderSource()는 셰이더와 소스를 연결하고, gl.compileShader()는 셰이더 소스를 컴파일한다. gl.getShaderParameter()를 실행하면 컴파일 결과의 상태를 반환한다.
function createShader(gl, type, source) {
const shader = gl.createShader(type); // gl.VERTEX_SHADER or gl.FRAGMENT_SHADER
gl.shaderSource(shader, source);
gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
프로그램 만들기
정점 셰이더와 프래그먼트 셰이더를 만든 후에는, 아래와 같이 셰이더들을 이용해 프로그램을 만든다. gl.createProgram()은 프로그램 객체를 생성하고 초기화하며, gl.attachShader()는 프로그램과 셰이더를 연결하고, gl.linkProgram()은 정점 셰이더와 프래그먼트 셰이더를 연결한다.
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);
}
마지막으로 gl.useProgram()은 WebGL에 해당 프로그램을 사용하도록 알린다.
gl.useProgram(program);
속성 변수 만들기
정점에 position, normal과 같은 속성(attribute)을 추가하면 보다 다채로운 오브젝트를 만들 수 있다. 이 속성들을 이용해 물리 법칙에 기반한 색상을 설정할 수 있다. 속성 (ex. a_position, a_normal 등) 에 value를 설정하려면, 사전에 버퍼를 만들어두어야 한다. 이 버퍼는 하나의 속성에 속하므로 모든 속성마다 만들어야 한다. 다음으로 gl.bindBuffer는 버퍼를 ARRAY_BUFFER에 바인딩하고, gl.bufferData는 버퍼를 특정 형식으로 초기화한다. 그러면 name과 그 attributeLocation을 통해 버퍼에 접근할 수 있다. gl.enableVertexAttribArray는 속성을 활성화하고, gl.vertexAttribPointer는 속성의 값을 설정한다.
function createAttribute(gl, program, name, size, value) {
// create a buffer
const buffer = gl.createBuffer();
// bind it to ARRAY_BUFFER
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(value), gl.STATIC_DRAW);
// look up where the vertex data needs to go.
const attributeLocation = gl.getAttribLocation(program, name);
// turn on the attribute
gl.enableVertexAttribArray(attributeLocation);
// tell the attribute how to get data out of buffer (ARRAY_BUFFER)
const normalize = false; // don't normalize the data
const stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
const offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(attributeLocation, size, gl.FLOAT, normalize, stride, offset);
}
전체 코드
아래는 셰이더 프로그램을 생성하고, 동작시키는 예제다. 이 예제에서 캔버스의 색상은 마우스 포인터의 위치와 좌표에 따라 변한다. 정점 셰이더와 프래그먼트 셰이더 내부 코드를 작성하는 방법은 다음 글에서 설명한다.
precision mediump float; attribute vec2 a_position; attribute vec3 a_normal; // uniform vec2 u_resolution; varying vec2 v_position; varying vec3 v_normal; void main() { // 위치를 픽셀에서 0.0과 1.0사이로 변환 vec2 zeroToOne = a_position;// / u_resolution; // 0->1에서 -1->+1로 변환 (클립 공간) vec2 clipSpace = zeroToOne * 2.0 - 1.0; clipSpace.y *= -1.; v_position = a_position; v_normal = a_normal; gl_Position = vec4(clipSpace, 0., 1.); }
precision mediump float; uniform vec2 u_mousePosition; // uniform vec2 u_resolution; varying vec2 v_position; varying vec3 v_normal; void main() { vec2 position = v_position;// / u_resolution; vec2 mousePosition = u_mousePosition; // / u_resolution; float x = clamp(mousePosition.x, 0., 1.); float y = clamp(mousePosition.y, 0., 1.); vec3 color = vec3(position, x); color *= y; // float color = 1.; // color *= step(x, position.x); // color *= step(y, position.y); // gl_FragColor는 프래그먼트 셰이더가 설정을 담당하는 특수 변수 gl_FragColor = vec4(color, 1.); // 자주색 반환 }
Touch or hover your mouse here
fragment shader
// frag.js
const fragment = /* glsl */ `
precision mediump float;
uniform vec2 u_mousePosition;
varying vec2 v_position;
varying vec3 v_normal;
void main() {
vec2 position = v_position;
vec2 mousePosition = u_mousePosition;
vec3 color = vec3(position, mousePosition.x);
color *= mousePosition.y;
gl_FragColor = vec4(color, 1.);
}
`
export default fragment
vertex shader
// vert.js
const vertex = /* glsl */ `
precision mediump float;
attribute vec2 a_position;
attribute vec3 a_normal;
varying vec2 v_position;
varying vec3 v_normal;
void main() {
vec2 zeroToOne = a_position;
// convert coordinate from (x, y): ([0, 1], [0, 1]) to (x, y): ([-1, 1], [1, -1])
vec2 clipSpace = zeroToOne * 2.0 - 1.0;
clipSpace.y *= -1.;
// deliver vertex attributes to fragment shader
v_position = a_position;
v_normal = a_normal;
gl_Position = vec4(clipSpace, 0., 1.);
}
`
export default vertex
script
import fragment from './frag.js'
import vertex from './vert.js'
// create canvas element
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.style.width = '100%';
canvas.style.height = '100%';
// get webgl context
const gl = canvas.getContext('webgl');
if (!gl) alert('WebGL is not supported by your browser');
// get dpr value
const dpr = window.devicePixelRatio;
// create shaders
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertex);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragment);
// create shader program
const program = createProgram(gl, vertexShader, fragmentShader);
// use program
gl.useProgram(program);
// enable to cull back face
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK); // BACK (default), FRONT, FRONT_AND_BACK
// In default, if the winding order of vertices is CCW, their triangle create a front face
gl.frontFace(gl.CCW); // CCW (default), CW
// create attributes
createAttribute(gl, program, "a_position", 2, [
0, 0,
0, 1,
1, 1,
0, 0,
1, 1,
1, 0,
]);
createAttribute(gl, program, "a_normal", 3, [
1, 0, 0,
0, 1, 1,
0, 0, 1,
1, 0, 0,
0, 0, 1,
0, 1, 1,
]);
// look up uniform locations
const u_mousePosition = gl.getUniformLocation(program, "u_mousePosition");
// update uniform value with a range: [0, 1]
canvas.addEventListener('mousemove', function(evt){
const invScaleW = 1 / canvas.width;
const invScaleH = 1 / canvas.height;
gl.uniform2f(u_mousePosition, dpr*evt.offsetX * invScaleW, dpr*evt.offsetY * invScaleH);
});
canvas.addEventListener('touchmove', function(evt){
const invScaleW = 1 / canvas.width;
const invScaleH = 1 / canvas.height;
gl.uniform2f(u_mousePosition, dpr*evt.changedTouches[0].offsetX * invScaleW, dpr*evt.changedTouches[0].offsetY * invScaleH);
});
// update the dimension of canvas
window.addEventListener('resize', resize);
resize();
// render
window.requestAnimationFrame(render);
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
const 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);
}
function createAttribute(gl, program, name, size, value) {
// create a buffer
const buffer = gl.createBuffer();
// bind it to ARRAY_BUFFER
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(value), gl.STATIC_DRAW);
// look up where the vertex data needs to go.
const attributeLocation = gl.getAttribLocation(program, name);
// turn on the attribute
gl.enableVertexAttribArray(attributeLocation);
// tell the attribute how to get data out of buffer (ARRAY_BUFFER)
const normalize = false; // don't normalize the data
const stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
const offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(attributeLocation, size, gl.FLOAT, normalize, stride, offset);
}
function resize() {
// get displayed canvas size in pixels
const displayWidth = Math.round(canvas.clientWidth * dpr);
const displayHeight = Math.round(canvas.clientHeight * dpr);
// update canvas size
canvas.width = displayWidth;
canvas.height = displayHeight;
// update gl viewport size
gl.viewport(0, 0, canvas.width, canvas.height);
}
function render() {
// set color to clear canvas
gl.clearColor(0, 0, 0, 0);
// clear the canvas with the above color
gl.clear(gl.COLOR_BUFFER_BIT);
// draw triangles
const primitiveType = gl.TRIANGLES;
const offset = 0;
const count = 6;
gl.drawArrays(primitiveType, offset, count);
requestAnimationFrame(render.bind(this));
}