Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ef94af488 | |||
| 27b4a2c392 | |||
| d5feb07ff0 | |||
| c33d973f12 | |||
| 396e7e4ff0 |
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState, useMemo, useCallback } from "react";
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { EbikeGPSMap } from "@/components/ebike/EbikeGPSMap";
|
import { EbikeGPSMap } from "@/components/ebike/EbikeGPSMap";
|
||||||
|
import { EbikeSpeedometer } from "@/components/ebike/EbikeSpeedometer";
|
||||||
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
@@ -37,6 +38,11 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
const setMissionStep = useGameStore((state) => state.setMissionStep);
|
const setMissionStep = useGameStore((state) => state.setMissionStep);
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const updateEbikeSounds = useEbikeSounds();
|
const updateEbikeSounds = useEbikeSounds();
|
||||||
|
const repairGameOwnsEbikeModel =
|
||||||
|
mainState === "ebike" &&
|
||||||
|
ebikeStep !== "locked" &&
|
||||||
|
ebikeStep !== "waiting" &&
|
||||||
|
ebikeStep !== "inspected";
|
||||||
|
|
||||||
// Map active mainState to target repair zone coordinate
|
// Map active mainState to target repair zone coordinate
|
||||||
const destPos = useMemo(() => {
|
const destPos = useMemo(() => {
|
||||||
@@ -169,16 +175,30 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
debugRestingPosition[1] + EBIKE_DROP_PLAYER_TRANSFORM.position[1],
|
debugRestingPosition[1] + EBIKE_DROP_PLAYER_TRANSFORM.position[1],
|
||||||
debugRestingPosition[2] + EBIKE_DROP_PLAYER_TRANSFORM.position[2],
|
debugRestingPosition[2] + EBIKE_DROP_PLAYER_TRANSFORM.position[2],
|
||||||
];
|
];
|
||||||
|
const interactionLabel =
|
||||||
|
mainState === "ebike"
|
||||||
|
? "Réparer l'e-bike"
|
||||||
|
: movementMode === "walk"
|
||||||
|
? "Monter sur le bike"
|
||||||
|
: "Descendre du bike";
|
||||||
|
|
||||||
const handleInteract = useCallback((): void => {
|
const handleInteract = useCallback((): void => {
|
||||||
if (window.ebikeBreakdownActive === true) return;
|
if (window.ebikeBreakdownActive === true) return;
|
||||||
|
|
||||||
if (movementMode === "walk") {
|
if (movementMode === "walk") {
|
||||||
if (mainState === "ebike" && ebikeStep === "waiting") {
|
if (
|
||||||
|
mainState === "ebike" &&
|
||||||
|
(ebikeStep === "locked" || ebikeStep === "waiting")
|
||||||
|
) {
|
||||||
setMissionStep("ebike", "inspected");
|
setMissionStep("ebike", "inspected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mainState === "ebike" && ebikeStep === "inspected") {
|
||||||
|
setMissionStep("ebike", "fragmented");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const cameraOffset = new THREE.Vector3(
|
const cameraOffset = new THREE.Vector3(
|
||||||
...EBIKE_CAMERA_TRANSFORM.position,
|
...EBIKE_CAMERA_TRANSFORM.position,
|
||||||
);
|
);
|
||||||
@@ -258,51 +278,50 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<group
|
{!repairGameOwnsEbikeModel ? (
|
||||||
ref={groupRef}
|
<group
|
||||||
position={position}
|
ref={groupRef}
|
||||||
rotation={[0, EBIKE_WORLD_ROTATION_Y, 0]}
|
|
||||||
>
|
|
||||||
<primitive object={model} />
|
|
||||||
<InteractableObject
|
|
||||||
kind="trigger"
|
|
||||||
label={
|
|
||||||
mainState === "ebike" && ebikeStep === "waiting"
|
|
||||||
? "Inspecter l'e-bike"
|
|
||||||
: movementMode === "walk"
|
|
||||||
? "Monter sur le bike"
|
|
||||||
: "Descendre du bike"
|
|
||||||
}
|
|
||||||
position={position}
|
position={position}
|
||||||
radius={15}
|
rotation={[0, EBIKE_WORLD_ROTATION_Y, 0]}
|
||||||
onPress={handleInteract}
|
|
||||||
>
|
>
|
||||||
<mesh>
|
<primitive object={model} />
|
||||||
<boxGeometry args={[10, 13, 2]} />
|
<InteractableObject
|
||||||
<meshBasicMaterial colorWrite={false} depthWrite={false} />
|
kind="trigger"
|
||||||
</mesh>
|
label={interactionLabel}
|
||||||
</InteractableObject>
|
position={position}
|
||||||
|
radius={15}
|
||||||
|
onPress={handleInteract}
|
||||||
|
>
|
||||||
|
<mesh>
|
||||||
|
<boxGeometry args={[10, 13, 2]} />
|
||||||
|
<meshBasicMaterial colorWrite={false} depthWrite={false} />
|
||||||
|
</mesh>
|
||||||
|
</InteractableObject>
|
||||||
|
|
||||||
{/* Dynamic 3D GPS Dashboard Screen */}
|
{/* Dynamic 3D GPS Dashboard Screen */}
|
||||||
<group position={[0, 7, 0]} rotation={[0, 90, 0]}>
|
<group position={[0, 7, 0]} rotation={[0, 90, 0]}>
|
||||||
<EbikeGPSMap
|
<EbikeGPSMap
|
||||||
width={0.8}
|
width={0.8}
|
||||||
height={0.8}
|
height={0.8}
|
||||||
startPos={gpsStartPos}
|
startPos={gpsStartPos}
|
||||||
destPos={destPos}
|
destPos={destPos}
|
||||||
mapImageUrl="/assets/world/gps/map_background.png"
|
mapImageUrl="/assets/world/gps/map_background.png"
|
||||||
worldBounds={{
|
worldBounds={{
|
||||||
minX: -166,
|
minX: -166,
|
||||||
maxX: 163,
|
maxX: 163,
|
||||||
minZ: -142,
|
minZ: -142,
|
||||||
maxZ: 138,
|
maxZ: 138,
|
||||||
}}
|
}}
|
||||||
zoom={4}
|
zoom={4}
|
||||||
/>
|
/>
|
||||||
|
</group>
|
||||||
|
<group position={[0, 6.35, 0]} rotation={[0, 90, 0]}>
|
||||||
|
<EbikeSpeedometer />
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
) : null}
|
||||||
|
|
||||||
{showCameraPoints && (
|
{showCameraPoints && !repairGameOwnsEbikeModel && (
|
||||||
<>
|
<>
|
||||||
<mesh position={camPointPos}>
|
<mesh position={camPointPos}>
|
||||||
<sphereGeometry args={[0.3, 16, 16]} />
|
<sphereGeometry args={[0.3, 16, 16]} />
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ export interface EbikeGPSMapProps {
|
|||||||
* Default: 1
|
* Default: 1
|
||||||
*/
|
*/
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
|
|
||||||
|
renderOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,6 +109,7 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
|||||||
position = [0, 0, 0],
|
position = [0, 0, 0],
|
||||||
canvasSize = 1024,
|
canvasSize = 1024,
|
||||||
zoom = 1,
|
zoom = 1,
|
||||||
|
renderOrder = 10_000,
|
||||||
}) => {
|
}) => {
|
||||||
const [waypoints, setWaypoints] = useState<Waypoint[]>([]);
|
const [waypoints, setWaypoints] = useState<Waypoint[]>([]);
|
||||||
const [mapImage, setMapImage] = useState<
|
const [mapImage, setMapImage] = useState<
|
||||||
@@ -506,12 +509,13 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
|||||||
}, [draw]);
|
}, [draw]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh castShadow receiveShadow position={position}>
|
<mesh position={position} renderOrder={renderOrder}>
|
||||||
<planeGeometry args={[width, height]} />
|
<planeGeometry args={[width, height]} />
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
toneMapped={false}
|
toneMapped={false}
|
||||||
transparent={true}
|
transparent={true}
|
||||||
opacity={1}
|
opacity={1}
|
||||||
|
depthTest={false}
|
||||||
depthWrite={false}
|
depthWrite={false}
|
||||||
side={THREE.DoubleSide}
|
side={THREE.DoubleSide}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import { useTexture } from "@react-three/drei";
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
const SPEEDOMETER_DIAL_TEXTURE = "/assets/world/gps/cadran.png";
|
||||||
|
const SPEEDOMETER_NEEDLE_TEXTURE = "/assets/world/gps/fleche.png";
|
||||||
|
const SPEEDOMETER_MIN_ANGLE = Math.PI / 2;
|
||||||
|
const SPEEDOMETER_MAX_ANGLE = -Math.PI / 2;
|
||||||
|
const SPEEDOMETER_RENDER_ORDER = 10_000;
|
||||||
|
|
||||||
|
interface EbikeSpeedometerProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EbikeSpeedometer({
|
||||||
|
width = 0.9,
|
||||||
|
height = 0.5,
|
||||||
|
}: EbikeSpeedometerProps): React.JSX.Element {
|
||||||
|
const needleGroupRef = useRef<THREE.Group>(null);
|
||||||
|
const speedFactorRef = useRef(0);
|
||||||
|
const [dialTexture, needleTexture] = useTexture([
|
||||||
|
SPEEDOMETER_DIAL_TEXTURE,
|
||||||
|
SPEEDOMETER_NEEDLE_TEXTURE,
|
||||||
|
]) as [THREE.Texture, THREE.Texture];
|
||||||
|
const needleWidth = width * 0.68;
|
||||||
|
const needleHeight = needleWidth / 2;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
[dialTexture, needleTexture].forEach((texture) => {
|
||||||
|
texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
});
|
||||||
|
}, [dialTexture, needleTexture]);
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
const targetSpeedFactor = THREE.MathUtils.clamp(
|
||||||
|
window.ebikeSpeedFactor ?? 0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
speedFactorRef.current = THREE.MathUtils.lerp(
|
||||||
|
speedFactorRef.current,
|
||||||
|
targetSpeedFactor,
|
||||||
|
Math.min(1, delta * 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (needleGroupRef.current) {
|
||||||
|
needleGroupRef.current.rotation.z = THREE.MathUtils.lerp(
|
||||||
|
SPEEDOMETER_MIN_ANGLE,
|
||||||
|
SPEEDOMETER_MAX_ANGLE,
|
||||||
|
speedFactorRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group renderOrder={SPEEDOMETER_RENDER_ORDER}>
|
||||||
|
<mesh renderOrder={SPEEDOMETER_RENDER_ORDER}>
|
||||||
|
<planeGeometry args={[width, height]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
map={dialTexture}
|
||||||
|
transparent
|
||||||
|
depthTest={false}
|
||||||
|
depthWrite={false}
|
||||||
|
toneMapped={false}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<group ref={needleGroupRef} position={[0, -height * 0.38, 0.002]}>
|
||||||
|
<mesh
|
||||||
|
position={[0, needleHeight / 2, 0]}
|
||||||
|
renderOrder={SPEEDOMETER_RENDER_ORDER + 1}
|
||||||
|
>
|
||||||
|
<planeGeometry args={[needleWidth, needleHeight]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
map={needleTexture}
|
||||||
|
transparent
|
||||||
|
depthTest={false}
|
||||||
|
depthWrite={false}
|
||||||
|
toneMapped={false}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ export function SiteMobileBlocker(): React.JSX.Element {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/assets/logo/logo.jpg"
|
src="/assets/logo.png"
|
||||||
alt="Logo Altera"
|
alt="Logo Altera"
|
||||||
style={{ width: 120, height: "auto" }}
|
style={{ width: 120, height: "auto" }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as THREE from "three";
|
|||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import { useThree } from "@react-three/fiber";
|
import { useThree } from "@react-three/fiber";
|
||||||
import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig";
|
import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig";
|
||||||
|
import { flattenLaFabrikTerrainFootprint } from "@/data/world/laFabrikConfig";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||||
|
|
||||||
@@ -65,9 +66,10 @@ export function TerrainModel({
|
|||||||
const terrainModel = useMemo(() => {
|
const terrainModel = useMemo(() => {
|
||||||
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||||
const model = scene.clone(true);
|
const model = scene.clone(true);
|
||||||
|
flattenLaFabrikTerrainFootprint(model, position, rotation, scale);
|
||||||
applyTerrainMaterialSettings(model, receiveShadow);
|
applyTerrainMaterialSettings(model, receiveShadow);
|
||||||
return model;
|
return model;
|
||||||
}, [maxAnisotropy, scene, receiveShadow]);
|
}, [maxAnisotropy, position, receiveShadow, rotation, scale, scene]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onLoaded?.();
|
onLoaded?.();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { AppLoadingIndicator } from "@/components/ui/AppLoadingIndicator";
|
|||||||
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
||||||
|
|
||||||
const LOADING_BACKGROUND_PATH = "/assets/bg-site.png";
|
const LOADING_BACKGROUND_PATH = "/assets/bg-site.png";
|
||||||
const LOADING_LOGO_PATH = "/assets/logo/logo.jpg";
|
const LOADING_LOGO_PATH = "/assets/logo.png";
|
||||||
|
|
||||||
for (const path of [LOADING_BACKGROUND_PATH, LOADING_LOGO_PATH]) {
|
for (const path of [LOADING_BACKGROUND_PATH, LOADING_LOGO_PATH]) {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const EBIKE_DROP_PLAYER_TRANSFORM: CameraTransform = {
|
|||||||
rotation: [0, 0, 0],
|
rotation: [0, 0, 0],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EBIKE_WORLD_POSITION: Vector3Tuple = [61.5, 10, 62.4];
|
export const EBIKE_WORLD_POSITION: Vector3Tuple = [61.5, 8.4, 62.4];
|
||||||
export const EBIKE_WORLD_ROTATION_Y = 2.4107;
|
export const EBIKE_WORLD_ROTATION_Y = 2.4107;
|
||||||
|
|
||||||
export const EBIKE_INTRO_RIDE_DURATION_MS = 5000;
|
export const EBIKE_INTRO_RIDE_DURATION_MS = 5000;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
import { LA_FABRIK_PLAYER_SPAWN } from "@/data/world/laFabrikConfig";
|
||||||
|
|
||||||
export const PLAYER_EYE_HEIGHT = 1.75;
|
export const PLAYER_EYE_HEIGHT = 1.75;
|
||||||
export const PLAYER_CAPSULE_RADIUS = 0.35;
|
export const PLAYER_CAPSULE_RADIUS = 0.35;
|
||||||
@@ -14,5 +15,5 @@ export const PLAYER_XZ_DAMPING_FACTOR = 8;
|
|||||||
export const PLAYER_FALL_RESPAWN_Y = -20;
|
export const PLAYER_FALL_RESPAWN_Y = -20;
|
||||||
export const PLAYER_FALL_RESPAWN_DELAY = 3;
|
export const PLAYER_FALL_RESPAWN_DELAY = 3;
|
||||||
|
|
||||||
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [59.5, 10, 64.64];
|
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = LA_FABRIK_PLAYER_SPAWN;
|
||||||
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
|
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface CharacterConfig {
|
|||||||
scale: Vector3Tuple;
|
scale: Vector3Tuple;
|
||||||
animations: readonly string[];
|
animations: readonly string[];
|
||||||
defaultAnimation: string;
|
defaultAnimation: string;
|
||||||
|
snapToTerrain?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CHARACTER_CONFIGS = {
|
export const CHARACTER_CONFIGS = {
|
||||||
@@ -42,6 +43,7 @@ export const CHARACTER_CONFIGS = {
|
|||||||
scale: [1.55, 1.55, 1.55],
|
scale: [1.55, 1.55, 1.55],
|
||||||
animations: ["idle", "walk"],
|
animations: ["idle", "walk"],
|
||||||
defaultAnimation: "idle",
|
defaultAnimation: "idle",
|
||||||
|
snapToTerrain: false,
|
||||||
},
|
},
|
||||||
fermier: {
|
fermier: {
|
||||||
id: "fermier",
|
id: "fermier",
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import * as THREE from "three";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
export const LA_FABRIK_CENTER: Vector3Tuple = [59.4973, 6.2746, 64.6354];
|
||||||
|
export const LA_FABRIK_ROTATION_Y = 2.4107;
|
||||||
|
export const LA_FABRIK_HALF_EXTENTS = {
|
||||||
|
x: 8.5,
|
||||||
|
z: 7.5,
|
||||||
|
} as const;
|
||||||
|
export const LA_FABRIK_FLOOR_Y = 6.3;
|
||||||
|
export const LA_FABRIK_PLAYER_SPAWN: Vector3Tuple = [59.5, 8.05, 64.64];
|
||||||
|
export const LA_FABRIK_INTERIOR_LIGHT_POSITION: Vector3Tuple = [59.5, 9, 64.64];
|
||||||
|
|
||||||
|
const _terrainMatrix = new THREE.Matrix4();
|
||||||
|
const _meshWorldMatrix = new THREE.Matrix4();
|
||||||
|
const _inverseMeshWorldMatrix = new THREE.Matrix4();
|
||||||
|
const _worldPosition = new THREE.Vector3();
|
||||||
|
|
||||||
|
export function isInsideLaFabrikFootprint(
|
||||||
|
x: number,
|
||||||
|
z: number,
|
||||||
|
padding = 0,
|
||||||
|
): boolean {
|
||||||
|
const dx = x - LA_FABRIK_CENTER[0];
|
||||||
|
const dz = z - LA_FABRIK_CENTER[2];
|
||||||
|
const cos = Math.cos(-LA_FABRIK_ROTATION_Y);
|
||||||
|
const sin = Math.sin(-LA_FABRIK_ROTATION_Y);
|
||||||
|
const localX = dx * cos - dz * sin;
|
||||||
|
const localZ = dx * sin + dz * cos;
|
||||||
|
|
||||||
|
return (
|
||||||
|
Math.abs(localX) <= LA_FABRIK_HALF_EXTENTS.x + padding &&
|
||||||
|
Math.abs(localZ) <= LA_FABRIK_HALF_EXTENTS.z + padding
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenLaFabrikTerrainFootprint(
|
||||||
|
object: THREE.Object3D,
|
||||||
|
position: Vector3Tuple,
|
||||||
|
rotation: Vector3Tuple,
|
||||||
|
scale: Vector3Tuple,
|
||||||
|
): void {
|
||||||
|
_terrainMatrix.compose(
|
||||||
|
new THREE.Vector3(...position),
|
||||||
|
new THREE.Quaternion().setFromEuler(new THREE.Euler(...rotation)),
|
||||||
|
new THREE.Vector3(...scale),
|
||||||
|
);
|
||||||
|
object.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
object.traverse((child) => {
|
||||||
|
if (!(child instanceof THREE.Mesh)) return;
|
||||||
|
const geometry = child.geometry;
|
||||||
|
const positions = geometry.getAttribute("position");
|
||||||
|
if (!positions) return;
|
||||||
|
|
||||||
|
_meshWorldMatrix.multiplyMatrices(_terrainMatrix, child.matrixWorld);
|
||||||
|
_inverseMeshWorldMatrix.copy(_meshWorldMatrix).invert();
|
||||||
|
|
||||||
|
for (let index = 0; index < positions.count; index++) {
|
||||||
|
_worldPosition
|
||||||
|
.fromBufferAttribute(positions, index)
|
||||||
|
.applyMatrix4(_meshWorldMatrix);
|
||||||
|
|
||||||
|
if (!isInsideLaFabrikFootprint(_worldPosition.x, _worldPosition.z, 0.8)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_worldPosition.y = Math.min(_worldPosition.y, LA_FABRIK_FLOOR_Y - 0.35);
|
||||||
|
_worldPosition.applyMatrix4(_inverseMeshWorldMatrix);
|
||||||
|
positions.setXYZ(
|
||||||
|
index,
|
||||||
|
_worldPosition.x,
|
||||||
|
_worldPosition.y,
|
||||||
|
_worldPosition.z,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
positions.needsUpdate = true;
|
||||||
|
geometry.computeVertexNormals();
|
||||||
|
geometry.computeBoundingBox();
|
||||||
|
geometry.computeBoundingSphere();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@ import { useMemo } from "react";
|
|||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig";
|
import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig";
|
||||||
|
import {
|
||||||
|
isInsideLaFabrikFootprint,
|
||||||
|
LA_FABRIK_FLOOR_Y,
|
||||||
|
} from "@/data/world/laFabrikConfig";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
import { getMapNodesByName } from "@/utils/map/loadMapSceneData";
|
import { getMapNodesByName } from "@/utils/map/loadMapSceneData";
|
||||||
|
|
||||||
@@ -66,6 +70,10 @@ function createTerrainHeightSampler(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
getHeight: (x, z) => {
|
getHeight: (x, z) => {
|
||||||
|
if (isInsideLaFabrikFootprint(x, z, 0.6)) {
|
||||||
|
return LA_FABRIK_FLOOR_Y;
|
||||||
|
}
|
||||||
|
|
||||||
localOrigin.set(x, RAYCAST_Y, z).applyMatrix4(inverseTerrainMatrix);
|
localOrigin.set(x, RAYCAST_Y, z).applyMatrix4(inverseTerrainMatrix);
|
||||||
raycaster.set(localOrigin, localDirection);
|
raycaster.set(localOrigin, localDirection);
|
||||||
hits.length = 0;
|
hits.length = 0;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
useTerrainHeightSampler,
|
useTerrainHeightSampler,
|
||||||
} from "@/hooks/three/useTerrainHeight";
|
} from "@/hooks/three/useTerrainHeight";
|
||||||
import { WorldBoundsCollision } from "@/world/collision/WorldBoundsCollision";
|
import { WorldBoundsCollision } from "@/world/collision/WorldBoundsCollision";
|
||||||
|
import { flattenLaFabrikTerrainFootprint } from "@/data/world/laFabrikConfig";
|
||||||
import type { MapNode } from "@/types/map/mapScene";
|
import type { MapNode } from "@/types/map/mapScene";
|
||||||
import type { OctreeReadyHandler } from "@/types/three/three";
|
import type { OctreeReadyHandler } from "@/types/three/three";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
@@ -213,7 +214,7 @@ function CollisionModelInstance({
|
|||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
onLoaded: () => void;
|
onLoaded: () => void;
|
||||||
terrainHeight: TerrainHeightSampler;
|
terrainHeight: TerrainHeightSampler;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element | null {
|
||||||
const { position, rotation, scale } = node;
|
const { position, rotation, scale } = node;
|
||||||
const normalizedScale = normalizeMapScale(scale);
|
const normalizedScale = normalizeMapScale(scale);
|
||||||
const { scene } = useLoggedGLTF(modelUrl, {
|
const { scene } = useLoggedGLTF(modelUrl, {
|
||||||
@@ -223,22 +224,46 @@ function CollisionModelInstance({
|
|||||||
scale: normalizedScale,
|
scale: normalizedScale,
|
||||||
});
|
});
|
||||||
const sceneInstance = useClonedObject(scene);
|
const sceneInstance = useClonedObject(scene);
|
||||||
|
const collisionSceneInstance = useMemo(() => {
|
||||||
|
if (node.name === "terrain") {
|
||||||
|
flattenLaFabrikTerrainFootprint(
|
||||||
|
sceneInstance,
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
normalizedScale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return sceneInstance;
|
||||||
|
}, [node.name, normalizedScale, position, rotation, sceneInstance]);
|
||||||
const collisionPosition = useMemo(() => {
|
const collisionPosition = useMemo(() => {
|
||||||
if (node.name === "terrain") return position;
|
if (node.name === "terrain") return position;
|
||||||
|
|
||||||
const [x, y, z] = position;
|
const [x, y, z] = position;
|
||||||
const height = terrainHeight.getHeight(x, z);
|
const height = terrainHeight.getHeight(x, z);
|
||||||
const bottomOffset = getObjectBottomOffset(sceneInstance, normalizedScale);
|
const bottomOffset = getObjectBottomOffset(
|
||||||
|
collisionSceneInstance,
|
||||||
|
normalizedScale,
|
||||||
|
);
|
||||||
return [x, height !== null ? height + bottomOffset : y, z] as const;
|
return [x, height !== null ? height + bottomOffset : y, z] as const;
|
||||||
}, [node.name, normalizedScale, position, sceneInstance, terrainHeight]);
|
}, [
|
||||||
|
node.name,
|
||||||
|
normalizedScale,
|
||||||
|
position,
|
||||||
|
collisionSceneInstance,
|
||||||
|
terrainHeight,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onLoaded();
|
onLoaded();
|
||||||
}, [onLoaded]);
|
}, [onLoaded]);
|
||||||
|
|
||||||
|
if (node.name === "lafabrik") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<primitive
|
<primitive
|
||||||
object={sceneInstance}
|
object={collisionSceneInstance}
|
||||||
position={collisionPosition}
|
position={collisionPosition}
|
||||||
rotation={rotation}
|
rotation={rotation}
|
||||||
scale={normalizedScale}
|
scale={normalizedScale}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
SUN_Z_MIN,
|
SUN_Z_MIN,
|
||||||
SUN_Z_STEP,
|
SUN_Z_STEP,
|
||||||
} from "@/data/world/lightingConfig";
|
} from "@/data/world/lightingConfig";
|
||||||
|
import { LA_FABRIK_INTERIOR_LIGHT_POSITION } from "@/data/world/laFabrikConfig";
|
||||||
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
import { LIGHTING_STATE } from "@/world/lightingState";
|
import { LIGHTING_STATE } from "@/world/lightingState";
|
||||||
|
|
||||||
@@ -121,6 +122,13 @@ export function Lighting(): React.JSX.Element {
|
|||||||
castShadow
|
castShadow
|
||||||
/>
|
/>
|
||||||
<object3D ref={sunTarget} />
|
<object3D ref={sunTarget} />
|
||||||
|
<pointLight
|
||||||
|
position={LA_FABRIK_INTERIOR_LIGHT_POSITION}
|
||||||
|
color="#dbeafe"
|
||||||
|
intensity={1.2}
|
||||||
|
distance={14}
|
||||||
|
decay={1.6}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ import { AnimatedModel } from "@/components/three/models/AnimatedModel";
|
|||||||
import {
|
import {
|
||||||
CHARACTER_CONFIGS,
|
CHARACTER_CONFIGS,
|
||||||
CHARACTER_IDS,
|
CHARACTER_IDS,
|
||||||
|
type CharacterConfig,
|
||||||
type CharacterId,
|
type CharacterId,
|
||||||
} from "@/data/world/characters/characterConfig";
|
} from "@/data/world/characters/characterConfig";
|
||||||
import { useTerrainSnappedPosition } from "@/hooks/three/useTerrainHeight";
|
import { useTerrainSnappedPosition } from "@/hooks/three/useTerrainHeight";
|
||||||
import { useCharacterDebugStore } from "@/managers/stores/useCharacterDebugStore";
|
import { useCharacterDebugStore } from "@/managers/stores/useCharacterDebugStore";
|
||||||
|
|
||||||
function CharacterModel({ id }: { id: CharacterId }): React.JSX.Element {
|
function CharacterModel({ id }: { id: CharacterId }): React.JSX.Element {
|
||||||
const config = CHARACTER_CONFIGS[id];
|
const config: CharacterConfig = CHARACTER_CONFIGS[id];
|
||||||
const state = useCharacterDebugStore((store) => store.characters[id]);
|
const state = useCharacterDebugStore((store) => store.characters[id]);
|
||||||
const position = useTerrainSnappedPosition(state.position);
|
const snappedPosition = useTerrainSnappedPosition(state.position);
|
||||||
|
const position =
|
||||||
|
config.snapToTerrain === false ? state.position : snappedPosition;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedModel
|
<AnimatedModel
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import {
|
|||||||
GRASS_COLORS,
|
GRASS_COLORS,
|
||||||
GRASS_CONFIG,
|
GRASS_CONFIG,
|
||||||
} from "@/data/world/grassConfig";
|
} from "@/data/world/grassConfig";
|
||||||
|
import {
|
||||||
|
LA_FABRIK_CENTER,
|
||||||
|
LA_FABRIK_HALF_EXTENTS,
|
||||||
|
LA_FABRIK_ROTATION_Y,
|
||||||
|
} from "@/data/world/laFabrikConfig";
|
||||||
import {
|
import {
|
||||||
grassFragmentShader,
|
grassFragmentShader,
|
||||||
grassVertexShader,
|
grassVertexShader,
|
||||||
@@ -169,6 +174,17 @@ function createGrassMaterial(
|
|||||||
uMaxBladeHeight: { value: GRASS_CONFIG.maxBladeHeight },
|
uMaxBladeHeight: { value: GRASS_CONFIG.maxBladeHeight },
|
||||||
uRandomHeightAmount: { value: GRASS_CONFIG.randomHeightAmount },
|
uRandomHeightAmount: { value: GRASS_CONFIG.randomHeightAmount },
|
||||||
uSurfaceOffset: { value: GRASS_CONFIG.surfaceOffset },
|
uSurfaceOffset: { value: GRASS_CONFIG.surfaceOffset },
|
||||||
|
uLaFabrikCenter: {
|
||||||
|
value: new THREE.Vector2(LA_FABRIK_CENTER[0], LA_FABRIK_CENTER[2]),
|
||||||
|
},
|
||||||
|
uLaFabrikHalfExtents: {
|
||||||
|
value: new THREE.Vector2(
|
||||||
|
LA_FABRIK_HALF_EXTENTS.x,
|
||||||
|
LA_FABRIK_HALF_EXTENTS.z,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uLaFabrikRotation: { value: LA_FABRIK_ROTATION_Y },
|
||||||
|
uLaFabrikNoGrassFeather: { value: 1.4 },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export const grassVertexShader = /* glsl */ `
|
|||||||
uniform float uMaxBladeHeight;
|
uniform float uMaxBladeHeight;
|
||||||
uniform float uRandomHeightAmount;
|
uniform float uRandomHeightAmount;
|
||||||
uniform float uSurfaceOffset;
|
uniform float uSurfaceOffset;
|
||||||
|
uniform vec2 uLaFabrikCenter;
|
||||||
|
uniform vec2 uLaFabrikHalfExtents;
|
||||||
|
uniform float uLaFabrikRotation;
|
||||||
|
uniform float uLaFabrikNoGrassFeather;
|
||||||
|
|
||||||
float random(vec2 st) {
|
float random(vec2 st) {
|
||||||
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
|
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
|
||||||
@@ -132,6 +136,18 @@ export const grassVertexShader = /* glsl */ `
|
|||||||
smoothstep(uBoundingBoxMax.z, uBoundingBoxMax.z - 2.0, worldPos.z);
|
smoothstep(uBoundingBoxMax.z, uBoundingBoxMax.z - 2.0, worldPos.z);
|
||||||
heightModifier *= edgeFade * mix(0.45, 1.0, clumpMask);
|
heightModifier *= edgeFade * mix(0.45, 1.0, clumpMask);
|
||||||
|
|
||||||
|
vec2 laFabrikDelta = worldPos.xz - uLaFabrikCenter;
|
||||||
|
float laFabrikCos = cos(-uLaFabrikRotation);
|
||||||
|
float laFabrikSin = sin(-uLaFabrikRotation);
|
||||||
|
vec2 laFabrikLocal = vec2(
|
||||||
|
laFabrikDelta.x * laFabrikCos - laFabrikDelta.y * laFabrikSin,
|
||||||
|
laFabrikDelta.x * laFabrikSin + laFabrikDelta.y * laFabrikCos
|
||||||
|
);
|
||||||
|
vec2 laFabrikDistance = abs(laFabrikLocal) - uLaFabrikHalfExtents;
|
||||||
|
float laFabrikOutsideDistance = max(laFabrikDistance.x, laFabrikDistance.y);
|
||||||
|
float laFabrikGrassMask = smoothstep(0.0, uLaFabrikNoGrassFeather, laFabrikOutsideDistance);
|
||||||
|
heightModifier *= laFabrikGrassMask;
|
||||||
|
|
||||||
float sideFactor = (color.r == 0.1) ? 1.0 : (color.b == 0.1) ? -1.0 : 0.0;
|
float sideFactor = (color.r == 0.1) ? 1.0 : (color.b == 0.1) ? -1.0 : 0.0;
|
||||||
float tipFactor = color.g;
|
float tipFactor = color.g;
|
||||||
float width = smoothstep(0.02, uMaxBladeHeight * 0.85, heightModifier) * uBladeWidth * bladeVisibility;
|
float width = smoothstep(0.02, uMaxBladeHeight * 0.85, heightModifier) * uBladeWidth * bladeVisibility;
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ export function GeneratedMapNodeInstance({
|
|||||||
node,
|
node,
|
||||||
onLoaded,
|
onLoaded,
|
||||||
}: GeneratedMapNodeInstanceProps): React.JSX.Element | null {
|
}: GeneratedMapNodeInstanceProps): React.JSX.Element | null {
|
||||||
const position = useTerrainSnappedPosition(node.position);
|
const snappedPosition = useTerrainSnappedPosition(node.position);
|
||||||
|
const position = node.name === "lafabrik" ? node.position : snappedPosition;
|
||||||
const scale = normalizeMapScale(node.scale);
|
const scale = normalizeMapScale(node.scale);
|
||||||
|
|
||||||
if (node.name === "ecole") {
|
if (node.name === "ecole") {
|
||||||
|
|||||||
Reference in New Issue
Block a user