10 Commits

Author SHA1 Message Date
Tom Boullay 68253fae41 Update lightingConfig.ts
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-06-02 14:52:54 +02:00
Tom Boullay 2dabb73d3d fix(vegetation): scale-correct sapin and buisson LOD meshes
The exported sapin-LOD and buisson-LOD models do not match their full
detail counterpart's baseline scale, so they appear oversized once the
LOD swap kicks in. Add MAP_LOD_SCALE_MULTIPLIERS keyed by map name
(sapin: 0.5, buisson: 0.8) and apply it on top of the chunk's existing
scaleMultiplier whenever the resolved model path is the LOD variant.
2026-06-02 14:50:07 +02:00
Tom Boullay 4f1b3b4ff3 fix(graphics): tune presets, single-line ui, vegetation LOD by nearest instance
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
- Bump high to 30m chunk / HD<20m and ultra to HD<30m so HD models
  persist further before swapping.
- Render the 5 graphics preset cards on a single row.
- Vegetation LOD selection now uses the distance to the nearest instance
  in each chunk instead of the chunk centre, matching MapInstancingSystem
  and avoiding premature LOD swaps when the camera enters a chunk.
2026-06-02 14:33:16 +02:00
Tom Boullay 627c8d4eb9 feat(vegetation): wire arbre/sapin/buisson into LOD system
Register the three new vegetation LOD models in MAP_LOD_MODEL_PATHS and
extend VegetationSystem with per-chunk distance-based LOD selection
(mirroring MapInstancingSystem). Chunk model paths are re-evaluated on
the existing CHUNK_CONFIG.updateInterval cadence and Suspense keys
include the resolved path so a swap unmounts the previous instanced mesh
cleanly.
2026-06-02 14:17:19 +02:00
Tom Boullay 0b801888f0 update: fix name assets + add LOD 2026-06-02 14:13:55 +02:00
Tom Boullay a180b89ee6 fix(pylone): convert lampe BLEND to MASK in GLTFs
Switching alphaMode from BLEND to MASK with a 0.5 cutoff lets the lampe
material participate in instanced shadow rendering correctly. BLEND is
known to interact badly with InstancedMesh shadow casting, producing
incorrect or missing shadows on the pylone lampe.
2026-06-02 13:52:08 +02:00
Tom Boullay 3e66e31117 feat(graphics): add max preset (no chunk streaming, LOD@50m)
Restore ultra to its original behaviour (50m chunk streaming, HD within
20m, no fog) and introduce a new max preset that disables chunk streaming
entirely (loads all chunks unconditionally) and pushes the HD/LOD swap
distance to 50m. Add chunkStreamingEnabled flag to GraphicsPresetConfig
so the streaming gate honours the preset. The settings card label shows
'All' when streaming is off.
2026-06-02 13:51:33 +02:00
Tom Boullay 2c194cdd2e fix: use player ebike speed
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-06-02 09:48:44 +02:00
Tom Boullay feaf502e44 feat: support webm mission notifications 2026-06-02 09:40:18 +02:00
math-pixel 489499f5d2 Merge pull request 'Feat/polish-mission1' (#12) from feat/polish-mission1 into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
Reviewed-on: #12
2026-06-01 21:51:09 +00:00
18 changed files with 202 additions and 44 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4 -2
View File
@@ -105,6 +105,9 @@ function GraphicsPresetButton({
const lodLabel = config.forceLodModels const lodLabel = config.forceLodModels
? "LOD forcé" ? "LOD forcé"
: `HD ${config.lodHighDetailDistance}m`; : `HD ${config.lodHighDetailDistance}m`;
const chunkLabel = config.chunkStreamingEnabled
? formatChunkDistance(config.chunkLoadRadius)
: "All";
return ( return (
<button <button
@@ -115,8 +118,7 @@ function GraphicsPresetButton({
> >
<span>{config.label}</span> <span>{config.label}</span>
<small> <small>
{formatChunkDistance(config.chunkLoadRadius)} · {lodLabel} ·{" "} {chunkLabel} · {lodLabel} · {config.fogEnabled ? "Fog" : "Clear"}
{config.fogEnabled ? "Fog" : "Clear"}
</small> </small>
</button> </button>
); );
+19 -5
View File
@@ -14,6 +14,7 @@ export function MissionNotification({
}: MissionNotificationProps): React.JSX.Element { }: MissionNotificationProps): React.JSX.Element {
const src = const src =
imagePath ?? (mission ? MISSION_NOTIFICATION_IMAGE_PATHS[mission] : ""); imagePath ?? (mission ? MISSION_NOTIFICATION_IMAGE_PATHS[mission] : "");
const isVideo = src.toLowerCase().endsWith(".webm");
return ( return (
<div <div
@@ -22,11 +23,24 @@ export function MissionNotification({
> >
<div className="mission-notification__glow" /> <div className="mission-notification__glow" />
<span className="mission-notification__image-wrap"> <span className="mission-notification__image-wrap">
<img {isVideo ? (
className="mission-notification__image" <video
src={src} className="mission-notification__image"
alt="Nouvel objectif de mission" src={src}
/> aria-label="Nouvel objectif de mission"
autoPlay
loop
muted
playsInline
preload="auto"
/>
) : (
<img
className="mission-notification__image"
src={src}
alt="Nouvel objectif de mission"
/>
)}
</span> </span>
</div> </div>
); );
-1
View File
@@ -22,7 +22,6 @@ export const EBIKE_WORLD_SCALE = 0.35;
export const EBIKE_INTRO_BREAKDOWN_DISTANCE = 15; export const EBIKE_INTRO_BREAKDOWN_DISTANCE = 15;
export const EBIKE_BREAKDOWN_DIALOGUE_DELAY_MS = 250; export const EBIKE_BREAKDOWN_DIALOGUE_DELAY_MS = 250;
export const EBIKE_MAX_SPEED = 3;
export const EBIKE_ACCELERATION_DURATION_MS = 2000; export const EBIKE_ACCELERATION_DURATION_MS = 2000;
export const EBIKE_DECELERATION_DURATION_MS = 2000; export const EBIKE_DECELERATION_DURATION_MS = 2000;
+3 -3
View File
@@ -5,7 +5,7 @@ export const INTRO_MISSION_NOTIFICATION_IMAGE_PATH =
export const MISSION_NOTIFICATION_IMAGE_PATHS: Record<RepairMissionId, string> = export const MISSION_NOTIFICATION_IMAGE_PATHS: Record<RepairMissionId, string> =
{ {
ebike: "/assets/world/UI/ebike-mission-notification.png", ebike: "/assets/world/UI/ebike-mission-notification.webm",
pylon: "/assets/world/UI/pylon-mission-notification.png", pylon: "/assets/world/UI/pylon-mission-notification.webm",
farm: "/assets/world/UI/farm-mission-notification.png", farm: "/assets/world/UI/farm-mission-notification.webm",
}; };
+3 -3
View File
@@ -21,7 +21,7 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
"Repair the damaged cooling module before relaunching the bike", "Repair the damaged cooling module before relaunching the bike",
modelPath: "/models/ebike/model.gltf", modelPath: "/models/ebike/model.gltf",
modelScale: 0.3, modelScale: 0.3,
stageUiPath: "/assets/world/UI/ebike.webm", stageUiPath: "/assets/world/UI/ebike-mission-notification.webm",
interactUiPath: REPAIR_INTERACT_UI_PATH, interactUiPath: REPAIR_INTERACT_UI_PATH,
brokenUiPath: REPAIR_BROKEN_UI_PATH, brokenUiPath: REPAIR_BROKEN_UI_PATH,
case: DEFAULT_REPAIR_CASE, case: DEFAULT_REPAIR_CASE,
@@ -59,7 +59,7 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
description: description:
"Restore the pylon lamp relay and damaged panel before reconnecting the grid", "Restore the pylon lamp relay and damaged panel before reconnecting the grid",
modelPath: "/models/pylone/model.gltf", modelPath: "/models/pylone/model.gltf",
stageUiPath: "/assets/world/UI/centrale.webm", stageUiPath: "/assets/world/UI/pylon-mission-notification.webm",
interactUiPath: REPAIR_INTERACT_UI_PATH, interactUiPath: REPAIR_INTERACT_UI_PATH,
brokenUiPath: REPAIR_BROKEN_UI_PATH, brokenUiPath: REPAIR_BROKEN_UI_PATH,
case: DEFAULT_REPAIR_CASE, case: DEFAULT_REPAIR_CASE,
@@ -104,7 +104,7 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
description: description:
"Stabilize the irrigation loop and humidity sensor before restarting the farm", "Stabilize the irrigation loop and humidity sensor before restarting the farm",
modelPath: "/models/fermeverticale/model.gltf", modelPath: "/models/fermeverticale/model.gltf",
stageUiPath: "/assets/world/UI/laferme.webm", stageUiPath: "/assets/world/UI/farm-mission-notification.webm",
interactUiPath: REPAIR_INTERACT_UI_PATH, interactUiPath: REPAIR_INTERACT_UI_PATH,
brokenUiPath: REPAIR_BROKEN_UI_PATH, brokenUiPath: REPAIR_BROKEN_UI_PATH,
case: DEFAULT_REPAIR_CASE, case: DEFAULT_REPAIR_CASE,
+1 -1
View File
@@ -5,7 +5,7 @@ export const PLAYER_EYE_HEIGHT = 1.75;
export const PLAYER_CAPSULE_RADIUS = 0.35; export const PLAYER_CAPSULE_RADIUS = 0.35;
export const PLAYER_WALK_SPEED = 5; export const PLAYER_WALK_SPEED = 5;
export const PLAYER_EBIKE_SPEED = 30; export const PLAYER_EBIKE_SPEED = 20;
export const PLAYER_AIR_CONTROL_FACTOR = 0.35; export const PLAYER_AIR_CONTROL_FACTOR = 0.35;
export const PLAYER_JUMP_SPEED = 9; export const PLAYER_JUMP_SPEED = 9;
export const PLAYER_GRAVITY = 30; export const PLAYER_GRAVITY = 30;
+25 -5
View File
@@ -1,9 +1,16 @@
export const GRAPHICS_PRESET_KEYS = ["low", "medium", "high", "ultra"] as const; export const GRAPHICS_PRESET_KEYS = [
"low",
"medium",
"high",
"ultra",
"max",
] as const;
export type GraphicsPreset = (typeof GRAPHICS_PRESET_KEYS)[number]; export type GraphicsPreset = (typeof GRAPHICS_PRESET_KEYS)[number];
export interface GraphicsPresetConfig { export interface GraphicsPresetConfig {
chunkLoadRadius: number; chunkLoadRadius: number;
chunkStreamingEnabled: boolean;
chunkUnloadRadius: number; chunkUnloadRadius: number;
fogEnabled: boolean; fogEnabled: boolean;
forceLodModels: boolean; forceLodModels: boolean;
@@ -16,6 +23,7 @@ export const GRAPHICS_PRESETS = {
label: "Basse", label: "Basse",
chunkLoadRadius: 10, chunkLoadRadius: 10,
chunkUnloadRadius: 18, chunkUnloadRadius: 18,
chunkStreamingEnabled: true,
fogEnabled: true, fogEnabled: true,
forceLodModels: true, forceLodModels: true,
lodHighDetailDistance: 0, lodHighDetailDistance: 0,
@@ -24,25 +32,37 @@ export const GRAPHICS_PRESETS = {
label: "Moyenne", label: "Moyenne",
chunkLoadRadius: 20, chunkLoadRadius: 20,
chunkUnloadRadius: 30, chunkUnloadRadius: 30,
chunkStreamingEnabled: true,
fogEnabled: true, fogEnabled: true,
forceLodModels: true, forceLodModels: true,
lodHighDetailDistance: 0, lodHighDetailDistance: 0,
}, },
high: { high: {
label: "High", label: "High",
chunkLoadRadius: 35, chunkLoadRadius: 30,
chunkUnloadRadius: 45, chunkUnloadRadius: 40,
chunkStreamingEnabled: true,
fogEnabled: false, fogEnabled: false,
forceLodModels: false, forceLodModels: false,
lodHighDetailDistance: 10, lodHighDetailDistance: 20,
}, },
ultra: { ultra: {
label: "Ultra", label: "Ultra",
chunkLoadRadius: 50, chunkLoadRadius: 50,
chunkUnloadRadius: 65, chunkUnloadRadius: 65,
chunkStreamingEnabled: true,
fogEnabled: false, fogEnabled: false,
forceLodModels: false, forceLodModels: false,
lodHighDetailDistance: 20, lodHighDetailDistance: 30,
},
max: {
label: "Max",
chunkLoadRadius: 50,
chunkUnloadRadius: 65,
chunkStreamingEnabled: false,
fogEnabled: false,
forceLodModels: false,
lodHighDetailDistance: 50,
}, },
} as const satisfies Record<GraphicsPreset, GraphicsPresetConfig>; } as const satisfies Record<GraphicsPreset, GraphicsPresetConfig>;
+2 -2
View File
@@ -3,9 +3,9 @@ const SUN_LIGHT_COLOR = "#ffe2bf";
export const LIGHTING_DEFAULTS = { export const LIGHTING_DEFAULTS = {
ambientColor: AMBIENT_LIGHT_COLOR, ambientColor: AMBIENT_LIGHT_COLOR,
ambientIntensity: 0.9, ambientIntensity: 0.7,
sunColor: SUN_LIGHT_COLOR, sunColor: SUN_LIGHT_COLOR,
sunIntensity: 2.2, sunIntensity: 1.9,
sunX: 70, sunX: 70,
sunY: 45, sunY: 45,
sunZ: 35, sunZ: 35,
+17
View File
@@ -14,6 +14,9 @@ export const MAP_LOD_MODEL_PATHS = {
maison1: "/models/maison1-LOD/model.gltf", maison1: "/models/maison1-LOD/model.gltf",
panneauaffichage: "/models/panneauaffichage-LOD/model.gltf", panneauaffichage: "/models/panneauaffichage-LOD/model.gltf",
talkie: "/models/talkie-LOD/model.gltf", talkie: "/models/talkie-LOD/model.gltf",
arbre: "/models/arbre-LOD/model.glb",
buisson: "/models/buisson-LOD/model.glb",
sapin: "/models/sapin-LOD/model.glb",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export function getMapLodModelPath(modelName: string): string | null { export function getMapLodModelPath(modelName: string): string | null {
@@ -22,6 +25,20 @@ export function getMapLodModelPath(modelName: string): string | null {
); );
} }
export const MAP_LOD_SCALE_MULTIPLIERS = {
sapin: 0.35,
buisson: 0.7,
} as const satisfies Partial<Record<keyof typeof MAP_LOD_MODEL_PATHS, number>>;
export function getMapLodScaleMultiplier(modelName: string): number {
return (
MAP_LOD_SCALE_MULTIPLIERS[
modelName as keyof typeof MAP_LOD_SCALE_MULTIPLIERS
] ?? 1
);
}
export function selectMapModelPathByDistance({ export function selectMapModelPathByDistance({
distance, distance,
modelName, modelName,
+1 -1
View File
@@ -1544,7 +1544,7 @@ canvas {
} }
.game-settings-menu__choice-group--presets { .game-settings-menu__choice-group--presets {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
} }
.game-settings-menu__choice-group button, .game-settings-menu__choice-group button,
@@ -149,6 +149,7 @@ export function MapInstancingSystem({
const streamingEnabled = const streamingEnabled =
streaming && streaming &&
CHUNK_CONFIG.enabled && CHUNK_CONFIG.enabled &&
graphicsPresetConfig.chunkStreamingEnabled &&
sceneMode === "game" && sceneMode === "game" &&
cameraMode === "player"; cameraMode === "player";
+1 -2
View File
@@ -33,7 +33,6 @@ import {
EBIKE_ACCELERATION_DURATION_MS, EBIKE_ACCELERATION_DURATION_MS,
EBIKE_CAMERA_TRANSFORM, EBIKE_CAMERA_TRANSFORM,
EBIKE_DECELERATION_DURATION_MS, EBIKE_DECELERATION_DURATION_MS,
EBIKE_MAX_SPEED,
} from "@/data/ebike/ebikeConfig"; } from "@/data/ebike/ebikeConfig";
/** Global window properties used for ebike communication */ /** Global window properties used for ebike communication */
@@ -415,7 +414,7 @@ export function PlayerController({
} }
const movementSpeed = isEbikeMounted const movementSpeed = isEbikeMounted
? EBIKE_MAX_SPEED * ebikeSpeedFactor.current ? currentSpeed * ebikeSpeedFactor.current
: currentSpeed; : currentSpeed;
const accel = onFloor.current const accel = onFloor.current
? movementSpeed ? movementSpeed
+112 -15
View File
@@ -1,8 +1,24 @@
import { Suspense, useMemo } from "react"; import {
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig"; import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
import {
getMapLodModelPath,
getMapLodScaleMultiplier,
selectMapModelPathByDistance,
} from "@/data/world/mapLodConfig";
import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode";
import { useGraphicsPresetConfig } from "@/hooks/world/useGraphicsSettings"; import {
useGraphicsPreset,
useGraphicsPresetConfig,
} from "@/hooks/world/useGraphicsSettings";
import { useVisibleWorldChunks } from "@/hooks/world/useVisibleWorldChunks"; import { useVisibleWorldChunks } from "@/hooks/world/useVisibleWorldChunks";
import { import {
isMapModelVisible, isMapModelVisible,
@@ -18,6 +34,7 @@ import {
} from "@/data/world/vegetationConfig"; } from "@/data/world/vegetationConfig";
import { isInsideLaFabrikFootprint } from "@/data/world/laFabrikConfig"; import { isInsideLaFabrikFootprint } from "@/data/world/laFabrikConfig";
import { createWorldInstanceChunks } from "@/utils/world/chunkInstances"; import { createWorldInstanceChunks } from "@/utils/world/chunkInstances";
import type { GraphicsPreset } from "@/data/world/graphicsConfig";
interface VegetationSystemProps { interface VegetationSystemProps {
onlyMapName?: string | null; onlyMapName?: string | null;
@@ -70,12 +87,78 @@ function removeLaFabrikVegetation(
}); });
} }
function areChunkModelPathsEqual(
a: ReadonlyMap<string, string>,
b: ReadonlyMap<string, string>,
): boolean {
return (
a.size === b.size && [...a].every(([key, value]) => b.get(key) === value)
);
}
function useVegetationChunkModelPaths(
chunks: readonly VegetationChunk[],
preset: GraphicsPreset,
): ReadonlyMap<string, string> {
const camera = useThree((state) => state.camera);
const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
const modelPathsRef = useRef<Map<string, string>>(new Map());
const [modelPaths, setModelPaths] = useState<ReadonlyMap<string, string>>(
() => new Map(),
);
const updateModelPaths = useCallback(() => {
const cameraX = camera.position.x;
const cameraZ = camera.position.z;
const next = new Map<string, string>();
for (const chunk of chunks) {
let nearestDistance = Number.POSITIVE_INFINITY;
for (const instance of chunk.instances) {
const distance = Math.hypot(
instance.position[0] - cameraX,
instance.position[2] - cameraZ,
);
if (distance < nearestDistance) nearestDistance = distance;
}
const modelPath = selectMapModelPathByDistance({
distance: nearestDistance,
modelName: VEGETATION_TYPES[chunk.type].mapName,
modelPath: chunk.modelPath,
preset,
});
next.set(chunk.key, modelPath);
}
if (areChunkModelPathsEqual(next, modelPathsRef.current)) return;
modelPathsRef.current = next;
setModelPaths(next);
}, [camera, chunks, preset]);
useEffect(() => {
updateModelPaths();
}, [updateModelPaths]);
useFrame(({ clock }) => {
const now = clock.elapsedTime * 1000;
if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return;
lastUpdateRef.current = now;
updateModelPaths();
});
return modelPaths;
}
export function VegetationSystem({ export function VegetationSystem({
onlyMapName = null, onlyMapName = null,
streaming = true, streaming = true,
}: VegetationSystemProps): React.JSX.Element | null { }: VegetationSystemProps): React.JSX.Element | null {
const cameraMode = useCameraMode(); const cameraMode = useCameraMode();
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
const graphicsPresetKey = useGraphicsPreset();
const graphicsPreset = useGraphicsPresetConfig(); const graphicsPreset = useGraphicsPresetConfig();
const groups = useMapPerformanceStore((state) => state.groups); const groups = useMapPerformanceStore((state) => state.groups);
const models = useMapPerformanceStore((state) => state.models); const models = useMapPerformanceStore((state) => state.models);
@@ -83,6 +166,7 @@ export function VegetationSystem({
const streamingEnabled = const streamingEnabled =
streaming && streaming &&
CHUNK_CONFIG.enabled && CHUNK_CONFIG.enabled &&
graphicsPreset.chunkStreamingEnabled &&
sceneMode === "game" && sceneMode === "game" &&
cameraMode === "player"; cameraMode === "player";
@@ -112,25 +196,38 @@ export function VegetationSystem({
unloadRadius: graphicsPreset.chunkUnloadRadius, unloadRadius: graphicsPreset.chunkUnloadRadius,
}); });
const chunkModelPaths = useVegetationChunkModelPaths(
visibleChunks,
graphicsPresetKey,
);
if (isLoading || !data) { if (isLoading || !data) {
return null; return null;
} }
return ( return (
<group name="vegetation-system"> <group name="vegetation-system">
{visibleChunks.map((chunk) => ( {visibleChunks.map((chunk) => {
<Suspense key={chunk.key} fallback={null}> const modelPath = chunkModelPaths.get(chunk.key) ?? chunk.modelPath;
<InstancedVegetation const mapName = VEGETATION_TYPES[chunk.type].mapName;
modelPath={chunk.modelPath} const isLod = modelPath === getMapLodModelPath(mapName);
instances={chunk.instances} const scaleMultiplier =
scaleMultiplier={chunk.scaleMultiplier} chunk.scaleMultiplier *
castShadow={chunk.castShadow} (isLod ? getMapLodScaleMultiplier(mapName) : 1);
receiveShadow={chunk.receiveShadow} return (
windStrength={chunk.windStrength} <Suspense key={`${chunk.key}:${modelPath}`} fallback={null}>
rotationOffset={chunk.rotationOffset} <InstancedVegetation
/> modelPath={modelPath}
</Suspense> instances={chunk.instances}
))} scaleMultiplier={scaleMultiplier}
castShadow={chunk.castShadow}
receiveShadow={chunk.receiveShadow}
windStrength={chunk.windStrength}
rotationOffset={chunk.rotationOffset}
/>
</Suspense>
);
})}
</group> </group>
); );
} }