현대 웹 개발에서는 클래식 애플리케이션과 웹 애플리케이션 사이의 경계가 매일 모호해지고 있습니다. 오늘날 우리는 대화형 웹사이트뿐만 아니라 브라우저에서 바로 본격적인 게임도 만들 수 있습니다. 이를 가능하게 하는 도구 중 하나는 React 기술을 사용하여 기반의 3D 그래픽을 생성하기 위한 강력한 도구인 라이브러리입니다.
React Three Fiber는 웹에서 3D 그래픽을 생성하기 위해 React 의 구조와 원리를 사용하는 Three.js 에 대한 래퍼입니다. 이 스택을 통해 개발자는 Three.js 의 강력한 기능과 React 의 편리함 및 유연성을 결합하여 애플리케이션을 보다 직관적이고 체계적으로 만드는 프로세스를 만들 수 있습니다.
React Three Fiber 의 핵심은 장면에서 생성하는 모든 것이 React 구성 요소라는 아이디어입니다. 이를 통해 개발자는 익숙한 패턴과 방법론을 적용할 수 있습니다.
React Three Fiber 의 주요 장점 중 하나는 React 생태계와의 통합이 쉽다는 것입니다. 이 라이브러리를 사용하면 다른 React 도구도 쉽게 통합할 수 있습니다.
Web-GameDev는 최근 몇 년 동안 간단한 2D 게임에서 데스크탑 애플리케이션에 필적하는 복잡한 3D 프로젝트로 발전하는 등 큰 변화를 겪었습니다. 이러한 인기와 기능의 성장으로 인해 Web-GameDev는 무시할 수 없는 영역이 되었습니다.
최신 브라우저는 상당히 단순한 웹 검색 도구에서 복잡한 애플리케이션과 게임을 실행하기 위한 강력한 플랫폼으로 발전하면서 많은 발전을 이루었습니다. Chrome , Firefox , Edge 등과 같은 주요 브라우저는 고성능을 보장하기 위해 지속적으로 최적화 및 개발되고 있으므로 복잡한 애플리케이션 개발에 이상적인 플랫폼이 됩니다.
브라우저 기반 게임 개발을 촉진한 주요 도구 중 하나는 입니다. 이 표준을 통해 개발자는 하드웨어 그래픽 가속을 사용할 수 있어 3D 게임의 성능이 크게 향상되었습니다. 다른 webAPI와 함께 WebGL은 브라우저에서 직접 인상적인 웹 애플리케이션을 만들 수 있는 새로운 가능성을 열어줍니다.
우선 React 프로젝트 템플릿이 필요합니다. 그럼 설치부터 시작해 보겠습니다.
npm create vite@latest
npm install three @react-three/fiber @react-three/drei @react three/rapier zustand @tweenjs/tween.js
main.jsx 파일에서 페이지에 범위로 표시될 div 요소를 추가합니다. Canvas 구성 요소를 삽입하고 카메라의 시야를 설정합니다. Canvas 구성 요소 내부에 App 구성 요소를 배치합니다.
index.css 에 스타일을 추가하여 UI 요소를 화면의 전체 높이까지 늘리고 범위를 화면 중앙에 원으로 표시해 보겠습니다.
App 구성 요소에는 하늘 형태로 게임 장면의 배경으로 표시되는 Sky 구성 요소를 추가합니다.
Ground 컴포넌트를 생성하여 App 컴포넌트에 배치해 보겠습니다.
Ground 에서 평평한 표면 요소를 생성합니다. Y축에서 아래쪽으로 이동하여 이 평면이 카메라의 시야에 들어오도록 합니다. 또한 X축의 평면을 뒤집어 수평으로 만듭니다.
기본적으로 장면에는 조명이 없으므로 모든 방향에서 개체를 비추고 지향성 광선이 없는 광원 주변 조명을 추가해 보겠습니다. 매개변수로 글로우의 강도를 설정합니다.
자산 폴더에 텍스처가 포함된 PNG 이미지를 추가합니다.
장면에 텍스처를 로드하려면 @react-3/drei 패키지의 useTexture 후크를 사용하겠습니다. 그리고 후크에 대한 매개변수로 파일로 가져온 텍스처 이미지를 전달합니다. 수평축에서 이미지의 반복을 설정합니다.
@react-3/drei 패키지의 PointerLockControls 컴포넌트를 사용하여 마우스를 움직일 때 커서가 움직이지 않고 장면에서 카메라의 위치가 변경되도록 화면의 커서를 고정합니다.
Ground 구성요소를 약간 수정해 보겠습니다.
<mesh position={[0, 3, -5]}> <boxGeometry /> </mesh>
@react-3/rapier 패키지의 물리 구성 요소를 사용하여 장면에 "물리"를 추가하세요. 매개변수로 중력장을 구성하여 축을 따라 중력을 설정합니다.
<Physics gravity={[0, -20, 0]}> <Ground /> <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> </Physics>
그러나 우리 큐브는 물리 구성 요소 안에 있지만 아무 일도 일어나지 않습니다. 큐브가 실제 물리적 개체처럼 동작하도록 하려면 @react-3/rapier 패키지의 RigidBody 구성 요소에 큐브를 래핑해야 합니다.
Ground 구성 요소로 돌아가서 바닥 표면 위에 래퍼로 RigidBody 구성 요소를 추가해 보겠습니다.
장면의 캐릭터를 제어할 Player 구성 요소를 만들어 보겠습니다.
캐릭터는 추가된 큐브와 동일한 물리적 개체이므로 장면의 큐브뿐만 아니라 바닥 표면과도 상호 작용해야 합니다. 이것이 바로 RigidBody 구성 요소를 추가하는 이유입니다. 그리고 캡슐 형태로 캐릭터를 만들어 보겠습니다.
Player 구성 요소를 Physics 구성 요소 안에 배치합니다.
캐릭터는 WASD 키를 사용하여 제어되고 스페이스바를 사용하여 점프합니다.
자체적인 반응 후크를 사용하여 캐릭터 이동 논리를 구현합니다.
Hooks.js 파일을 생성하고 거기에 새로운 usePersonControls 함수를 추가해 보겠습니다.
usePersonControls 후크를 구현한 후 캐릭터를 제어할 때 사용해야 합니다. Player 구성 요소에서는 모션 상태 추적을 추가하고 캐릭터 이동 방향의 벡터를 업데이트합니다.
캐릭터의 위치를 업데이트하려면 @react-3/섬유 패키지에서 제공하는 Frame을 사용해 보겠습니다. 이 후크는 requestAnimationFrame 과 유사하게 작동하며 초당 약 60회 함수 본문을 실행합니다.
코드 설명:
1. const playerRef = useRef(); 플레이어 개체에 대한 링크를 만듭니다. 이 링크를 사용하면 장면의 플레이어 개체와 직접 상호 작용할 수 있습니다.
2. const { 앞으로, 뒤로, 왼쪽, 오른쪽, 점프 } = usePersonControls(); 후크를 사용하면 플레이어가 현재 어떤 컨트롤 버튼을 누르고 있는지 나타내는 부울 값이 포함된 개체가 반환됩니다.
3. useFrame((state) => { ... }); 후크는 애니메이션의 각 프레임에서 호출됩니다. 이 후크 내에서 플레이어의 위치와 선형 속도가 업데이트됩니다.
4. if (!playerRef.current) return; 플레이어 개체가 있는지 확인합니다. 플레이어 개체가 없으면 오류를 방지하기 위해 함수 실행이 중지됩니다.
5. const 속도 = playerRef.current.linvel(); 플레이어의 현재 선형 속도를 가져옵니다.
6. frontVector.set(0, 0, 뒤로 - 앞으로); 누른 버튼을 기준으로 전진/후진 모션 벡터를 설정합니다.
7. sideVector.set(왼쪽 - 오른쪽, 0, 0); 왼쪽/오른쪽 이동 벡터를 설정합니다.
8. 방향.subVectors(frontVector, sideVector).normalize().multiplyScalar(MOVE_SPEED); 이동 벡터를 빼고 결과를 정규화하고(벡터 길이가 1이 되도록) 이동 속도 상수를 곱하여 플레이어 이동의 최종 벡터를 계산합니다.
9. playerRef.current.wakeUp(); 플레이어 개체를 "깨워서" 변경 사항에 반응하는지 확인합니다. 이 방법을 사용하지 않으면 일정 시간이 지나면 개체가 "잠자기" 상태가 되어 위치 변경에 반응하지 않게 됩니다.
10. playerRef.current.setLinvel({ x: 방향.x, y: 속도.y, z: 방향.z }); 계산된 이동 방향을 기반으로 플레이어의 새로운 선형 속도를 설정하고 현재 수직 속도를 유지합니다(점프나 추락에 영향을 주지 않도록).
그 결과 WASD 키를 누르면 캐릭터가 장면 주위를 움직이기 시작했습니다. 둘 다 물리적 개체이기 때문에 그는 큐브와 상호 작용할 수도 있습니다.
점프를 구현하기 위해 @dimforge/rapier3d-compat 및 @react- three/rapier 패키지의 기능을 사용해 보겠습니다. 이번 예시에서는 캐릭터가 지면에 누워 있고 점프키가 눌려져 있는지 확인해 보겠습니다. 이 경우에는 캐릭터의 방향과 가속력을 Y축으로 설정합니다.
플레이어 의 경우 모든 축에 질량과 블록 회전을 추가하여 장면의 다른 개체와 충돌할 때 다른 방향으로 넘어지지 않도록 합니다.
코드 설명:
- const 세계 = rapier.world; Rapier 물리 엔진 장면에 액세스합니다. 여기에는 모든 물리적 개체가 포함되어 있으며 상호 작용을 관리합니다.
- const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 })); 여기에서 "레이캐스팅"(레이캐스팅)이 발생합니다. 플레이어의 현재 위치에서 시작하여 y축 아래를 가리키는 광선이 생성됩니다. 이 광선은 장면에 "캐스트"되어 장면의 객체와 교차하는지 확인합니다.
- const 접지 = ray && ray.collider && Math.abs(ray.toi) <= 1.5; 플레이어가 지상에 있으면 조건이 확인됩니다.
- ray - 광선이 생성되었는지 여부.
- ray.collider - 광선이 장면의 객체와 충돌했는지 여부.
- Math.abs(ray.toi) - 광선의 "노출 시간"입니다. 이 값이 주어진 값보다 작거나 같으면 플레이어가 "지상"으로 간주될 만큼 표면에 충분히 가깝다는 것을 나타낼 수 있습니다.
또한 장면의 다른 객체와 상호 작용할 물리적 객체를 추가하여 "착륙" 상태를 결정하기 위한 광선 추적 알고리즘이 올바르게 작동하도록 지면 구성 요소를 수정해야 합니다.
카메라를 이동하기 위해 플레이어의 현재 위치를 가져와 프레임이 새로 고쳐질 때마다 카메라 위치를 변경합니다. 그리고 캐릭터가 카메라가 향하는 궤적을 따라 정확히 이동하려면 applyEuler 를 추가해야 합니다.
코드 설명:
applyEuler 메소드는 지정된 오일러 각도를 기반으로 벡터에 회전을 적용합니다. 이 경우 카메라 회전은 방향 벡터에 적용됩니다. 이는 카메라 방향을 기준으로 모션을 일치시키는 데 사용되므로 플레이어는 카메라가 회전하는 방향으로 움직입니다.
Player 의 크기를 약간 조정하고 큐브에 비해 더 크게 만들어 CapsuleCollider 의 크기를 늘리고 "점프" 논리를 수정하겠습니다.
장면이 완전히 비어있는 느낌이 들지 않도록 큐브 생성을 추가해 보겠습니다. json 파일에서 각 큐브의 좌표를 나열한 다음 장면에 표시합니다. 이렇게 하려면 좌표 배열을 나열하는 Cubes.json 파일을 만듭니다.
[ [0, 0, -7], [2, 0, -7], [4, 0, -7], [6, 0, -7], [8, 0, -7], [10, 0, -7] ]
Cube.jsx 파일에서 루프에서 큐브를 생성하는 Cubes 구성 요소를 만듭니다. 그리고 Cube 구성 요소는 직접 생성된 개체가 됩니다.
import {RigidBody} from "@react-three/rapier"; import cubes from "./cubes.json"; export const Cubes = () => { return cubes.map((coords, index) => <Cube key={index} position={coords} />); } const Cube = (props) => { return ( <RigidBody {...props}> <mesh castShadow receiveShadow> <meshStandardMaterial color="white" /> <boxGeometry /> </mesh> </RigidBody> ); }
기존 단일 큐브를 삭제하여 생성된 Cubes 컴포넌트를 App 컴포넌트에 추가해 보겠습니다.
모델을 장면으로 가져오는 데 필요한 형식을 얻으려면 gltf-pipeline 추가 기능 패키지를 설치해야 합니다.
npm i -D gltf-pipeline
gltf-pipeline 패키지를 사용하여 모델을 GLTF 형식 에서 GLB 형식 으로 다시 변환합니다. 이 형식에서는 모든 모델 데이터가 하나의 파일에 저장되기 때문입니다. 생성된 파일의 출력 디렉터리로 공용 폴더를 지정합니다.
gltf-pipeline -i weapon/scene.gltf -o public/weapon.glb
그런 다음 이 모델의 마크업을 포함하는 반응 구성 요소를 생성하여 장면에 추가해야 합니다. @react-3/섬유 개발자의 사용해 보겠습니다.
변환기로 이동하려면 변환된 Weapon.glb 파일을 로드해야 합니다.
변환기에서 우리는 생성된 반응 구성 요소를 볼 수 있습니다. 이 코드는 새 파일 WeaponModel.jsx 에서 프로젝트로 전송되어 구성 요소의 이름을 파일과 동일한 이름으로 변경합니다.
이제 생성된 모델을 장면으로 가져오겠습니다. App.jsx 파일에 WeaponModel 구성 요소를 추가합니다.
장면에 그림자를 활성화하려면 Canvas 구성 요소에 그림자 속성을 추가해야 합니다.
다음으로, 새로운 광원을 추가해야 합니다. 장면에 이미 AmbientLight가 있음에도 불구하고 방향성 광선이 없기 때문에 객체에 대한 그림자를 만들 수 없습니다. 이제 DirectionalLight 라는 새로운 광원을 추가하고 구성해 보겠습니다. " 캐스트 " 섀도우 모드를 활성화하는 속성은 CastShadow 입니다. 이 개체가 다른 개체에 그림자를 투사할 수 있음을 나타내는 것은 이 매개변수의 추가입니다.
그런 다음 또 다른 속성 receiveShadow를 Ground 구성 요소에 추가해 보겠습니다. 이는 장면의 구성 요소가 자체적으로 그림자를 수신하고 표시할 수 있음을 의미합니다.
유사한 속성을 장면의 다른 개체(큐브 및 플레이어)에 추가해야 합니다. 큐브의 경우 그림자를 투사하고 받을 수 있으므로 CastShadow 및 receiveShadow를 추가하고 플레이어의 경우 CastShadow 만 추가합니다.
Player 에 CastShadow를 추가해 보겠습니다.
Cube 에 대해 CastShadow 및 receiveShadow를 추가합니다.
그 이유는 기본적으로 카메라가 DirectionalLight 에서 표시된 그림자의 작은 영역만 캡처하기 때문입니다. 이 가시성 영역을 확장하기 위해 추가 속성 Shadow-camera-(상단, 하단, 왼쪽, 오른쪽)를 추가하여 DirectionalLight 구성 요소에 사용할 수 있습니다. 이러한 속성을 추가하면 그림자가 약간 흐려집니다. 품질을 향상시키기 위해 Shadow-mapSize 속성을 추가하겠습니다.
이제 1인칭 무기 디스플레이를 추가해 보겠습니다. 무기 동작 논리와 3D 모델 자체를 포함하는 새로운 무기 구성 요소를 만듭니다.
import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); }
이 구성 요소를 캐릭터의 RigidBody 와 동일한 수준에 배치하고 useFrame 후크에서 카메라의 값 위치를 기반으로 위치와 회전 각도를 설정하겠습니다.
캐릭터의 걸음걸이를 좀 더 자연스럽게 만들기 위해 움직일 때 무기의 약간의 흔들림을 추가하겠습니다. 애니메이션을 만들기 위해 설치된 tween.js 라이브러리를 사용합니다.
Weapon 구성 요소는 useRef 후크를 통해 참조를 추가할 수 있도록 그룹 태그로 래핑됩니다.
애니메이션을 저장하기 위해 useState를 추가해 보겠습니다.
코드 설명:
- const twSwayingAnimation = new TWEEN.Tween(currentPosition) ... 현재 위치에서 새 위치로 "스윙"하는 객체의 애니메이션을 생성합니다.
- const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... 첫 번째 애니메이션이 완료된 후 시작 위치로 돌아가는 객체의 애니메이션을 만듭니다.
- twSwayingAnimation.chain(twSwayingBackAnimation); 첫 번째 애니메이션이 완료되면 두 번째 애니메이션이 자동으로 시작되도록 두 애니메이션을 연결합니다.
useEffect 에서는 애니메이션 초기화 함수를 호출합니다.
코드 설명:
- const isMoving = 방향.길이() > 0; 여기서는 객체의 이동 상태를 확인합니다. 방향 벡터의 길이가 0보다 크다면 물체가 움직이는 방향이 있다는 뜻입니다.
- if (isMoving && isSwayingAnimationFinished) { ... } 이 상태는 객체가 움직이고 있고 "스윙" 애니메이션이 완료된 경우 실행됩니다.
앱 구성 요소에서 트윈 애니메이션을 업데이트할 useFrame을 추가해 보겠습니다.
TWEEN.update()는 TWEEN.js 라이브러리의 모든 활성 애니메이션을 업데이트합니다. 이 메서드는 모든 애니메이션이 원활하게 실행되도록 하기 위해 각 애니메이션 프레임에서 호출됩니다.
총알이 발사되는 순간, 즉 마우스 버튼을 누르는 순간을 정의해야 합니다. 이 상태를 저장하는 useState , 무기 객체에 대한 참조를 저장하는 useRef , 마우스 버튼을 누르고 놓기 위한 두 개의 이벤트 핸들러를 추가해 보겠습니다.
마우스 버튼을 클릭할 때 반동 애니메이션을 구현해 보겠습니다. 이를 위해 tween.js 라이브러리를 사용합니다.
반동 애니메이션의 임의 벡터를 얻는 함수인 generateRecoilOffset 및 generateNewPositionOfRecoil을 만들어 보겠습니다.
반동 애니메이션을 초기화하는 함수를 만듭니다. 또한 useEffect를 추가하여 "샷" 상태를 종속성으로 지정하여 각 샷에서 애니메이션이 다시 초기화되고 새로운 끝 좌표가 생성되도록 합니다.
그리고 useFrame 에서 발사를 위해 마우스 키를 "유지"하는 검사를 추가하여 키를 놓을 때까지 발사 애니메이션이 멈추지 않도록 합시다.
이를 위해 useState를 통해 몇 가지 새로운 상태를 추가해 보겠습니다.