perf(map): snap assets to terrain

This commit is contained in:
Tom Boullay
2026-05-25 00:51:03 +02:00
parent 50fa94b3ad
commit d17738eaf1
18 changed files with 402 additions and 62 deletions
+20
View File
@@ -0,0 +1,20 @@
import { useGLTF } from "@react-three/drei";
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
import type { Vector3Tuple } from "@/types/three/three";
const ECOLE_MODEL_PATH = "/models/ecole/model.gltf";
interface EcoleModelProps {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
castShadow?: boolean;
receiveShadow?: boolean;
onLoaded?: () => void;
}
export function EcoleModel(props: EcoleModelProps): React.JSX.Element {
return <MergedStaticMapModel modelPath={ECOLE_MODEL_PATH} {...props} />;
}
useGLTF.preload(ECOLE_MODEL_PATH);
@@ -0,0 +1,24 @@
import { useGLTF } from "@react-three/drei";
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
import type { Vector3Tuple } from "@/types/three/three";
const FERME_VERTICALE_MODEL_PATH = "/models/fermeverticale/model.gltf";
interface FermeVerticaleModelProps {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
castShadow?: boolean;
receiveShadow?: boolean;
onLoaded?: () => void;
}
export function FermeVerticaleModel(
props: FermeVerticaleModelProps,
): React.JSX.Element {
return (
<MergedStaticMapModel modelPath={FERME_VERTICALE_MODEL_PATH} {...props} />
);
}
useGLTF.preload(FERME_VERTICALE_MODEL_PATH);
@@ -0,0 +1,22 @@
import { useGLTF } from "@react-three/drei";
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
import type { Vector3Tuple } from "@/types/three/three";
const GENERATEUR_MODEL_PATH = "/models/generateur/model.gltf";
interface GenerateurModelProps {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
castShadow?: boolean;
receiveShadow?: boolean;
onLoaded?: () => void;
}
export function GenerateurModel(
props: GenerateurModelProps,
): React.JSX.Element {
return <MergedStaticMapModel modelPath={GENERATEUR_MODEL_PATH} {...props} />;
}
useGLTF.preload(GENERATEUR_MODEL_PATH);
@@ -0,0 +1,20 @@
import { useGLTF } from "@react-three/drei";
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
import type { Vector3Tuple } from "@/types/three/three";
const LAFABRIK_MODEL_PATH = "/models/lafabrik/model.gltf";
interface LafabrikModelProps {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
castShadow?: boolean;
receiveShadow?: boolean;
onLoaded?: () => void;
}
export function LafabrikModel(props: LafabrikModelProps): React.JSX.Element {
return <MergedStaticMapModel modelPath={LAFABRIK_MODEL_PATH} {...props} />;
}
useGLTF.preload(LAFABRIK_MODEL_PATH);
@@ -1,12 +1,13 @@
import { useEffect, useRef } from "react";
import * as THREE from "three";
import { useGLTF } from "@react-three/drei";
import { useThree } from "@react-three/fiber";
import * as THREE from "three";
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
import type { Vector3Tuple } from "@/types/three/three";
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
const ECOLE_MODEL_PATH = "/models/ecole/model.gltf";
interface EcoleModelProps {
interface MergedStaticMapModelProps {
modelPath: string;
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
@@ -117,21 +118,26 @@ function createMergedMeshes(scene: THREE.Group): MergedMeshData[] {
.filter((meshData): meshData is MergedMeshData => meshData !== null);
}
export function EcoleModel({
export function MergedStaticMapModel({
modelPath,
position,
rotation,
scale,
castShadow = true,
receiveShadow = true,
onLoaded,
}: EcoleModelProps): React.JSX.Element {
const { scene } = useGLTF(ECOLE_MODEL_PATH);
}: MergedStaticMapModelProps): React.JSX.Element {
const { scene } = useGLTF(modelPath);
const maxAnisotropy = useThree((state) =>
state.gl.capabilities.getMaxAnisotropy(),
);
const groupRef = useRef<THREE.Group>(null);
useEffect(() => {
const group = groupRef.current;
if (!group) return;
optimizeGLTFSceneTextures(scene, maxAnisotropy);
const mergedMeshes = createMergedMeshes(scene);
const meshes = mergedMeshes.map((meshData) => {
const mesh = new THREE.Mesh(meshData.geometry, meshData.material);
@@ -153,7 +159,7 @@ export function EcoleModel({
disposeMaterial(mesh.material);
}
};
}, [castShadow, onLoaded, receiveShadow, scene]);
}, [castShadow, maxAnisotropy, modelPath, onLoaded, receiveShadow, scene]);
return (
<group
@@ -164,5 +170,3 @@ export function EcoleModel({
/>
);
}
useGLTF.preload(ECOLE_MODEL_PATH);
+7 -1
View File
@@ -1,7 +1,9 @@
import { useEffect, useMemo, useRef } from "react";
import * as THREE from "three";
import { useGLTF } from "@react-three/drei";
import { useThree } from "@react-three/fiber";
import type { Vector3Tuple } from "@/types/three/three";
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
const TERRAIN_DEFAULT_POSITION: Vector3Tuple = [0, 0, 0];
@@ -39,12 +41,16 @@ export function TerrainModel({
const internalRef = useRef<THREE.Group>(null);
const ref = groupRef ?? internalRef;
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
const maxAnisotropy = useThree((state) =>
state.gl.capabilities.getMaxAnisotropy(),
);
const terrainModel = useMemo(() => {
optimizeGLTFSceneTextures(scene, maxAnisotropy);
const model = scene.clone(true);
applyTerrainMaterialSettings(model, receiveShadow);
return model;
}, [scene, receiveShadow]);
}, [maxAnisotropy, scene, receiveShadow]);
useEffect(() => {
onLoaded?.();
+4 -4
View File
@@ -2,16 +2,16 @@ import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
export const FOG_CONFIG = {
enabled: true,
color: "#c8dbbe",
near: 34,
far: 58,
color: "#eef3f5",
near: 38,
far: 45,
};
export const CHUNK_CONFIG = {
enabled: true,
chunkSize: 35,
loadRadius: 45,
unloadRadius: 58,
unloadRadius: 45,
updateInterval: 350,
};
+9
View File
@@ -1,18 +1,27 @@
import { useEffect, useRef } from "react";
import { useGLTF } from "@react-three/drei";
import { useThree } from "@react-three/fiber";
import {
logModelLoadSuccess,
type ModelLoadLogContext,
} from "@/utils/three/modelLoadLogger";
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
export function useLoggedGLTF(
modelPath: string,
context: Omit<ModelLoadLogContext, "modelPath">,
) {
const gltf = useGLTF(modelPath);
const maxAnisotropy = useThree((state) =>
state.gl.capabilities.getMaxAnisotropy(),
);
const hasLoggedRef = useRef(false);
const { position, rotation, scale, scope } = context;
useEffect(() => {
optimizeGLTFSceneTextures(gltf.scene, maxAnisotropy);
}, [gltf.scene, maxAnisotropy]);
useEffect(() => {
if (hasLoggedRef.current) return;
+64
View File
@@ -0,0 +1,64 @@
import { useMemo } from "react";
import { useGLTF } from "@react-three/drei";
import * as THREE from "three";
import type { Vector3Tuple } from "@/types/three/three";
const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
const RAYCAST_Y = 500;
const RAYCAST_FAR = 1000;
const DOWN = new THREE.Vector3(0, -1, 0);
interface TerrainHeightSampler {
getHeight: (x: number, z: number) => number | null;
}
function createTerrainHeightSampler(
scene: THREE.Object3D,
): TerrainHeightSampler {
const meshes: THREE.Mesh[] = [];
const raycaster = new THREE.Raycaster(
new THREE.Vector3(),
DOWN,
0,
RAYCAST_FAR,
);
scene.updateMatrixWorld(true);
scene.traverse((child) => {
if (child instanceof THREE.Mesh) {
meshes.push(child);
}
});
return {
getHeight: (x, z) => {
raycaster.set(new THREE.Vector3(x, RAYCAST_Y, z), DOWN);
const hit = raycaster.intersectObjects(meshes, false)[0];
return hit?.point.y ?? null;
},
};
}
export function useTerrainHeightSampler(): TerrainHeightSampler {
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
return useMemo(() => createTerrainHeightSampler(scene), [scene]);
}
export function useTerrainSnappedPosition(
position: Vector3Tuple,
): Vector3Tuple {
const terrainHeight = useTerrainHeightSampler();
return useMemo(() => {
const [x, y, z] = position;
const height = terrainHeight.getHeight(x, z);
return [x, height ?? y, z];
}, [position, terrainHeight]);
}
export function normalizeMapScale(scale: Vector3Tuple): Vector3Tuple {
const [x, y, z] = scale;
const isUniform = Math.abs(x - y) < 0.001 && Math.abs(x - z) < 0.001;
return isUniform ? scale : [x, x, x];
}
+68
View File
@@ -0,0 +1,68 @@
import * as THREE from "three";
const TEXTURE_KEYS = [
"map",
"alphaMap",
"aoMap",
"bumpMap",
"displacementMap",
"emissiveMap",
"envMap",
"lightMap",
"metalnessMap",
"normalMap",
"roughnessMap",
] as const;
type TextureKey = (typeof TEXTURE_KEYS)[number];
type TexturedMaterial = THREE.Material &
Partial<Record<TextureKey, THREE.Texture>>;
const optimizedTextures = new WeakSet<THREE.Texture>();
function optimizeTexture(texture: THREE.Texture, maxAnisotropy: number): void {
if (optimizedTextures.has(texture)) return;
optimizedTextures.add(texture);
texture.anisotropy = Math.min(4, Math.max(1, maxAnisotropy));
if (!(texture instanceof THREE.CompressedTexture)) {
texture.generateMipmaps = true;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter;
}
texture.needsUpdate = true;
}
function optimizeMaterialTextures(
material: THREE.Material,
maxAnisotropy: number,
): void {
const texturedMaterial = material as TexturedMaterial;
for (const key of TEXTURE_KEYS) {
const texture = texturedMaterial[key];
if (texture instanceof THREE.Texture) {
optimizeTexture(texture, maxAnisotropy);
}
}
}
export function optimizeGLTFSceneTextures(
scene: THREE.Object3D,
maxAnisotropy: number,
): void {
scene.traverse((child) => {
if (!(child instanceof THREE.Mesh)) return;
if (Array.isArray(child.material)) {
for (const material of child.material) {
optimizeMaterialTextures(material, maxAnisotropy);
}
return;
}
optimizeMaterialTextures(child.material, maxAnisotropy);
});
}
+13 -6
View File
@@ -10,6 +10,10 @@ import {
import * as THREE from "three";
import { useClonedObject } from "@/hooks/three/useClonedObject";
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
import {
normalizeMapScale,
useTerrainSnappedPosition,
} from "@/hooks/three/useTerrainHeight";
import { TerrainModel } from "@/components/three/world/TerrainModel";
import {
isMapModelVisible,
@@ -33,7 +37,7 @@ interface LoadedMapNode {
modelUrl: string | null;
}
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking"]);
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking", "terrain"]);
const LITE_MAP_SKIPPED_NODE_NAMES = new Set([
"arbre",
"buisson",
@@ -333,11 +337,13 @@ function ModelInstance({
onLoaded: () => void;
}): React.JSX.Element {
const { position, rotation, scale } = node;
const snappedPosition = useTerrainSnappedPosition(position);
const normalizedScale = normalizeMapScale(scale);
const { scene } = useLoggedGLTF(modelUrl, {
scope: "GameMap.ModelInstance",
position,
position: snappedPosition,
rotation,
scale,
scale: normalizedScale,
});
const sceneInstance = useClonedObject(scene);
@@ -354,18 +360,19 @@ function ModelInstance({
return (
<primitive
object={sceneInstance}
position={position}
position={snappedPosition}
rotation={rotation}
scale={scale}
scale={normalizedScale}
/>
);
}
function FallbackMapNode({ node }: { node: MapNode }): React.JSX.Element {
const { position, rotation, scale } = node;
const normalizedScale = normalizeMapScale(scale);
return (
<mesh position={position} rotation={rotation} scale={scale}>
<mesh position={position} rotation={rotation} scale={normalizedScale}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="#64748b" wireframe />
</mesh>
@@ -1,4 +1,11 @@
import { EcoleModel } from "@/components/three/models/generated/EcoleModel";
import { EcoleModel } from "@/components/three/world/EcoleModel";
import { FermeVerticaleModel } from "@/components/three/world/FermeVerticaleModel";
import { GenerateurModel } from "@/components/three/world/GenerateurModel";
import { LafabrikModel } from "@/components/three/world/LafabrikModel";
import {
normalizeMapScale,
useTerrainSnappedPosition,
} from "@/hooks/three/useTerrainHeight";
import type { MapNode } from "@/types/editor/editor";
interface GeneratedMapNodeInstanceProps {
@@ -10,12 +17,48 @@ export function GeneratedMapNodeInstance({
node,
onLoaded,
}: GeneratedMapNodeInstanceProps): React.JSX.Element | null {
const position = useTerrainSnappedPosition(node.position);
const scale = normalizeMapScale(node.scale);
if (node.name === "ecole") {
return (
<EcoleModel
position={node.position}
position={position}
rotation={node.rotation}
scale={node.scale}
scale={scale}
onLoaded={onLoaded}
/>
);
}
if (node.name === "fermeverticale") {
return (
<FermeVerticaleModel
position={position}
rotation={node.rotation}
scale={scale}
onLoaded={onLoaded}
/>
);
}
if (node.name === "generateur") {
return (
<GenerateurModel
position={position}
rotation={node.rotation}
scale={scale}
onLoaded={onLoaded}
/>
);
}
if (node.name === "lafabrik") {
return (
<LafabrikModel
position={position}
rotation={node.rotation}
scale={scale}
onLoaded={onLoaded}
/>
);
@@ -1,4 +1,9 @@
const GENERATED_MAP_MODEL_NAMES = new Set(["ecole"]);
const GENERATED_MAP_MODEL_NAMES = new Set([
"ecole",
"fermeverticale",
"generateur",
"lafabrik",
]);
export function isGeneratedMapModelName(name: string): boolean {
return GENERATED_MAP_MODEL_NAMES.has(name);
+30 -5
View File
@@ -1,7 +1,13 @@
import { useEffect, useRef } from "react";
import { useEffect, useMemo, useRef } from "react";
import * as THREE from "three";
import { useGLTF } from "@react-three/drei";
import { useThree } from "@react-three/fiber";
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
import {
normalizeMapScale,
useTerrainHeightSampler,
} from "@/hooks/three/useTerrainHeight";
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
import type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData";
interface InstancedMapAssetProps {
@@ -153,21 +159,40 @@ export function InstancedMapAsset({
receiveShadow,
}: InstancedMapAssetProps): React.JSX.Element | null {
const { scene } = useGLTF(modelPath);
const terrainHeight = useTerrainHeightSampler();
const maxAnisotropy = useThree((state) =>
state.gl.capabilities.getMaxAnisotropy(),
);
const groupRef = useRef<THREE.Group>(null);
const groundedInstances = useMemo(
() =>
instances.map((instance) => {
const [x, y, z] = instance.position;
const height = terrainHeight.getHeight(x, z);
return {
...instance,
position: [x, height ?? y, z] as MapAssetInstance["position"],
scale: normalizeMapScale(instance.scale),
};
}),
[instances, terrainHeight],
);
useEffect(() => {
const group = groupRef.current;
if (!group || instances.length === 0) return;
if (!group || groundedInstances.length === 0) return;
optimizeGLTFSceneTextures(scene, maxAnisotropy);
const meshDataList = extractMeshes(scene);
const instancedMeshes = meshDataList.map((meshData, index) => {
const instancedMesh = new THREE.InstancedMesh(
meshData.geometry,
meshData.material,
instances.length,
groundedInstances.length,
);
setInstanceMatrices(instancedMesh, instances);
setInstanceMatrices(instancedMesh, groundedInstances);
instancedMesh.castShadow = castShadow;
instancedMesh.receiveShadow = receiveShadow;
instancedMesh.name = `instanced-map-asset-${index}`;
@@ -187,7 +212,7 @@ export function InstancedMapAsset({
disposeInstancedMapMesh(mesh);
}
};
}, [castShadow, instances, receiveShadow, scene]);
}, [castShadow, groundedInstances, maxAnisotropy, receiveShadow, scene]);
if (instances.length === 0) {
return null;
@@ -1,25 +1,4 @@
export const MAP_INSTANCING_ASSETS = {
generateur: {
mapName: "generateur",
modelPath: "/models/generateur/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: true,
},
lafabrik: {
mapName: "lafabrik",
modelPath: "/models/lafabrik/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: true,
},
fermeverticale: {
mapName: "fermeverticale",
modelPath: "/models/fermeverticale/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: true,
},
boiteauxlettres: {
mapName: "boiteauxlettres",
modelPath: "/models/boiteauxlettres/model.gltf",
+42 -7
View File
@@ -1,12 +1,16 @@
import { useEffect, useMemo, useRef } from "react";
import * as THREE from "three";
import { useGLTF } from "@react-three/drei";
import { useThree } from "@react-three/fiber";
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
import type { VegetationInstance } from "@/world/vegetation/useVegetationData";
interface InstancedVegetationProps {
modelPath: string;
instances: VegetationInstance[];
scaleMultiplier: number;
castShadow: boolean;
receiveShadow: boolean;
}
@@ -70,12 +74,17 @@ function extractMeshes(scene: THREE.Group): MeshData[] {
function createInstanceMatrices(
instances: VegetationInstance[],
scaleMultiplier: number,
): THREE.Matrix4[] {
const matrices: THREE.Matrix4[] = [];
const position = new THREE.Vector3();
const rotation = new THREE.Euler();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3(1, 1, 1);
const scale = new THREE.Vector3(
scaleMultiplier,
scaleMultiplier,
scaleMultiplier,
);
for (const instance of instances) {
const matrix = new THREE.Matrix4();
@@ -93,16 +102,36 @@ function createInstanceMatrices(
export function InstancedVegetation({
modelPath,
instances,
scaleMultiplier,
castShadow,
receiveShadow,
}: InstancedVegetationProps): React.JSX.Element | null {
const { scene } = useGLTF(modelPath);
const terrainHeight = useTerrainHeightSampler();
const maxAnisotropy = useThree((state) =>
state.gl.capabilities.getMaxAnisotropy(),
);
const groupRef = useRef<THREE.Group>(null);
const meshDataList = useMemo(() => extractMeshes(scene), [scene]);
const meshDataList = useMemo(() => {
optimizeGLTFSceneTextures(scene, maxAnisotropy);
return extractMeshes(scene);
}, [maxAnisotropy, scene]);
const groundedInstances = useMemo(
() =>
instances.map((instance) => {
const [x, y, z] = instance.position;
const height = terrainHeight.getHeight(x, z);
return {
...instance,
position: [x, height ?? y, z] as VegetationInstance["position"],
};
}),
[instances, terrainHeight],
);
const matrices = useMemo(
() => createInstanceMatrices(instances),
[instances],
() => createInstanceMatrices(groundedInstances, scaleMultiplier),
[groundedInstances, scaleMultiplier],
);
const instancedMeshes = useMemo(() => {
@@ -110,7 +139,7 @@ export function InstancedVegetation({
const instancedMesh = new THREE.InstancedMesh(
meshData.geometry,
meshData.material,
instances.length,
groundedInstances.length,
);
for (let i = 0; i < matrices.length; i++) {
@@ -129,7 +158,13 @@ export function InstancedVegetation({
return instancedMesh;
});
}, [meshDataList, matrices, instances.length, castShadow, receiveShadow]);
}, [
meshDataList,
matrices,
groundedInstances.length,
castShadow,
receiveShadow,
]);
useEffect(() => {
const group = groupRef.current;
@@ -162,7 +197,7 @@ export function InstancedVegetation({
};
}, [meshDataList]);
if (instances.length === 0) {
if (groundedInstances.length === 0) {
return null;
}
+7 -4
View File
@@ -21,6 +21,7 @@ interface VegetationChunk {
key: string;
type: VegetationType;
modelPath: string;
scaleMultiplier: number;
castShadow: boolean;
receiveShadow: boolean;
centerX: number;
@@ -66,6 +67,7 @@ function createVegetationChunks(
key: `${type}:${chunkKey}`,
type,
modelPath: config.modelPath,
scaleMultiplier: config.scaleMultiplier,
castShadow: config.castShadow,
receiveShadow: config.receiveShadow,
centerX: center.x / chunkInstances.length,
@@ -103,6 +105,10 @@ export function VegetationSystem(): React.JSX.Element | null {
});
}, [data, groups, models]);
const visibleChunks = streamingEnabled
? chunks.filter((chunk) => activeChunkKeys.has(chunk.key))
: chunks;
useFrame(({ clock }) => {
if (!streamingEnabled) return;
@@ -143,10 +149,6 @@ export function VegetationSystem(): React.JSX.Element | null {
return null;
}
const visibleChunks = streamingEnabled
? chunks.filter((chunk) => activeChunkKeys.has(chunk.key))
: chunks;
return (
<group name="vegetation-system">
{visibleChunks.map((chunk) => (
@@ -154,6 +156,7 @@ export function VegetationSystem(): React.JSX.Element | null {
<InstancedVegetation
modelPath={chunk.modelPath}
instances={chunk.instances}
scaleMultiplier={chunk.scaleMultiplier}
castShadow={chunk.castShadow}
receiveShadow={chunk.receiveShadow}
/>
+6
View File
@@ -8,6 +8,7 @@ export const VEGETATION_TYPES = {
buissons: {
mapName: "buisson",
modelPath: "/models/buisson/model.gltf",
scaleMultiplier: 2,
castShadow: true,
receiveShadow: true,
enabled: true,
@@ -15,6 +16,7 @@ export const VEGETATION_TYPES = {
sapin: {
mapName: "sapin",
modelPath: "/models/sapin/model.gltf",
scaleMultiplier: 2,
castShadow: true,
receiveShadow: true,
enabled: true,
@@ -22,6 +24,7 @@ export const VEGETATION_TYPES = {
arbre: {
mapName: "arbre",
modelPath: "/models/arbre/model.gltf",
scaleMultiplier: 1,
castShadow: true,
receiveShadow: true,
enabled: true,
@@ -29,6 +32,7 @@ export const VEGETATION_TYPES = {
champdeble: {
mapName: "champdeble",
modelPath: "/models/champdeble/model.gltf",
scaleMultiplier: 1,
castShadow: true,
receiveShadow: true,
enabled: true,
@@ -36,6 +40,7 @@ export const VEGETATION_TYPES = {
champdesoja: {
mapName: "champdesoja",
modelPath: "/models/champdesoja/model.gltf",
scaleMultiplier: 1,
castShadow: true,
receiveShadow: true,
enabled: true,
@@ -43,6 +48,7 @@ export const VEGETATION_TYPES = {
champsdetournesol: {
mapName: "champsdetournesol",
modelPath: "/models/champsdetournesol/model.gltf",
scaleMultiplier: 1,
castShadow: true,
receiveShadow: true,
enabled: true,