TypeScriptにおける`WebGL`
この記事ではTypeScriptにおけるWebGLについて説明します。
WebGL の概念、最小構成、描画、拡張、クラス設計の順で、段階的にサンプルコードと共に紹介します。
YouTube Video
TypeScriptにおけるWebGL
WebGL はブラウザ上で GPU を直接操作できる低レベル API です。
TypeScript を使うことで、型安全、補完、構造化により、WebGLにおける「実装ミス」を大きく減らせます。
特に以下の点で TypeScript は有効です。
WebGLオブジェクトの型が明確になります。- シェーダ変数の扱いミスを減らせます。
- 構造を追いやすくなります。
WebGL の最小構成
WebGL で描画するには、最低限以下が必要です。
<canvas>要素WebGLコンテキスト- 頂点シェーダ
- フラグメントシェーダ
まずは **「画面を初期化できる状態」**を作ります。
CanvasとWebGL コンテキストを取得する
最初に canvas と WebGLRenderingContext を取得します。
ここでは 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}- この時点で
glがWebGLのすべての入口になります。
画面をクリアして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`;precisionはWebGLでは必ず指定します。
シェーダをコンパイルする関数を作る
シェーダ作成処理は毎回同じなので、関数化します。
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の初期化と描画処理をまとめて管理し、シェーダとメッシュを使って画面に描画する役割を担っています。 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();- この形にしておくと、アニメーションや複数メッシュの追加が容易になります。
TypeScriptでWebGL を書くときの実践的なコツ
TypeScriptを用いてWebGL を利用することで、以下のような利点があります。
WebGLの処理はクラス化すると、役割ごとに整理でき、保守や拡張がしやすくなります。- 描画処理やシェーダ管理などの責務を分けることで、コードの見通しが良くなります。
- TypeScriptの型補完を活用することで、
WebGL APIの呼び出しミスや引数の指定ミスを減らせます。
まとめ
TypeScriptを使うことで、WebGL の低レベルな処理も、型安全と構造化によって安定して扱えます。最小構成から描画までの流れを理解し、さらにクラス設計によって初期化、描画、リソース管理などの役割を分離することで、可読性と保守性を高められます。段階的に実装することで、WebGL をブラックボックスにせず、実務でも応用できる知識として身につけることができます。
YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。