1탄에서는 threejs에서 간단하게 mesh를 띄워보는 시간을 가졌다.
2탄에서는 camera를 좀 더 자연스럽게 다루는 방법과, 화면 비율이 바뀌었을 때에 대한 처리 등, 1탄에서 어색했던 문제를 해결해보는 시간을 가져볼까 한다.
2탄 역시, vanila javascript와 react 두 곳에서의 코드를 모두 작성해 볼 예정이다.
vanila javascript three
먼저, 이전에 작업했던 velog-threejs-vanila 코드를 실행시키고, 브라우저를 연 뒤, 브라우저의 크기를 변경시켜보자.
캔버스의 위치는 그대로 있고, 전체 창 크기만 변화하는 것이 조금 어색하게 느껴진다.
이 문제를 해결해 보도록 하자.
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const renderer = new THREE.WebGLRenderer();
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 geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: "green" });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.render(scene, camera);
});
renderer.render(scene, camera);
다음은, 카메라의 앵글을 수정할 때, 매번 하드코딩을 통해 바꾸는 것은 매우 번거롭기에, 카메라를 자유자재로 다룰 수 있는 방법에 대해 알아보자.
추가로, 위에서 작성한 resize 이벤트의 callback함수는 별도의 함수로 분리하여 관리하도록 하자.
카메라 앵글을 마우스 이벤트를 통해 조작하려면, threejs에서 예제로 구현해 놓은 클래스인 OrbitControls를 이용하면 쉽게 조작 가능하다.
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const renderer = new THREE.WebGLRenderer();
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 geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: "green" });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const orbitControls = new OrbitControls(camera, renderer.domElement);
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();
자 이제, 마우스 좌클릭 후 드래그를 통해, 카메라 앵글 이동이 가능하고, 우클릭 후 드래그를 통해, 카메라 위치 이동이 가능할 것이다.
그러나, 몇몇 어색하거나 예쁘지 않은 포인트들이 눈에 띄기 시작한다.
첫 번째로는 orbitControls를 통해 앵글이 이동하는게 딱딱 끊기는 것이 보기 좋지 않을 수도 있다.
두 번째로는 특정 앵글에서 mesh의 모서리가 울퉁불퉁해 보이는 부분을 발견할 수 있는데, 이 또한 보기 좋지 않다.
이러한 문제를 해결해보자.
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.MeshBasicMaterial({ color: "green" });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
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();
다음은, 간단한 animation 기능을 구현해보자.
mesh가 회전하면서 위로 올라가는 애니메이션을 구현해보고자 한다.
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.MeshBasicMaterial({ color: "green" });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const handleRender = () => {
orbitControls.update();
renderer.render(scene, camera);
renderer.setAnimationLoop(handleRender);
mesh.position.y += 0.001;
mesh.rotation.y += 0.01;
};
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();
코드편 2탄은 여기까지 이다. 아래는 이 코드를 그대로, React화 하여 옮겨보도록 하자.
react three
vanila Threejs(이하, vThree)에서는 매 resize마다 renderer의 사이즈를 업데이트 해주어야 하지만, react Threejs(fiber 혹은 drei)(이하, rThree)는 컴포넌트 리랜더 시, 자동으로 업데이트된다.
또한 orbitControls를 구현하기 위해서는 drei에서 제공해주는 OrbitControls 컴포넌트를 import 후 사용하기만 하면 된다.
import { OrbitControls } from "@react-three/drei";
import "./App.css";
import { Canvas } from "@react-three/fiber";
function App() {
return (
<div style={{ width: "100vw", height: "100vh", background: "#000" }}>
<Canvas
camera={{
isPerspectiveCamera: true,
fov: 75,
aspect: window.innerWidth / window.innerHeight,
near: 0.01,
far: 1000,
position: [0, 2, 5],
}}
>
<OrbitControls dampingFactor={0.05} />
<mesh position={[0, 0, 0]}>
<boxGeometry args={[1, 1, 1]} />
<meshBasicMaterial color={"green"} />
</mesh>
</Canvas>
</div>
);
}
export default App;
애니메이션을 구현하기 위해선, useFrame hook을 이용하면 된다.
useFrame 이 vThree에서 renderer.setAnimationLoop와 대응한다고 생각하면 된다.
vThree와 달리 수정이 필요한 부분이 존재한다.
mesh의 ref가 필요하며, useFrame은 Canvas 컴포넌트 내에서 사용이 가능하므로, mesh컴포넌트와 그 하위 컴포넌트들을 별도의 컴포넌트로 분리해주는 작업이 필요하다.
import { OrbitControls } from "@react-three/drei";
import "./App.css";
import { Canvas, useFrame } from "@react-three/fiber";
import { useRef } from "react";
import { Mesh } from "three";
const MeshComponent = () => {
const meshRef = useRef<Mesh>(null);
useFrame(() => {
const mesh = meshRef.current;
if (mesh) {
mesh.position.y += 0.001;
mesh.rotation.y += 0.01;
}
});
return (
<mesh ref={meshRef} position={[0, 0, 0]}>
<boxGeometry args={[1, 1, 1]} />
<meshBasicMaterial color={"green"} />
</mesh>
);
};
function App() {
return (
<div style={{ width: "100vw", height: "100vh", background: "#000" }}>
<Canvas
camera={{
isPerspectiveCamera: true,
fov: 75,
aspect: window.innerWidth / window.innerHeight,
near: 0.01,
far: 1000,
position: [0, 2, 5],
}}
>
<OrbitControls dampingFactor={0.05} />
<MeshComponent />
</Canvas>
</div>
);
}
export default App;
React Threejs Wrapper Library를 활용하면 같은 코드도 비교적 쉽게(?) 구현이 가능하다.
코드편 2탄은 여기서 마무리하고, 3탄에서는 light를 다루는 법과 그림자를 다루는 법에 대해서 알아보도록 하자