Merge branch 'develop' into feat/shader-net
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
This commit is contained in:
@@ -1,34 +1,126 @@
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useRef } from "react";
|
||||
import { Component, useMemo, useRef, type ReactNode } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
|
||||
interface SkyModelProps {
|
||||
modelPath: string;
|
||||
fallbackModelPath?: string | undefined;
|
||||
fallbackScale?: number | undefined;
|
||||
scale?: number | undefined;
|
||||
}
|
||||
|
||||
interface SkyModelContentProps {
|
||||
modelPath: string;
|
||||
scale: number;
|
||||
}
|
||||
|
||||
interface SkyModelErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback: ReactNode;
|
||||
}
|
||||
|
||||
interface SkyModelErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
const SKY_MODEL_SCALE = 1;
|
||||
const SKY_MODEL_RENDER_ORDER = -1000;
|
||||
const LEGACY_SKY_MODEL_PATH = "/models/sky/model.glb";
|
||||
|
||||
export function SkyModel({ modelPath }: SkyModelProps): React.JSX.Element {
|
||||
class SkyModelErrorBoundary extends Component<
|
||||
SkyModelErrorBoundaryProps,
|
||||
SkyModelErrorBoundaryState
|
||||
> {
|
||||
constructor(props: SkyModelErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(): SkyModelErrorBoundaryState {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export function SkyModel({
|
||||
fallbackModelPath,
|
||||
fallbackScale = SKY_MODEL_SCALE,
|
||||
modelPath,
|
||||
scale = SKY_MODEL_SCALE,
|
||||
}: SkyModelProps): React.JSX.Element {
|
||||
const fallback = fallbackModelPath ? (
|
||||
<SkyModelContent modelPath={fallbackModelPath} scale={fallbackScale} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<SkyModelErrorBoundary key={modelPath} fallback={fallback}>
|
||||
<SkyModelContent modelPath={modelPath} scale={scale} />
|
||||
</SkyModelErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function SkyModelContent({
|
||||
modelPath,
|
||||
scale,
|
||||
}: SkyModelContentProps): React.JSX.Element {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const { scene } = useLoggedGLTF(modelPath, {
|
||||
scope: "SkyModel",
|
||||
scale: SKY_MODEL_SCALE,
|
||||
scale,
|
||||
});
|
||||
const model = useClonedObject(scene);
|
||||
const model = useMemo(() => createSkyModel(scene), [scene]);
|
||||
|
||||
useFrame(() => {
|
||||
groupRef.current?.position.copy(camera.position);
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={groupRef} scale={SKY_MODEL_SCALE} frustumCulled={false}>
|
||||
<group
|
||||
ref={groupRef}
|
||||
renderOrder={SKY_MODEL_RENDER_ORDER}
|
||||
scale={scale}
|
||||
frustumCulled={false}
|
||||
>
|
||||
<primitive object={model} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload("/models/sky/model.glb");
|
||||
function createSkyModel(scene: THREE.Object3D): THREE.Object3D {
|
||||
const model = scene.clone(true);
|
||||
|
||||
model.traverse((object) => {
|
||||
object.frustumCulled = false;
|
||||
object.renderOrder = SKY_MODEL_RENDER_ORDER;
|
||||
|
||||
if (!(object instanceof THREE.Mesh)) return;
|
||||
|
||||
object.material = Array.isArray(object.material)
|
||||
? object.material.map(createSkyMaterial)
|
||||
: createSkyMaterial(object.material);
|
||||
});
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
function createSkyMaterial<T extends THREE.Material>(material: T): T {
|
||||
const skyMaterial = material.clone();
|
||||
skyMaterial.side = THREE.BackSide;
|
||||
skyMaterial.depthTest = false;
|
||||
skyMaterial.depthWrite = false;
|
||||
|
||||
return skyMaterial as T;
|
||||
}
|
||||
|
||||
useGLTF.preload("/models/skybox/skybox.gltf");
|
||||
useGLTF.preload(LEGACY_SKY_MODEL_PATH);
|
||||
|
||||
@@ -52,7 +52,7 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
||||
description:
|
||||
"Repair the damaged cooling module before relaunching the bike",
|
||||
modelPath: "/models/ebike/model.gltf",
|
||||
modelScale: 0.50,
|
||||
modelScale: 0.5,
|
||||
stageUiPath: "/assets/UI/ebike.webm",
|
||||
interactUiPath: REPAIR_INTERACT_UI_PATH,
|
||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export const GAME_SCENE_SKY_MODEL_PATH = "/models/sky/model.glb";
|
||||
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/skybox.gltf";
|
||||
export const GAME_SCENE_FALLBACK_SKY_MODEL_PATH = "/models/sky/model.glb";
|
||||
export const GAME_SCENE_SKY_MODEL_SCALE = 300;
|
||||
export const GAME_SCENE_FALLBACK_SKY_MODEL_SCALE = 1;
|
||||
export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018";
|
||||
|
||||
@@ -54,6 +54,7 @@ const docsChildRoutes = [
|
||||
{ path: "architecture", component: DocsArchitectureRoute },
|
||||
{ path: "scene-runtime", component: DocsSceneRuntimeRoute },
|
||||
{ path: "repair-game", component: DocsRepairGameRoute },
|
||||
{ path: "mission-flow", component: DocsMissionFlowRoute },
|
||||
{ path: "interaction", component: DocsInteractionRoute },
|
||||
{ path: "target-architecture", component: DocsTargetArchitectureRoute },
|
||||
{ path: "technical-editor", component: DocsTechnicalEditorRoute },
|
||||
|
||||
@@ -4,6 +4,7 @@ import { parseMapNodes } from "@/utils/map/mapNodeValidation";
|
||||
const MAP_JSON_PATH = "/map.json";
|
||||
const MODEL_FILE_NAMES = ["model.glb", "model.gltf"];
|
||||
const HTML_CONTENT_TYPE = "text/html";
|
||||
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking"]);
|
||||
type ModelEntry = [modelName: string, modelUrl: string];
|
||||
|
||||
export async function loadMapSceneData(): Promise<SceneData | null> {
|
||||
@@ -26,7 +27,13 @@ async function createSceneData(mapNodes: MapNode[]): Promise<SceneData> {
|
||||
async function loadMapModelUrls(
|
||||
mapNodes: MapNode[],
|
||||
): Promise<Map<string, string>> {
|
||||
const uniqueModelNames = [...new Set(mapNodes.map((node) => node.name))];
|
||||
const uniqueModelNames = [
|
||||
...new Set(
|
||||
mapNodes
|
||||
.filter((node) => !MAP_STRUCTURE_NODE_NAMES.has(node.name))
|
||||
.map((node) => node.name),
|
||||
),
|
||||
];
|
||||
const modelEntries = await Promise.all(
|
||||
uniqueModelNames.map((modelName) => loadModelEntry(modelName)),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import {
|
||||
GAME_SCENE_FALLBACK_SKY_MODEL_PATH,
|
||||
GAME_SCENE_FALLBACK_SKY_MODEL_SCALE,
|
||||
GAME_SCENE_SKY_MODEL_PATH,
|
||||
GAME_SCENE_SKY_MODEL_SCALE,
|
||||
PHYSICS_SCENE_BACKGROUND_COLOR,
|
||||
} from "@/data/world/environmentConfig";
|
||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||
@@ -14,5 +17,12 @@ export function Environment(): React.JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
return <SkyModel modelPath={GAME_SCENE_SKY_MODEL_PATH} />;
|
||||
return (
|
||||
<SkyModel
|
||||
fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH}
|
||||
fallbackScale={GAME_SCENE_FALLBACK_SKY_MODEL_SCALE}
|
||||
modelPath={GAME_SCENE_SKY_MODEL_PATH}
|
||||
scale={GAME_SCENE_SKY_MODEL_SCALE}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+41
-1
@@ -22,6 +22,16 @@ interface LoadedMapNode {
|
||||
modelUrl: string | null;
|
||||
}
|
||||
|
||||
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking"]);
|
||||
const LITE_MAP_SKIPPED_NODE_NAMES = new Set([
|
||||
"arbre",
|
||||
"buissons",
|
||||
"champdeble",
|
||||
"champdesoja",
|
||||
"champsdetournesol",
|
||||
"sapin",
|
||||
]);
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback: ReactNode;
|
||||
@@ -133,7 +143,17 @@ export function GameMap({
|
||||
status: "loading",
|
||||
});
|
||||
|
||||
const loadedMapNodes = sceneData.mapNodes.map((node) => {
|
||||
const visibleMapNodes = sceneData.mapNodes.filter(liteMap);
|
||||
const skippedMapNodeCount =
|
||||
sceneData.mapNodes.length - visibleMapNodes.length;
|
||||
|
||||
if (skippedMapNodeCount > 0) {
|
||||
logger.warn("GameMap", "Lite map skipped heavy map nodes", {
|
||||
skippedMapNodeCount,
|
||||
});
|
||||
}
|
||||
|
||||
const loadedMapNodes = visibleMapNodes.map((node) => {
|
||||
const modelUrl = sceneData.models.get(node.name);
|
||||
return { node, modelUrl: modelUrl ?? null };
|
||||
});
|
||||
@@ -214,6 +234,26 @@ export function GameMap({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary development-only map reducer.
|
||||
*
|
||||
* TODO: replace this with a real map performance pass: merged static geometry,
|
||||
* instancing for repeated props, LOD, and/or zone-based loading. For now this
|
||||
* keeps the app usable on local machines by not rendering the densest exported
|
||||
* nodes from map.json.
|
||||
*/
|
||||
function liteMap(node: MapNode): boolean {
|
||||
if (MAP_STRUCTURE_NODE_NAMES.has(node.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.type === "Mesh") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !LITE_MAP_SKIPPED_NODE_NAMES.has(node.name);
|
||||
}
|
||||
|
||||
function MapNodeInstance({
|
||||
node,
|
||||
modelUrl,
|
||||
|
||||
@@ -106,7 +106,8 @@ export function PlayerController({
|
||||
const velocity = useRef(new THREE.Vector3());
|
||||
const onFloor = useRef(false);
|
||||
const wantsJump = useRef(false);
|
||||
const initializedRef = useRef(false); const canMove = useGameStore((state) => state.missionFlow.canMove);
|
||||
const initializedRef = useRef(false);
|
||||
const canMove = useGameStore((state) => state.missionFlow.canMove);
|
||||
|
||||
const capsule = useRef(createSpawnCapsule(spawnPosition));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user