three.js, WebGPU에서 지오메트리를 구성하는 좌표에 파티클 배치하기

위의 결과를 위해서 3가지 방식이 있습니다. 첫번째는 …

private setupModel() {
  const knotGeometry = new THREE.TorusKnotGeometry(1, 0.3, 256, 64);
  const positionAttribute = knotGeometry.attributes.position;
  const count = positionAttribute.count;

  const instancePositions = new Float32Array(count * 3);
  for (let i = 0; i < count; i++) {
    instancePositions[i * 3] = positionAttribute.getX(i);
    instancePositions[i * 3 + 1] = positionAttribute.getY(i);
    instancePositions[i * 3 + 2] = positionAttribute.getZ(i);
  }

  const spriteMaterial = new THREE.SpriteNodeMaterial({
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending
  });

  const dist = uv().distance(vec2(0.5, 0.5));
  const alphaNode = dist.smoothstep(0.5, 0.0);

  spriteMaterial.colorNode = color(0xff0000);
  spriteMaterial.opacityNode = alphaNode;

  const instancePositionNode = instancedBufferAttribute(
    new THREE.InstancedBufferAttribute(instancePositions, 3)
  );

  spriteMaterial.positionNode = instancePositionNode;
  spriteMaterial.scaleNode = vec2(.05);

  // const spriteGeometry = new THREE.PlaneGeometry(0.08, 0.08);
  // const instancedSprites = new THREE.Mesh(spriteGeometry, spriteMaterial);
  const instancedSprites = new THREE.Sprite(spriteMaterial); // 상기 코드와 동일
  instancedSprites.count = count;

  this.scene.add(instancedSprites);
}

두번째 방식은 ...

private setupModel() {
  const knotGeometry = new THREE.TorusKnotGeometry(1, 0.3, 256, 64);
  const positionAttribute = knotGeometry.attributes.position;
  const count = positionAttribute.count; // 정점 개수

  const spriteMaterial = new THREE.SpriteNodeMaterial({
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending
  });

  const dist = uv().distance(vec2(0.5, 0.5));
  const alphaNode = dist.smoothstep(0.5, 0.0);

  spriteMaterial.colorNode = color(0xff0000);
  spriteMaterial.opacityNode = alphaNode;
  spriteMaterial.vertexNode = billboarding({
    horizontal: true,
    vertical: true,
    horizontalRotation: false
  });

  const spriteGeometry = new THREE.PlaneGeometry(0.05, 0.05);
  const instancedSprites = new THREE.InstancedMesh(spriteGeometry, spriteMaterial, count);

  const dummy = new THREE.Object3D();
  const vertex = new THREE.Vector3();

  for (let i = 0; i < count; i++) {
    vertex.fromBufferAttribute(positionAttribute, i);

    dummy.position.copy(vertex);
    dummy.updateMatrix();

    instancedSprites.setMatrixAt(i, dummy.matrix);
  }
  instancedSprites.instanceMatrix.needsUpdate = true;

  this.scene.add(instancedSprites);
}

세번째 방식은 다음과 같습니다.

private setupModel() {
  const knotGeometry = new THREE.TorusKnotGeometry(1, 0.3, 256, 64);
  const positionAttr = knotGeometry.attributes.position;
  const count = positionAttr.count;

  const positionArray = new Float32Array(count * 3);
  for (let i = 0; i < count; i++) {
    positionArray[i * 3] = positionAttr.getX(i);
    positionArray[i * 3 + 1] = positionAttr.getY(i);
    positionArray[i * 3 + 2] = positionAttr.getZ(i);
  }

  const positionStorageBuffer = new THREE.StorageBufferAttribute(positionArray, 3);
  const positionStorageNode = storage(positionStorageBuffer, 'vec3', count);

  const spriteMaterial = new THREE.SpriteNodeMaterial({
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending
  });

  const dist = uv().distance(vec2(0.5, 0.5));
  const alphaNode = dist.smoothstep(0.5, 0.0);

  spriteMaterial.colorNode = color(0xff0000);
  spriteMaterial.opacityNode = alphaNode;

  spriteMaterial.positionNode = positionStorageNode.element(THREE.TSL.instanceIndex);

  const spriteGeometry = new THREE.PlaneGeometry(0.08, 0.08);
  const instancedSprites = new THREE.Mesh(spriteGeometry, spriteMaterial);

  instancedSprites.count = count;

  this.scene.add(instancedSprites);
}

three.js, TSL에서 compute 쉐이더를 위한 버퍼 생성 코드

컴퓨트 쉐이더를 위한 버퍼를 생성하기 위한 일반적인 코드는 다음과 같습니다.

const positionBuffer = new THREE.StorageInstancedBufferAttribute(count, 3, Float32Array)
const positionStorage = storage(positionBuffer, 'vec3', count)

위의 코드는 다음 한줄로 줄일 수 있습니다.

const positionStorage = instancedArray(count, 'vec3')

three.js, 이동, 회전, 스케일에 대한 행렬 만들기

2가지 방식이 있습니다. 먼저 Object3D의 position, scale, rotation을 지정해 행렬을 뽑아내는 다음의 방식입니다.

const matrixArray = new Float32Array(count * 16);
const matrixBuffer = new THREE.InstancedBufferAttribute(matrixArray, 16);

const dummy = new THREE.Object3D();

for (let i = 0; i < count; i++) {
  dummy.position.set(i * 2, 0, 0);
  dummy.rotation.y = Math.PI * 0.25 * i;
  dummy.updateMatrix();

  // Matrix4 데이터를 Float32Array의 i * 16 위치에 복사
  dummy.matrix.toArray(matrixArray, i * 16);
}


2번째는 직접 행렬을 통해서 얻는 아래의 방식입니다.

const matricesArray = new Float32Array(count * 16)
const matricesBuffer = new THREE.InstancedBufferAttribute(matricesArray, 16)

for (let i = 0; i < count; i++) {
    const progress = i / (count - 1)
    const position = new THREE.Vector3((progress - 0.5) * 4, 0, 0)
    const scale = new THREE.Vector3(1, 1, 1)
    const rotation = new THREE.Quaternion()
        .setFromEuler(new THREE.Euler(0, progress * 3, 0))

    const matrix = new THREE.Matrix4().compose(position, rotation, scale)
    matrix.toArray(matricesArray, i * 16)
}

three.js, 사용자 정의 TSL 노드 (add 노드를 예시로…)

three.js의 TSL에서 재사용이 가능한 코드를 TSL 노드화하여 제공하면 DX를 향상시킬 수 있습니다. 이미 제공하고 있는 add 노드와 동일하게 작동하는 myAdd 노드를 예시로 사용자 정의 노드를 만드는 코드입니다.

두가지 방식이 가능하며 첫번째는 프리셋 노드를 활용하는 방식입니다.

class MyAddNode extends THREE.TempNode {
  static get type() {
    return 'MyAddNode';
  }

  constructor(aNode, bNode) {
    super('float'); // 출력 타입을 float로 고정

    this.aNode = aNode;
    this.bNode = bNode;
  }

  // 실제 연산 로직 - 기존 TSL 연산자를 조합해서 구성
  setup() {
    const { aNode, bNode } = this;

    return float(aNode).add(float(bNode));
  }
}

export default MyAddNode;

이렇게 정의된 노들 함수형 문법과 메서드 체이닝 문법으로 사용될 수 있도록 하는 코드는 다음과 같습니다.

export default MyAddNode;

// 함수형 문법으로 노출: myAdd( a, b )
export const myAdd = /*@__PURE__*/ nodeProxy(MyAddNode).setParameterLength(2).setName('myAdd');

// 메서드 체이닝 지원: a.myAdd( b )
addMethodChaining('myAdd', myAdd);

프리셋 노드를 사용하는 방식은 커스텀 노드 개발이 간단하다는 장점은 있으나 원하는 기능을 가진 프리셋 노드가 없을 경우 WGSL 코드나 GLSL 코드를 직접 이용하는 방식은 아래와 같습니다. 앞서 봤던 것과 동일한 기능을 가진 노드의 클래스입니다.

class MyAddNode extends THREE.TempNode {
  static get type() {
    return 'MyAddNode';
  }

  constructor(aNode, bNode) {
    super('float');
    this.aNode = aNode;
    this.bNode = bNode;
  }

  generate(builder, output) {
    const type = this.getNodeType(builder); // 'float'

    const a = this.aNode.build(builder, type);
    const b = this.bNode.build(builder, type);

    return builder.format(`( ${a} + ${b} )`, type, output);
  }
}