[ReactThreeFiber] 그림자
shadow
실제로는 몇개의 shadow가 더 있으니 https://github.com/pmndrs/drei 에서 확인하도록 하자.
예시
app.js
<Canvas
shadows
camera={{
near: 1,
far: 100,
position: [7, 7, 0],
}}
>
<Shadow />
</Canvas>
그림자를 만들기 위해서는 Canvas에 shadows 속성을 추가해야 한다.
Shadow.jsx
import { OrbitControls } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
const torusGeometry = new THREE.TorusGeometry(0.4, 0.1, 32, 32);
const torusMaterial = new THREE.MeshStandardMaterial({
color: "#9b59b6",
roughness: 0.5,
metalness: 0.9,
});
function Shadow() {
useFrame((state) => {
const time = state.clock.elapsedTime;
const smallSpherePivot = state.scene.getObjectByName("smallSpherePivot");
smallSpherePivot.rotation.y = THREE.MathUtils.degToRad(time * 50);
});
return (
<>
<OrbitControls />
<ambientLight intensity={0.2} />
<directionalLight
castShadow
color="#ffffff"
intensity={3}
position={[0, 5, 0]}
target-position={[0, 0, 0]}
/>
<mesh rotation-x={THREE.MathUtils.degToRad(-90)}>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial
color="#2c3e50"
roughness={0.5}
metalness={0.5}
side={THREE.DoubleSide}
/>
</mesh>
<mesh position-y={1.7}>
<torusKnotGeometry args={[1, 0.2, 128, 32]} />
<meshStandardMaterial color="#ffffff" roughness={0.1} metalness={0.2} />
</mesh>
{new Array(10).fill().map((item, idx) => {
return (
<group key={idx} rotation-y={THREE.MathUtils.degToRad(36 * idx)}>
<mesh
geometry={torusGeometry}
material={torusMaterial}
position={[3, 0.5, 0]}
/>
</group>
);
})}
<group name="smallSpherePivot">
<mesh position={[3, 0.5, 0]}>
<sphereGeometry args={[0.3, 32, 32]} />
<meshStandardMaterial
color="#e74c3c"
roughness={0.2}
metalness={0.5}
/>
</mesh>
</group>
</>
);
}
export default Shadow;
그림자는 directionalLight에서 만들어지며 castShadow 속성을 추가한다.
plane메쉬에 대해서 그림자를 받아서 자신의 표면에 표현하기 위해서 receiveShadow 속성을 추가한다.
torusKnot메쉬는 자신의 그림자를 만들으라는 의미로 castShadow를 추가하고 동시에 receiveShadow 속성도 추가한다.
torus메쉬도 castShadow, receiveShadow 추가
sphere메쉬도 castShadow, receiveShadow 추가
directialLight
그림자가 짤리는 현상이 발생할 수 있는데 이를 확인하기 위해 directialLight가 구를 쫓도록 해보자
//..
useFrame((state) => {
//..
smallSpherePivot.children[0].getWorldPosition(light.current.target.position);
});
const light = useRef();
const { scene } = useThree();
useEffect(() => {
scene.add(light.current.target);
return () => {
scene.remove(light.current.target);
};
}, [light.current]);
//..
<directionalLight
ref={light}
//..
useRef로 directionalLight를 참조, useFrame에 smallSpherePivot의 좌표를 가져와서 directionalLight의 position에 추가한다.
하지만 이대로는 장면에 추가되지 않는데 직접 scene을 가져와서 useEffect로 추가해주어야 한다. 또한 clean-up까지 해준다.
사진처럼 그림자가 짤리는 부분을 확인할 수 있다.
그림자가 짤리는 이유
directionalLight에는 그림자 이미지를 만들기 위한 카메라를 가지고 있다.
그 카메라로 만든 그림자 이미지를 또 다른 메쉬의 표면에 텍스쳐 맵핑에서 그림자를 표현하는 방법인데
실제로 그림자를 만들기 위한 카메라를 시각화해보면
//..
const shadowCamera = useRef();
useEffect(() => {
//..
shadowCamera.current = new THREE.CameraHelper(light.current.shadow.camera);
scene.add(shadowCamera.current);
//..
light.current.shadow.camera
- light의 그림자를 위한 카메라 객체를 얻어와서 CameraHelper 객체를 생성한다.
이렇게 만든 카메라 객체를 useRef로 참조한 shadowCamera.current
에 저장한다.
그리도 화면에 표현한다. - scene.add(shadowCamera.current)
카메라는 orthographic 카메라에 해당된다.
주황색선이 카메라의 절두체
잘보면 짤리는 그림자는 절두체를 벋어나있다. 그렇기 때문에 짤리는 현상이 발생하는 것이다.
따라서 절두체 안에 모든 그림자가 절두체를 벋어나지 않게 하기위해 절두체의 크기를 키운다.
<directionalLight
shadow-camera-top={6}
shadow-camera-bottom={-6}
shadow-camera-left={-6}
shadow-camera-right={6}
dirctionalLight에 그림자 카메라 크기를 키울 수 있는 속성을 조절한다.
항상 절두체를 포함시키기 위해 그림자 카메라의 크기를 터무니없이 크게 조절하면 그림자의 품질이 떨어진다.
따라서, 모든 객체가 안으로 들어올 수 있는 최소의 크기를 유지하는 것이 좋을 것 같다.
그림자틑 텍스쳐 이미지이고 이미지의 크기는 이미 정해져있기 때문에 절두체가 커질 수록 품질이 떨어지게 되는 메커니즘인데, 이때 그림자의 이미지 크기는 쉐도우의 맵 사이즈로 또 지정할 수가 있다.
shadow-mapSize={[512, 512]}
그림자 이미지의 가로와 세로에 대해서 각각 512픽셀로 설정. 이 값은 기본값, 크기를 키우면 많은 픽셀이 들어가니 더 깔끔한 그림자가 만들어진다.
pointLight
<pointLight castShadow color="#ffffff" intensity={50} position={[0, 5, 0]} />
구를 추적하도록 하자
useFrame에 smallSpherePivot.children[0].getWorldPosition(light.current.position)
를 추가하면 구 자체가 포인트라이트가 된다.
spotLight
<spotLight
ref={light}
castShadow
color="#ffffff"
intensity={50}
position={[0, 5, 0]}
angle={THREE.MathUtils.degToRad(30)}
/>
angle={THREE.MathUtils.degToRad(30)}
이건 조명의 각도인데 각이 잘을 수록 원뿔의 밑면의 넓이가 줄어든다. 기본 각도는 120정도인듯
이 역시 구의 위치를 추적해보도록 하자.
useEffect에 smallSpherePivot.children[0].getWorldPosition(light.current.position)
를 추가해봤더니 구 자체가 light가 되어서 가운데 메쉬를 돌면서 비추는데 괜찮은 느낌 나중에 써먹을 수도 있겠다. 하지만 내가 원하는건 구를 따라다니는 조명이다.
//..
useFrame((state) => {
//..
smallSpherePivot.children[0].getWorldPosition(light.current.target.position);
});
const light = useRef();
const { scene } = useThree();
useEffect(() => {
scene.add(light.current.target);
return () => {
scene.remove(light.current.target);
};
}, [light.current]);
//..
이 상태에서 그림자에 대한 품질을 향상시켜보자
그림자의 이미지 크기를 더 크게 만들면 된다.
shadow-mapSize={[512 * 5, 512 * 5]}
그림자 이미지의 크기가 커지면 그림자의 경계 또한 날카로워지는데 때론 흐린 느낌을 내고 싶을 때가 있을 것이다. 그림자 이미지의 크기를 줄여서 해결할 수도 있지만 어색하다.
이때 사용하는 것이 shadow-radius, shadow-blurSamples
shadow-radius, shadow-blurSamples
이 두개의 속성을 적용하기 위해서 그림자 생성 알고리즘을 얻어야한다.
R3F에서 사용하는 그림자 알고리즘은 PCFSoftShadowMap이지만 shadow-radius, shadow-blurSamples 를 사용하기 위해서는 VSMShadowMap을 사용해야한다.
그림자 생성 알고리즘을 VSMShadowMap으로 변경하기 위해서
<Canvas
shadows="variance"
camera={{
near: 1,
far: 100,
position: [7, 7, 0],
}}
>
<Shadow />
</Canvas>
shadow의 값으로 지정해주면 된다.
하지만 그림자에 노이즈가 생긴다.
해결하기 위해서
<spotLight
ref={light}
shadow-bias={-0.0001
shadow-bias={-0.0001}
추가
그림자 이미지를 메쉬의 표면에 매핑시킬 때 이 값만큼 간격을 벌리라는 뜻. 최대한 작게 설정하는 것이 좋다.
shadow-radius는 블러링 효과의 반경 값. 클 수록 번짐이 심해진다.
shadow-blurSamples는 블러링 효과를 적용할 횟수. 횟수가 많아질 수록 블러의 퀄리티가 올라간다. 하지만 메모리 사용이 상당한가보다 32만해도 애니메이션에서 끊김현상이 나타난다.
댓글남기기