3Dギターに「マテリアル」を指定して、本物のギターそっくりの材質を再現してみよう

はじめに
今回はマテリアル(材質)を好きな数だけ色分けして、1個以上のマテリアルを持つ3Dギターをカラフルに表示してみます。前回作った白色の3Dギターを茶色や黄土色の色分けをした表現にします。
前回までにコーディングしてきた「lib/Matrix3D.js」「lib/Model3D.js」「lib/UltraMotion3D.js」「lib/Vector3D.js」「lib/WGSL.js」「models/Guitar.js」「index.html」に追記して、今回は図のような3Dギターをレンダリングします。
3Dデータからマテリアルを反映させる
マテリアルデータを色に反映させるにはWGSLの「フラグメントシェーダー」を使います。まず頂点シェーダーに色と反射光を渡し、そこからそれらをフラグメントシェーダーに渡します。マテリアルデータには色と反射光の他に拡散光と周囲光と自己照明と反射強度もありますが、本連載では省きます。ただし次回のテクスチャマテリアルでは自己照明も反映します。マテリアルデータのセットはModel3Dクラスを継承したGuitarクラスからsetMaterialメソッドを呼び出します。
Materialクラスの実装
マテリアルデータは「Material」クラスのインスタンスを作って保持します。R(赤)、G(緑)、B(青)、A(不透明度)、dif(拡散光)、amb(周囲光)、emi(自己照明)、spc(反射光)、power(反射強度)をプロパティに持ちます。
・サンプルコード「lib」→「Material.js」class Material {
constructor(r,g,b,a,dif,amb,emi,spc,power) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
this.dif = dif;
this.amb = amb;
this.emi = emi;
this.spc = spc;
this.power = power;
}
setColor(r,g,b,a) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
}
【サンプルコードの解説】
Materialクラスのコンストラクタ(constructor)で(Red,Green,Blue,Alpha)、Diffuse、Ambient、Emissive、Specular、Powerを引数からセットします。
「setColor」メソッドで(R,G,B,A)をセットします。
パイプラインで頂点シェーダーにマテリアルデータを渡す
色と反射光も頂点シェーダーに渡すパラメータに加えます。頂点ごとに色と反射光も渡します。
本連載ではマテリアルデータを頂点ごとに1つずつ用意していますが、マテリアル単位でまとめて用意した方が良いかもしれません。例えば、ユニフォームバッファでマテリアルデータごとに分けると良いでしょう。ただし、ユニフォームバッファ数には上限が必要になりますが、本連載ではそのやり方はしません。
・サンプルコード「lib」→「UltraMotion3D.js」var _num = 0;
var _camera = new Matrix3D();
var _fov = 45.0;
const MAX_NUM = 10000;
const UNIFORM_BUFFER_SIZE = 4 * 16 * (4 + 32); // 4byte(float32) * 4x4 matrix * (4 + 32)
const OFFSET_SIZE = UNIFORM_BUFFER_SIZE; //256の倍数でなければならない
var _passEncoder = null;
async function initWebGPU(canvas) {
(中略)
}
function setPipeline(vertexWGSL,fragmentWGSL) {
const pipeline = _device.createRenderPipeline({
layout: 'auto',
vertex: {
module: _device.createShaderModule({
code: vertexWGSL,
}),
entryPoint: 'main',
buffers: [{
attributes: [
{// position
shaderLocation: 0,
offset: 0,
format: 'float32x4',
},{// normal
shaderLocation: 1,
offset: 4*4,
format: 'float32x3',
},{// color
shaderLocation: 2,
offset: 4*(4+3),
format: 'float32x4',
},{// specular
shaderLocation: 3,
offset: 4*(4+3+4),
format: 'float32',
}
],
arrayStride: 4*(4+3+4+1),
stepMode: "vertex",
},],
},
fragment: {
module: _device.createShaderModule({
code: fragmentWGSL,
}),
entryPoint: 'main',
targets: [// 0
{
format: _presentationFormat,
},
],
},
primitive: {
topology: 'triangle-list',
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus',
},
});
return pipeline;
}
(後略)
【サンプルコードの解説】
デバイスの「createRenderPipeline」メソッドで、アトリビュートバッファ「shaderLocation」の2番にRGBAの4つを、shaderLocationの3番に反射光の1つを渡します。
arrayStride(配列の歩)」にfloat32の4バイト*(頂点の4個+法線の3個+色の4個+反射光の1個)の個数を渡します。
頂点シェーダーに色など頂点ごとのデータを渡す
Model3Dクラスから頂点シェーダーに頂点座標と法線ベクトルと色と反射光の頂点データを渡します。これらのデータは頂点1個ごとに配列で用意します。「setV」メソッドでは一時的に配列「preVertices」プロパティに頂点データを保持し、「setI」メソッドで頂点バッファに渡す本番用の配列「vertices」プロパティに配列データを用意します。
・サンプルコード「lib」→「Model3D.js」class Model3D {
constructor() {
this.materials = [];
this.preVertices = [];
this.vertices = [];
this.indices = [];
this.pos = new Vector3D(0,0,0);
this.rotate = new Vector3D(0,0,0);
this.scale = new Vector3D(1,1,1);
this.num = _num;
_num++;
}
setMaterial(r,g,b,a,dif,amb,emi,spc,power,texture) {
this.materials.push(new Material(r,g,b,a,dif,amb,emi,spc,power));
}
setV(x,y,z,nx=0,ny=0,nz=1,material=0,u=0,v=0) {
this.preVertices.push(x,y,z,nx,ny,nz,material,u,v);
}
setI(i0,i1,i2) {
var indices = [];
indices.push(i0,i1,i2);
for ( let i = 0; i < indices.length; i++ ) {
const j = indices[i];
const x = this.preVertices[j*9 ];
const y = this.preVertices[j*9+1];
const z = this.preVertices[j*9+2];
const nx = this.preVertices[j*9+3];
const ny = this.preVertices[j*9+4];
const nz = this.preVertices[j*9+5];
const m = this.preVertices[j*9+6];
const r = this.materials[m].r;
const g = this.materials[m].g;
const b = this.materials[m].b;
const a = this.materials[m].a;
const s = this.materials[m].spc;
this.vertices.push(x,y,z,1,nx,ny,nz,r,g,b,a,s);
}
}
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() {
(中略)
}
getVertexCount() {
return ~~(this.vertexArray.length/12);
}
(中略)
}
【サンプルコードの解説】
Model3Dクラスの「setMaterial」メソッドでMaterialクラスのインスタンスを配列「materials」プロパティに追加します。
「setI」メソッドで頂点インデックスごとに頂点座標、法線ベクトル、RGBA色、反射光を配列「vertices」プロパティに追加します。
「getVertexCount」メソッドで取得できる頂点数は、頂点配列「vertexArray」プロパティの要素数を(X,Y,Z,W)、(NX,NY,NZ)、(R,G,B,A)、Specularの12個で除算した整数です。
頂点シェーダーとフラグメントシェーダー
頂点シェーダーでは、頂点バッファのデータから変形後の座標や法線、色、反射光を求め、フラグメントシェーダーに出力します。フラグメントシェーダーでそれらを反映した色を計算し戻り値で返します。
・サンプルコード「lib」→「WGSL.js」const vertWGSL = `
struct Uniforms {
projectionMatrix : mat4x4f,
viewMatrix : mat4x4f,
worldMatrix : mat4x4f,
normalMatrix : mat4x4f,
}
@group(0) @binding(0) var<uniform> uniforms : Uniforms;
struct VertexOutput {
@builtin(position) position : vec4f,
@location(0) normal : vec3f,
@location(1) color : vec4f,
@location(2) specular : f32,
}
@vertex
fn main(
@location(0) position: vec4f,
@location(1) normal: vec3f,
@location(2) color: vec4f,
@location(3) specular: f32,
) -> VertexOutput {
var output : VertexOutput;
output.position = uniforms.projectionMatrix *
uniforms.viewMatrix * uniforms.worldMatrix * position;
output.normal = (uniforms.normalMatrix * vec4f(normal,1)).xyz;
output.color = color;
output.specular = specular;
return output;
}
`;
const fragWGSL = `
struct VertexOutput {
@builtin(position) position : vec4f,
@location(0) normal : vec3f,
@location(1) color : vec4f,
@location(2) specular : f32,
}
@fragment
fn main(fragData: VertexOutput) -> @location(0) vec4f {
var lightDirection = normalize(vec3f(-1.0,-2.0,-4.0));
var normal = normalize(fragData.normal);
var diffuseLightWeighting = (1+dot(normal,lightDirection))/2;
var reflect = normalize(2.0*diffuseLightWeighting*normal-lightDirection);
var spc = pow(clamp(dot(reflect,lightDirection),0.0,1.0),5.0);
var specular = vec4(fragData.specular,fragData.specular,fragData.specular,1);
var color = fragData.color*clamp(diffuseLightWeighting,0.0,1.0)+spc*specular;
return vec4f(color.rgb,1.0);
}
`;
【サンプルコードの解説】
頂点シェーダーvertWGSLではVertexOutput構造体を定義し、頂点座標・法線ベクトル・色・反射光をプロパティとして宣言します。パイプラインから受け取った頂点データを基に(第2引数が色、第3引数が反射光)、これらの値をVertexOutput構造体に格納してフラグメントシェーダーに渡します。
フラグメントシェーダー「fragWGSL」定数で法線(ポリゴン面の向き)がライト(光源)の向きの正反対になるほど色を明るくして、反射光でテカりを表現した色を決めます。
実行ファイルindex.htmlファイル
それでは、次のサンプルコードをコーディングして「Google Chrome」で「index.html」を実行してみましょう。カラフルな色の付いた3Dギターが表示されます。ギターはマウスドラッグで回転させることができます。
・サンプルコード「index.html」<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>UltraMotion3D</title>
<meta name="viewport" content="width=device-width">
<script src="lib/Material.js"></script>
<script src="lib/Vector3D.js"></script>
<script src="lib/Matrix3D.js"></script>
<script src="lib/Model3D.js"></script>
<script src="lib/WGSL.js"></script>
<script src="lib/UltraMotion3D.js"></script>
<script src="models/Guitar.js"></script>
<script type="text/javascript">
var _model;
var _pos = new Vector2D(0,0);
var _click = false;
function init() {
_model = new Guitar();
_model.pos.z = -300;
document.onmousedown = onMouseDown;
document.onmousemove = onMouseMove;
document.onmouseup = onMouseUp;
}
function draw() {
_camera.lookAt(new Vector3D(0,0,500),new Vector3D(0,0,0),new Vector3D(0,1,0))
_model.draw();
}
function onMouseDown(event) {
_pos = getPos(event);
_click = true;
}
function onMouseMove(event) {
if ( _click ) {
pos = getPos(event);
_model.rotate.addX(pos.y-_pos.y);
_model.rotate.addY(pos.x-_pos.x);
_pos = pos;
}
}
function onMouseUp(event) {
_click = false;
}
</script>
</head>
<body onload='initWebGPU("CanvasAnimation");'>
<canvas id="CanvasAnimation" width="1000" height="900"></canvas>
</body>
</html>
【サンプルコードの解説】
<script>タグで「Material.js」ファイルを読み込みます。
「document.onmousedown」でマウスがクリックされたら「onMouseDown」関数を呼び出し、「document.onmousemove」でマウスが動いたら「onMouseMove」関数を呼び出し、「document.onmouseup」でマウスが離されたら「onMouseUp」関数を呼び出します。
onMouseDown関数でマウス座標を取得し「_click」をtrueにします。
onMouseMove関数でマウスをドラッグした(_clickがtrueの時にマウスが動いた)だけギターのXY回転を加算します。
onMouseUp関数で_clickをfalseにします。
本連載では音については解説しませんが、3Dオブジェクト同士が衝突した時の音をマテリアルからシミュレートするようなことはできないかな? と思いました。
おわりに
今回はModel3Dクラスにマテリアル情報を渡し、フラグメントシェーダーを通して様々な色を表示する実装をしました。
次回は、さらにマテリアルデータにテクスチャデータも使います。テクスチャはPNGファイルなどの2D画像データでポリゴンの表面に貼るシールのような模様のことです。
連載バックナンバー
Think ITメルマガ会員登録受付中
全文検索エンジンによるおすすめ記事
- 「Blender」で3Dモデルをデザインして「UltraMotion3D」ライブラリで表示してみよう
- オブジェクトの頂点変換やピクセルを色付けするシェーダー言語「WGSL」の文法を学ぼう
- 「WebGPU」でシェーダーを使って三角形を1つだけ描画してみよう
- 「ユニフォームバッファ」を使って三角形を描画してみよう
- WebGPUライブラリ「UltraMotion3D」で3DのサンプルWebコンテンツを動かしてみよう
- ES2015のモジュール管理
- はじめての「WebGPU」で背景色を塗りつぶしてみよう
- スマホアプリ開発にも便利な位置情報API- Geolocation API-
- ES2015で導入された、より洗練された構文 Part 1
- Unityちゃんの障害物ゲームを作る


