🙌🏻 해당 글은 Three.js Journey의 강의 노트입니다.
이전 레슨에서 햄버거를 Three.js로 가져왔을 때 색상이 어딘가 이상하게 출력되는 것을 살펴볼 수 있었다. 때때로 우리는 사실적인 렌더링을 원한다. 특히 웹사이트에서 실생활에서 사용하는 제품을 선보이고 싶을 때나, 3D 아티스트로서 최상의 결과로 작업을 보여주고 싶을 때 그럴 것이다. 이번 레슨에서는 어떻게 렌더링의 질을 개선할 수 있을지 배운다.
지난 레슨에 만든 햄버거를 이용할 수 도 있지만, 다양한 텍스쳐가 적용된 모델 저장소의 Flight Helmet으로 실습하도록 한다. 우선 starter 팩에는 구체가 하나 만들어져 있다. 조명 설정을 하는데 사용하도록 한다. 우선 testSphere의 재질을 MeshStandardMaterial로 변경해 조명을 확인하도록 한다.
new THREE.MeshStandardMaterial()
우리는 오직 하나의 DirectionalLight를 사용할 것이다. 조명을 더 많이 제어하고 그림자를 생성하려면, DirectionalLight가 몹시 중요하다.
const directionalLight = new THREE.DirectionalLight('#ffffff', 1)
directionalLight.position.set(0.25, 3, - 2.25)
scene.add(directionalLight)
Three.js 빛의 강도의 Default value는 현실을 그닥 잘 반영하지 못한다. 그닥 중요하지 않게 생각할 수도 있지만, 현실적이고 표준적인 값을 반영할 수 있다면, 그렇게 하는 것이 더 좋은 선택일 것이다. 보다 현실적인 값을 적용해주기 위해서는 webGLRenderer의 physicallyCorrectLights 속성을 true로 바꿔주어야 한다.
renderer.physicallyCorrectLights = true
이제 light를 세팅했기 때문에 Model을 로드해보도록 하자. 먼저, GLTFLoader를 인스턴스화한다. 모델이 압축되어 있지 않기 때문에 DRACOLoader가 필요하지는 않다.
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
const gltfLoader = new GLTFLoader();
gltfLoader.load("/models/FlightHelmet/glTF/FlightHelmet.gltf", (gltf) => {
gltf.scene.scale.set(10, 10, 10);
gltf.scene.position.set(0, -4, 0);
gltf.scene.rotation.y = Math.PI * 0.5;
scene.add(gltf.scene);
gui
.add(gltf.scene.rotation, "y")
.min(-Math.PI)
.max(Math.PI)
.step(0.001)
.name("rotation");
});
약한 DirectionalLight가 하나만 있기 때문에 모델을 제대로 확인할 수 없다. 조명은 environment map에 의해 처리된다. 우리는 Material 레슨에서 이미 environmental map에 대해 알아보았다. environmental map은 마치 주변을 둘러싼 사진과 같다. 360도 사진일 수도 있고, 큐브의 모양을 한 6장의 사진일 수도 있다. 우리는 배경과 모델을 비추기 위해 environmental map을 사용할 것이다.
/**
* Environment map
*/
const environmentMap = cubeTextureLoader.load([
'/textures/environmentMaps/0/px.jpg',
'/textures/environmentMaps/0/nx.jpg',
'/textures/environmentMaps/0/py.jpg',
'/textures/environmentMaps/0/ny.jpg',
'/textures/environmentMaps/0/pz.jpg',
'/textures/environmentMaps/0/nz.jpg'
])
environment map을 scene의 배경으로 추가하려면, 일단 거대한 큐브를 만들고, 안쪽에서도 큐브의 면을 볼 수 있도록 세팅해주어야 한다. 그리고 나서 texture를 적용하면 된다.
scene.background = environmentMap
리얼한 렌더의 핵심은 environment map을 통해 모델을 비추는 것이다. envMap 속성을 사용해 MeshStandardMaterial에 environment map을 적용하는 방법을 이미 배웠다. 문제는 우리의 모델이 많은 Mesh로 구성되어 있다는 것이다. 우리는 traverse() 메서드와 Group 및 Mesh와 같은 상속 클래스를 사용할 수 있다. 콜백을 통해 이를 처리하지 않고, updateAllMaterials 함수를 만든다.
const updateAllMaterials = () => {
scene.traverse((child) => {
console.log(child);
});
};
콘솔창에 children을 확인할 수 있다. 사실 우리가 하고 싶은 것은 children을 콘솔에 찍는 것이 아니라, environment map을 적용하고 싶은 것이 아니다. environment map을 조명, 카메라, 그룹에 적용하는 것은 의미가 없다. 우리는 Mesh에만 environment map를 적용하고 싶다.
const updateAllMaterials = () => {
scene.traverse((child) => {
if (
child instanceof THREE.Mesh &&
child.material instanceof THREE.MeshStandardMaterial
) {
child.material.envMap = environmentMap;
child.material.envMapIntensity = 5;
}
});
};
environment map을 적용하는 더 쉬운 방법이 있다. 우리는 scene의 environment 속성을 사용할 수 있다. 물론 이러한 방법의 경우 scene에서 직접 각 재질의 environment map의 강도를 변경할 수 없다.
scene.environment = environmentMap
이제 색상에 대한 작업이 필요하다. 이는 WebGLRenderer를 통해 작업해줄 수 있다.
outputEncoding
속성은 출력 렌더링 인코딩을 제어한다.
renderer.outputEncoding = THREE.sRGBEncoding
또 다른 가능한 값은 THREE.GammaEncoding
이다. 감마 인코딩은 사람의 눈 감도에 따라 밝고 어두운 값이 저장되는 방식을 최적화해 색상을 저장하는 방식이다. sRGBEncoding을 이용하는 것은 일반적인 값 2.2의 기본 감마 계수로 GammaEncoding을 하는 것과 같다. 더 많은 것을 컨트롤 할 수 있기 때문에 감마인코딩이 더 좋게 들릴 수 있지만, 물리적으로 그닥 바람지갛지 않다. "밝기"를 관리하는 더 나은 방법들이 있다.
눈치챘을 수 있지만, 현재 environment map 색상이 어딘가 이상하다. 이대로 맘에 들 수 있지만, 올바른 색상을 유지할 줄은 일단 알아야 한다. 문제는 우리의 renderer outputEncoding이 THREE.sRGBEncoding라는 것이다. 또한 environment map texture는 기본적으로 THREE.LinearEncoding이다. 우리는 우리가 직접 볼 수 있는 모든 texture는 THREE.sRGBEncoding로, 그렇지 않은 모든 것들의 texture는 THREE.LinearEncoding으로 인코딩할 것이다.
environmentMap.encoding = THREE.sRGBEncoding
Tone mapping은 HDR값을 LDR값으로 변환한다. Tone mapping효과를 통해 더 사실적인 결과를 얻을 수 있다. Tone mapping을 변경하려면 WebGLRenderer에서 Tone mapping 속성을 업데이트하면 된다. 가능한 값은 여러가지가 있다.
renderer.toneMapping = THREE.ACESFilmicToneMapping
우리는 Tone mapping의 노출을 변경할 수도 있다.
renderer.toneMappingExposure = 3
geometry의 가장자리를 확대했을 때 계단과 같은 이미지가 보이는 것을 aliasing이라고 부른다. 현재 우리의 모델은 세부사항이 많기 때문에 괜찮지만 픽셀 비율이 1인 화면으로 가장자리를 보면 aliasing을 확인할 수 있다. 픽셀 렌더링이 발생하면 해당 픽셀에서 어떤 지오메트리가 렌더링되는지를 테스트하는데, 보통 가장자리는 화면 픽셀의 수직선 및 수평선과 완벽하게 정렬되지 않아 발생하는 문제이다. 한가지 쉬운 해결책은 렌더의 해상도를 높이는 것이다. 물론 쉬운 접근이지만 성능에 문제가 생길 수 있다. 또 다른 해결책은 다중 샘플링이다. 렌더의 해상도를 높이긴 하지만, 도형의 가장자리에서만 렌더링을 하는 것이다. 그런다음 픽셀값을 평균화해 최종 픽셀값을 얻는다. Three.js에서도 다중 샘플링을 적용할 수 있다!
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
})
안티앨리어싱을 사용하면 리소스가 소모되긴한다. 바람직한 방식은 픽셀비율이 2 미만인 화면에서만 이를 활성화하는 것이다.
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
directionalLight.castShadow = true
directionalLight.shadow.camera.far = 15
directionalLight.shadow.mapSize.set(1024, 1024)
child.castShadow = true
child.receiveShadow = true
이제 드디어 지난 레슨에서 만들었던 hamburger를 적용해볼 시간이다.
gltfLoader.load(
'/models/hamburger.glb',
(gltf) =>
{
gltf.scene.scale.set(0.3, 0.3, 0.3)
gltf.scene.position.set(0, - 1, 0)
scene.add(gltf.scene)
updateAllMaterials()
}
)
확대하면 표면이 지글지글해지는 문제가 생긴다! 이런 것을 shadow acne라고 부른다.
directionalLight.shadow.normalBias = 0.05