수백만개의 BOIDS에 대한 데이터를 빠르게 처리하기 위한 방안 중 가장 최고의 선택은 GPU에서 처리하는 것입니다. 처리하고자 하는 BOID의 개수가 다음과 같다고 할때 최종적으로 GPU에서 읽고 쓸 수 있는 StorageBufferAttribute 객체를 생성하는 과정을 코드로 정리합니다.
const BOIDS = 9;
BOID의 위치값에 대한 데이터라고 한다면 먼저 Float32Array 객체를 생성합니다.
const positionArray = new Float32Array(BOIDS * 3);
이 배열 객체에 BOID의 정보를 채움니다. 아래는 격자 형태로 BOID들이 배치되도록 합니다.
const cellSize = 0.5;
for (let i = 0; i < BOIDS; i++) {
const offset = i * 3;
const row = (i % 3) - 1;
const col = (~~(i / 3)) - 1;
positionArray[offset + 0] = col * cellSize;
positionArray[offset + 1] = row * cellSize;
positionArray[offset + 2] = 0; // 이미 0으로 초기화 되어 있으므로 불필요함 코드
}
이렇게 만든 배열 객체를 StorageBufferAttribute로 생성하기 위한 코드는 다음과 같습니다.
const positionStorage = attributeArray(positionArray, "vec3");
이제 positionStorage를 통해 GPU에서 BOID의 위치를 읽고 쓸 수 있습니다.
GPU를 통해 positionStorage를 읽는 TSL 코드 예시는 다음과 같습니다.
const flockVertexTSL = Fn(() => {
const instanceID = attribute("instanceID");
const finalVert = modelWorldMatrix.mul(
positionLocal.add(positionStorage.element(instanceID))
).toConst();
return cameraProjectionMatrix.mul(cameraViewMatrix).mul(finalVert);
});
2번 코드는 positionStorage에 저장된 BOID 중 읽을 녀석에 대한 인덱스값입니다. 위의 경우는 별도의 attribute에 BOID의 인덱스를 저장해 사용하고 있습니다. flockVerteTSL은 재질의 vertexNode에 지정하면 됩니다.
만약 positionStorage에 저장된 BOID 들의 위치를 변경하고자 할때 GPU에서 수행할 수 있고 이를 Compute Shader를 통해 동시에 처리가 가능합니다. 아래는 해당 처리를 수행하는 GPU에서 실행되는 함수의 예시입니다.
const radius = uniform(float(0.7));
const delta = uniform(float(1));
const computePosition = Fn(() => {
const PI2 = float(6.2832).toConst();
const theta = PI2.div(BOIDS).toConst();
const idx = instanceIndex.toConst();
const posx = cos(time.add(theta.mul(idx))).mul(radius).toConst();
const posy = sin(time.add(theta.mul(idx))).mul(radius).toConst();
const cellSize = .5;
const row = float(idx).mod(3.0).sub(1.0).toConst();
const col = floor(float(idx).div(3.0)).sub(1.0).toConst();
const v1 = vec3(posx, posy, 0).toConst();
const v2 = vec3(col.mul(cellSize), row.mul(cellSize), 0).toConst();
positionStorage.element(idx).assign(mix(v2, v1, delta));
})().compute(BOIDS);
위의 코드 중 instanceIndex는 처리되고 있는 BOID의 인덱스입니다. 코드의 마지막에 compute 매서드를 통해 인자로 받은 개수만큼 병렬로 동시에 실행되어질 수 있도록 해줍니다. 위에서 정의된 computePosition는 (필요하다면 필드화하여) 매 프레임마다 렌더링될 때 실행해줘야 하며 아래는 그 코드 예시입니다.
this._renderer.compute(this._computePosition);
