앗! 취업에 도움되는(?)Threejs를 vanila 및 react-three-fiber 버전의 예제와 함께 복습해보자. [코드편 2탄]

Design.C·2023년 5월 6일
1
post-thumbnail
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);

// 브라우저의 크기를 조작하는 행위는 resize 이벤트이므로, 해당 이벤트 발생 시, 
// callback함수 내에서 camera의 aspect와, renderer의 size를 재설정해주면 된다.
window.addEventListener("resize", () => {
  // 바뀐 브라우저의 가로, 세로를 카메라의 aspect로 설정한다.
  camera.aspect = window.innerWidth / window.innerHeight;
  
  // 바뀐 속성대로 카메라를 업데이트 해주는 메소드라고 생각하면 된다.
  camera.updateProjectionMatrix();
  
  // renderer의 사이즈를 바뀐 브라우저의 가로, 세로 크기로 정해준다.
  renderer.setSize(window.innerWidth, window.innerHeight);
  
  // 바뀐 속성대로 render(그려준다)한다.
  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);

// OrbitControls를 만든다.
const orbitControls = new OrbitControls(camera, renderer.domElement);

// orbit이 바뀔때마다, 바뀐 앵글에 대한 새로운 장면을 render 해야 하므로, setAnimationLoop 함수를 이용하여 재귀적으로 무한히 실행한다.
const handleRender = () => {
  orbitControls.update();
  renderer.render(scene, camera);
  renderer.setAnimationLoop(handleRender);
};

// resize 이벤트의 callback 함수를 아래와 같이 정의했다.
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로 주게되면, mesh 모서리가 울퉁불퉁해 보이는 현상을 방지할 수 있다. 
  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);
// orbit을 변경할 때, enableDamping 속성을 true로 주게되면 부자연스러울 정도로 딱딱 끊기게 멈추는 현상을 막을 수 있다.
orbitControls.enableDamping = true;

// dampingFactor의 값을 작게하면 damping(드래그가 멈췄을 때, 조금 더 움직이는 느낌)을 강화할 수 있다.
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);
  // 매 frame마다 mesh의 y좌표를 0.001 만큼 올려준다(위로 올라간다)
  mesh.position.y += 0.001;
  // 매 frame마다 mesh를 y축에 대하여 0.01 라디안 만큼 회전시킨다.(y축이 중심기둥이 되고, 놀이기구(회전컵 같은)처럼 회전한다. 
  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],
        }}
      >
        // import하여 바로 사용하면 된다.
        <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";

// useFrame은 Canvas 컴포넌트 하위에서 사용이 가능하므로, Mesh와 그 하위 컴포넌트들을 별도 컴포넌트로 분리하였다.
const MeshComponent = () => {
  const meshRef = useRef<Mesh>(null);
  // 매 프레임마다 실행되는 hook이며, 이를 이용하며 애니메이션을 구현할 수 있다.
  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를 다루는 법과 그림자를 다루는 법에 대해서 알아보도록 하자

profile
코더가 아닌 프로그래머를 지향하는 개발자

0개의 댓글