Instanced Mesh in Shader

인스턴스드 매시는 다음처럼 생성할 수 있습니다. 지오메트리의 좌표 구성을 위해 BoxGeometry의 것을 가져다 쓰는 경우입니다. 지오메트리의 index와 position만을 필요로 하니 아래처럼 했고, 그냥 new THREE.InstancedBufferGeometry.copy(baseGeometry)로 하면 지오메트리를 그대로 복사합니다.

const baseGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const geometry = new THREE.InstancedBufferGeometry();

geometry.index = baseGeometry.index;
geometry.attributes.position = baseGeometry.attributes.position;

// 위의 코드는 참조인지라 아래의 코드로 대체하는게 맞죠.
// geometry.setIndex(baseGeometry.index);
// geometry.setAttribute("position", baseGeometry.attributes.position);

인스턴스로 만들 개수를 지정해야 합니다.

const count = 100;
geometry.instanceCount = count;

인스턴스화된 것들에 대한 개별 요소들은 위치, 회전, 크기, 색상에 대해 개별적으로 지정이 가능한데 위치와 색상에 대한 지정 코드입니다.

const offsets = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
  offsets[i * 3 + 0] = (Math.random() - 0.5) * 10; // x
  offsets[i * 3 + 1] = (Math.random() - 0.5) * 10; // y
  offsets[i * 3 + 2] = (Math.random() - 0.5) * 10; // z
}
geometry.setAttribute("instanceOffset", new THREE.InstancedBufferAttribute(offsets, 3));

const colors = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
  colors[i * 3 + 0] = Math.random(); // R
  colors[i * 3 + 1] = Math.random(); // G
  colors[i * 3 + 2] = Math.random(); // B
}
geometry.setAttribute("instanceColor", new THREE.InstancedBufferAttribute(colors, 3));

쉐이더를 통해 직접 인스턴스 매시를 렌더링하기 위해 재질을 설정하는 코드입니다.

const material = new THREE.ShaderMaterial({
  vertexShader: /*glsl*/ `
    attribute vec3 instanceOffset;
    attribute vec3 instanceColor;
    varying vec3 vColor;
    
    void main() {
      vec3 transformed = position + instanceOffset;
      vColor = instanceColor;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
    }
  `,
  fragmentShader: /*glsl*/ `
    varying vec3 vColor;
    void main() {
      gl_FragColor = vec4(vColor, 1.0);
    }
  `
});

이제 장면에 매시를 넣으면 화면에 딱... 표시되어야 합니다.

const mesh = new THREE.Mesh(geometry, material);
this._scene.add(mesh);

하나의 지오메트리에 대한 매시에 여러 개의 재질 반영하기

매시는 하나의 지오메트리와 2개 이상의 재질로 정의됩니다. three.js 중급 개발자라면 메시가 1개가 아닌 2개 이상의 재질로 정의된다는 점에 의구심을 가질 수 있을텐데요. 하지만 맞습니다. 매시는 2개 이상의 재질을 갖습니다. 하지만 지오메트리는 1개입니다. 이상하죠? 지오메트리가 1개면 그에 대한 재질도 1개여야 맞는거 같은데 말이죠. 그래서 지오메트리의 구성 좌표를 그룹화할 수 있습니다. 재질이 2개라면 2개의 그룹으로 지오메트리의 구성 좌표를 구성하는거죠.

예를들어 다음과 같은 결과를 봅시다.

SphereGeometry로 만든 매시입니다. 그런데 위 아래로 다른 재질이 부여되어 있습니다. 마치 2개의 매시를 뭍여 놓은 것처럼요.

아래는 코드입니다.

const geom = new THREE.SphereGeometry(2);

const indexCount = geom.index.count;
const midIndex = Math.floor(indexCount / 2);

geom.clearGroups();
geom.addGroup(0, midIndex, 0);
geom.addGroup(midIndex, indexCount - midIndex, 1);

const mesh = new THREE.Mesh(geom, [
    new THREE.MeshPhysicalMaterial({ metalness: 1, roughness: 0 }),
    new THREE.MeshNormalMaterial()
]);

scene.add(mesh);

지오메트리에 대한 2개의 그룹핑, 2개의 재질을 적용해 만든 매시에 대한 코드가 보이죠? 지오메트리에 대한 정점의 그룹핑 단위는 인덱스(삼각형을 구성하는 인덱스)입니다. 하지만 경우에 따라서 인덱스가 아닌 정점 하나 하나를 지정해서 만든 넌-인덱스 방식도 존재합니다. 아래는 결과는 동일하지만 넌-인덱스 방식의 지오메트리에 대한 그룹핑 코드입니다.

let geom = new THREE.SphereGeometry(2, 128, 64);
geom = geom.toNonIndexed(); // Non-indexed 지오메트리

const vertexCount = geom.getAttribute("position").count;
const midVertex = Math.floor(vertexCount / 2);

geom.clearGroups();
geom.addGroup(0, midVertex, 0);
geom.addGroup(midVertex, vertexCount - midVertex, 1);

const mesh = new THREE.Mesh(geom, [
    new THREE.MeshPhysicalMaterial({ metalness: 1, roughness: 0 }),
    new THREE.MeshNormalMaterial()
]);

scene.add(mesh);

이번에는 지오메트리에 대한 2개의 그룹을 만들기 위해 버텍스에 대한 인덱스를 사용한 것을 알 수 있습니다.

여튼 지금까지 봤던 three.js에서의 지오메트리에 대한 그룹핑과 매시에 여러개의 재질을 지정할 수 있다는 것을 아셔야, 3D 모델링 툴에서 만들어진 모델들의 구성을 이해할 수 있게 됩니다.