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

2025年11月19日(水)
大西 武 (オオニシ タケシ)
第11回の今回は、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ギター

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画像データでポリゴンの表面に貼るシールのような模様のことです。

著者
大西 武 (オオニシ タケシ)
1975年香川県生まれ。大阪大学経済学部経営学科中退。プログラミング入門書など30冊以上を商業出版する作家。Microsoftで大賞やNTTドコモでグランプリなど20回以上全国区のコンテストに入賞するアーティスト。オリジナルの間違い探し「3Dクイズ」が全国放送のTVで約10回出題。
https://profile.vixar.jp

連載バックナンバー

開発言語技術解説
第11回

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

2025/11/19
第11回の今回は、3Dギターに「マテリアル」で複数の色を指定して、本物のギターとそっくりなカラフルな材質な見た目にする解説をします。
開発言語技術解説
第10回

「Blender」で3Dモデルをデザインして「UltraMotion3D」ライブラリで表示してみよう

2025/10/29
第10回の今回は「Blender」で3Dのギターをデザインし「Vixar Vision」で「UltraMotion3D」向けに書き出してWebブラウザへ表示する解説をします。
開発言語技術解説
第9回

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

2025/10/8
第9回の今回は、WebGPUでユニフォームバッファを通して行列をユニフォーム定数としてシェーダーに渡し、三角形を描画する解説をします。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています