#概要
WebGLには拡張機能でインスタンシングなるものがあります。GPUに一つだけ3Dモデルのデータをロードし、GPU側でまとめて描画させることで高速に大量のモデルを描画する手法です。
Three.jsにはそれをラップしたInstancedBufferGeometryがあるのでそちらの使用法を解説したいと思います。
#どうやって作るの?
手順は以下のようになります。
- ベースとなるモデルのジオメトリを生成
- InstancedBufferGeometryを用意
- InstancedBufferGeometryに全てのモデルに共通のAttribute(頂点座標やノーマル、インデックスなど)を設定
- モデルごとで異なる値のAttributeを生成、InstancedBufferGeometryに設定
- シェーダーを作成。
- メッシュ(ラインなどでも)を生成
#コード
let originBox = new THREE.BoxBufferGeometry(0.3,0.3,0.3);
let geo = new THREE.InstancedBufferGeometry();
let vertice = originBox.attributes.position.clone();
geo.addAttribute('position', vertice);
let normal = originBox.attributes.normal.clone();
geo.addAttribute('normals', normal);
let uv = originBox.attributes.normal.clone();
geo.addAttribute('uv', uv);
let indices = originBox.index.clone();
geo.setIndex(indices);
let offsetPos = new THREE.InstancedBufferAttribute(new Float32Array(this.num * 3), 3 );
let num = new THREE.InstancedBufferAttribute(new Float32Array(this.num * 1), 1 );
for (let i = 0; i < this.num; i++) {
let range = 5;
let x = Math.random() * range - range / 2;
let y = Math.random() * range - range / 2;
let z = Math.random() * range - range / 2;
offsetPos.setXYZ(i,x,y,z);
num.setX(i,i);
}
geo.addAttribute('offsetPos', offsetPos);
geo.addAttribute('num', num);
let cUni = {
time: {
value: 0
}
}
this.uni = THREE.UniformsUtils.merge([THREE.ShaderLib.standard.uniforms,cUni]);
this.uni.diffuse.value = new THREE.Vector3(1.0,1.0,1.0);
this.uni.roughness.value = 0.1;
let mat = new THREE.ShaderMaterial({
vertexShader: vert,
fragmentShader: THREE.ShaderLib.standard.fragmentShader,
uniforms: this.uni,
flatShading: true,
lights: true
})
this.obj = new THREE.Mesh(geo, mat);
##コード解説
順番に解説します。
1.ベースとなるモデルのジオメトリを生成
let originBox = new THREE.BoxBufferGeometry(0.3,0.3,0.3);
Three.jsのBoxBufferGeometryです
2. InstancedBufferGeometryを用意
let geo = new THREE.InstancedBufferGeometry();
3. InstancedBufferGeometryに全てのモデルに共通のAttributeを設定
let vertice = originBox.attributes.position.clone();
geo.addAttribute('position', vertice);
let normal = originBox.attributes.normal.clone();
geo.addAttribute('normals', normal);
let uv = originBox.attributes.normal.clone();
geo.addAttribute('uv', uv);
let indices = originBox.index.clone();
geo.setIndex(indices);
originBoxからposition
、normal
、UV
のattributeをコピーしてそのまま設定してます。
4. モデルごとで異なる値のAttributeを生成、設定
let offsetPos = new THREE.InstancedBufferAttribute(new Float32Array(this.num * 3), 3 );
let num = new THREE.InstancedBufferAttribute(new Float32Array(this.num * 1), 1 );
for (let i = 0; i < this.num; i++) {
let range = 5;
let x = Math.random() * range - range / 2;
let y = Math.random() * range - range / 2;
let z = Math.random() * range - range / 2;
offsetPos.setXYZ(i,x,y,z);
num.setX(i,i);
}
geo.addAttribute('offsetPos', offsetPos);
geo.addAttribute('num', num);
ここが肝です。
一つのBoxごとに位置を変えたいので、ワールド座標を示すoffsetPos
のAttributeを生成します。
こちらは個々で別の値を入れるため、InstancedBufferAttributeを使います。
### 5. シェーダー、マテリアルを作成
```javascript
let cUni = {
time: {
value: 0
}
}
this.uni = THREE.UniformsUtils.merge([THREE.ShaderLib.standard.uniforms,cUni]);
this.uni.diffuse.value = new THREE.Vector3(1.0,1.0,1.0);
this.uni.roughness.value = 0.1;
let mat = new THREE.ShaderMaterial({
vertexShader: vert,
fragmentShader: THREE.ShaderLib.standard.fragmentShader,
uniforms: this.uni,
flatShading: true,
lights: true
})
フラグメントシェーダーはThreeのStandardを使ってます。
attribute vec3 offsetPos;
varying vec3 vViewPosition;
uniform float time;
float PI = 3.141592653589793;
highp mat2 rotate(float rad){
return mat2(cos(rad),sin(rad),-sin(rad),cos(rad));
}
void main() {
vec3 pos = position;
float s = max(0.0,sin(-time * 4.0 + length(offsetPos)));
pos *= s;
pos.xz *= rotate(s * 4.0);
pos.xy *= rotate(s * 4.0);
vec4 mvPosition = modelViewMatrix * vec4(pos + offsetPos, 1.0);
gl_Position = projectionMatrix * mvPosition;
vViewPosition = -mvPosition.xyz;
}
boxを回転してから位置を移動してます。
6. メッシュ(ラインなどでも)を生成
this.obj = new THREE.Mesh(geo, mat);
今回はInstancedBufferGeomtryを使ってジオメトリを効率的に使用することできました。ComputationRendererとも相性がとても良さそうですね!