푸릇푸릇 프로젝트 중 친구와 주사위 게임하는 부분을 3D로 구현하고 싶어졌다
찾아보니 블렌더를 이용해 3D 모델을 만들고 그것을 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 주사위를 굴려주고 싶었는데 이렇게 하려면 주사위를 물리 엔진에 위치시키고 힘을 가해줘야 한다
먼저, 주사위를 던질 평면이 필요하다
아래 참고자료의 코드를 참고해 평면을 만들어주었다
나는 주사위의 윗면을 보는 상태로, 주사위를 굴려주고 싶었기 때문에 평면을 옆에서 보도록 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에 저장하면 될 듯하다
구현된 코드는 여기서 확인할 수 있다