はじめに
今回は頂点の座標をモデルごとに一度にまとめてシェーダーに渡すのとは別に、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」メソッドでワールド行列を取得します。
スマホアプリとして、待ち受け画面にスケジュールを書くアイデアを考えました。それに派生して、リアルの世界では曜日が書かれた磁石でスケジュールのメモを冷蔵庫にくっつけたらどうかと考えました。でも、もう以前から磁石がくっつかない冷蔵庫が当たり前でした。筆者は昭和の古い人間だったと悲しくなります…。
- この記事のキーワード