これだけ覚えればできる！Three.jsのGPU Instancing

#概要
WebGLには拡張機能で**インスタンシング**なるものがあります。GPUに一つだけ3Dモデルのデータをロードし、**GPU側でまとめて描画**させることで高速に大量のモデルを描画する手法です。
Three.jsにはそれをラップした**InstancedBufferGeometry**があるのでそちらの使用法を解説したいと思います。

#作るもの
![image.png](https://qiita-image-store.s3.amazonaws.com/0/280075/ee027de3-3572-6cd7-1449-88a01e230c0b.png)
Gitは[こちら](https://github.com/ukonpower/glsl-graphics/tree/master/src/gl21/js/utils/InstancingBox)です。
デモは[こちら](https://oregl.ukon.dev/gl/InstancingBox-Demo-/)です。

#どうやって作るの？
手順は以下のようになります。

1. ベースとなるモデルのジオメトリを生成
2. InstancedBufferGeometryを用意
3. InstancedBufferGeometryに**全てのモデルに共通のAttribute**（頂点座標やノーマル、インデックスなど）を設定
4. **モデルごとで異なる値のAttribute**を生成、InstancedBufferGeometryに設定
5. シェーダーを作成。
6. メッシュ（ラインなどでも）を生成

#コード

```javascript
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.ベースとなるモデルのジオメトリを生成
```javascript
let originBox = new THREE.BoxBufferGeometry(0.3,0.3,0.3);
```
Three.jsのBoxBufferGeometryです

### 2. InstancedBufferGeometryを用意

```javascript
let geo = new THREE.InstancedBufferGeometry();
```

### 3. InstancedBufferGeometryに**全てのモデルに共通のAttribute**を設定
```javascript
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**を生成、設定
```javascript
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を使ってます。

```glsl
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. メッシュ（ラインなどでも）を生成
```javascript
this.obj = new THREE.Mesh(geo, mat);
```

#できました
![image.png](https://qiita-image-store.s3.amazonaws.com/0/280075/ee027de3-3572-6cd7-1449-88a01e230c0b.png)

今回はInstancedBufferGeomtryを使ってジオメトリを効率的に使用することできました。**ComputationRendererとも相性がとても良さそうですね！**