refactor: prepare main feature gameplay object and use GLB sky model

This commit is contained in:
Tom Boullay
2026-04-30 10:02:00 +02:00
parent f66609178b
commit 01c583ba96
26 changed files with 152 additions and 53 deletions
@@ -0,0 +1,42 @@
import { TriggerObject } from "@/components/three/TriggerObject";
import { RepairCaseModel } from "@/components/three/RepairCaseModel";
import { AudioManager } from "@/managers/AudioManager";
import type { Vector3Tuple } from "@/types/three";
interface MainFeatureObjectProps {
position: Vector3Tuple;
open: boolean;
onToggle: () => void;
}
const CASE_MODEL_PATH = "/models/packderelance/model.gltf";
const CASE_SOUND_PATH = "/sounds/effect/fa.mp3";
const CASE_OPEN_SOUND_RATE = 1.08;
const CASE_CLOSE_SOUND_RATE = 0.82;
export function MainFeatureObject({
position,
open,
onToggle,
}: MainFeatureObjectProps): React.JSX.Element {
return (
<TriggerObject
position={position}
colliders="cuboid"
label={open ? "Fermer la mallette" : "Ouvrir la mallette"}
onTrigger={() => {
AudioManager.getInstance().playSound(CASE_SOUND_PATH, 1, {
playbackRate: open ? CASE_CLOSE_SOUND_RATE : CASE_OPEN_SOUND_RATE,
});
onToggle();
}}
>
<RepairCaseModel
modelPath={CASE_MODEL_PATH}
open={open}
position={[0, -0.45, 0]}
scale={1.5}
/>
</TriggerObject>
);
}
+5 -15
View File
@@ -1,11 +1,9 @@
import { useState } from "react";
import { Text } from "@react-three/drei";
import { TriggerObject } from "@/components/three/TriggerObject";
import { MainFeatureObject } from "@/components/three/MainFeatureObject";
import { ModelSelectorPlaceholder } from "@/components/three/ModelSelectorPlaceholder";
import { RepairCaseModel } from "@/components/three/RepairCaseModel";
const ZONE_ORIGIN = [10, 0.4, -8] as const;
const CASE_MODEL_PATH = "/models/packderelance/model.gltf";
const ZONE_RADIUS = 4.2;
export function MainFeatureZone(): React.JSX.Element {
@@ -44,19 +42,11 @@ export function MainFeatureZone(): React.JSX.Element {
Pack de Relance Feature
</Text>
<TriggerObject
<MainFeatureObject
position={[ZONE_ORIGIN[0], ZONE_ORIGIN[1], ZONE_ORIGIN[2]]}
colliders="cuboid"
label={caseOpen ? "Fermer la mallette" : "Ouvrir la mallette"}
onTrigger={() => setCaseOpen((value) => !value)}
>
<RepairCaseModel
modelPath={CASE_MODEL_PATH}
open={caseOpen}
position={[0, -0.45, 0]}
scale={0.35}
/>
</TriggerObject>
open={caseOpen}
onToggle={() => setCaseOpen((value) => !value)}
/>
<ModelSelectorPlaceholder
label="Module A"
+25 -14
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { useGLTF } from "@react-three/drei";
import gsap from "gsap";
import * as THREE from "three";
import type { Vector3Tuple } from "@/types/three";
@@ -13,8 +13,9 @@ interface RepairCaseModelProps {
}
const CASE_LID_NODE_NAME = "partiesup";
const CASE_OPEN_ANGLE = THREE.MathUtils.degToRad(115);
const CASE_OPEN_SPEED = 7;
const CASE_OPEN_ROTATION_OFFSET_Z = 0;
const CASE_CLOSED_ROTATION_OFFSET_Z = THREE.MathUtils.degToRad(-115);
const CASE_ANIMATION_DURATION = 1.2;
export function RepairCaseModel({
modelPath,
@@ -26,29 +27,39 @@ export function RepairCaseModel({
const { scene } = useGLTF(modelPath);
const model = useMemo(() => scene.clone(true), [scene]);
const lidRef = useRef<THREE.Object3D | null>(null);
const closedRotationX = useRef(0);
const initialOpen = useRef(open);
const openedRotationZ = useRef(0);
const parsedScale =
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
useEffect(() => {
const lid = model.getObjectByName(CASE_LID_NODE_NAME);
lidRef.current = lid ?? null;
closedRotationX.current = lid?.rotation.x ?? 0;
openedRotationZ.current = lid?.rotation.z ?? 0;
if (lid && !initialOpen.current) {
lid.rotation.z = openedRotationZ.current + CASE_CLOSED_ROTATION_OFFSET_Z;
}
}, [model]);
useFrame((_, delta) => {
useEffect(() => {
const lid = lidRef.current;
if (!lid) return;
const targetRotation =
closedRotationX.current - (open ? CASE_OPEN_ANGLE : 0);
lid.rotation.x = THREE.MathUtils.damp(
lid.rotation.x,
targetRotation,
CASE_OPEN_SPEED,
delta,
);
});
openedRotationZ.current +
(open ? CASE_OPEN_ROTATION_OFFSET_Z : CASE_CLOSED_ROTATION_OFFSET_Z);
gsap.to(lid.rotation, {
z: targetRotation,
duration: CASE_ANIMATION_DURATION,
ease: "power2.inOut",
overwrite: true,
});
return () => {
gsap.killTweensOf(lid.rotation);
};
}, [open]);
return (
<group position={position} rotation={rotation} scale={parsedScale}>
+29
View File
@@ -0,0 +1,29 @@
import { useFrame, useThree } from "@react-three/fiber";
import { useGLTF } from "@react-three/drei";
import { useMemo, useRef } from "react";
import * as THREE from "three";
interface SkyModelProps {
modelPath: string;
}
const SKY_MODEL_SCALE = 1;
export function SkyModel({ modelPath }: SkyModelProps): React.JSX.Element {
const camera = useThree((state) => state.camera);
const groupRef = useRef<THREE.Group>(null);
const { scene } = useGLTF(modelPath);
const model = useMemo(() => scene.clone(true), [scene]);
useFrame(() => {
groupRef.current?.position.copy(camera.position);
});
return (
<group ref={groupRef} scale={SKY_MODEL_SCALE} frustumCulled={false}>
<primitive object={model} />
</group>
);
}
useGLTF.preload("/models/sky/model.glb");
+1
View File
@@ -6,6 +6,7 @@ export type { SimpleModelConfig } from "./SimpleModel";
export { ExplodableModel } from "./ExplodableModel";
export { MainFeatureZone } from "./MainFeatureZone";
export { MainFeatureObject } from "./MainFeatureObject";
export { ModelSelectorPlaceholder } from "./ModelSelectorPlaceholder";
export { RepairCaseModel } from "./RepairCaseModel";
+1 -5
View File
@@ -6,7 +6,6 @@ import {
HandTrackingContext,
} from "@/hooks/useHandTrackingSnapshot";
import { useRemoteHandTracking } from "@/hooks/useRemoteHandTracking";
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
export function HandTrackingProvider({
children,
@@ -15,10 +14,7 @@ export function HandTrackingProvider({
}): React.JSX.Element {
const sceneMode = useSceneMode();
const { nearby, holding, handHolding } = useInteraction();
const enabled =
isDebugEnabled() &&
sceneMode === "physics" &&
(nearby || holding || handHolding);
const enabled = sceneMode === "physics" && (nearby || holding || handHolding);
const snapshot = useRemoteHandTracking({ enabled });
return (