타입스크립트의 WebGL
이 글에서는 타입스크립트에서 WebGL을 설명합니다.
WebGL의 개념, 최소 구성, 렌더링, 확장, 클래스 설계까지 샘플 코드를 통해 단계적으로 소개합니다.
YouTube Video
타입스크립트의 WebGL
WebGL은 브라우저에서 GPU를 직접 다룰 수 있게 해주는 저수준 API입니다.
TypeScript를 사용하면 타입 안전성, 코드 자동 완성, 구조화된 코딩을 통해 WebGL에서 '구현 실수'를 크게 줄일 수 있습니다.
타입스크립트는 특히 다음의 점에서 효과적입니다:.
- WebGL 객체의 타입이 명확해집니다.
- 셰이더 변수 취급 실수를 줄일 수 있습니다.
- 구조를 추적하기 쉬워집니다.
WebGL의 최소 구성
WebGL로 렌더링하기 위한 최소 조건은 다음과 같습니다:.
<canvas>요소WebGL컨텍스트- 버텍스 셰이더
- 프래그먼트 셰이더
먼저 화면이 초기화될 수 있는 상태를 만듭시다.
캔버스와 WebGL 컨텍스트 얻기
우선 canvas와 WebGLRenderingContext를 획득합니다.
여기서는 null safety를 중시하여 타입스크립트로 코딩합니다.
1const canvas = document.getElementById('gl') as HTMLCanvasElement | null;
2
3if (!canvas) {
4 throw new Error('Canvas not found');
5}
6
7const gl = canvas.getContext('webgl');
8
9if (!gl) {
10 throw new Error('WebGL not supported');
11}- 이 시점에서
gl이 모든 WebGL 연산의 진입점이 됩니다.
WebGL 동작 확인을 위해 화면을 지움
렌더링 전에 배경색이 채워지는지 확인합니다.
WebGL이 정상적으로 동작하는지 초기 확인입니다.
1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);- 여기까지 하면 캔버스가 어두운 회색으로 채워집니다.
셰이더란?
WebGL에서는 GLSL이라는 특별한 언어로 도형을 그리는 과정을 기술합니다. GLSL에서는 주로 두 가지 종류의 셰이더, 즉 버텍스 셰이더와 프래그먼트 셰이더를 준비합니다.
먼저, 이 두 셰이더를 사용하여 '최소 작동 셰이더'를 만듭니다.
버텍스 셰이더 작성하기
버텍스 셰이더는 그릴 점 또는 도형의 위치를 결정합니다.
아래는 화면 중앙에 점 하나를 배치하는 간단한 코드입니다.
1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5 gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;a_position은 자바스크립트 쪽에서 전달된 좌표입니다.
프래그먼트 셰이더 작성하기
프래그먼트 셰이더는 화면에 표시될 색상을 정합니다.
이번에는 항상 빨간색을 출력합니다.
1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5 gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;- WebGL에서는
precision을 반드시 지정해야 합니다.
셰이더를 컴파일하는 함수 만들기
셰이더 생성 및 컴파일 과정은 항상 같으므로, 함수로 만듭니다.
1function createShader(
2 gl: WebGLRenderingContext,
3 type: number,
4 source: string
5): WebGLShader {
6 const shader = gl.createShader(type);
7 if (!shader) {
8 throw new Error('Failed to create shader');
9 }
10
11 gl.shaderSource(shader, source);
12 gl.compileShader(shader);
13
14 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
15 throw new Error(gl.getShaderInfoLog(shader) ?? 'Shader compile error');
16 }
17
18 return shader;
19}- 이 함수는 지정한 타입의 셰이더를 생성하고, 소스 코드를 컴파일하며, 실패 시 에러를 발생시키고 성공 시 컴파일된 셰이더를 반환합니다.
프로그램(셰이더 집합) 만들기
버텍스 셰이더와 프래그먼트 셰이더를 하나로 결합합니다.
이것을 WebGLProgram이라고 합니다.
1const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
2const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
3
4const program = gl.createProgram();
5if (!program) {
6 throw new Error('Failed to create program');
7}
8
9gl.attachShader(program, vertexShader);
10gl.attachShader(program, fragmentShader);
11gl.linkProgram(program);
12
13if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
14 throw new Error(gl.getProgramInfoLog(program) ?? 'Program link error');
15}- 이 코드는 버텍스와 프래그먼트 셰이더를 하나의 프로그램으로 연결하고, 렌더링에 사용할 수 있는지 확인합니다.
버텍스 데이터 준비하기
여기서는 삼각형을 그리기 위한 버텍스 데이터를 준비합니다.
1// Vertex positions for a triangle in clip space (x, y)
2// Coordinates range from -1.0 to 1.0
3const vertices = new Float32Array([
4 0.0, 0.5, // Top vertex
5 -0.5, -0.5, // Bottom-left vertex
6 0.5, -0.5, // Bottom-right vertex
7]);- WebGL의 좌표계는
-1.0에서1.0까지입니다.
버퍼 생성 및 GPU로 전송하기
다음으로, 버텍스 데이터 배열을 GPU에서 사용할 수 있도록 버퍼를 이용해 데이터를 전송합니다.
1const buffer = gl.createBuffer();
2if (!buffer) {
3 throw new Error('Failed to create buffer');
4}
5
6gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
7gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);- 이 코드는 버텍스 데이터를 저장할 버퍼를 만들고 GPU에 데이터를 전송합니다.
셰이더 어트리뷰트와 버퍼 연결하기
셰이더의 a_position을 이전에 만든 버퍼와 연결합니다.
1const positionLocation = gl.getAttribLocation(program, 'a_position');
2
3gl.enableVertexAttribArray(positionLocation);
4gl.vertexAttribPointer(
5 positionLocation,
6 2,
7 gl.FLOAT,
8 false,
9 0,
10 0
11);렌더링
마지막으로 프로그램을 이용해 렌더링 명령을 실행합니다.
1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);- 이 프로그램을 실행하면 빨간 삼각형이 나타납니다.
WebGL의 클래스 설계
WebGL은 명령형 API이므로 그대로 작성하면 코드가 쉽게 비대해집니다. 클래스 설계를 통해 초기화, 렌더링, 리소스 관리를 명확하게 분리할 수 있습니다.
여기서는 타입스크립트의 강점을 살려 관심사의 분리, 재사용성, 유지보수성에 중점을 두고 점진적으로 설계를 발전시킵니다.
설계 방침 (최소하지만 실용적)
클래스는 다음과 같은 역할로 나뉩니다.
GLApp이 전체 초기화와 렌더링을 담당합니다.ShaderProgram이 셰이더를 관리합니다.TriangleMesh가 버텍스 데이터를 관리합니다.
먼저 전체를 제어하는 클래스를 만듭시다.
WebGL 어플리케이션의 진입 클래스
GLApp은 캔버스와 WebGL 컨텍스트를 관리하고, 렌더링의 시작점 역할을 합니다.
1export class GLApp {
2 private gl: WebGLRenderingContext;
3 private shader!: ShaderProgram;
4 private mesh!: TriangleMesh;
5
6 constructor(private canvas: HTMLCanvasElement) {
7 const gl = canvas.getContext('webgl');
8 if (!gl) {
9 throw new Error('WebGL not supported');
10 }
11 this.gl = gl;
12 }
13
14 initialize() {
15 this.gl.clearColor(0.1, 0.1, 0.1, 1.0);
16
17 this.shader = new ShaderProgram(this.gl);
18 this.mesh = new TriangleMesh(this.gl);
19 }
20
21 render() {
22 this.gl.clear(this.gl.COLOR_BUFFER_BIT);
23
24 this.shader.use();
25 this.mesh.draw(this.shader);
26 }
27}- 이 클래스는 WebGL의 초기화 및 렌더링 과정을 관리하며, 셰이더와 메쉬를 사용하여 화면에 그리는 일을 담당합니다.
initialize와render를 분리하여 향후 재초기화나 애니메이션 지원이 용이해집니다.
셰이더 관리를 위한 클래스
다음으로 셰이더 생성을 비롯해 링크 및 사용까지 한 곳에서 관리할 수 있도록 셰이더 관리 클래스를 만듭니다. 이로써 렌더링 쪽은 셰이더 '사용'에만 집중할 수 있습니다.
1export class ShaderProgram {
2 private program: WebGLProgram;
3
4 constructor(private gl: WebGLRenderingContext) {
5 const vertexSource = `
6 attribute vec2 a_position;
7 void main() {
8 gl_Position = vec4(a_position, 0.0, 1.0);
9 }
10 `;
11
12 const fragmentSource = `
13 precision mediump float;
14 void main() {
15 gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
16 }
17 `;
18
19 const vs = this.createShader(this.gl.VERTEX_SHADER, vertexSource);
20 const fs = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource);
21
22 const program = this.gl.createProgram();
23 if (!program) throw new Error('Program create failed');
24
25 this.gl.attachShader(program, vs);
26 this.gl.attachShader(program, fs);
27 this.gl.linkProgram(program);
28
29 if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
30 throw new Error(this.gl.getProgramInfoLog(program) ?? 'Link error');
31 }
32
33 this.program = program;
34 }
35
36 use() {
37 this.gl.useProgram(this.program);
38 }
39
40 getAttribLocation(name: string): number {
41 return this.gl.getAttribLocation(this.program, name);
42 }
43
44 private createShader(type: number, source: string): WebGLShader {
45 const shader = this.gl.createShader(type);
46 if (!shader) throw new Error('Shader create failed');
47
48 this.gl.shaderSource(shader, source);
49 this.gl.compileShader(shader);
50
51 if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
52 throw new Error(this.gl.getShaderInfoLog(shader) ?? 'Compile error');
53 }
54
55 return shader;
56 }
57}- 이 클래스는 버텍스/프래그먼트 셰이더의 생성, 연결, 사용을 통합하여 렌더링 쪽에서 안전하게 셰이더를 쓸 수 있게 합니다.
getAttribLocation을 공개하여 메쉬 쪽에서 안전하게 참조할 수 있습니다.
메쉬(버텍스 데이터) 관리를 위한 클래스
버텍스 데이터를 관리할 클래스도 만듭니다. 객체별로 메쉬 클래스를 분리하여 확장이 쉬워집니다.
1export class TriangleMesh {
2 private buffer: WebGLBuffer;
3 private vertexCount = 3;
4
5 constructor(private gl: WebGLRenderingContext) {
6 const vertices = new Float32Array([
7 0.0, 0.5,
8 -0.5, -0.5,
9 0.5, -0.5,
10 ]);
11
12 const buffer = gl.createBuffer();
13 if (!buffer) throw new Error('Buffer create failed');
14
15 this.buffer = buffer;
16
17 gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
18 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
19 }
20
21 draw(shader: ShaderProgram) {
22 const gl = this.gl;
23 const positionLoc = shader.getAttribLocation('a_position');
24
25 gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
26 gl.enableVertexAttribArray(positionLoc);
27 gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
28
29 gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
30 }
31}- 이 클래스는 삼각형의 버텍스 데이터를 관리하며, 셰이더와 연결해 실제 렌더링을 처리합니다.
- 렌더링에 필요한 모든 정보는
TriangleMesh에 포함됩니다.
진입점(사용 예시)
마지막에 이 클래스들을 조합해 앱을 실행합니다.
1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();- 이 구조라면 애니메이션이나 여러 메쉬 추가도 쉽게 할 수 있습니다.
타입스크립트로 WebGL을 쓸 때의 실용 팁
타입스크립트와 WebGL을 함께 사용하면 아래와 같은 장점이 있습니다:.
- WebGL 처리를 클래스로 변환하여 역할별로 정리하면 유지보수와 확장이 쉬워집니다.
- 렌더링과 셰이더 관리 등 책임을 분리하면 코드 가독성이 향상됩니다.
- 타입스크립트의 타입 완성을 활용해 WebGL API 호출이나 파라미터 지정에서 실수를 줄일 수 있습니다.
요약
타입스크립트를 사용하면 타입 안정성과 구조화로 저수준 WebGL 처리도 안정적으로 다룰 수 있습니다. 최소 구성에서 렌더링까지의 흐름을 이해하고, 초기화·렌더링·리소스 관리 등 역할 분리를 클래스 설계로 적용하면 가독성과 유지보수성이 향상됩니다. 단계적으로 구현함으로써 WebGL을 블랙박스가 아닌 실무에 적용 가능한 실용적 지식으로 습득할 수 있습니다.
위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.