[푸릇푸릇] 3D 주사위 굴리기 (React.js)

토리·2024년 2월 11일
0
post-thumbnail

Overview

푸릇푸릇 프로젝트 중 친구와 주사위 게임하는 부분을 3D로 구현하고 싶어졌다
찾아보니 블렌더를 이용해 3D 모델을 만들고 그것을 Three.js를 이용해 리액트 앱에 띄우면 되는 구조이다

아래 포스팅에서 대략적인 구조를 얻을 수 있었다

참고자료

필요한 과정

  • blender을 이용한 주사위 3D 모델링
  • Three.js와 react-three-fiber 사용하여 3D 주사위 가져오기
  • 물리엔진을 이용해 주사위 굴리기

blender을 이용한 주사위 3D 모델링

blender 설치

주사위 3D 모델링

export as glTF

  • 제대로 적용하기 위해 glTF로 추출합니다

Three.js와 react-three-fiber 사용하여 3D 주사위 가져오기

Three.js와 react-three-fiber

  • Three.js
    - WebGL(브라우저에서 3D 그래픽을 렌더링하기 위한 JS API)을 사용하게 쉽게 만든 라이브러리
  • react-three-fiber
    - 리액트와 Three.js를 쉽게 결합할 수 있게 도와주는 라이브러리
    • 리액트 컴포넌트 내에서 Three.js를 사용할 수 있음

참고자료

주사위 가져오기

import { Canvas, useLoader } from "@react-three/fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from '@react-three/drei'

function Dice() {
  const gltf = useLoader(GLTFLoader, "/dice.glb");
  return <primitive object={gltf.scene} />;
}

function App() {
  return (
    <>
      <Canvas>
        <ambientLight intensity={Math.PI / 2} />
        <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
        <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
        <Dice />
        <OrbitControls />
      </Canvas>
    </>
  );
}

export default App;

  • 주사위 빛 조정하는 부분이 꽤나 중요했다

물리엔진을 이용해 주사위 굴리기

가장 힘들었던 부분이 이곳이었다
화면을 터치하거나 흔들었을 때 3D 주사위를 굴려주고 싶었는데 이렇게 하려면 주사위를 물리 엔진에 위치시키고 힘을 가해줘야 한다

주사위를 던질 평면(Plane) 만들어주기

먼저, 주사위를 던질 평면이 필요하다
아래 참고자료의 코드를 참고해 평면을 만들어주었다
나는 주사위의 윗면을 보는 상태로, 주사위를 굴려주고 싶었기 때문에 평면을 옆에서 보도록 rotation하는 과정은 필요하지 않았다

참고자료

import { Canvas, useLoader, useFrame } from "@react-three/fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from '@react-three/drei';
import { useState } from 'react';
import { usePlane, Physics } from "@react-three/cannon";

function Dice() {
  ...
  (생략)
  ...
}

function Plane(props) {
  const [ref, api] = usePlane(() => ({
    // rotation: [-Math.PI / 2, 0, 0],
    ...props,
  }));
  return (
    <mesh ref={ref} receiveShadow>
      <planeGeometry args={[10, 10]} />
      <meshStandardMaterial color="#ffffff" />
    </mesh>
  );
}

function App() {
  return (
    <>
      <Canvas>
        <ambientLight intensity={Math.PI / 2} />
        <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
        <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
        <Physics>
          <Plane position={[0, 0, 0]} />
          <Dice />
        </Physics>
        <OrbitControls />
      </Canvas>
    </>
  );
}

export default App;

평면을 만들어줬지만, 주사위가 중앙을 관통하여 생긴 모습을 확인할 수 있다.
주사위가 평면 바로 위에 얹어진 형태로 만드는 작업이 필요하다.

사실 이것을 해결하는 방법으로
1) 3D 모델 자체의 위치값을 변경하여 저장
2) 코드 내에서 주사위의 한 변의 길이을 알아와서 그만큼 띄움

이정도 방법이 있겠다

1), 2) 둘 다 시도했지만 복잡해서 그냥 <Plane position={[0, 0, -1]} />으로 때려박았다
(2번 방법은 어렵지는 않지만 코드가 복잡해 나중에 뜯어볼 때 힘들어 보였음..)

주사위 굴리기

주사위에 랜덤한 힘 가하기

먼저, useBox 함수를 이용해 사각형 모양의 충돌체를 생성하고, 주사위 모델과 일치하도록 만든다.
이후에 클릭 이벤트 발생 시 랜덤한 힘이 작동하도록 해준다.

여기서 많이 헤맸는데 Three.js의 구성 요소나 cannon의 작동 방법을 아는 것이 중요하다

그리고 이전에 plane을 그저 세웠던 부분을 수정해주었다
plane과 중력, 무게 등은 일반적인 방식 (위에서 아래로 중력 작용, plane은 바닥에 위치함) 으로 설정해두고 카메라 위치 (바라보는 위치)만 위에서 바라본다고 생각하고 수정했다

function Dice() {
  const gltf = useLoader(GLTFLoader, "/dice.glb");
  
  const box = new Box3().setFromObject(gltf.scene);
  const size = box.getSize(new Vector3());

  const [ref, api] = useBox(() => ({
    mass: 1,
    position: [0, size.y / 2, 0],
    args: [size.x, size.y, size.z],
  }));

  const rollDice = () => {
    const randomForce = [Math.random() - 0.5, 0, Math.random() - 0.5].map(v => v * 2000);
    api.applyForce(randomForce, [0, 0, 0]);
  };

  return <primitive ref={ref} object={gltf.scene} onClick={rollDice} />;
}

주사위가 화면에서 벗어나지 않도록 벽 설정하기

주사위가 화면 밖으로 벗어나면 안 된다.
화면 밖으로 벗어나지 않도록 벽 네 개를 세워준다

아래 코드와 같은 방식으로 물리 엔진 안에 함께 넣어준다

function Wall({ position, args }) {
  const [ref] = useBox(() => ({ type: 'Static', args, position }));
  return <mesh ref={ref} />;
}

...
(생략)
...

<Physics gravity={[0, -9.8, 0]}>
  <Plane position={[0, 0, 0]} />
  <Dice />
  <Wall position={[-5, 0, 0]} args={[1, 10, 10]} />
  <Wall position={[5, 0, 0]} args={[1, 10, 10]} />
  <Wall position={[0, 0, -5]} args={[10, 10, 1]} />
  <Wall position={[0, 0, 5]} args={[10, 10, 1]} />
</Physics>

완성된 모습은 이런 모양이다

추후 구현사항

이제 남은 부분은 주사위 수를 알아내는 부분이다
euler 각도를 이용해 포지션을 확인하고 state에 저장하면 될 듯하다

구현된 코드는 여기서 확인할 수 있다

0개의 댓글