타입스크립트의 WebGL

타입스크립트의 WebGL

이 글에서는 타입스크립트에서 WebGL을 설명합니다.

WebGL의 개념, 최소 구성, 렌더링, 확장, 클래스 설계까지 샘플 코드를 통해 단계적으로 소개합니다.

YouTube Video

타입스크립트의 WebGL

WebGL은 브라우저에서 GPU를 직접 다룰 수 있게 해주는 저수준 API입니다.

TypeScript를 사용하면 타입 안전성, 코드 자동 완성, 구조화된 코딩을 통해 WebGL에서 '구현 실수'를 크게 줄일 수 있습니다.

타입스크립트는 특히 다음의 점에서 효과적입니다:.

  • WebGL 객체의 타입이 명확해집니다.
  • 셰이더 변수 취급 실수를 줄일 수 있습니다.
  • 구조를 추적하기 쉬워집니다.

WebGL의 최소 구성

WebGL로 렌더링하기 위한 최소 조건은 다음과 같습니다:.

  • <canvas> 요소
  • WebGL 컨텍스트
  • 버텍스 셰이더
  • 프래그먼트 셰이더

먼저 화면이 초기화될 수 있는 상태를 만듭시다.

캔버스와 WebGL 컨텍스트 얻기

우선 canvasWebGLRenderingContext를 획득합니다.

여기서는 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의 초기화 및 렌더링 과정을 관리하며, 셰이더와 메쉬를 사용하여 화면에 그리는 일을 담당합니다.
  • initializerender를 분리하여 향후 재초기화나 애니메이션 지원이 용이해집니다.

셰이더 관리를 위한 클래스

다음으로 셰이더 생성을 비롯해 링크 및 사용까지 한 곳에서 관리할 수 있도록 셰이더 관리 클래스를 만듭니다. 이로써 렌더링 쪽은 셰이더 '사용'에만 집중할 수 있습니다.

 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를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video