코드편 3탄에서는 light(조명), shadow(그림자), material의 빛과 관련된 속성에 대해서 알아보도록 하겠다.
이번에도 역시, 같은 코드를 vanila javascript와 react 두 가지 방식으로 모두 작성해보겠다.
vanila javascript three
Light의 효과를 알기 위해선 우선, mesh의 material을 MeshStandardMaterial로 바꾸고,
가장 기본적인 조명인 AmbientLight를 추가해보자.
또한, 2탄에서 작성했던 애니메이션 관련 코드는 잠시 주석처리 해주자.
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const renderer = new THREE.WebGLRenderer({
antialias: true,
});
const app = document.querySelector("#app");
app.appendChild(renderer.domElement);
renderer.setSize(window.innerWidth, window.innerHeight);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.01,
1000
);
camera.position.z = 5;
camera.position.y = 2;
scene.add(camera);
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.05;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: "green" });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const ambientLight = new THREE.AmbientLight();
ambientLight.intensity = 0.4;
scene.add(ambientLight);
const handleRender = () => {
orbitControls.update();
renderer.render(scene, camera);
renderer.setAnimationLoop(handleRender);
};
const handleResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.render(scene, camera);
};
window.addEventListener("resize", handleResize);
handleRender();
ambientLight를 추가했더니, 사실 meshBasicMaterial과 큰 차이가 없어보인다.
빛이 있다면 항상 존재하는 그림자가 생기지 않고, 어디에서든 같은 밝기를 제공하기 때문이다.
이번에는 그림자를 만들어 보도록 하겠다. 그러기 위해서는 그림자가 생길 바닥면이 필요하다.
따라서, 바닥면과 빛을 만들 수 있는 directionalLight를 추가해보고, 일부 속성을 변경해보자.
threejs에서 그림자를 생성하려면, 우선 renderer의 shadowMap.enabled를 true로 설정해주어야한다. shadowMap.type은 여러 종류가 있지만 기본은 PCFShadowMap이고, PCFSoftShadowMap은 이름에서 알 수있듯 좀 더 부드러운 그림자 연출이 가능하다.
추가적으로 mesh 혹은 light가 자신으로 인해, 그림자를 생기게 하려면, 각 객체의 castShadow 속성을 true로 설정해 주어야 한다.
반대로 mesh가 자신에게 그림자가 드리워지게 하려면, receiveShadow 속성을 true로 설정해 주어야 한다.
그림자의 퀄리티 또한 조정이 가능한데, 이는 각 light의 shadow.mapSize 의 width, height 값을 조정함으로써 가능하다. 각 값의 기본 값은 512이며, 아래는 임의로 2048로 조정했다. 값이 높아질수록 그림자의 퀄리티는 좋아지지만 성능에 지장을 줄 수 있으므로, 적절히 타협해야 한다.
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const renderer = new THREE.WebGLRenderer({
antialias: true,
});
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const app = document.querySelector("#app");
app.appendChild(renderer.domElement);
renderer.setSize(window.innerWidth, window.innerHeight);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.01,
1000
);
camera.position.z = 5;
camera.position.y = 2;
scene.add(camera);
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.05;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: "green" });
const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
scene.add(mesh);
const directionalLight = new THREE.DirectionalLight();
directionalLight.position.set(3, 3, 3);
directionalLight.lookAt(0, 0, 0);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 1;
directionalLight.shadow.camera.far = 60;
scene.add(directionalLight);
const planeGeometry = new THREE.PlaneGeometry(100, 100);
const planeMaterial = new THREE.MeshStandardMaterial();
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
plane.position.y -= 0.5;
plane.receiveShadow = true;
scene.add(plane);
const handleRender = () => {
orbitControls.update();
renderer.render(scene, camera);
renderer.setAnimationLoop(handleRender);
};
const handleResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.render(scene, camera);
};
window.addEventListener("resize", handleResize);
handleRender();
잘 따라왔다면, 그림자가 예쁘게 생겼을 것이다.
이제 똑같은 로직을 React로 작성해보자.
react three
import { OrbitControls } from "@react-three/drei";
import "./App.css";
import { Canvas, useFrame } from "@react-three/fiber";
import { useEffect, useRef } from "react";
import * as THREE from "three";
const MeshComponent = () => {
const meshRef = useRef<THREE.Mesh>(null);
return (
<mesh castShadow receiveShadow ref={meshRef} position={[0, 0, 0]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={"green"} />
</mesh>
);
};
const PlaneComponent = () => {
return (
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]}>
<planeGeometry args={[100, 100]} />
<meshStandardMaterial />
</mesh>
);
};
const LightComponent = () => {
const lightRef = useRef<THREE.DirectionalLight>(null);
useEffect(() => {
const light = lightRef.current;
if (light) {
light.lookAt(0, 0, 0);
light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;
light.shadow.camera.near = 1;
light.shadow.camera.far = 60;
}
});
return <directionalLight ref={lightRef} castShadow position={[3, 3, 3]} />;
};
function App() {
return (
<div style={{ width: "100vw", height: "100vh", background: "#000" }}>
<Canvas
shadows={{
enabled: true,
autoUpdate: true,
type: THREE.PCFSoftShadowMap,
}}
camera={{
isPerspectiveCamera: true,
fov: 75,
aspect: window.innerWidth / window.innerHeight,
near: 0.01,
far: 1000,
position: [0, 2, 5],
}}
>
<OrbitControls dampingFactor={0.05} />
<LightComponent />
<PlaneComponent />
<MeshComponent />
</Canvas>
</div>
);
}
export default App;
이로써, 3탄도 끝이다!!
당신은, threejs를 vanila javascript로도, react 방식으로도 활용할 수 있게 되었다.
다음 시리즈는 threejs를 활용하여 자신만의 포트폴리오 페이지를 만드는 프로젝트를 하려한다.
여기까지 읽어주셔서 매우 감사드립니다.