ライブラリっぽくしてみよう。

さて、前回までだとただのWebGL入門のサンプルコードと実質なんら変わりません。

リファクタリングして、少しライブラリっぽい体裁にしてみましょう。

今回は、6つのファイルに内容を分割しました。

image.png

以下、リファクタリング後のコードを示します。

まずは、定義系をdefinitions.tsというファイルに分けました。

// definitions.ts export enum ShaderType { Vertex, Fragment } export interface WebGLProgram { _attributePosition: number; } export interface WebGLBuffer { _vertexComponentNumber: number; _vertexNumber: number; }

上の2つのinterface文はなんでしょうか? 先程のコードでは、attributePositionグローバル変数にgl.getAttribLocation関数の戻り値を設定していました。また、vertexComponentNumberグローバル変数やvertexNumberグローバル変数にも頂点のコンポーネント数(x, y, zの3個)、頂点数を設定していましたね。

しかし、グローバル変数の使用は褒められたものではありませんし、そもそも今回はファイルを分けますので、余計に受け渡しがややこしくなります。どうすればよいでしょうか。

方法は一つではありませんが、割とWebGLでよくやるのは、gl.getAttribLocation関数の戻り値はshaderProgram変数に、子供のプロパティ名を指定して代入してしまうことです。本来ならきちんとしたクラスを作って、そこのフィールド変数として格納してやるのが筋ですが、それも少々仰々しくなりますし、とりあえず今は良いでしょう。実際、shaderProgramと対応関係にある変数値ですので、shaderProgramに入れてしまうのは横着ですがリーズナブルでもあります。

ただ、TypeScriptの場合は型がありますので、普通にやったのではコンパイルエラーで弾かれてしまいます。そこで、既存の型に新たな追加のプロパティを拡張するのが上記のinterface構文を使う方法です。テクニックとして覚えてしまいましょう。少なくとも、as any構文で無理やり型制約を破ってしまうよりずっと良いやり方です。

他のファイルも見てみます。

// context.ts export function initWebGL(canvas: HTMLCanvasElement) { const gl = canvas.getContext('webgl') as WebGLRenderingContext; if (gl == null) { alert('Failed to initialize WebGL.'); } return gl; }
// shader.ts import { ShaderType, WebGLProgram } from "./definitions.js"; const vertexShaderStr = ` precision highp float; attribute vec3 a_position; void main(void) { gl_Position = vec4(a_position, 1.0); } `; const fragmentShaderStr = ` precision highp float; void main(void) { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } `; function compileShader(gl: WebGLRenderingContext, shaderType: ShaderType, shaderStr: string) { let shader: WebGLShader | null; if (shaderType == ShaderType.Vertex) { shader = gl.createShader(gl.VERTEX_SHADER); } else if (shaderType == ShaderType.Fragment) { shader = gl.createShader(gl.FRAGMENT_SHADER); } if (shader! == null) { alert('Failed to create WebGL shader.'); return null; } gl.shaderSource(shader, shaderStr); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert(gl.getShaderInfoLog(shader)); return null; } return shader; } export function initProgram(gl: WebGLRenderingContext) { var vertexShader = compileShader(gl, ShaderType.Vertex, vertexShaderStr) as WebGLShader; var fragmentShader = compileShader(gl, ShaderType.Fragment, fragmentShaderStr) as WebGLShader; const shaderProgram = gl.createProgram() as WebGLProgram; if (shaderProgram == null) { alert('Failed to create WebGL program.'); return null; } gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert("Could not initialise shaders"); return null } gl.useProgram(shaderProgram); shaderProgram._attributePosition = gl.getAttribLocation(shaderProgram, "a_position"); gl.enableVertexAttribArray(shaderProgram._attributePosition); return shaderProgram; }
// buffer.ts import { WebGLBuffer } from "./definitions.js"; export function initBuffers(gl: WebGLRenderingContext) { const vertexBuffer = gl.createBuffer() as WebGLBuffer; gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); var vertices = [ 0.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0 ]; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); vertexBuffer._vertexComponentNumber = 3; vertexBuffer._vertexNumber = 3; return vertexBuffer; }
// render.ts import { WebGLProgram, WebGLBuffer } from "./definitions.js"; export function drawScene(gl: WebGLRenderingContext, vertexBuffer: WebGLBuffer, shaderProgram: WebGLProgram) { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.vertexAttribPointer( shaderProgram._attributePosition, vertexBuffer._vertexComponentNumber, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer._vertexNumber); }
// index.ts import { initWebGL } from "./context.js"; import { initProgram } from "./shader.js"; import { initBuffers } from "./buffer.js"; import { drawScene } from "./render.js"; export default function main() { const canvas = document.getElementById('world') as HTMLCanvasElement; const gl = initWebGL(canvas); if (gl == null) { return false; } const shaderProgram = initProgram(gl); if (shaderProgram == null) { return false; } const vertexBuffer = initBuffers(gl); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); drawScene(gl, vertexBuffer, shaderProgram); return true; }

関数ごとにファイルに切り分けただけですが、だいぶすっきりしました。

しかし決まりきった処理しかできないのではまだライブラリとは言えません。次はユーザープログラムから渡した頂点データを使えるようにしましょう。

次のように修正します。

// index.ts import { initWebGL } from "./context.js"; import { initProgram } from "./shader.js"; import { initBuffers } from "./buffer.js"; import { drawScene } from "./render.js"; export default function main(vertices: number[], vertexComponentNumber: number) { // <---変更 const canvas = document.getElementById('world') as HTMLCanvasElement; const gl = initWebGL(canvas); if (gl == null) { return false; } const shaderProgram = initProgram(gl); if (shaderProgram == null) { return false; } const vertexBuffer = initBuffers(gl, vertices, vertexComponentNumber); // <---変更 gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); drawScene(gl, vertexBuffer, shaderProgram); return true; }
// buffer.ts import { WebGLBuffer } from "./definitions.js"; export function initBuffers(gl: WebGLRenderingContext, vertices:number[], vertexComponentNumber: number) { // <---変更 const vertexBuffer = gl.createBuffer() as WebGLBuffer; gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); vertexBuffer._vertexComponentNumber = vertexComponentNumber; // <---変更 vertexBuffer._vertexNumber = vertices.length / vertexComponentNumber; // <---変更 return vertexBuffer; }
// main.js import main from '../dist/index.js'; const vertices = [ // <---追加 0.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0 ] main(vertices, 3); // <---変更

結果が以下です。三角形が上下逆になりましたね。

image.png

TypeScriptコードのimport文について

今までのTypeScriptコードのimport文について、TypeScriptに慣れている方は拡張子が.jsであることに違和感を覚えたかもしれません。これについては少々複雑な背景があります。

興味がある方は以下のページをご参照ください。ややこしい話ですしCGの本質ではありませんので、興味のない方はとりあえず無視しても問題ありません。

コラム:現在のTypeScriptとES Moduleの状況

まとめ

ファイルを分割したことで、少しライブラリっぽい雰囲気が出てきましたね。

これから機能を増やして、抽象化して、を繰り返してますますライブラリ感が出てきます。楽しくなっていきますよ。

この時点のプロジェクトコードはこちらで公開しています 

2日目:三角形を描画してみよう

4日目:ユーザー指定のシェーダーを使えるようにしよう