background-image

【Three.js】 WebGPUで球体を頂点群表示する方法|Pointsが使えない場合のinstanceMesh活用

投稿日:
Three.js WebGPUで球体を頂点群表示する方法|Pointsが使えない場合のinstanceMesh活用

球体を頂点群で描画する(WebGL)

WebGLRenderer+GLSLシェーダーを使って3Dオブジェクトを頂点群で表現しようと思った場合は、下記のようなコードで実現できます。

 //Geometryの設定 
 const geometry = new THREE.SphereGeometry(200, 30, 30);

 //Materialの定義
  const material = new THREE.ShaderMaterial({
    vertexShader,
    fragmentShader,
  });

  //Mesh化
  const sphere = new THREE.Points(geometry, material);
  scene.add(sphere);
//vertexShaderのコード
void main() {
 gl_PointSize = 10.0; // 頂点の大きさを変更
 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
//fragmentShaderのコード
void main(){
  //0.0~1.0までの座標を持つgl_PointCoordと中点(0.5, 0.5)の距離が0.5を超える場合
  if(distance(gl_PointCoord, vec2(0.5, 0.5)) > 0.5) {
    //fragmentを破棄する(つまり円になる)
    discard;
  }
  gl_FragColor = vec4(1.0, 0., 0., 1.);// 頂点は赤色で描画
}

レンダリング結果

もしくはシェーダーを使用して複雑なエフェクトを組む予定がなければもっと簡単に、PointsMaterialを使用してsizeの値を変えてしまえばそれで事足ります。

  const material = new THREE.PointsMaterial({ color: 0xff00000, size: 10 }); //頂点の色と大きさを設定
  const sphere = new THREE.Points(geometry, material);

※シェーダーで頂点の加工を行っていないので、各頂点の見た目は正方形になっています。

ですが、タイトルにもある通りWebGPUではこのように簡単にはいきません。

WebGPUでは頂点のサイズは1pxに限定される

これはPointsNodeMaterialの公式ドキュメントにも書かれています。

例えば、下記のようにPointsNodeMaterialを使用してsizeNodeを変更したとします。(10倍)

  //WebGPUでのレンダリング
 const renderer = new THREE.WebGPURenderer({ antialias: true });
 await renderer.init();

 //Geometryの設定 
 const geometry = new THREE.SphereGeometry(200, 30, 30);

 //Materialの定義
  const material = new THREE.PointsNodeMaterial({
    color: 0xff0000,
    sizeNode: 10.0,
  });

結果はご覧の通り、頂点サイズに変化はありません。

instanceMeshで頂点を描画する

WebGPUではTSLを使用しても、PointsNodeMaterialを使用しても頂点サイズを変更できないことが分かったので、今回はinstanceMeshというものを使用して頂点を別の3Dオブジェクトとして扱い疑似的に頂点を作成するという方法で行っていきます。
そうすれば、サイズも色も自由に変更可能です。

具体的には下記のような手順で行っていきます。

具体的な手順
  • 1.元となる球体を準備
  • 2.元の球体から頂点の情報を取得(頂点数や配置場所)
  • 3.各頂点となるinstanceMeshを作成
  • 4.instanceMeshのattributeに元の球体の頂点情報を与える
  • 5.TSLを使用してattribute情報をmaterialに適用
import * as THREE from "three/webgpu";
import {
  attribute,
  uniform,
  uv,
  Fn,
  vec4,
  positionLocal,
} from "three/tsl";
import fragment from "./fragment.js";
import vertex from "./vertex.js";



// 1.元となる球体を準備 
 const geometry = new THREE.SphereGeometry(200, 30, 30);

  // 2.元の球体から頂点の情報を取得(頂点数や配置場所)
  const vertexCount = sphereGeometry.attributes.position.count;
  const positionArray = sphereGeometry.attributes.position.array;

  // 3.各頂点となるinstanceMeshを作成
  const instanceGeometry = new THREE.SphereGeometry(1, 8, 8);
  // Materialの設定
  const material = new THREE.MeshBasicNodeMaterial();

  // InstancedMeshを作成
  const instancedMesh = new THREE.InstancedMesh(
    instanceGeometry,
    material,
    vertexCount,
  );


  // 4.instanceMeshのattributeに元の球体の頂点情報を与える
  const instancePositions = new Float32Array(vertexCount * 3);
  for (let i = 0; i < vertexCount; i++) {
    //元の球体のi番目の頂点のx座標をinstanceMeshのi番目の頂点のx座標に代入
    instancePositions[i * 3] = positionArray[i * 3];
    //元の球体のi番目の頂点のy座標をinstanceMeshのi番目の頂点のy座標に代入
    instancePositions[i * 3 + 1] = positionArray[i * 3 + 1];
    //元の球体のi番目の頂点のz座標をinstanceMeshのi番目の頂点のz座標に代入
    instancePositions[i * 3 + 2] = positionArray[i * 3 + 2];
  }
  instancedMesh.geometry.setAttribute(
    "instancePosition",
    new THREE.InstancedBufferAttribute(instancePositions, 3),
  );


  // 5.TSLを使用してattribute情報をmaterialに適用

  // シェーダーに与えたい変数を定義
  const uPointSize = uniform(5.0);
  
  // instanceMeshのattributeの取得
  const aInstancePosition = attribute("instancePosition");

  // vertexとfragmentの両方で使用したい変数
  const vUv = uv(); 

  // vertex関数とfragment関数に引数として渡すぃオブジェクト
  const options = {
    aInstancePosition,
    uPointSize,
    vUv,
  };

  const customVertex = Fn(() => {
    // vertex関数で変形後の球体頂点位置を計算
    const transformedInstancePos = vertex(options);

    // instanceMeshのローカル頂点位置
    const localPos = positionLocal;

    // 任意の頂点サイズを適用
    const scaledLocalPos = localPos.mul(5.0);

    // 最終位置 = 変形後の球体頂点位置 + サイズ適用された小球の頂点位置
    const finalPos = transformedInstancePos.add(scaledLocalPos);

    return vec4(finalPos, 1.0);
  })();

  material.positionNode = customVertex;

  // 頂点色の定義
  const color = fragment(options);
  material.colorNode = color;

 scene.add(instancedMesh);
export default function vertex(opt) {
  return Fn(() => {
    const {  
     aInstancePosition,
     uPointSize,
     vUv,
    } = opt;


    const pos = aInstancePosition.toVar();

    //頂点を変形させたり、アニメーションさせたい場合は元の球体の頂点情報であるposを使用して行うが、ここでは省略

    return pos;
  })();
}
export default function fragment(opt) {
  return Fn(() => {
    const {  
     aInstancePosition,
     uPointSize,
     vUv,
    } = opt;

  // optionの情報などを利用して、各頂点の色を変更したい場合はここに記述。今回は省略

    const color = vec4(1.0, 0.0, 0.0, 1.0); // とりあえず頂点はすべて赤色で表示

    return color;
  })();
}

レンダリング結果はこのようになります。

今回はアニメーションや変形を行わずにただ表示するだけなので、vertex関数やfragment関数はほぼ空の状態です。
例えば元の球体の頂点座標である「aInstancePosition」をシェーダーを使用して変形させることで、instanceMeshの配置もそれに合わせて移動します。

fragmentに関しては新たに元の球体のUV座標を取得して、シェーダーで色を変えたりすることも可能です。

©UCHIWA Creative Studio.all rights reserved.