일주일마다 한 개의 사이드 프로젝트 만들기를 진행하고 있는데 저번 Business Card Maker 만들기에 이어서 이번 주에는 3D 주사위 게임 만들기를 주제로 진행해봤습니다.
처음에는 웹 소켓을 이용한 뭔가를 해보고 싶었습니다. 그러다가 문뜩 게임을 만들어서 이를 이용해 웹 소켓을 활용해보는게 재밌을 거 같았습니다. 게임이라고 하면 사실 이전 회사에서 하고 싶었는데 못했던 Three.js를 이용한 3D 주사위 게임이 생각났습니다. 간단하게 주사위를 3D로 만들어서 던질 수 있고 해당 눈금만큼 말이 이동하는 게임이죠.
나중에 이를 더 발전시켜 웹 소켓을 붙여보면 더욱 재미있을거 같았습니다.
만들기 전에 이미 누가 주사위 게임을 만든게 있는지 찾아보기로 했습니다. 다른 사람은 어떻게 만들었는지 보고 영감을 받을 수 있기 때문이기도 하죠.
먼저 네이버 주사위 굴리기가 있습니다. 근데 3D가 아니기도 하고 평범하다면 평범한거 같습니다. 정말 기능에 충실한 듯 합니다.
참고 : https://codepen.io/ksenia-k/pen/QWZVvxm
참고 : https://tympanus.net/codrops/2023/01/25/crafting-a-dice-roller-with-three-js-and-cannon-es/
아무래도 현 시점에서 가장 참고하기 좋을만한게 codepen에 있었습니다. 심지어 튜토리얼도 있네요. 다만 주사위를 사용자가 위로 올려서 던지는게 아니라 버튼으로 제어한다는 차이가 있습니다. 그래도 주사위 눈이 최종적으로 무엇인지 판단할 수도 있어서 좋은 참고 자료가 될 거 같습니다. 튜토리얼을 잠시 보니 꽤나 수학적이고 복잡해보이긴 합니다. 허허...
처음에 프로젝트 진행 시 최소 요구 사항을 아래와 같이 정했습니다.
이번 프로젝트를 통해 Blender를 이용한 간단한 3D 디자인과 이를 적용하는 방법 혹은 포맷 형식에 대해 알아보고, ThreeJS + R3F(React Three Fiber)를 공부해보면서 간단한 3D 주사위 던지기 실습까지 진행해보는 것을 목적으로 합니다.
참고 : https://velog.io/@ckstn0777/Blender-첫-사용기-및-3D-Dice-만들기-실습
Blender를 처음 설치해보고 유뷰트를 보면서 따라서 만들어봤습니다. 어휴 근데 따라하기도 쉽지 않고, 이것저것 기능이 많아서 시행착오도 겪었습니다.
참고 : https://velog.io/@ckstn0777/Three.js와-R3FReact-Three-Fiber-기초
Three.js와 R3F(React Three Fiber) 에 대해서도 기초를 공부해봤습니다. 하지만 이때는 몰랐죠... 생각보다 이것저것 되게 많다는 사실을... 조명하나를 보더라도 조명 개수가 8개 정도 되는거 같고, 거기에 딸린 속성도 여러개가 됩니다. 카메라, 렌더러 등은 말할 것도 없죠. 이걸 다 알고 진행하기에는 시간이 오래걸리긴 합니다만 어쨌거나 유뷰트에 튜토리얼이라도 한번 쭉 보고 진행하는것을 추천합니다...
공부하면서 참고 했던 자료입니다. 많은 도움이 되었습니다.
저만 그런건지 모르겠는데 Three.js 색상이 알고보니까 이상하더군요. 분명 밝은 Orange와 Pink가 나와야 되는데 어두껌껌합니다. 처음에는 조명 문제인줄 알고 해보다가 그건 아닌거 같더군요. 그래서 Next.js 랑 호환이 안되나 싶어서 Vite (React) 프로젝트를 만들었지만 여전했습니다.
참고 : https://codesandbox.io/s/rrppl0y8l4
그래서 codesandbox에서 예시 코드를 찾아서 그대로 제 프로젝트 코드에 적용해봤습니다. 그래도 해결이 안되더군요. 이제 남은건 버전 문제라고 밖에 생각할 수 없었습니다.
아무리 그래도 "three": "^0.155.0"에서 "three": "0.154.0” 바꾸면 해결이 될까? 싶었는데 바꾸니까 해결되더군요. 버전 기록을 보니 17일전에 업데이트 된거 같은데... 깃허브 이슈에는 아직 별다른 이슈가 없는거 같고... 방법이 바뀐건가 싶어서 공식문서도 뒤져보고 했지만 잘 모르겠더군요.
원인을 찾아서 정말 이게 문제라면 코드를 분석해서 해결해보고 싶지만 그냥 버전을 낮추고 넘어가기로 했습니다.
먼저 사용자가 주사위를 던지도록 만들어보고 싶었습니다. 이때 @react-three/cannon 물리엔진 라이브러리를 사용해볼 수 있습니다.
근데 좀 아쉬운 점이 문서가 매우 빈약한거 같습니다. 공식 문서도 없고... 대신에 안에 들어있는 속성은 꽤 많더군요. 아무래도 자신에게 맞는 geometry에 대한 usePlane이던, useCylinder던 선택해서 사용하면 되는 구조인거 같습니다. 그러면 그 물체에 맞는 물리엔진 효과를 적용할 수 있어 보이구요.
(Update) 정정합니다. 문서가 빈약한게 아니라 제가 문서를 잘못 봤던겁니다. use-cannon을 찾아볼게 아니라 핵심인 cannon-es를 찾아보시길 바랍니다. use-cannon은 R3F에서 적용할 때 사용하는겁니다. 여기 보니까 속성도 엄청 많고 검색하면 다 나오네요.
참고 : https://velog.io/@outclassstudio/Three.js-2-Plane-만들기
그렇게 해서 바닥을 만드는데, 근데 보면 rotation를 해줘야 합니다. 이러한 이유는 plane을 기본 생성하면 쌩뚱맞게 plane이 세로로 세워져 있는데, 그 친구를 돌려놓기 위해서 rotation을 시켜줘야 하는 것입니다.
import { PlaneProps, usePlane } from "@react-three/cannon";
import { Mesh } from "three";
export default function Plane(props: PlaneProps) {
const [ref] = usePlane<Mesh>(() => ({
rotation: [-Math.PI / 2, 0, 0],
...props,
}));
return (
<mesh ref={ref} receiveShadow>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial color="#171720" />
</mesh>
);
}
<Physics>
<Plane position={[0, 0, 0]} />
<BoxLoader />
</Physics>
참고 : https://cannon.pmnd.rs/
참고 : https://github.com/pmndrs/use-cannon/blob/master/packages/react-three-cannon-examples/src/demos/MondayMorning/index.tsx
처음에는 위 예시를 보고 따라해보려는 시도를 해봤습니다. 근데 뭔가 영... 부자연스러운 느낌도 있고 제대로 안되더군요. 그래서 다른 방법을 찾아봤습니다.
참고 : https://velog.io/@9rganizedchaos/Three.js-journey-강의노트-19
그러다가 Three.js에 Raycaster 라는게 있는걸 알게되었습니다. Raycaster는 '광선 투사', '광선을 쏜다' 라는 뜻이 있습니다. 기본적으로 특정 방향으로 광선을 쏘고 어떤 물체가 해당 광선과 교차하는지 테스트할 수 있습니다. 이를 통해 마우스 아래에 현재 무엇이 있는지 감지하는 것도 가능합니다.
"Raycaster를 이용하면 raycaster의 origin을 마우스로, direction을 카메라로 설정해주면, 마우스를 기준으로 ray를 cast해주게 되고, 결국 마우스가 호버됨에 따라 object정보를 받아줄 수 있게 된다."
참고 : https://docs.pmnd.rs/react-three-fiber/tutorials/how-it-works#pointer-events
R3F에서는 Pointer Events를 사용하면 mouse picking에 대한 raycaster를 생성해준다고 합니다. 실제로 주사위 위에 마우스를 올려놓고 클릭하면 콘솔에 출력됩니다.
<group ref={ref} dispose={null} onPointerDown={console.log}>
mousedown, mousemove, mouseup 처럼 Pointer Events를 이용하면 마우스를 클릭 후 주사위를 이동시킬 수 있겠다고 생각했습니다.
import React, { useEffect, useRef } from "react";
import { useGLTF } from "@react-three/drei";
import { DiceGLTF } from "@/types/DiceGLTF";
import { useBox, useCylinder } from "@react-three/cannon";
import { Group, Mesh } from "three";
export default function DiceModel() {
const { nodes, materials } = useGLTF("/dice.glb") as DiceGLTF;
const [ref, api] = useBox(
() => ({
args: [2, 2, 2],
mass: 10,
position: [0, 20, 0],
linearFactor: [1, 1.4, 1],
type: "Dynamic",
}),
useRef<Group>(null)
);
const [isDragging, setIsDragging] = React.useState(false);
return (
<group
ref={ref}
dispose={null}
onPointerDown={() => setIsDragging(true)}
onPointerMove={(e) => {
if (isDragging) {
const intersection = e.intersections[0];
const { point } = intersection;
api.position.set(0, point.y <= 1 ? 1 : point.y, 0);
}
}}
onPointerUp={() => {
setIsDragging(false);
const force = 1 + 2 * Math.random();
api.applyImpulse([-force, force, 0], [0, 0, 0.2]);
}}
onPointerLeave={() => setIsDragging(false)}
>
<group position={[0, 0, 0]}>
<mesh
castShadow
receiveShadow
geometry={nodes.Cube_1.geometry}
material={materials["black"]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Cube_2.geometry}
material={materials.white}
/>
</group>
</group>
);
}
useGLTF.preload("/dice.glb");
그래도 어찌저찌 주사위를 위로 올렸다가 떨어뜨려서 굴리는거 까지는 되는데... 해결하지 못한 문제가 많습니다.
아직 구현하지 못한 부분, 해결하지 못한 부분이 많지만 프로젝트는 여기까지 해야 될 거 같습니다. 제가 너무 주사위 굴리는걸 얕본게 아닌가 싶습니다. 주제도 모르고 무작정 덤볐다가 시간을 좀 낭비한 느낌도 있습니다.
그리고 이왕할거면 R3F 로 바로 코드를 작성하려는 시도보다는 Three.js 자체에 대한 공부와 실습이 더 필요하지 않았나 싶습니다. 무엇보다 수많은 숨겨진 속성들... 3차원 + 물리엔진 요소들이 힘들었던거 같습니다. 공식문서나 관련 자료도 생각보다 찾기 어려운 측면도 있는거 같습니다.
아쉬움이 많이 남네요. 더 멋진 무언가를 포트폴리오 형식처럼 만들어봤으면 어땠을까 싶기도 하고. 상품에 대한 소개 예시를 3D로 만들어보면 더 간단하게 있어보이지 않았을까 싶기도 합니다. 음... 앞으로 Three.js에 대한 공부를 좀 더 해봐야 될까요? 고민되네요. 더 공부해볼지...
Three.js 관련글 찾아 돌다가 들어왔는데
블렌더로 주사위까지 직접 만드시고 대단하시군요..
도움되는 글 너무 잘 보고 갑니다 ! ! 감사합니다