12 Commits

Author SHA1 Message Date
Tom Boullay 28f7db172c upatde: add new models
🔍 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
2026-05-14 00:20:20 +02:00
Tom Boullay 2063656f29 chore: update eslint config and CI workflow 2026-05-14 00:18:02 +02:00
Tom Boullay 592cfa405f feat: add graphics settings hook 2026-05-14 00:17:53 +02:00
Tom Boullay 0a32cd1d21 feat: add map node caching for vegetation system 2026-05-14 00:17:44 +02:00
Tom Boullay 4516cf4ec6 feat: configure shadow map settings for better shadow quality 2026-05-14 00:17:33 +02:00
Tom Boullay c6f60d1ca7 update: vegetation models 2026-05-14 00:17:08 +02:00
Tom Boullay aa35e97cbb update: skybox model files 2026-05-14 00:16:58 +02:00
Tom Boullay 57c142c8ef fix: correct texture paths case sensitivity in GLTF models 2026-05-14 00:16:47 +02:00
Tom Boullay 4843bf1d75 fix: correct skybox model path 2026-05-14 00:16:37 +02:00
Tom Boullay d02cf29a1d debug: add logging for scene loading flow 2026-05-14 00:16:28 +02:00
Tom Boullay 3b4c9c2529 feat: add WebGL context loss handler and GPU performance config 2026-05-14 00:16:18 +02:00
Tom Boullay fdf03349cf fix: enable terrain loading for collision octree build 2026-05-14 00:16:09 +02:00
49 changed files with 228 additions and 33 deletions
@@ -2,7 +2,7 @@ name: 🔁 Branch Promotions
on:
schedule:
- cron: "0 6 * * 1,4" # Lundi et Jeudi à 6h UTC (design → develop)
- cron: "0 6 * * 1,4" # Lundi et Jeudi à 6h UTC (design → develop)
- cron: "0 6 * * 1" # Lundi à 6h UTC (develop → main)
workflow_dispatch:
inputs:
+1 -1
View File
@@ -7,7 +7,7 @@ import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores(["dist"]),
globalIgnores(["dist", "POC-grass"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/skybox.gltf";
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/model.gltf";
export const GAME_SCENE_FALLBACK_SKY_MODEL_PATH = "/models/sky/model.glb";
export const GAME_SCENE_SKY_MODEL_SCALE = 100;
export const GAME_SCENE_FALLBACK_SKY_MODEL_SCALE = 1;
+10
View File
@@ -3,6 +3,7 @@ import type { RefObject } from "react";
import type { Object3D } from "three";
import { Octree } from "three/addons/math/Octree.js";
import type { OctreeReadyHandler } from "@/types/three/three";
import { logger } from "@/utils/core/Logger";
export function useOctreeGraphNode(
graphNodeRef: RefObject<Object3D | null>,
@@ -17,16 +18,25 @@ export function useOctreeGraphNode(
}, [rebuildKey]);
useEffect(() => {
logger.debug("useOctreeGraphNode", "Check", {
enabled,
octreeBuilt: octreeBuilt.current,
hasGraphNode: !!graphNodeRef.current,
rebuildKey,
});
if (!enabled) return;
const graphNode = graphNodeRef.current;
if (!enabled || octreeBuilt.current || !graphNode) return;
octreeBuilt.current = true;
logger.info("useOctreeGraphNode", "Building octree from graph node");
graphNode.updateMatrixWorld(true);
const octree = new Octree();
octree.fromGraphNode(graphNode);
logger.info("useOctreeGraphNode", "Octree built, calling onOctreeReady");
onOctreeReady(octree);
}, [enabled, graphNodeRef, onOctreeReady, rebuildKey]);
}
+6 -6
View File
@@ -6,7 +6,7 @@ export function useGraphicsSettings(): GraphicsState {
}
export function useSetGraphicsSettings(): (
graphics: Partial<GraphicsState>
graphics: Partial<GraphicsState>,
) => void {
return useWorldSettingsStore((state) => state.setGraphics);
}
@@ -33,19 +33,19 @@ export function useGrassDensity(): number {
export function useGraphicsSetters() {
const setDynamicGrass = useWorldSettingsStore(
(state) => state.setDynamicGrass
(state) => state.setDynamicGrass,
);
const setDynamicTrees = useWorldSettingsStore(
(state) => state.setDynamicTrees
(state) => state.setDynamicTrees,
);
const setDynamicClouds = useWorldSettingsStore(
(state) => state.setDynamicClouds
(state) => state.setDynamicClouds,
);
const setShadowsEnabled = useWorldSettingsStore(
(state) => state.setShadowsEnabled
(state) => state.setShadowsEnabled,
);
const setGrassDensity = useWorldSettingsStore(
(state) => state.setGrassDensity
(state) => state.setGrassDensity,
);
return {
+4
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import type { Octree } from "three/addons/math/Octree.js";
import type { SceneMode } from "@/types/debug/debug";
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
import { logger } from "@/utils/core/Logger";
interface UseWorldSceneLoadingOptions {
onLoadingStateChange?: SceneLoadingChangeHandler | undefined;
@@ -31,10 +32,12 @@ export function useWorldSceneLoading({
(sceneMode === "physics" && octree !== null);
const handleGameMapLoaded = useCallback(() => {
logger.info("WorldSceneLoading", "GameMap loaded");
setGameMapLoaded(true);
}, []);
const handleGameStageLoaded = useCallback(() => {
logger.info("WorldSceneLoading", "GameStage loaded");
setGameStageLoaded(true);
onLoadingStateChange?.({
currentStep: "Initialisation gameplay",
@@ -45,6 +48,7 @@ export function useWorldSceneLoading({
const handleOctreeReady = useCallback(
(nextOctree: Octree) => {
logger.info("WorldSceneLoading", "Octree ready");
setOctree(nextOctree);
onLoadingStateChange?.({
currentStep: "Collision prête",
+20
View File
@@ -15,6 +15,7 @@ import {
type SceneLoadingChangeHandler,
type SceneLoadingState,
} from "@/types/world/sceneLoading";
import { logger } from "@/utils/core/Logger";
const SAVE_ERROR_MESSAGE = "Erreur lors de l'enregistrement";
@@ -243,8 +244,27 @@ export function EditorPage(): React.JSX.Element {
<Canvas
camera={{ position: [0, 50, 100], fov: 50 }}
style={{ width: "100%", height: "100%" }}
gl={{
powerPreference: "high-performance",
antialias: true,
stencil: false,
}}
onCreated={({ gl }) => {
gl.setClearColor("#050505");
const canvas = gl.domElement;
const handleContextLost = (event: Event) => {
event.preventDefault();
logger.error("WebGL", "Context lost - GPU resources exhausted");
};
const handleContextRestored = () => {
logger.info("WebGL", "Context restored");
};
canvas.addEventListener("webglcontextlost", handleContextLost);
canvas.addEventListener(
"webglcontextrestored",
handleContextRestored,
);
}}
>
<EditorSceneLoadingTracker
+26
View File
@@ -12,6 +12,7 @@ import {
INITIAL_SCENE_LOADING_STATE,
type SceneLoadingState,
} from "@/types/world/sceneLoading";
import { logger } from "@/utils/core/Logger";
import { World } from "@/world/World";
export function HomePage(): React.JSX.Element {
@@ -51,11 +52,36 @@ export function HomePage(): React.JSX.Element {
[],
);
const handleCanvasCreated = useCallback(
({ gl }: { gl: THREE.WebGLRenderer }) => {
const canvas = gl.domElement;
const handleContextLost = (event: Event) => {
event.preventDefault();
logger.error("WebGL", "Context lost - GPU resources exhausted");
};
const handleContextRestored = () => {
logger.info("WebGL", "Context restored");
};
canvas.addEventListener("webglcontextlost", handleContextLost);
canvas.addEventListener("webglcontextrestored", handleContextRestored);
},
[],
);
return (
<HandTrackingProvider>
<Canvas
camera={{ position: [85, 60, 85], fov: 42 }}
shadows={{ type: THREE.PCFShadowMap }}
gl={{
powerPreference: "high-performance",
antialias: true,
stencil: false,
}}
onCreated={handleCanvasCreated}
>
<Suspense fallback={null}>
<World onLoadingStateChange={handleSceneLoadingStateChange} />
+29
View File
@@ -7,7 +7,36 @@ const HTML_CONTENT_TYPE = "text/html";
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking"]);
type ModelEntry = [modelName: string, modelUrl: string];
let cachedSceneData: SceneData | null = null;
let loadingPromise: Promise<SceneData | null> | null = null;
export async function loadMapSceneData(): Promise<SceneData | null> {
if (cachedSceneData) {
return cachedSceneData;
}
if (loadingPromise) {
return loadingPromise;
}
loadingPromise = loadMapSceneDataInternal();
cachedSceneData = await loadingPromise;
loadingPromise = null;
return cachedSceneData;
}
export function getMapNodes(): MapNode[] | null {
return cachedSceneData?.mapNodes ?? null;
}
export function getMapNodesByName(name: string): MapNode[] {
const nodes = cachedSceneData?.mapNodes;
if (!nodes) return [];
return nodes.filter((node) => node.name === name);
}
async function loadMapSceneDataInternal(): Promise<SceneData | null> {
const response = await fetch(MAP_JSON_PATH);
if (!response.ok) {
+12 -1
View File
@@ -7,9 +7,12 @@ import {
useRef,
useState,
} from "react";
import * as THREE from "three";
import { useClonedObject } from "@/hooks/three/useClonedObject";
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
import { TerrainModel } from "@/components/three/world/TerrainModel";
import { GameMapCollision } from "@/world/GameMapCollision";
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
import { logger } from "@/utils/core/Logger";
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
@@ -222,6 +225,8 @@ export function GameMap({
</ModelErrorBoundary>
))}
</group>
<VegetationSystem />
<TerrainModel />
<GameMapCollision
buildOctree={buildOctree}
mapReady={mapReady}
@@ -299,8 +304,14 @@ function ModelInstance({
const sceneInstance = useClonedObject(scene);
useEffect(() => {
sceneInstance.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
onLoaded();
}, [onLoaded]);
}, [onLoaded, sceneInstance]);
return (
<primitive
+10
View File
@@ -14,6 +14,7 @@ import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
import type { MapNode } from "@/types/editor/editor";
import type { OctreeReadyHandler } from "@/types/three/three";
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
import { logger } from "@/utils/core/Logger";
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
export interface GameMapCollisionNode {
@@ -108,6 +109,14 @@ export function GameMapCollision({
const collisionReady =
mapReady && settledCollisionNodeCount >= collisionNodes.length;
logger.debug("GameMapCollision", "State", {
mapReady,
collisionNodesCount: collisionNodes.length,
settledCollisionNodeCount,
collisionReady,
buildOctree,
});
const notifyLoaded = useCallback(() => {
if (loadedNotifiedRef.current) return;
@@ -124,6 +133,7 @@ export function GameMapCollision({
const handleOctreeReady = useCallback<OctreeReadyHandler>(
(octree) => {
logger.info("GameMapCollision", "Octree built, calling onOctreeReady");
onLoadingStateChange?.({
currentStep: "Collision prête",
progress: 0.92,
+20 -1
View File
@@ -1,4 +1,4 @@
import { useRef } from "react";
import { useEffect, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import type { AmbientLight, DirectionalLight } from "three";
import {
@@ -23,6 +23,11 @@ import {
} from "@/data/world/lightingConfig";
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
const SHADOW_MAP_SIZE = 2048;
const SHADOW_CAMERA_SIZE = 100;
const SHADOW_CAMERA_NEAR = 0.5;
const SHADOW_CAMERA_FAR = 200;
type LightingState = {
ambientIntensity: number;
sunIntensity: number;
@@ -37,6 +42,20 @@ export function Lighting(): React.JSX.Element {
const ambient = useRef<AmbientLight>(null);
const sun = useRef<DirectionalLight>(null);
useEffect(() => {
if (!sun.current) return;
sun.current.shadow.mapSize.width = SHADOW_MAP_SIZE;
sun.current.shadow.mapSize.height = SHADOW_MAP_SIZE;
sun.current.shadow.camera.left = -SHADOW_CAMERA_SIZE;
sun.current.shadow.camera.right = SHADOW_CAMERA_SIZE;
sun.current.shadow.camera.top = SHADOW_CAMERA_SIZE;
sun.current.shadow.camera.bottom = -SHADOW_CAMERA_SIZE;
sun.current.shadow.camera.near = SHADOW_CAMERA_NEAR;
sun.current.shadow.camera.far = SHADOW_CAMERA_FAR;
sun.current.shadow.camera.updateProjectionMatrix();
}, []);
useDebugFolder("Lighting", (folder) => {
folder
.add(