「ユニフォームバッファ」を使って三角形を描画してみよう

はじめに
今回は頂点の座標をモデルごとに一度にまとめてシェーダーに渡すのとは別に、1フレームごとにおいては変化のない定数を「ユニフォームバッファ」からシェーダーに渡します。具体的にはパースペクティブ行列やカメラ行列、ワールド行列、法線行列をユニフォームバッファを通して、シェーダーにユニフォーム定数として渡します。
前回までにコーディングしてきた「lib/Matrix3D.js」「lib/Model3D.js」「lib/UltraMotion3D.js」「lib/Vector3D.js」「lib/WGSL.js」「index.html」に追記して、今回は図1のように遠近法やカメラを考慮した三角形を描画します。まだ三角形だけですが、ワールド行列で回転アニメーションもさせてみます。
ユニフォームバッファを準備する
各行列はJavaScriptのメインコード側でユニフォームバッファに格納し、その値をWGSLに送ります。
ユニフォームバッファを用意する
次のサンプルコード「lib」→「UltraMotion3D.js」ファイルでは、ユニフォームバッファを用意するためにデバイスの「createBuffer」メソッドの「usage」に「GPUBufferUsage.UNIFORM」を指定して作成します。その「size」には、ここでは「4 * 16 * 4(float32の4byte * 4x4行列がパースペクティブ行列、ビュー行列、ワールド行列、法線行列の4個)」バイトを指定します。
・サンプルコード「lib」→「UltraMotion3D.js」ファイルvar _camera = new Matrix3D(); var _fov = 45.0; const UNIFORM_BUFFER_SIZE = 4 * 16 * 4; // 4byte(float32) * 4x4 matrix * 4 var _passEncoder = null; async function initWebGPU(canvas) { if (!navigator.gpu) { throw Error('WebGPU not supported.'); } const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { throw Error('Could not request WebGPU adapter.'); } _device = await adapter.requestDevice(); _canvas = document.getElementById(canvas); _context = _canvas.getContext('webgpu'); _presentationFormat = navigator.gpu.getPreferredCanvasFormat(); _context.configure({ device: _device, format: _presentationFormat, alphaMode: 'premultiplied', }); _pipeline = setPipeline(vertWGSL,fragWGSL); _uniformBuffer = _device.createBuffer({ size: UNIFORM_BUFFER_SIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); window.addEventListener("resize", resize, false); resize(); init(); everyFrame(); } function setPipeline(vertexWGSL,fragmentWGSL) { (中略) } function everyFrame() { _commandEncoder = _device.createCommandEncoder(); const textureView = _context.getCurrentTexture().createView(); _renderPassDescriptor = { colorAttachments: [ { view: textureView, clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, loadOp: 'clear', storeOp: 'store', }, ], depthStencilAttachment: { view: _depthTexture.createView(), depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store', }, }; _passEncoder = _commandEncoder.beginRenderPass(_renderPassDescriptor); draw(); _passEncoder.end(); _device.queue.submit([_commandEncoder.finish()]); requestAnimationFrame(everyFrame.bind(everyFrame)); } function resize() { (中略) } function degree2radian(degree) { return degree * Math.PI / 180; } function radian2degree(radian) { return radian * 180 / Math.PI; } function getPos(event) { const clientRect = _canvas.getBoundingClientRect(); if ( 'ontouchend' in document ) { const touch = event.changedTouches.item(0); const x = touch.clientX - clientRect.left; const y = touch.clientY - clientRect.top; return new Vector2D(x,y); } else { const x = event.clientX - clientRect.left; const y = event.clientY - clientRect.top; return new Vector2D(x,y); } }
【サンプルコードの解説】
カメラ行列「_camera」変数を用意します。
フィールドオブビュー(視野角)を_fov変数で45度にします。
ユニフォームバッファのサイズを「UNIFORM_BUFFER_SIZE」定数に宣言します。
ユニフォームバッファを「_uniformBuffer」変数に用意します。
「everyFrame」関数が終わるときに、繰り返し「requestAnimationFrame」でeveryFrameをリクエストして呼び出して毎フレーム描画します。
「degree2radian」関数でディグリー角度をラジアン角度に変換し取得します。
「radian2degree」関数でラジアン角度をディグリー角度に変換し取得します。
「getPos」関数でキャンバス上のマウス座標を取得します。
ユニフォームバッファをシェーダーに送る
「Model3D」クラスの「init」メソッドでユニフォームバッファを「uniformBindGroup」にバインド(結び付ける、関連付ける)して、「setTransform」メソッドでユニフォームバッファにプロジェクション行列(ここではパースペクティブ行列)とビュー行列(ここではカメラ行列)とワールド行列(ここでは平行移動・回転・スケーリング行列)をWGSLに渡します。まだ今回は法線行列は渡しません。
「draw」メソッドで「setTransform」メソッドを呼び出し、パスエンコーダーの「setBindGroup」メソッドで「uniformBindGroup」をグループ0番にバインドします。
・「lib」→「Model3D.js」ファイルclass Model3D { constructor() { this.vertices = [0,100,0,1, -100,-100,0,1, 100,-100,0,1]; this.indices = [0,1,2]; this.pos = new Vector3D(0,0,0); this.rotate = new Vector3D(0,0,0); this.scale = new Vector3D(1,1,1); } initBuffers() { this.vertexArray = new Float32Array(this.vertices); this.verticesBuffer = _device.createBuffer({ size: this.vertexArray.byteLength, usage: GPUBufferUsage.VERTEX, mappedAtCreation: true, }); new Float32Array(this.verticesBuffer.getMappedRange()).set(this.vertexArray); this.verticesBuffer.unmap(); this.init(); } async init() { this.uniformBindGroup = _device.createBindGroup({ layout: _pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: _uniformBuffer, offset: 0, size: UNIFORM_BUFFER_SIZE, }, }, ], }); } getVertexCount() { return ~~(this.vertexArray.length/4); } draw() { if (this.uniformBindGroup) { _passEncoder.setPipeline(_pipeline); this.setTransform(); _passEncoder.setBindGroup(0,this.uniformBindGroup); _passEncoder.setVertexBuffer(0,this.verticesBuffer); _passEncoder.draw(this.getVertexCount()); } } setTransform() { const projectionMatrix = this.getProjectionMatrix(); _device.queue.writeBuffer( _uniformBuffer, 4 * 16 * 0, projectionMatrix.e.buffer, 0, 4 * 16 ); _device.queue.writeBuffer( _uniformBuffer, 4 * 16 * 1, _camera.e.buffer, 0, 4 * 16 ); const worldMatrix = this.getWorldMatrix(); _device.queue.writeBuffer( _uniformBuffer, 4 * 16 * 2, worldMatrix.e.buffer, 0, 4 * 16 ); } getProjectionMatrix() { const projectionMatrix = new Matrix3D(); const aspect = _canvas.width/_canvas.height; projectionMatrix.perspective(_fov, aspect, 1, 100000.0); return projectionMatrix; } getWorldMatrix() { const worldMatrix = new Matrix3D(); worldMatrix.translate(this.pos); worldMatrix.rotateX(degree2radian(this.rotate.x)); worldMatrix.rotateY(degree2radian(this.rotate.y)); worldMatrix.rotateZ(degree2radian(this.rotate.z)); worldMatrix.scale(this.scale); return worldMatrix; } }
【サンプルコードの解説】
Model3Dクラスで「pos(位置)」「rotate(回転)」「scale(拡大縮小)」プロパティを用意します。
「initBuffers」メソッドで「init」メソッドを呼び出します。
「init」メソッド内でデバイスの「createBindGroup」メソッドを使い、「_unifomBuffer」変数を「binding」の0番にバインドします。
「draw」メソッドで「setTransform」メソッドを呼び出し、「uniformBindGroup」プロパティをグループ0番にバインドします。
「setTransform」メソッドでプロジェクション行列を4*16*0オフセットに4*16バイト用意します。
ビュー行列を4*16*1オフセットに4*16バイト用意します。
ワールド行列を4*16*2オフセットに4*16バイト用意します(4*16*3オフセットが開いていますがそれは次回法線行列を用意します)。
「getProjectionMatrix」メソッドでパースペクティブ行列(遠近法)を取得します。
「getWorldMatrix」メソッドでワールド行列を取得します。
スマホアプリとして、待ち受け画面にスケジュールを書くアイデアを考えました。それに派生して、リアルの世界では曜日が書かれた磁石でスケジュールのメモを冷蔵庫にくっつけたらどうかと考えました。でも、もう以前から磁石がくっつかない冷蔵庫が当たり前でした。筆者は昭和の古い人間だったと悲しくなります…。
連載バックナンバー
Think ITメルマガ会員登録受付中
全文検索エンジンによるおすすめ記事
- 「WebGPU」でシェーダーを使って三角形を1つだけ描画してみよう
- WebGPUライブラリ「UltraMotion3D」で3DのサンプルWebコンテンツを動かしてみよう
- はじめての「WebGPU」で背景色を塗りつぶしてみよう
- オブジェクトの頂点変換やピクセルを色付けするシェーダー言語「WGSL」の文法を学ぼう
- 2D〜4Dで向きや大きさを持つ数字の集まり「ベクトル」について学ぼう
- 「Krita」と「Python」でアーティスティックな絵を描こう
- ES2015で導入された、より洗練された構文 Part 1
- WebGPUを使うための「HTML5+CSS+JavaScript」の文法を学ぼう
- ECMAScript
- 外部モジュールとの連携