【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オブジェクトとして扱い疑似的に頂点を作成するという方法で行っていきます。
そうすれば、サイズも色も自由に変更可能です。
instanceMeshとは
同じ形状・同じマテリアルのオブジェクトを大量に描画するときに使う Three.js の仕組みです。
ひとことで言うと「1回の描画命令で、同じMeshを何個も描くための仕組み」です。
具体的には下記のような手順で行っていきます。
- 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座標を取得して、シェーダーで色を変えたりすることも可能です。

