들어가며
초기에는 간단하게 position을 통해서 전진 후진 회전만 구현하였습니다.
https://ungumungum.tistory.com/121
R3F에서 시바를 키보드 입력을 통해 회전시키기(트러블 슈팅)
들어가며좀 더 interactive한 웹을 만들기 위해서 최근 three.js 기반의 R3F 라이브러리를 활용하여, 웹 페이지를 만들고 있습니다.강아지를 활용하여서 이동하게 하려고 하는데, 강아지의 이동 및 회
ungumungum.tistory.com
총 4가지의 방법을 도입했었고, 결국 어떤 방법을 도입했는지에 대한 설명을 하고자 합니다.
1. position 기반 이동시키기
2. velocity 기반 이동시키기
3. force 기반 이동시키기
4. chassicBody를 이용한 자동차형으로 이동시키기
그리고 R3F에서 위치를 이동시키기 위해선 키보드 이벤트를 감지하여 이동시켜야 합니다.
이때 useInput 커스텀훅을 사용하여서 wasd space를 감지하였고,, 이를 기반으로 이동을 시키는데, 2가지 방식으로 이동이 가능합니다.
1. useEffect : 상태가 변화했을 때 실행
2번과 4번같은 경우에는 물체의 속도를 정하고, 바퀴의 회전속도를 정하여서 이동합니다. 따라서 전진 후진등의 버튼의 상태가 변화했을 때 적용시키면 됩니다.
2. useFrame : 매 프레임마다 상태를 확인하여 실행
1번과 3번의 position을 이동시키거나 힘을 가하는 방식에서는 매 프레임 확인하여 위치를 변경해야합니다.
이 중에서 어떤 방식을 왜 기각했고, 결국 왜 position 기반 이동 방식을 도입하게 되었는지에 대해서 작성하고자 합니다.
제가 고민 및 도입했던 과정은 1 4 3 2 1 순이며, 1번의 경우 앞선 글에서 설명했으니, 4 3 2 1 순으로 설명하고자 합니다.
각각의 방식 도입과 기각 이유
chassicBody를 이용한 자동차형으로 이동
도입배경은 useCannon에서 충돌체가 불안정하기 때문입니다. 참고 : https://ungumungum.tistory.com/122
R3F에서 물체가 지나갈 수 있는 언덕 만들기(react-three/cannon)
아래와 같은 환경에서 언덕을 만들어주려고 합니다.cannon-es 공식문서에서 간단하게 Trimesh를 이용해서 삼각형을 만드는 법을 알려주는데, 이를 기반으로 react-three/cannon에서 삼각형 모양의 기
ungumungum.tistory.com
box나 cylinder 이외의 다른 충돌체의 경우 충돌이 불안정하였고, 이중 cylinder가 타 충돌체와의 상호작용이 안정적이었습니다.
따라서 다리를 만들어하는 제 입장에서 바퀴가 cylinder인 방식을 도입한다면 좋을 것이라 판단하였습니다.
useCannon에서는 기본적으로 자동차 엔진과 유사한 형식으로 작동하는 훅(useRayCastVehicle)을 제공하고 있습니다.
(useCannon은 결국 cannon을 확장해서 만든거라 설명은 cannon-es가 더 자세합니다.)
이를 기반으로 바퀴를 조정하는 바퀴 및 이동 물체의 정보를 받아서, 조종하는 훅을 만들었습니다. (관심사 분리가 명확하지 않은점은 죄송합니다 ㅠ.)
//useMoveCar.ts
import { useEffect, useRef } from 'react';
import { useInput } from './useInput';
import {
PublicApi,
RaycastVehiclePublicApi,
useRaycastVehicle,
} from '@react-three/cannon';
import { Group, Object3DEventMap, Quaternion, Vector3 } from 'three';
import { useWheels } from './useWheels';
type MoveCarProps = {
chassisApi: PublicApi;
chassisBody: React.RefObject<Group<Object3DEventMap>>;
worldDirection: Vector3;
bodyInfo: {
width: number;
height: number;
front: number;
};
};
export const useMoveCar = ({
chassisApi,
worldDirection,
chassisBody,
bodyInfo,
}: MoveCarProps) => {
const { forward, backward, left, right, jump, stand } = useInput();
const engineForce = 400;
const velocity = useRef([0, 0, 0]);
const { width, height, front } = bodyInfo;
const [wheels, wheelInfos] = useWheels({ width, height, front });
const [vehicle, vehicleApi] = useRaycastVehicle(
() => ({
chassisBody,
wheelInfos,
wheels,
}),
useRef<Group>(null)
);
useEffect(() => {
chassisApi.velocity.subscribe((v) => (velocity.current = v));
if (forward) {
vehicleApi.applyEngineForce(engineForce, 2);
vehicleApi.applyEngineForce(engineForce, 3);
} else if (backward) {
vehicleApi.applyEngineForce(-engineForce, 2);
vehicleApi.applyEngineForce(-engineForce, 3);
} else {
vehicleApi.applyEngineForce(0, 2);
vehicleApi.applyEngineForce(0, 3);
}
if (left) {
vehicleApi.setSteeringValue(0.35, 2);
vehicleApi.setSteeringValue(0.35, 3);
vehicleApi.setSteeringValue(-0.1, 0);
vehicleApi.setSteeringValue(-0.1, 1);
} else if (right) {
vehicleApi.setSteeringValue(-0.35, 2);
vehicleApi.setSteeringValue(-0.35, 3);
vehicleApi.setSteeringValue(0.1, 0);
vehicleApi.setSteeringValue(0.1, 1);
} else {
for (let i = 0; i < 4; i++) {
vehicleApi.setSteeringValue(0, i);
}
}
if (jump) {
const [x, y, z] = velocity.current;
chassisApi.velocity.set(x * 1.2, y + 7, z * 1.2);
}
if (stand) {
const rotateQuaternion = new Quaternion().setFromAxisAngle(
new Vector3(0, 1, 0),
Math.PI / 2
);
const currentQuaternion = new Quaternion().setFromUnitVectors(
new Vector3(1, 0, 0),
worldDirection
);
currentQuaternion.multiply(rotateQuaternion); // y축 회전 적용
chassisApi.quaternion.set(
currentQuaternion.x,
currentQuaternion.y,
currentQuaternion.z,
currentQuaternion.w
); // 쿼터니언을 직접 설정
}
}, [
backward,
stand,
worldDirection,
chassisApi,
forward,
jump,
left,
right,
vehicleApi,
]);
return { vehicle, vehicleApi };
};
기능은 총 4가지로 만들었습니다.
1. 전후진 , 2. 좌우 바퀴회전 , 3. 점프(바퀴가 공중에 뜨기 때문에 x.z축으로 약간의 가속), 4. 서기(전복 시 사용)
이동과 trimesh로 만든 다리를 건너는 기능까지는 가능합니다.
문제점
하지만 여러 단점이 발견되었고, 결국 일반적인 Box로 움직이도록 하기로 결정하였습니다.
1. 하지만 문제점이 언덕에서 잘 미끄러짐.
2. 바퀴의 크기가 작으면 몸체가 충돌하여 언덕 잘 못 올라감
3. 바퀴가 크면 언덕을 올라가기전에 종종 충돌해서 멈춤
4. 시바가 자동차처럼 움직여서 부자연스러움
force 를 이용한 이동
const [chassisBody, chassisApi] = useCompoundBody(
() => ({
position,
mass: mass,
rotation: [0, 0, 0],
collisionFilterGroup: 5,
angularDamping: 0.95,
onCollide: () => {
!isLanded && setIsLanded(true);
},
shapes: [
{
args: chassisBodyArgs,
position: [0, 0, 0],
type: 'Box',
},
],
}),
useRef<Group>(null)
);
chassicApi.applyForce([x,y,z])
다음과 같이 cannon의 충돌체에는 힘을 가할 수 있는 옵션이 있습니다. 이를 바탕으로 시바가 바라보고 있는 방향으로 힘을 가하여 이동시키는 로직에, 회전의 경우 기존 y축 기준으로 회전시키는 방법을 도입하면 어떤가 했습니다.
문제점
기본적으로 -y축 방향으로 9.8의 중력이 가해지고 있습니다. 이때 힘을 가하여 이동시키니 시바가 이동하는게 아닌 구르기 시작했습니다..
그렇기에 회전시에 마찰력을 존재하게 해서 구르지 못하도록 하는 방식을 도입하려고 했습니다.
위의 코드의 useCompoundBody 훅을 보시면 angularDamping이라는 각회전시 마찰력이 존재합니다. 이를 1로 하면 회전하지 않게 되는데, 이런 경우에는 언덕을 못 올라가는 문제가 발생하였습니다..
언덕을 볼 때는 30도 위를 쳐다봐야하는데, 그러지 않으니 힘을 가해도 언덕에 부딪히기만 하는 문제가 생겼습니다.
velocity 를 이용한 이동
chassicApi.velocity.set(x,y,z)
마찬가지로 속력을 가하여 이동시키는 방식을 도입하였는데, 해당 방식의 경우에 이동은 제대로 되었습니다.
언덕도 올라갈 수 있고, 적당한 angularDamping (0.95)를 설정시 구르지도 않게 되었습니다.
문제점
하지만 전진할 때 속력을 부여하는 방식은 점프와 같이 쓰기엔 불안정했습니다.
일반적인 이동에서는 y방향으로의 속력을 0으로 설정합니다. 문제는 공중에 떠 있을 때 입니다.
중력의 영향을 받기에 내려가야하는데 , y축의 속력이 0이 아니기에 낙하하는 속력을 구해서 직접 반영해야합니다.
매번 shiba의 y축 속력을 구해서 이동시키는건 너무 비효율적인 방식이어서 최종적으로 position을 기반으로 이동하는 방식을 도입하였습니다.
position 방식과 코드
추후에 이벤트 발생과도 연계해야했기에, 점프를 하고 있다면 전역변수를 통해 착륙하지 않음(isLanded = false)를 만들었고, 타 물체와 충돌 시에 착륙으로 만드는 로직을 구현하였습니다.
import { Group, Object3DEventMap, Quaternion, Vector3 } from 'three';
import { useInput } from './useInput';
import { PublicApi } from '@react-three/cannon';
import { useMemo } from 'react';
import { useShibaStore } from '@/store/shiba';
type MovePositionProps = {
worldPosition: Vector3;
worldDirection: Vector3;
chassisApi: PublicApi;
chassisBody: React.RefObject<Group<Object3DEventMap>>;
};
export const useMovePosition = ({
worldDirection,
worldPosition,
chassisApi,
chassisBody,
}: MovePositionProps) => {
const { forward, backward, left, right, jump, stand } = useInput();
const worldQuaternion = useMemo(() => new Quaternion(), []);
const { isLanded, setIsLanded, blockEvent, eventable } = useShibaStore();
const controlMovement = (delta: number) => {
if (forward || backward) {
const speed = delta * 7;
let { x, y, z } = worldPosition;
let { x: rx, y: ry, z: rz } = worldDirection;
let [newX, newY, newZ] = [x, y, z];
if (forward) {
newX += rx * speed;
newZ += rz * speed;
}
if (backward) {
newX -= rx * speed;
newZ -= rz * speed;
}
chassisApi.position.set(newX, newY, newZ);
}
if (right || left) {
const turnAngle = delta;
const turnQuaternion = new Quaternion();
chassisBody?.current!.getWorldPosition(worldPosition);
if (right) {
turnQuaternion.setFromAxisAngle(new Vector3(0, 1, 0), -turnAngle);
}
if (left) {
turnQuaternion.setFromAxisAngle(new Vector3(0, 1, 0), turnAngle);
}
worldQuaternion.multiplyQuaternions(turnQuaternion, worldQuaternion);
chassisApi.quaternion.set(
worldQuaternion.x,
worldQuaternion.y,
worldQuaternion.z,
worldQuaternion.w
);
}
if (jump) {
if (isLanded) {
setIsLanded(false);
}
chassisApi.velocity.set(0, 5, 0);
}
};
return controlMovement;
};
'프로젝트' 카테고리의 다른 글
Draco를 활용하여 glb파일 압축하여 성능 향상시키기 (0) | 2024.07.01 |
---|---|
ManulPopup창을 관심사 분리하기 (값,계산,액션, SOLID) (0) | 2024.06.29 |
Warning: Extra attributes from the server: style 에러 해결하기( next.js) (0) | 2024.06.20 |
R3F에서 물체가 지나갈 수 있는 언덕 만들기(react-three/cannon) (0) | 2024.06.04 |
R3F에서 시바를 키보드 입력을 통해 회전시키기(트러블 슈팅) (0) | 2024.05.30 |