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

2025年10月29日(水)
大西 武 (オオニシ タケシ)
第10回の今回は「Blender」で3Dのギターをデザインし「Vixar Vision」で「UltraMotion3D」向けに書き出してWebブラウザへ表示する解説をします。

モデルデータを表示する

以降で解説するサンプルコードをコーディングしてGoogle Chromeでindex.htmlを開くと、図8のように白いギターが表示されます。前出の「Vixar Vision」から書き出した「Guitar.js」コードには「setMaterial」メソッド、「setV」メソッド、「setI」メソッドがあるので、それを「Model3D」クラスで実装します。

図8:Google Chromeに3Dギターを表示(再掲)

Vixar Visionで書き出したjsファイルに対応する

次のサンプルコード「lib」→「Model3D.js」ファイルのようにコーディングします。setMaterialメソッドではまだ何もしません。setVメソッドでは1頂点ずつのデータをpreVertices配列に追加します。setIメソッドではsetVで追加した頂点を指すインデックスをindices配列に追加し、頂点データをvertices配列に追加します。

・サンプルコード「lib」→「Model3D.js」ファイル
class Model3D {
  constructor() {
    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) {
  }
  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];
      this.vertices.push(x,y,z,1,nx,ny,nz);
    }
  }
  initBuffers() {
(中略)
  }
  async init() {
(中略)
  }
  getVertexCount() {
    return ~~(this.vertexArray.length/7);
  }
  draw() {
(中略)
  }
  setTransform() {
    const projectionMatrix = this.getProjectionMatrix();
    _device.queue.writeBuffer(
      _uniformBuffer,
      4 * 16 * 0 + OFFSET_SIZE*this.num,
      projectionMatrix.e.buffer,
      0,
      4 * 16
    );
    _device.queue.writeBuffer(
      _uniformBuffer,
      4 * 16 * 1 + OFFSET_SIZE*this.num,
      _camera.e.buffer,
      0,
      4 * 16
    );
    const worldMatrix = this.getWorldMatrix();
    _device.queue.writeBuffer(
      _uniformBuffer,
      4 * 16 * 2 + OFFSET_SIZE*this.num,
      worldMatrix.e.buffer,
      0,
      4 * 16
    );
    let wvMatrix = this.getWorldViewMatrix();
    let normalMatrix = Matrix3D.inverse(wvMatrix);
    normalMatrix = normalMatrix.transpose();
    _device.queue.writeBuffer(
      _uniformBuffer,
      4 * 16 * 3 + OFFSET_SIZE*this.num,
      normalMatrix.e.buffer,
      0,
      4 * 16
    );

  }
  getProjectionMatrix() {
(中略)
  }
  getWorldMatrix() {
(中略)
  }
  getWorldViewMatrix() {
    const worldMatrix = this.getWorldMatrix();
    return Matrix3D.multiply(_camera,worldMatrix);
  }
}

【サンプルコードの解説】
preVertices配列は一時的にGuitarモデルの頂点データを保持します。
「num」プロパティはModel3Dのインスタンスの一意の番号の「_num」変数を代入します。
setMaterialメソッドは次回マテリアルを追加します。
setVメソッドはpreVertices配列に頂点(x,y,z)、法線(nx,ny,nz)、マテリアル番号、UV座標(u,v)を追加します。
setIメソッドはindices配列にインデックス(i0,i1,i2)を追加し、vertices配列に頂点(x,y,z,1)、法線(nx,ny,nz)を追加します。
getVertexCountはvertexArrayの長さをvertices配列の1頂点の7データ(1頂点と1法線のデータ数)で除算して、頂点数を取得します。
「setTransform」メソッドでプロジェクション行列のオフセットを4 * 16 * 0 + OFFSET_SIZE*this.numに、ビュー行列のオフセットを4 * 16 * 1 + OFFSET_SIZE*this.numに、ワールド行列のオフセットを4 * 16 * 2 + OFFSET_SIZE*this.numに、法線行列のオフセットを4 * 16 * 3 + OFFSET_SIZE*this.numにして、ユニフォームバッファに渡します。
法線行列はワールド行列とビュー行列を乗算した行列の逆行列を転置した行列です。

シェーダーに法線を追加

次のサンプルコード「lib」→「WGSL.js」ファイルでは、ユニフォーム定数に法線行列も加わります。法線は面の向きを示すベクトルで、法線行列はその法線ベクトルをワールドビュー変換後も正しい方向に保つための行列です。頂点シェーダーで法線に法線行列を乗算した「normal」プロパティも「VertexOutput」構造体に加えて戻り値を返します。

フラグメントシェーダーでは、vec3f(-1.0,-2.0,-4.0)を正規化したベクトルがライトの向きになります。ライトの向きに対して法線がより正反対の向きに近くなるほどよく光が当たるため、明るくなるようにグレーから白に色をグラデーションします。

・サンプルコード「lib」→「WGSL.js」ファイル
const vertWGSL = `
struct Uniforms {
  projectionMatrix : mat4x4f,
  viewMatrix : mat4x4f,
  worldMatrix : mat4x4f,
  normalMatrix : mat4x4f,
}
@group(0) @binding(0) var uniforms : Uniforms;

struct VertexOutput {
  @builtin(position) position : vec4f,
  @location(0) normal : vec3f,
}

@vertex
fn main(
  @location(0) position: vec4f,
  @location(1) normal: vec3f,
) -> VertexOutput {
  var output : VertexOutput;
  output.position = uniforms.projectionMatrix *
    uniforms.viewMatrix * uniforms.worldMatrix * position;
  output.normal = (uniforms.normalMatrix * vec4f(normal,1)).xyz;
  return output;
}
`;
const fragWGSL = `
struct VertexOutput {
  @builtin(position) position : vec4f,
  @location(0) normal : vec3f,
}
@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 color = vec3f(1.0,1.0,1.0)*clamp(diffuseLightWeighting,0.0,1.0);
  return vec4f(color.rgb,1.0);
}
`;

法線もシェーダーに渡す

複数のモデルを描画することも考慮して、ユニフォームバッファのサイズの最大値を決めます。パイプラインで頂点シェーダーの「main」関数に渡す頂点と、さらに法線(normal)もセットします。

・「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; // 4byte(float32) * 4x4 matrix * 4
const OFFSET_SIZE = UNIFORM_BUFFER_SIZE; //256の倍数
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',
  });
  // create a render pipeline
  _pipeline = setPipeline(vertWGSL,fragWGSL);
  _uniformBuffer = _device.createBuffer({
    size: UNIFORM_BUFFER_SIZE + OFFSET_SIZE * MAX_NUM,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });
  window.addEventListener("resize", resize, false);
	resize();
  init();
  requestAnimationFrame(everyFrame.bind(everyFrame));
}

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',
        },
      ],
      arrayStride: 4*(4+3), // 4byte(float32) × (4+3)個
      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;
}
(後略)

【サンプルコードの解説】
「_num」変数は一意のモデルの番号です。
「MAX_NUM」定数はモデルの最大数を1万個にします。
「initWebGPU」関数では「createBuffer」でサイズを UNIFORM_BUFFER_SIZE + OFFSET_SIZE * MAX_NUM に設定したユニフォームバッファを作成し、「_uniformBuffer」に代入します。
「setPipeline」関数で、頂点シェーダーの「shaderLocation」が0番の頂点座標に加えて、shaderLocationが1番の法線ベクトルをオフセット4*4(なぜならshaderLocationが0番の頂点がfloat32の4バイト*4だから)にセットします。「arrayStride(配列の歩)」には1頂点ごとのサイズとして1頂点と1法線のサイズ「4*(4+3)」を渡します。

【コラム】「プログラミング記事」

今まで書いてきた本やWeb記事はある程度プログラミングの知識がある人向けでしたが、「初めてプログラミングをする人にどう教えるか」を考えたら、やっぱり「頭を使ってプログラミングすればすぐに理解できる」ということです。

「写経」というコードを書き写す勉強方法を耳にしますが、まず意味はありません。考えてコードを書けばすぐにプログラミングができるようになります。「丸覚え」ではなく「理解」です。「こうしたら動く」「ここがこう間違っているから、こうしたら良い」と考えれば良いだけです。記憶するにも、丸暗記より映像をイメージする方が覚えられますね?「プログラミングは頭を使う」とはそういうことなのです。

おわりに

今回はBlenderでギターの3Dモデルを作成し、それをUltraMotion3Dで描画しました。さらに法線も反映して、面の向きに合わせて陰影も加えてみました。

次回は、さらにギターに白以外の色を反映したマテリアルを設定していきます。

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

連載バックナンバー

開発言語技術解説
第10回

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

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

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

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

オブジェクトの頂点変換やピクセルを色付けするシェーダー言語「WGSL」の文法を学ぼう

2025/9/17
第8回の今回は、WebGPUで使うシェーダー言語「WGSL」の頂点シェーダーとフラグメントシェーダーの文法を解説します。

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

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

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

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