diff --git a/docs/technical/map-performance.md b/docs/technical/map-performance.md index 84d7538..0d52e55 100644 --- a/docs/technical/map-performance.md +++ b/docs/technical/map-performance.md @@ -137,6 +137,44 @@ Once the expensive model families are isolated, the real triangle fixes are: Chunked instancing is especially important. A single `InstancedMesh` containing every bush has one global bounding sphere. If that bounding sphere is visible, Three.js may keep the whole batch visible. Splitting instances into grid chunks allows entire offscreen chunks to be skipped. +## Player-Only Vegetation Streaming + +The first distance-streaming pass is intentionally limited to vegetation and crop instances: + +- `arbre` +- `sapin` +- `buisson` +- `champdeble` +- `champdesoja` +- `champsdetournesol` + +The behavior is configured in: + +```txt +src/data/world/fogConfig.ts +``` + +Current runtime values: + +```txt +chunkSize: 35 +loadRadius: 45 +unloadRadius: 58 +updateInterval: 350ms +fog near: 34 +fog far: 58 +``` + +The streaming and fog are scoped to the production game scene with the player camera only: + +```txt +sceneMode === "game" && cameraMode === "player" +``` + +This matters for debugging. In debug camera mode there is no fog and no distance streaming, so the developer can inspect the full map freely. In player mode, chunks mount and unmount around the camera to reduce visible triangle count while fog hides vegetation pop-in. + +Chunk cleanup is handled through React unmounting. `VegetationSystem` removes chunks from the tree, and `InstancedVegetation` removes its `THREE.InstancedMesh` objects from the group while disposing the locally created merged geometries/material clones in its own cleanup path. + ## Current Code-Side Optimization Repeated static assets are configured in: diff --git a/src/data/docs/docsSections.ts b/src/data/docs/docsSections.ts index 0070a5a..89b9834 100644 --- a/src/data/docs/docsSections.ts +++ b/src/data/docs/docsSections.ts @@ -80,6 +80,12 @@ export const docGroups: DocGroup[] = [ subtitle: "Step into Three.js internals", meta: "11", }, + { + path: "/docs/map-performance", + title: "Map Performance", + subtitle: "Draw calls, triangles, and streaming", + meta: "12", + }, ], }, { @@ -89,25 +95,25 @@ export const docGroups: DocGroup[] = [ path: "/docs/features", title: "Features", subtitle: "Implemented scope", - meta: "12", + meta: "13", }, { path: "/docs/main-feature", title: "Main Feature", subtitle: "Repair-game prototype", - meta: "13", + meta: "14", }, { path: "/docs/editor", title: "Editor User Guide", subtitle: "Editing workflow", - meta: "14", + meta: "15", }, { path: "/docs/animation", title: "Animation & 3D Model System", subtitle: "Components and usage", - meta: "15", + meta: "16", }, ], }, @@ -118,7 +124,7 @@ export const docGroups: DocGroup[] = [ path: "/docs/code-review", title: "Code Review Prep", subtitle: "Presentation support", - meta: "16", + meta: "17", }, ], }, diff --git a/src/data/world/fogConfig.ts b/src/data/world/fogConfig.ts index f142a12..edf90e6 100644 --- a/src/data/world/fogConfig.ts +++ b/src/data/world/fogConfig.ts @@ -3,15 +3,15 @@ import { TERRAIN_COLORS } from "@/data/world/terrainConfig"; export const FOG_CONFIG = { enabled: true, color: "#c8dbbe", - near: 48, - far: 78, + near: 34, + far: 58, }; export const CHUNK_CONFIG = { enabled: true, - chunkSize: 45, - loadRadius: 60, - unloadRadius: 75, + chunkSize: 35, + loadRadius: 45, + unloadRadius: 58, updateInterval: 350, }; diff --git a/src/pages/docs/map-performance/page.tsx b/src/pages/docs/map-performance/page.tsx new file mode 100644 index 0000000..d1a7f2d --- /dev/null +++ b/src/pages/docs/map-performance/page.tsx @@ -0,0 +1,13 @@ +import mapPerformance from "../../../../docs/technical/map-performance.md?raw"; +import { DocsDocument } from "@/components/docs/DocsDocument"; + +export function DocsMapPerformancePage(): React.JSX.Element { + return ( + + ); +} diff --git a/src/router.tsx b/src/router.tsx index 97fb9da..c836b77 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -17,6 +17,7 @@ import { DocsInteractionRoute, DocsLayoutRoute, DocsMainFeatureRoute, + DocsMapPerformanceRoute, DocsMissionFlowRoute, DocsReadmeRoute, DocsRepairGameRoute, @@ -62,6 +63,7 @@ const docsChildRoutes = [ { path: "hand-tracking", component: DocsHandTrackingRoute }, { path: "zustand", component: DocsZustandRoute }, { path: "three-debugging", component: DocsThreeDebuggingRoute }, + { path: "map-performance", component: DocsMapPerformanceRoute }, { path: "features", component: DocsFeaturesRoute }, { path: "main-feature", component: DocsMainFeatureRoute }, { path: "editor", component: DocsEditorRoute }, diff --git a/src/routes/DocsRoute.tsx b/src/routes/DocsRoute.tsx index 86f775a..950545f 100644 --- a/src/routes/DocsRoute.tsx +++ b/src/routes/DocsRoute.tsx @@ -99,6 +99,10 @@ const LazyDocsThreeDebuggingPage = lazyNamed( () => import("@/pages/docs/three-debugging/page"), "DocsThreeDebuggingPage", ); +const LazyDocsMapPerformancePage = lazyNamed( + () => import("@/pages/docs/map-performance/page"), + "DocsMapPerformancePage", +); export const DocsLayoutRoute = createDocsRoute(LazyDocsLayout); export const DocsReadmeRoute = createDocsRoute(LazyDocsReadmePage); @@ -124,3 +128,6 @@ export const DocsMissionFlowRoute = createDocsRoute(LazyDocsMissionFlowPage); export const DocsThreeDebuggingRoute = createDocsRoute( LazyDocsThreeDebuggingPage, ); +export const DocsMapPerformanceRoute = createDocsRoute( + LazyDocsMapPerformancePage, +); diff --git a/src/world/Environment.tsx b/src/world/Environment.tsx index ee20441..db7a280 100644 --- a/src/world/Environment.tsx +++ b/src/world/Environment.tsx @@ -7,6 +7,7 @@ import { PHYSICS_SCENE_BACKGROUND_COLOR, } from "@/data/world/environmentConfig"; import { FOG_CONFIG } from "@/data/world/fogConfig"; +import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { isMapModelVisible, @@ -15,6 +16,7 @@ import { import { SkyModel } from "@/components/three/world/SkyModel"; export function Environment(): React.JSX.Element { + const cameraMode = useCameraMode(); const sceneMode = useSceneMode(); const groups = useMapPerformanceStore((state) => state.groups); const models = useMapPerformanceStore((state) => state.models); @@ -28,7 +30,7 @@ export function Environment(): React.JSX.Element { return ( <> - {FOG_CONFIG.enabled ? ( + {FOG_CONFIG.enabled && sceneMode === "game" && cameraMode === "player" ? ( state.camera); + const cameraMode = useCameraMode(); + const sceneMode = useSceneMode(); const groups = useMapPerformanceStore((state) => state.groups); const models = useMapPerformanceStore((state) => state.models); const { data, isLoading } = useVegetationData(); @@ -82,6 +86,8 @@ export function VegetationSystem(): React.JSX.Element | null { const [activeChunkKeys, setActiveChunkKeys] = useState>( () => new Set(), ); + const streamingEnabled = + CHUNK_CONFIG.enabled && sceneMode === "game" && cameraMode === "player"; const chunks = useMemo(() => { if (!data) return []; @@ -98,7 +104,7 @@ export function VegetationSystem(): React.JSX.Element | null { }, [data, groups, models]); useFrame(({ clock }) => { - if (!CHUNK_CONFIG.enabled) return; + if (!streamingEnabled) return; const now = clock.elapsedTime * 1000; if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return; @@ -137,7 +143,7 @@ export function VegetationSystem(): React.JSX.Element | null { return null; } - const visibleChunks = CHUNK_CONFIG.enabled + const visibleChunks = streamingEnabled ? chunks.filter((chunk) => activeChunkKeys.has(chunk.key)) : chunks;