🙌🏻 해당 글은 Three.js Journey의 강의 노트입니다.
빛을 비추면 Object의 뒷면이 까매지고, 이를 core shadow라고 한다. 우리에게 현재 없는 것은 drop shadow이다. object 외부에 생기는 shadow. 개발자들에게 합리적인 주사율로 그림자를 렌더링하는 것은 언제나 도전이었다. Three.js에도 방법이 있지만 완벽하다고 하기는 힘들다.
렌더를 수행하면 Three.js는 그림자를 드리울 부분을 렌더한다. 이러한 렌더는 마치 빛이 카메라인 양 시뮬레이트한다. 그 결과는 shadow maps라고 부르는 텍스쳐에 저장된다. shadow map을 직접 볼 수는 없지만, 그림자를 받고 geometry에 투사될 모든 material에 사용된다.
renderer.shadowMap.enabled = true
sphere.castShadow = true
plane.receiveShadow = true
directionalLight.castShadow = true
위와 같이 적용을 해주고 나면 그림자가 scene에 보이는 것을 확인할 수 있다.
이 때 유의해야 할 점은 오직 PointLight, DirectionalLight, SpotLight만이 shadow를 지원한다는 것이다.
우리는 light의 shadow 프로퍼티를 통해서 shadow map에 접근할 수 있다.
기본적으로 shadow map의 크기는 512x512이다! 이를 개선하기 위해 아래와 같이 설정해줄 수 있다.
directionalLight.shadow.mapSize.width = 1024
directionalLight.shadow.mapSize.height = 1024
그림자의 테두리가 조금 더 선명해졌다.
Three.js는 카메라(여기서는 directionalLight.shadow.camera)를 이용해 shadow map을 렌더링한다. 카메라에서 인자로 근거리와 원거리를 전달하듯이 shadow map에서도 이를 설정해주어야 한다. 그림자의 품질 개선보다는 그림자기 갑자기 보이지 않거나 잘리는 버그를 방지할 수 있다.
directionalLight.shadow.camera.near = 1;
directionalLight.shadow.camera.far = 6;
위 캡쳐이미지에서도 확인할 수 있듯이, amplitude가 너무 크다. DirectionalLight를 사용하고 있기 때문에 Three.js는 OrthographicCamera를 사용한다. 우리는 카메라의 top, bottom, left, right를 제어할 수 있다. 값이 작아질 수록 그림자가 선명해진다.
directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.bottom = - 2
directionalLight.shadow.camera.left = - 2
radius 속성으로 Blur를 컨트롤할 수 있다.
카메라의 근접성을 이용하는 것이 아닌 방식이다.
directionalLight.shadow.radius = 10
shadow map에 다양한 알고리즘을 적용할 수 있다.
THREE.BasicShadowMap
: 성능 Good 품질 BadTHREE.PCFShadowMap
: 성능 Bad 가장자리 SmoothTHREE.PCFSoftShadowMap
: 성능 Bad 가장자리 SmootherTHREE.VSMShadowMap
: 성능 Bad 더 제약적, 예상 못한 결과를 마주할 수 있다.renderer.shadowMap.type = THREE.PCFSoftShadowMap
빛을 여러 개 사용하면, 이들이 개별적으로 제어되기 때문에 잘 merge 되지 않는다.
이에 대해 할 수 있는 것은 많지 않다.
const spotLight = new THREE.SpotLight(0xffffff, 0.4, 10, Math.PI * 0.3);
spotLight.castShadow = true;
spotLight.position.set(0, 2, 2);
scene.add(spotLight);
scene.add(spotLight.target);
const spotLightCameraHelper = new THREE.CameraHelper(spotLight.shadow.camera);
scene.add(spotLightCameraHelper);
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.fov = 30;
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 6;
spotLightCameraHelper.visible = false;
const pointLight = new THREE.PointLight(0xffffff, 0.3);
pointLight.castShadow = true;
pointLight.shadow.mapSize.width = 1024;
pointLight.shadow.mapSize.height = 1024;
pointLight.shadow.camera.near = 0.1;
pointLight.shadow.camera.far = 5;
pointLight.position.set(-1, 1, 0);
scene.add(pointLight);
const pointLightCameraHelper = new THREE.CameraHelper(pointLight.shadow.camera);
pointLightCameraHelper.visible = false;
scene.add(pointLightCameraHelper);
Three.js shadow는 단순한 단면에는 유용하지만, 반대의 경우 지저분해지기 십상이다.
Light에서도 이야기한 바와 같이 shadow도 texture에 Baking이 가능하다!
우선 아래와 같이 모든 그림자를 없애보자!
renderer.shadowMap.enabled = false
그리고 아예 이미지 파일을 로드해 plane에 입혀버린다.
/**
* Textures
*/
const textureLoader = new THREE.TextureLoader()
const bakedShadow = textureLoader.load('/textures/bakedShadow.jpg')
const plane = new THREE.Mesh(
new THREE.PlaneGeometry(5, 5),
new THREE.MeshBasicMaterial({
map: bakedShadow
})
)
딱 봐도 훨씬 퀄리티가 좋지만, 문제는 dynamic한 것이 아니라는 점!
sphere의 위치를 조금만 바꿔주면 엉성함이 탄로난다.
덜 realistic하지만, 더 dynamic한 대안이 존재한다.
그림자(이미지)를 구와 함께 이동하도록 만드는 것이다.
// 구 그림자
const sphereShadow = new THREE.Mesh(
new THREE.PlaneGeometry(1.5, 1.5),
new THREE.MeshBasicMaterial({
color: 0x000000,
transparent: true,
alphaMap: simpleShadow
})
)
sphereShadow.rotation.x = - Math.PI * 0.5
sphereShadow.position.y = plane.position.y + 0.01
scene.add(sphere, sphereShadow, plane)
// 애니메이션 적용
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update the sphere
sphere.position.x = Math.cos(elapsedTime) * 1.5
sphere.position.z = Math.sin(elapsedTime) * 1.5
sphere.position.y = Math.abs(Math.sin(elapsedTime * 3))
// Update the shadow
sphereShadow.position.x = sphere.position.x
sphereShadow.position.z = sphere.position.z
sphereShadow.material.opacity = (1 - sphere.position.y) * 0.3
// ...
}
tick()