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

매시는 하나의 지오메트리와 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 모델링 툴에서 만들어진 모델들의 구성을 이해할 수 있게 됩니다.

물리적 광원에 대한 개별 노드 지정 (TSL 방식)

three.js 쉐이더 언어인 TSL에서 물리적 광원에 대한 개별 노드를 지정하는 방식을 정리해 봅니다. TSL을 통해 표현하고자 하는 재질은 다음과 같습니다.

코드는 다음과 같습니다.

const geometry = new THREE.IcosahedronGeometry(1, 64);
const material = new THREE.MeshStandardNodeMaterial({
  color: "black",
  // wireframe: true,
});
const mesh = new THREE.Mesh(geometry, material)

const path = './Metal053B_2K-JPG';

const texColor = new THREE.TextureLoader().load(`${path}/Metal053B_2K-JPG_Color.jpg`);
material.colorNode = texture(texColor);

const texNormal = new THREE.TextureLoader().load(`${path}/Metal053B_2K-JPG_NormalGL.jpg`);
material.normalNode = normalMap(texture(texNormal), float(1.));

const texMetalness = new THREE.TextureLoader().load(`${path}/Metal053B_2K-JPG_Metalness.jpg`);
material.metalnessNode = mul(texture(texMetalness), 1.);

const texRoughness = new THREE.TextureLoader().load(`${path}/Metal053B_2K-JPG_Roughness.jpg`);
material.roughnessNode = mul(texture(texRoughness), float(0.7));

광원 노드에 집중하기 위해서 텍스쳐 데이터를 사용했습니다. 텍스쳐 본연의 표현을 위해 재질의 기본 색상을 블랙으로 지정했구요. 사용한 노드는 colorNode, normalNode, metalnessNode, roughnessNode입니다.

광원에 대한 노드는 아니지만 Displacement에 대한 노드를 알아보기 위해 표현하고자 하는 재질은 다음과 같습니다.

코드는 다음과 같습니다.

const geometry = new THREE.IcosahedronGeometry(1, 64);
const material = new THREE.MeshStandardNodeMaterial({
  color: "black",
  // wireframe: true,
});
const mesh = new THREE.Mesh(geometry, material)

const path = './Rock058_2K-JPG';

...

const texAO = new THREE.TextureLoader().load(`${path}/Rock058_2K-JPG_AmbientOcclusion.jpg`);
geometry.setAttribute('uv2', new THREE.BufferAttribute(geometry.attributes.uv.array, 2));
material.aoNode = mul(texture(texAO), float(1.));

const texDisplacement = new THREE.TextureLoader().load(`${path}/Rock058_2K-JPG_Displacement.jpg`);
const displacementNode = texture(texDisplacement);
const displaceStrength = 0.3;
const displacementValue = displacementNode.r.sub(0.5).mul(displaceStrength);
const newPosition = positionWorld.add(normalLocal.mul(displacementValue));
material.positionNode = newPosition;

Displacement 표현을 위해서는 Vertex Shader에 해당하는 positionNode를 이용해야 합니다. 추가로 AO 노드에 대한 사용 코드도 보입니다.