Feat/map-environment #6

Merged
math-pixel merged 116 commits from feat/map-environment into develop 2026-05-29 00:00:51 +00:00
12 changed files with 283 additions and 58 deletions
Showing only changes of commit 0b3d49e8d1 - Show all commits
+3
View File
@@ -15,6 +15,7 @@ export type MapPerformanceModelName =
| "champdeble" | "champdeble"
| "champdesoja" | "champdesoja"
| "champsdetournesol" | "champsdetournesol"
| "potager"
| "ecole" | "ecole"
| "generateur" | "generateur"
| "fermeverticale" | "fermeverticale"
@@ -50,6 +51,7 @@ export const MAP_PERFORMANCE_MODEL_NAMES: readonly MapPerformanceModelName[] = [
"champdeble", "champdeble",
"champdesoja", "champdesoja",
"champsdetournesol", "champsdetournesol",
"potager",
"ecole", "ecole",
"generateur", "generateur",
"fermeverticale", "fermeverticale",
@@ -78,6 +80,7 @@ export const MAP_PERFORMANCE_MODEL_GROUPS: Record<
champdeble: ["vegetation", "crops"], champdeble: ["vegetation", "crops"],
champdesoja: ["vegetation", "crops"], champdesoja: ["vegetation", "crops"],
champsdetournesol: ["vegetation", "crops"], champsdetournesol: ["vegetation", "crops"],
potager: ["vegetation", "crops"],
ecole: ["buildings", "landmarks"], ecole: ["buildings", "landmarks"],
generateur: ["landmarks"], generateur: ["landmarks"],
fermeverticale: ["buildings", "landmarks"], fermeverticale: ["buildings", "landmarks"],
+1 -9
View File
@@ -1,15 +1,9 @@
import type { import type { TerrainSurfaceColorConfig } from "@/types/world/terrainSurface";
TerrainSurfaceColorConfig,
TerrainSurfaceProjectionConfig,
} from "@/types/world/terrainSurface";
export const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf"; export const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
export const TERRAIN_WATER_HEIGHT = 0.8; export const TERRAIN_WATER_HEIGHT = 0.8;
export const TERRAIN_TILE_SIZE = 1; export const TERRAIN_TILE_SIZE = 1;
export const TERRAIN_SURFACE_COLOR_TOLERANCE = 5;
export const TERRAIN_SURFACE_PROJECTION =
{} satisfies TerrainSurfaceProjectionConfig;
export const TERRAIN_COLORS = { export const TERRAIN_COLORS = {
grass1: { grass1: {
@@ -60,5 +54,3 @@ export const TERRAIN_COLORS = {
kind: "rock", kind: "rock",
}, },
} satisfies Record<string, TerrainSurfaceColorConfig>; } satisfies Record<string, TerrainSurfaceColorConfig>;
export type TerrainColorKey = keyof typeof TERRAIN_COLORS;
+30 -6
View File
@@ -5,7 +5,8 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 1.5, scaleMultiplier: 1.5,
castShadow: true, castShadow: true,
receiveShadow: true, receiveShadow: true,
windStrength: 0.08, windStrength: 0.06,
rotationOffset: [0, 0, 0],
enabled: true, enabled: true,
}, },
sapin: { sapin: {
@@ -14,7 +15,8 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 4, scaleMultiplier: 4,
castShadow: true, castShadow: true,
receiveShadow: true, receiveShadow: true,
windStrength: 0.04, windStrength: 0.12,
rotationOffset: [0, 0, 0],
enabled: true, enabled: true,
}, },
arbre: { arbre: {
@@ -23,7 +25,8 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 1, scaleMultiplier: 1,
castShadow: true, castShadow: true,
receiveShadow: true, receiveShadow: true,
windStrength: 0.06, windStrength: 0.15,
rotationOffset: [0, 0, 0],
enabled: true, enabled: true,
}, },
champdeble: { champdeble: {
@@ -32,7 +35,8 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 1, scaleMultiplier: 1,
castShadow: true, castShadow: true,
receiveShadow: true, receiveShadow: true,
windStrength: 0.18, windStrength: 0.15,
rotationOffset: [0, 0, 0],
enabled: true, enabled: true,
}, },
champdesoja: { champdesoja: {
@@ -41,7 +45,8 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 1, scaleMultiplier: 1,
castShadow: true, castShadow: true,
receiveShadow: true, receiveShadow: true,
windStrength: 0.16, windStrength: 0.15,
rotationOffset: [0, 0, 0],
enabled: true, enabled: true,
}, },
champsdetournesol: { champsdetournesol: {
@@ -50,7 +55,18 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 1, scaleMultiplier: 1,
castShadow: true, castShadow: true,
receiveShadow: true, receiveShadow: true,
windStrength: 0.14, windStrength: 0.15,
rotationOffset: [0, 0, 0],
enabled: true,
},
potager: {
mapName: "potager",
modelPath: "/models/potager/potager.gltf",
scaleMultiplier: 1,
castShadow: true,
receiveShadow: true,
windStrength: 0,
rotationOffset: [0, 0, 0],
enabled: true, enabled: true,
}, },
} as const; } as const;
@@ -62,10 +78,18 @@ export const VEGETATION_TYPE_KEYS = [
"champdeble", "champdeble",
"champdesoja", "champdesoja",
"champsdetournesol", "champsdetournesol",
"potager",
] as const satisfies readonly (keyof typeof VEGETATION_TYPES)[]; ] as const satisfies readonly (keyof typeof VEGETATION_TYPES)[];
export type VegetationType = (typeof VEGETATION_TYPE_KEYS)[number]; export type VegetationType = (typeof VEGETATION_TYPE_KEYS)[number];
export function getVegetationModelScaleMultiplier(name: string): number {
return (
Object.values(VEGETATION_TYPES).find((config) => config.mapName === name)
?.scaleMultiplier ?? 1
);
}
export const INSTANCED_MAP_EXCEPTIONS = new Set([ export const INSTANCED_MAP_EXCEPTIONS = new Set([
"Scene", "Scene",
"blocking", "blocking",
+63 -15
View File
@@ -1,14 +1,14 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { INSTANCED_MAP_EXCEPTIONS } from "@/data/world/vegetationConfig"; import { INSTANCED_MAP_EXCEPTIONS } from "@/data/world/vegetationConfig";
import type { MapNode } from "@/types/map/mapScene"; import type { MapNode, VegetationInstance } from "@/types/map/mapScene";
import { import { mapNodeToInstanceTransform } from "@/utils/map/mapInstanceTransform";
type MapNodeInstanceTransform,
mapNodeToInstanceTransform,
} from "@/utils/map/mapInstanceTransform";
import { logger } from "@/utils/core/Logger"; import { logger } from "@/utils/core/Logger";
import { loadMapSceneData } from "@/utils/map/loadMapSceneData"; import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
import {
export type VegetationInstance = MapNodeInstanceTransform; createPotagerMapNode,
isPotagerSourceMapNode,
POTAGER_MAP_NAME,
} from "@/utils/map/potagerMapNodes";
interface InstancedMapEntry { interface InstancedMapEntry {
modelPath: string; modelPath: string;
@@ -17,12 +17,35 @@ interface InstancedMapEntry {
export type VegetationData = Map<string, InstancedMapEntry>; export type VegetationData = Map<string, InstancedMapEntry>;
function createPositionKey(node: MapNode): string {
return node.position.map((value) => value.toFixed(3)).join(":");
}
function extractVegetationData( function extractVegetationData(
mapNodes: MapNode[], mapNodes: MapNode[],
models: Map<string, string>, models: Map<string, string>,
): VegetationData { ): VegetationData {
const data: VegetationData = new Map(); const data: VegetationData = new Map();
function addInstance(
mapName: string,
modelPath: string,
node: MapNode,
): void {
const entry = data.get(mapName);
const instance = mapNodeToInstanceTransform(node);
if (entry) {
entry.instances.push(instance);
return;
}
data.set(mapName, {
modelPath,
instances: [instance],
});
}
for (const node of mapNodes) { for (const node of mapNodes) {
if (node.type !== "Object3D") continue; if (node.type !== "Object3D") continue;
if (INSTANCED_MAP_EXCEPTIONS.has(node.name)) continue; if (INSTANCED_MAP_EXCEPTIONS.has(node.name)) continue;
@@ -30,16 +53,36 @@ function extractVegetationData(
const modelPath = models.get(node.name); const modelPath = models.get(node.name);
if (!modelPath) continue; if (!modelPath) continue;
const entry = data.get(node.name); addInstance(node.name, modelPath, node);
}
if (entry) { const existingPotagerPositionKeys = new Set(
entry.instances.push(mapNodeToInstanceTransform(node)); mapNodes
} else { .filter((node) => node.name === POTAGER_MAP_NAME)
data.set(node.name, { .map(createPositionKey),
modelPath, );
instances: [mapNodeToInstanceTransform(node)],
}); for (const node of mapNodes) {
if (!isPotagerSourceMapNode(node)) continue;
if (existingPotagerPositionKeys.has(createPositionKey(node))) continue;
addInstance(
POTAGER_MAP_NAME,
"/models/potager/potager.gltf",
createPotagerMapNode(node),
);
}
const potagerEntry = data.get(POTAGER_MAP_NAME);
if (potagerEntry) {
const uniqueInstances = new Map<string, VegetationInstance>();
for (const instance of potagerEntry.instances) {
uniqueInstances.set(
instance.position.map((value) => value.toFixed(3)).join(":"),
instance,
);
} }
potagerEntry.instances = [...uniqueInstances.values()];
} }
return data; return data;
@@ -64,6 +107,11 @@ export function useVegetationData(): {
logger.error("Vegetation", "Failed to load vegetation data", { logger.error("Vegetation", "Failed to load vegetation data", {
error: error instanceof Error ? error : String(error), error: error instanceof Error ? error : String(error),
}); });
if (!cancelled) {
setData(null);
setIsLoading(false);
}
return;
} }
if (!cancelled) { if (!cancelled) {
+1 -6
View File
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsMapPerformancePage(): React.JSX.Element { export function DocsMapPerformancePage(): React.JSX.Element {
return ( return (
<DocsDocument <DocsDocument content={mapPerformance} meta="12" title="Map Performance" />
content={mapPerformance}
frContent={mapPerformance}
meta="12"
title="Map Performance"
/>
); );
} }
+9
View File
@@ -9,6 +9,15 @@ export interface MapNode {
sourcePath?: number[]; sourcePath?: number[];
} }
export interface MapNodeInstanceTransform {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
}
export type MapAssetInstance = MapNodeInstanceTransform;
export type VegetationInstance = MapNodeInstanceTransform;
export interface HierarchicalMapNode extends MapNode { export interface HierarchicalMapNode extends MapNode {
role?: "group"; role?: "group";
children?: HierarchicalMapNode[]; children?: HierarchicalMapNode[];
+107 -4
View File
@@ -3,7 +3,13 @@ import type {
MapNode, MapNode,
SceneData, SceneData,
} from "@/types/map/mapScene"; } from "@/types/map/mapScene";
import { logger } from "@/utils/core/Logger";
import { parseMapData } from "@/utils/map/mapNodeValidation"; import { parseMapData } from "@/utils/map/mapNodeValidation";
import {
createPotagerMapNode,
isPotagerSourceMapNode,
POTAGER_MAP_NAME,
} from "@/utils/map/potagerMapNodes";
const MAP_JSON_PATH = "/map.json"; const MAP_JSON_PATH = "/map.json";
const MODEL_FILE_NAMES = ["model.glb", "model.gltf"]; const MODEL_FILE_NAMES = ["model.glb", "model.gltf"];
@@ -59,9 +65,101 @@ async function loadMapSceneDataInternal(): Promise<SceneData | null> {
export async function createSceneDataFromMapPayload( export async function createSceneDataFromMapPayload(
mapPayload: unknown, mapPayload: unknown,
): Promise<SceneData> { ): Promise<SceneData> {
const { mapNodes, mapTree } = parseMapData(mapPayload); const { mapTree } = parseMapData(mapPayload);
const mapTreeWithPotagers = ensurePotagerMapTree(mapTree);
const mapNodes = flattenMapTree(mapTreeWithPotagers);
const deduplicatedNodes = deduplicateMapNodes(mapNodes); const deduplicatedNodes = deduplicateMapNodes(mapNodes);
return createSceneData(deduplicatedNodes, mapTree); return createSceneData(deduplicatedNodes, mapTreeWithPotagers);
}
function isSamePosition(a: MapNode, b: MapNode): boolean {
return a.position.every((value, index) => {
const otherValue = b.position[index];
return otherValue !== undefined && Math.abs(value - otherValue) < 0.0001;
});
}
function cloneMapTree(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): HierarchicalMapNode | HierarchicalMapNode[] {
return JSON.parse(JSON.stringify(mapTree)) as
| HierarchicalMapNode
| HierarchicalMapNode[];
}
function flattenMapNode(node: HierarchicalMapNode, path: number[]): MapNode[] {
const childNodes =
node.children?.flatMap((child, index) =>
flattenMapNode(child, [...path, index]),
) ?? [];
if (node.role === "group" || node.type === "Mesh") {
return childNodes;
}
return [
{
name: node.name,
type: node.type,
position: node.position,
rotation: node.rotation,
scale: node.scale,
sourcePath: path,
},
...childNodes,
];
}
function flattenMapTree(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): MapNode[] {
return Array.isArray(mapTree)
? mapTree.flatMap((node, index) => flattenMapNode(node, [index]))
: flattenMapNode(mapTree, []);
}
function collectExplicitPotagerNodes(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): MapNode[] {
return flattenMapTree(mapTree).filter(
(node) => node.name === POTAGER_MAP_NAME,
);
}
function ensurePotagerMapTree(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): HierarchicalMapNode | HierarchicalMapNode[] {
const nextTree = cloneMapTree(mapTree);
const explicitPotagers = collectExplicitPotagerNodes(nextTree);
function visit(node: HierarchicalMapNode): void {
if (!node.children) return;
const nextChildren: HierarchicalMapNode[] = [];
node.children.forEach((child) => {
nextChildren.push(child);
visit(child);
if (!isPotagerSourceMapNode(child)) return;
const hasMatchingPotager = explicitPotagers.some((potager) =>
isSamePosition(potager, child),
);
if (hasMatchingPotager) return;
nextChildren.push(createPotagerMapNode(child));
});
node.children = nextChildren;
}
if (Array.isArray(nextTree)) {
nextTree.forEach((node) => visit(node));
} else {
visit(nextTree);
}
return nextTree;
} }
function createPositionKey(node: MapNode): string { function createPositionKey(node: MapNode): string {
@@ -124,7 +222,7 @@ async function loadMapModelUrls(
} }
async function loadModelEntry(modelName: string): Promise<ModelEntry | null> { async function loadModelEntry(modelName: string): Promise<ModelEntry | null> {
for (const fileName of MODEL_FILE_NAMES) { for (const fileName of [...MODEL_FILE_NAMES, `${modelName}.gltf`]) {
const modelUrl = `/models/${modelName}/${fileName}`; const modelUrl = `/models/${modelName}/${fileName}`;
try { try {
@@ -133,7 +231,12 @@ async function loadModelEntry(modelName: string): Promise<ModelEntry | null> {
if (response.ok && !contentType.includes(HTML_CONTENT_TYPE)) { if (response.ok && !contentType.includes(HTML_CONTENT_TYPE)) {
return [modelName, modelUrl]; return [modelName, modelUrl];
} }
} catch { } catch (error) {
logger.warn("MapSceneData", "Failed to probe map model URL", {
modelName,
modelUrl,
error: error instanceof Error ? error : String(error),
});
continue; continue;
} }
} }
+1 -8
View File
@@ -1,11 +1,4 @@
import type { MapNode } from "@/types/map/mapScene"; import type { MapNode, MapNodeInstanceTransform } from "@/types/map/mapScene";
import type { Vector3Tuple } from "@/types/three/three";
export interface MapNodeInstanceTransform {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
}
export function mapNodeToInstanceTransform( export function mapNodeToInstanceTransform(
node: MapNode, node: MapNode,
@@ -8,6 +8,7 @@ const RUNTIME_VEGETATION_NODE_NAMES = new Set([
"champdeble", "champdeble",
"champdesoja", "champdesoja",
"champsdetournesol", "champsdetournesol",
"potager",
"sapin", "sapin",
]); ]);
+35
View File
@@ -0,0 +1,35 @@
import type { MapNode } from "@/types/map/mapScene";
export const POTAGER_MAP_NAME = "potager";
export const POTAGER_DEFAULT_ROTATION_OFFSET = [0, 0, 0] as const;
export const POTAGER_SOURCE_MAP_NAMES = new Set([
"champdeble",
"champdesoja",
"champsdetournesol",
]);
export function isPotagerSourceMapNode(node: MapNode): boolean {
const role = "role" in node ? node.role : undefined;
return (
node.type === "Object3D" &&
role !== "group" &&
POTAGER_SOURCE_MAP_NAMES.has(node.name) &&
!node.position.every((value) => Math.abs(value) < 0.0001)
);
}
export function createPotagerMapNode(sourceNode: MapNode): MapNode {
return {
name: POTAGER_MAP_NAME,
type: sourceNode.type,
position: sourceNode.position,
rotation: [
sourceNode.rotation[0] + POTAGER_DEFAULT_ROTATION_OFFSET[0],
sourceNode.rotation[1] + POTAGER_DEFAULT_ROTATION_OFFSET[1],
sourceNode.rotation[2] + POTAGER_DEFAULT_ROTATION_OFFSET[2],
],
scale: sourceNode.scale,
};
}
+11 -3
View File
@@ -4,7 +4,7 @@ import { useGLTF } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber"; import { useFrame, useThree } from "@react-three/fiber";
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js"; import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight"; import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
import type { VegetationInstance } from "@/hooks/world/useVegetationData"; import type { VegetationInstance } from "@/types/map/mapScene";
import { useWind } from "@/hooks/world/useWind"; import { useWind } from "@/hooks/world/useWind";
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene"; import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
@@ -15,6 +15,7 @@ interface InstancedVegetationProps {
castShadow: boolean; castShadow: boolean;
receiveShadow: boolean; receiveShadow: boolean;
windStrength: number; windStrength: number;
rotationOffset: readonly [number, number, number];
} }
interface MeshData { interface MeshData {
@@ -186,6 +187,7 @@ function extractMeshes(scene: THREE.Group): MeshData[] {
function createInstanceMatrices( function createInstanceMatrices(
instances: VegetationInstance[], instances: VegetationInstance[],
scaleMultiplier: number, scaleMultiplier: number,
rotationOffset: readonly [number, number, number],
geometryBottomY: number, geometryBottomY: number,
): THREE.Matrix4[] { ): THREE.Matrix4[] {
const matrices: THREE.Matrix4[] = []; const matrices: THREE.Matrix4[] = [];
@@ -203,7 +205,11 @@ function createInstanceMatrices(
position.set(...instance.position); position.set(...instance.position);
position.y += -geometryBottomY * scaleMultiplier; position.y += -geometryBottomY * scaleMultiplier;
rotation.set(...instance.rotation); rotation.set(
instance.rotation[0] + rotationOffset[0],
instance.rotation[1] + rotationOffset[1],
instance.rotation[2] + rotationOffset[2],
);
quaternion.setFromEuler(rotation); quaternion.setFromEuler(rotation);
matrix.compose(position, quaternion, scale); matrix.compose(position, quaternion, scale);
matrices.push(matrix); matrices.push(matrix);
@@ -233,6 +239,7 @@ export function InstancedVegetation({
castShadow, castShadow,
receiveShadow, receiveShadow,
windStrength, windStrength,
rotationOffset,
}: InstancedVegetationProps): React.JSX.Element | null { }: InstancedVegetationProps): React.JSX.Element | null {
const { scene } = useGLTF(modelPath); const { scene } = useGLTF(modelPath);
const wind = useWind(); const wind = useWind();
@@ -269,9 +276,10 @@ export function InstancedVegetation({
createInstanceMatrices( createInstanceMatrices(
groundedInstances, groundedInstances,
scaleMultiplier, scaleMultiplier,
rotationOffset,
getMeshBottomY(meshDataList), getMeshBottomY(meshDataList),
), ),
[groundedInstances, meshDataList, scaleMultiplier], [groundedInstances, meshDataList, rotationOffset, scaleMultiplier],
); );
const instancedMeshes = useMemo(() => { const instancedMeshes = useMemo(() => {
+21 -7
View File
@@ -8,16 +8,19 @@ import {
useMapPerformanceStore, useMapPerformanceStore,
} from "@/managers/stores/useMapPerformanceStore"; } from "@/managers/stores/useMapPerformanceStore";
import { InstancedVegetation } from "@/world/vegetation/InstancedVegetation"; import { InstancedVegetation } from "@/world/vegetation/InstancedVegetation";
import { import { useVegetationData } from "@/hooks/world/useVegetationData";
type VegetationInstance, import type { VegetationInstance } from "@/types/map/mapScene";
useVegetationData,
} from "@/hooks/world/useVegetationData";
import { import {
VEGETATION_TYPE_KEYS, VEGETATION_TYPE_KEYS,
VEGETATION_TYPES, VEGETATION_TYPES,
type VegetationType, type VegetationType,
} from "@/data/world/vegetationConfig"; } from "@/data/world/vegetationConfig";
interface VegetationSystemProps {
onlyModelName?: string | null;
streaming?: boolean;
}
interface VegetationChunk { interface VegetationChunk {
key: string; key: string;
type: VegetationType; type: VegetationType;
@@ -26,6 +29,7 @@ interface VegetationChunk {
castShadow: boolean; castShadow: boolean;
receiveShadow: boolean; receiveShadow: boolean;
windStrength: number; windStrength: number;
rotationOffset: readonly [number, number, number];
centerX: number; centerX: number;
centerZ: number; centerZ: number;
instances: VegetationInstance[]; instances: VegetationInstance[];
@@ -73,6 +77,7 @@ function createVegetationChunks(
castShadow: config.castShadow, castShadow: config.castShadow,
receiveShadow: config.receiveShadow, receiveShadow: config.receiveShadow,
windStrength: config.windStrength, windStrength: config.windStrength,
rotationOffset: config.rotationOffset,
centerX: center.x / chunkInstances.length, centerX: center.x / chunkInstances.length,
centerZ: center.z / chunkInstances.length, centerZ: center.z / chunkInstances.length,
instances: chunkInstances, instances: chunkInstances,
@@ -80,14 +85,20 @@ function createVegetationChunks(
}); });
} }
export function VegetationSystem(): React.JSX.Element | null { export function VegetationSystem({
onlyModelName = null,
streaming = true,
}: VegetationSystemProps): React.JSX.Element | null {
const cameraMode = useCameraMode(); const cameraMode = useCameraMode();
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
const groups = useMapPerformanceStore((state) => state.groups); const groups = useMapPerformanceStore((state) => state.groups);
const models = useMapPerformanceStore((state) => state.models); const models = useMapPerformanceStore((state) => state.models);
const { data, isLoading } = useVegetationData(); const { data, isLoading } = useVegetationData();
const streamingEnabled = const streamingEnabled =
CHUNK_CONFIG.enabled && sceneMode === "game" && cameraMode === "player"; streaming &&
CHUNK_CONFIG.enabled &&
sceneMode === "game" &&
cameraMode === "player";
const chunks = useMemo(() => { const chunks = useMemo(() => {
if (!data) return []; if (!data) return [];
@@ -95,6 +106,8 @@ export function VegetationSystem(): React.JSX.Element | null {
return VEGETATION_TYPE_KEYS.flatMap((type) => { return VEGETATION_TYPE_KEYS.flatMap((type) => {
const config = VEGETATION_TYPES[type]; const config = VEGETATION_TYPES[type];
if (onlyModelName && config.mapName !== onlyModelName) return [];
if (!config.enabled) return []; if (!config.enabled) return [];
if (!isMapModelVisible(config.mapName, { groups, models })) return []; if (!isMapModelVisible(config.mapName, { groups, models })) return [];
@@ -103,7 +116,7 @@ export function VegetationSystem(): React.JSX.Element | null {
return createVegetationChunks(type, entry.instances); return createVegetationChunks(type, entry.instances);
}); });
}, [data, groups, models]); }, [data, groups, models, onlyModelName]);
const visibleChunks = useVisibleWorldChunks(chunks, streamingEnabled); const visibleChunks = useVisibleWorldChunks(chunks, streamingEnabled);
@@ -122,6 +135,7 @@ export function VegetationSystem(): React.JSX.Element | null {
castShadow={chunk.castShadow} castShadow={chunk.castShadow}
receiveShadow={chunk.receiveShadow} receiveShadow={chunk.receiveShadow}
windStrength={chunk.windStrength} windStrength={chunk.windStrength}
rotationOffset={chunk.rotationOffset}
/> />
</Suspense> </Suspense>
))} ))}