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

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

// MeshStandardMaterial로 바꿔줌으로써, 빛의 영향을 받도록 한다.
const material = new THREE.MeshStandardMaterial({ color: "green" });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 모든 곳에 같은 밝기를 제공하는 AmbientLight를 만들고 scene에 추가한다.
const ambientLight = new THREE.AmbientLight();
ambientLight.intensity = 0.4; // 밝기를 조절할 수 있다.
scene.add(ambientLight);

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();
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 ambientLight = new THREE.AmbientLight();
// ambientLight.intensity = 0.4;
// scene.add(ambientLight);

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);

// mesh가 놓여있을 평면 mesh를 생성하고 추가하였다.
const planeGeometry = new THREE.PlaneGeometry(100, 100);
const planeMaterial = new THREE.MeshStandardMaterial();
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
// 평면이 바닥에 놓인 형태가 아닌 수직으로 서있는 형태이므로 -90도만큼 회전시켜준다.
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);
  //   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();

잘 따라왔다면, 그림자가 예쁘게 생겼을 것이다.

이제 똑같은 로직을 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);
  // useFrame(() => {
  //   const mesh = meshRef.current;
  //   if (mesh) {
  //     mesh.position.y += 0.001;
  //     mesh.rotation.y += 0.01;
  //   }
  // });
  
  // props에 castShadow, receiveShadow를 추가하였다.
  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>
  );
};

// directionalLight를 별도의 컴포넌트로 분리했다.
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를 활용하여 자신만의 포트폴리오 페이지를 만드는 프로젝트를 하려한다.

여기까지 읽어주셔서 매우 감사드립니다.

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

0개의 댓글