色んな種類のポリゴンデータを表示できるようにする。

さて、メッシュを描画するにあたって、必要となるデータは位置座標だけではありません。カラー、法線、テクスチャ座標など、いろいろありますね。さらに、頂点インデックスが必要なこともあるでしょう。こうした様々なケースにも対応できるよう、Meshクラスを改善しましょう。

ちなみに、同じ位置座標やカラーでも、渡すベクトルの要素の数がvec3だったりvec4だったりと何パターンかあると思います。本来実用的なライブラリでは、どういったパターンでも対応できる必要がありますが、本ライブラリはあくまで教育用としてシンプルにいきたいので、位置座標はvec3、カラーはvec4と決めうちにしてしまおうと思います。

// Mesh.ts import Material from "./Material.js"; import Context from "./Context.js"; export type VertexAttributeSet = { position: number[], color?: number[], normal?: number[], texcoord?: number[], indices?: number[] } export default class Mesh { private _positionBuffer: WebGLBuffer; private _colorBuffer: WebGLBuffer; private _indexBuffer?: WebGLBuffer; private _vertexNumber = 0; private _indexNumber = 0; private _material: Material; private _context: Context; private static readonly _positionComponentNumber = 3; private static readonly _colorComponentNumber = 4; constructor(material: Material, context: Context, vertexData: VertexAttributeSet) { this._material = material; this._context = context; this._vertexNumber = vertexData.position.length / Mesh._positionComponentNumber; this._positionBuffer = this._setupVertexBuffer(vertexData.position, [0, 0, 0]); this._colorBuffer = this._setupVertexBuffer(vertexData.color!, [1, 1, 1, 1]); if (vertexData.indices != null) { this._indexBuffer = this._setupIndexBuffer(vertexData.indices); this._indexNumber = vertexData.indices.length; } } private _setupVertexBuffer(_array: number[], defaultArray: number[]) { let array = _array; if (array == null) { array = []; for (let i=0; i<this._vertexNumber; i++) { array = array.concat(defaultArray); } } const gl = this._context.gl; const buffer = gl.createBuffer() as WebGLBuffer; gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(array), gl.STATIC_DRAW); return buffer; } private _setupIndexBuffer(indicesArray: number[]) { const gl = this._context.gl; const buffer = gl.createBuffer() as WebGLBuffer; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indicesArray), gl.STATIC_DRAW); return buffer; } private _setVertexAttribPointer(vertexBuffer: WebGLBuffer, attributeSlot: number, componentNumber: number) { if (vertexBuffer != null) { const gl = this._context.gl; gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.vertexAttribPointer( attributeSlot, componentNumber, gl.FLOAT, false, 0, 0); } } draw() { const gl = this._context.gl; this._setVertexAttribPointer(this._positionBuffer, this.material.program!._attributePosition, Mesh._positionComponentNumber); this._setVertexAttribPointer(this._colorBuffer!, this.material.program!._attributeColor, Mesh._colorComponentNumber); if (this._indexBuffer != null) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer); gl.drawElements(gl.TRIANGLES, this._indexNumber, gl.UNSIGNED_SHORT, 0); } else { gl.drawArrays(gl.TRIANGLES, 0, this.vertexNumber); } } get vertexNumber() { return this._vertexNumber; } get material() { return this._material; } }

また、WebGLProgramインターフェースに_attributeColorの定義を足しておきます。

// definitions.ts export enum ShaderType { Vertex, Fragment } export interface WebGLProgram { _attributePosition: number; _attributeColor: number; // 追記 }

_setupVertexBufferというメソッドを追加して、VertexBufferの作成をどの種類の頂点属性でも(位置座標でもカラーでも)、このメソッド呼び出しでできるようにしています。さらに、もしユーザープログラム側でカラーの定義を省略していた場合は、[1,1,1,1](真っ白)のデフォルトカラーとしてカラーのVertexBufferを作る、という配慮も行っています。このような方法の他に、WebGLではgl.vertexAttrib~という、VertexBufferのバインドを行わずに指定した頂点属性のデフォルト値をGPUに設定するAPIも存在するのですが、少しWebGLの呼び出しが複雑になってしまいますので、シンプルさを信条とする本ライブラリではこのAPIは使わないで、前述の方法で対処することにします。

また、描画時の頂点レイアウトの指定(gl.bindBuffer ~ gl.vertexAttribPointerの呼び出し)についても、どの種類の頂点属性でも_setVertexAttribPointerという同じメソッドで設定できるようにしています。

頂点インデックスについても、ユーザーサイドでインデックスを使っていても使っていなくても、どちらでも対応できるようにコード実装しました。

もう一つ、後述するユーザーコード側から指定するシェーダーではa_colorという、カラー情報をCPU側から受け取るattribute変数を追加します。そのattribute変数に対してCPU側から値を設定できるように、以下の行をMaterial.tsに追加します。

// Material.ts ... gl.useProgram(shaderProgram); shaderProgram._attributePosition = gl.getAttribLocation(shaderProgram, "a_position"); gl.enableVertexAttribArray(shaderProgram._attributePosition); shaderProgram._attributePosition = gl.getAttribLocation(shaderProgram, "a_color"); // 追記 gl.enableVertexAttribArray(shaderProgram._attributeColor); // 追記 this._program = shaderProgram; ...

さて、ユーザー側のコードですが、今後のコード量の増加に備え、今回からこちらもTypeScriptで記述することにしました。

// package.json ... "scripts": { "build": "npx tsc --project . --module es2015 --outDir ./dist", "sample-build": "npx tsc ./samples/main.ts --lib es2015,dom --target esNext", // 追記 "test": "echo \"Error: no test specified\" && exit 1", "start": "npx http-server ./ -o" }, ...

こうすることで、npm run sample-buildでTypeScriptのサンプルコードをJavaScriptにコンパイルすることができます。

// main.ts import Spinel from '../dist/index.js' import { VertexAttributeSet } from '../src/Mesh.js'; const vertexShaderStr = ` precision highp float; attribute vec3 a_position; attribute vec4 a_color; varying vec4 v_color; void main(void) { gl_Position = vec4(a_position, 1.0); v_color = a_color; } `; const fragmentShaderStr = ` precision highp float; varying vec4 v_color; void main(void) { gl_FragColor = v_color; } `; const vertexData: VertexAttributeSet = { position: [ 0.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0 ], color: [ 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0 ], indices: [ 0, 1, 2 ] } const vertexComponentNumber = 3; const canvas = document.getElementById('world') as HTMLCanvasElement; const context = new Spinel.Context(canvas); const material = new Spinel.Material(context, vertexShaderStr, fragmentShaderStr); const mesh = new Spinel.Mesh(material, context, vertexData); const gl = context.gl; gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); mesh.draw();

試しに、 colorやindicesの部分をコメントアウトして実行してみてください。きちんと動作すると思います。
特に、colorを省略したときは、デフォルトカラーとして[1,1,1,1]つまり白が適用されるはずです。

image.png

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

5日目: クラス構文を使ってもっとライブラリっぽくしよう

7日目: glTFからポリゴンデータを読み込んで表示する >>