TypeScriptにおける`WebGL`

TypeScriptにおける`WebGL`

この記事ではTypeScriptにおけるWebGLについて説明します。

WebGL の概念、最小構成、描画、拡張、クラス設計の順で、段階的にサンプルコードと共に紹介します。

YouTube Video

TypeScriptにおけるWebGL

WebGL はブラウザ上で GPU を直接操作できる低レベル API です。

TypeScript を使うことで、型安全、補完、構造化により、WebGLにおける「実装ミス」を大きく減らせます。

特に以下の点で TypeScript は有効です。

  • WebGL オブジェクトの型が明確になります。
  • シェーダ変数の扱いミスを減らせます。
  • 構造を追いやすくなります。

WebGL の最小構成

WebGL で描画するには、最低限以下が必要です。

  • <canvas> 要素
  • WebGL コンテキスト
  • 頂点シェーダ
  • フラグメントシェーダ

まずは **「画面を初期化できる状態」**を作ります。

CanvasとWebGL コンテキストを取得する

最初に canvasWebGLRenderingContext を取得します。

ここでは null安全を意識した TypeScript の書き方を使います。

 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}
  • この時点で glWebGL のすべての入口になります。

画面をクリアしてWebGL が動くことを確認する

描画の前に、まずは「背景色を塗れるか」を確認します。

これは WebGL が正しく動作しているかの最初のチェックです。

1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);
  • ここまでで、canvas が暗いグレーで塗りつぶされます。

シェーダとは何か

WebGL では、GLSL という専用の言語を使って描画処理を記述します。GLSLでは主に、頂点シェーダフラグメントシェーダという2種類のシェーダを用意します。

まずは、この2つを使った 「最小構成で動作するシェーダ」 を作成していきます。

頂点シェーダを書く

頂点シェーダは、描画する点や図形の位置を決定します。

以下は 画面中央に点を1つ置くだけのシンプルなコードです。

1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5	gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;
  • a_position は JavaScript 側から渡される座標です。

フラグメントシェーダを書く

フラグメントシェーダは、画面に表示される色を決定します。

今回は常に赤色を出力します。

1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;
  • precisionWebGL では必ず指定します。

シェーダをコンパイルする関数を作る

シェーダ作成処理は毎回同じなので、関数化します。

 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}
  • この関数は、指定した種類のシェーダを作成し、ソースコードをコンパイルしたうえで、失敗時はエラーを出し、成功したシェーダを返します。

プログラム(シェーダの組)を作成する

頂点シェーダとフラグメントシェーダを1つにまとめます。

これを 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つのプログラムにまとめてリンクし、描画に使用できる状態かを確認しています。

頂点データを用意する

ここで、三角形を描画するための頂点データを用意します。

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へ転送しています。

attribute とバッファを関連付ける

シェーダ内の 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のため、処理をそのまま書くとコードが急速に肥大化します。クラス設計により、初期化・描画・リソース管理を明確に分離できます。

ここでは、TypeScriptの利点を活かし、役割分離、再利用、保守性を意識した設計へ段階的に発展させます。

設計方針(最小だが実務向け)

クラスは以下の役割に分けます。

  • GLAppは、全体の初期化と描画を制御します。
  • ShaderProgramは、シェーダを管理します。
  • TriangleMeshは、頂点データを管理します。

まずは全体を制御するクラスから作ります。

WebGL アプリのエントリクラス

GLApp は canvas と 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();
  • この形にしておくと、アニメーションや複数メッシュの追加が容易になります。

TypeScriptでWebGL を書くときの実践的なコツ

TypeScriptを用いてWebGL を利用することで、以下のような利点があります。

  • WebGL の処理はクラス化すると、役割ごとに整理でき、保守や拡張がしやすくなります。
  • 描画処理やシェーダ管理などの責務を分けることで、コードの見通しが良くなります。
  • TypeScriptの型補完を活用することで、WebGL API の呼び出しミスや引数の指定ミスを減らせます。

まとめ

TypeScriptを使うことで、WebGL の低レベルな処理も、型安全と構造化によって安定して扱えます。最小構成から描画までの流れを理解し、さらにクラス設計によって初期化、描画、リソース管理などの役割を分離することで、可読性と保守性を高められます。段階的に実装することで、WebGL をブラックボックスにせず、実務でも応用できる知識として身につけることができます。

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video