Compare commits
4 Commits
a2fc417be6
...
f2cecfbbc9
| Author | SHA1 | Date | |
|---|---|---|---|
| f2cecfbbc9 | |||
| 1b9ac5c996 | |||
| e45fdbf97d | |||
| 08b01715c0 |
@@ -56,12 +56,19 @@ export function HomePage(): React.JSX.Element {
|
||||
({ gl }: { gl: THREE.WebGLRenderer }) => {
|
||||
const canvas = gl.domElement;
|
||||
|
||||
gl.shadowMap.enabled = true;
|
||||
gl.shadowMap.type = THREE.PCFShadowMap;
|
||||
gl.shadowMap.autoUpdate = true;
|
||||
|
||||
const handleContextLost = (event: Event) => {
|
||||
event.preventDefault();
|
||||
logger.error("WebGL", "Context lost - GPU resources exhausted");
|
||||
};
|
||||
|
||||
const handleContextRestored = () => {
|
||||
gl.shadowMap.enabled = true;
|
||||
gl.shadowMap.type = THREE.PCFShadowMap;
|
||||
gl.shadowMap.autoUpdate = true;
|
||||
logger.info("WebGL", "Context restored");
|
||||
};
|
||||
|
||||
|
||||
+17
-7
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import type { AmbientLight, DirectionalLight } from "three";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import type { AmbientLight, DirectionalLight, Object3D } from "three";
|
||||
import {
|
||||
AMBIENT_INTENSITY_MAX,
|
||||
AMBIENT_INTENSITY_MIN,
|
||||
@@ -22,17 +22,22 @@ import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||
import { LIGHTING_STATE } from "@/world/lightingState";
|
||||
|
||||
const SHADOW_MAP_SIZE = 2048;
|
||||
const SHADOW_CAMERA_SIZE = 170;
|
||||
const SHADOW_CAMERA_SIZE = 95;
|
||||
const SHADOW_CAMERA_NEAR = 0.5;
|
||||
const SHADOW_CAMERA_FAR = 300;
|
||||
|
||||
export function Lighting(): React.JSX.Element {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const ambient = useRef<AmbientLight>(null);
|
||||
const sun = useRef<DirectionalLight>(null);
|
||||
const sunTarget = useRef<Object3D>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sun.current) return;
|
||||
if (!sun.current || !sunTarget.current) return;
|
||||
|
||||
sun.current.target = sunTarget.current;
|
||||
sun.current.shadow.autoUpdate = true;
|
||||
sun.current.shadow.needsUpdate = true;
|
||||
sun.current.shadow.mapSize.width = SHADOW_MAP_SIZE;
|
||||
sun.current.shadow.mapSize.height = SHADOW_MAP_SIZE;
|
||||
sun.current.shadow.camera.left = -SHADOW_CAMERA_SIZE;
|
||||
@@ -82,14 +87,18 @@ export function Lighting(): React.JSX.Element {
|
||||
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
||||
}
|
||||
|
||||
if (sun.current) {
|
||||
if (sun.current && sunTarget.current) {
|
||||
sunTarget.current.position.set(camera.position.x, 0, camera.position.z);
|
||||
sunTarget.current.updateMatrixWorld();
|
||||
sun.current.position.set(
|
||||
LIGHTING_STATE.sunX,
|
||||
camera.position.x + LIGHTING_STATE.sunX,
|
||||
LIGHTING_STATE.sunY,
|
||||
LIGHTING_STATE.sunZ,
|
||||
camera.position.z + LIGHTING_STATE.sunZ,
|
||||
);
|
||||
sun.current.color.set(LIGHTING_STATE.sunColor);
|
||||
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
||||
sun.current.updateMatrixWorld();
|
||||
sun.current.shadow.needsUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,6 +120,7 @@ export function Lighting(): React.JSX.Element {
|
||||
color={LIGHTING_STATE.sunColor}
|
||||
castShadow
|
||||
/>
|
||||
<object3D ref={sunTarget} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Suspense } from "react";
|
||||
import { Suspense, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { CHUNK_CONFIG } from "@/data/world/fogConfig";
|
||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||
import {
|
||||
isMapModelVisible,
|
||||
useMapPerformanceStore,
|
||||
@@ -6,44 +10,174 @@ import {
|
||||
import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset";
|
||||
import {
|
||||
MAP_INSTANCING_ASSETS,
|
||||
type MapInstancingAssetConfig,
|
||||
type MapInstancingAssetType,
|
||||
} from "@/world/map-instancing/mapInstancingConfig";
|
||||
import { useMapInstancingData } from "@/world/map-instancing/useMapInstancingData";
|
||||
import {
|
||||
type MapAssetInstance,
|
||||
useMapInstancingData,
|
||||
} from "@/world/map-instancing/useMapInstancingData";
|
||||
|
||||
interface MapAssetChunk {
|
||||
key: string;
|
||||
config: MapInstancingAssetConfig;
|
||||
centerX: number;
|
||||
centerZ: number;
|
||||
instances: MapAssetInstance[];
|
||||
}
|
||||
|
||||
function getChunkKey(instance: MapAssetInstance): string {
|
||||
const [x, , z] = instance.position;
|
||||
const chunkX = Math.floor(x / CHUNK_CONFIG.chunkSize);
|
||||
const chunkZ = Math.floor(z / CHUNK_CONFIG.chunkSize);
|
||||
return `${chunkX}:${chunkZ}`;
|
||||
}
|
||||
|
||||
function createMapAssetChunks(
|
||||
type: MapInstancingAssetType,
|
||||
config: MapInstancingAssetConfig,
|
||||
instances: MapAssetInstance[],
|
||||
): MapAssetChunk[] {
|
||||
const chunks = new Map<string, MapAssetInstance[]>();
|
||||
|
||||
for (const instance of instances) {
|
||||
const key = getChunkKey(instance);
|
||||
const chunk = chunks.get(key);
|
||||
|
||||
if (chunk) {
|
||||
chunk.push(instance);
|
||||
} else {
|
||||
chunks.set(key, [instance]);
|
||||
}
|
||||
}
|
||||
|
||||
return [...chunks.entries()].map(([chunkKey, chunkInstances]) => {
|
||||
const center = chunkInstances.reduce(
|
||||
(sum, instance) => {
|
||||
sum.x += instance.position[0];
|
||||
sum.z += instance.position[2];
|
||||
return sum;
|
||||
},
|
||||
{ x: 0, z: 0 },
|
||||
);
|
||||
|
||||
return {
|
||||
key: `${type}:${chunkKey}`,
|
||||
config,
|
||||
centerX: center.x / chunkInstances.length,
|
||||
centerZ: center.z / chunkInstances.length,
|
||||
instances: chunkInstances,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function MapInstancingSystem(): React.JSX.Element | null {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const cameraMode = useCameraMode();
|
||||
const sceneMode = useSceneMode();
|
||||
const groups = useMapPerformanceStore((state) => state.groups);
|
||||
const models = useMapPerformanceStore((state) => state.models);
|
||||
const { data, isLoading } = useMapInstancingData();
|
||||
const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
|
||||
const [activeChunkKeys, setActiveChunkKeys] = useState<Set<string>>(
|
||||
() => new Set(),
|
||||
);
|
||||
const streamingEnabled =
|
||||
CHUNK_CONFIG.enabled && sceneMode === "game" && cameraMode === "player";
|
||||
|
||||
const chunks = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
return Object.entries(MAP_INSTANCING_ASSETS).flatMap(([type, config]) => {
|
||||
if (
|
||||
!config.enabled ||
|
||||
!isMapModelVisible(config.mapName, { groups, models })
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const instances = data.get(type as MapInstancingAssetType);
|
||||
if (!instances || instances.length === 0) return [];
|
||||
|
||||
return createMapAssetChunks(
|
||||
type as MapInstancingAssetType,
|
||||
config,
|
||||
instances,
|
||||
);
|
||||
});
|
||||
}, [data, groups, models]);
|
||||
|
||||
const visibleChunks = streamingEnabled
|
||||
? chunks.filter((chunk) => {
|
||||
if (activeChunkKeys.size > 0) {
|
||||
return activeChunkKeys.has(chunk.key);
|
||||
}
|
||||
|
||||
return (
|
||||
Math.hypot(
|
||||
chunk.centerX - camera.position.x,
|
||||
chunk.centerZ - camera.position.z,
|
||||
) <= CHUNK_CONFIG.loadRadius
|
||||
);
|
||||
})
|
||||
: chunks;
|
||||
|
||||
const updateActiveChunks = useCallback(() => {
|
||||
const nextKeys = new Set<string>();
|
||||
const cameraX = camera.position.x;
|
||||
const cameraZ = camera.position.z;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const distance = Math.hypot(
|
||||
chunk.centerX - cameraX,
|
||||
chunk.centerZ - cameraZ,
|
||||
);
|
||||
const wasActive = activeChunkKeys.has(chunk.key);
|
||||
const radius = wasActive
|
||||
? CHUNK_CONFIG.unloadRadius
|
||||
: CHUNK_CONFIG.loadRadius;
|
||||
|
||||
if (distance <= radius) {
|
||||
nextKeys.add(chunk.key);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
nextKeys.size === activeChunkKeys.size &&
|
||||
[...nextKeys].every((key) => activeChunkKeys.has(key))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveChunkKeys(nextKeys);
|
||||
}, [activeChunkKeys, camera, chunks]);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!streamingEnabled) return;
|
||||
|
||||
const now = clock.elapsedTime * 1000;
|
||||
if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return;
|
||||
lastUpdateRef.current = now;
|
||||
|
||||
updateActiveChunks();
|
||||
});
|
||||
|
||||
if (isLoading || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enabledAssets = Object.entries(MAP_INSTANCING_ASSETS).filter(
|
||||
([, config]) =>
|
||||
config.enabled && isMapModelVisible(config.mapName, { groups, models }),
|
||||
);
|
||||
|
||||
return (
|
||||
<group name="map-instancing-system">
|
||||
{enabledAssets.map(([type, config]) => {
|
||||
const instances = data.get(type as MapInstancingAssetType);
|
||||
|
||||
if (!instances || instances.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense key={type} fallback={null}>
|
||||
<InstancedMapAsset
|
||||
modelPath={config.modelPath}
|
||||
instances={instances}
|
||||
castShadow={config.castShadow}
|
||||
receiveShadow={config.receiveShadow}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
})}
|
||||
{visibleChunks.map((chunk) => (
|
||||
<Suspense key={chunk.key} fallback={null}>
|
||||
<InstancedMapAsset
|
||||
modelPath={chunk.config.modelPath}
|
||||
instances={chunk.instances}
|
||||
castShadow={chunk.config.castShadow}
|
||||
receiveShadow={chunk.config.receiveShadow}
|
||||
/>
|
||||
</Suspense>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,34 @@ export const MAP_INSTANCING_ASSETS = {
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
},
|
||||
panneauaffichage: {
|
||||
mapName: "panneauaffichage",
|
||||
modelPath: "/models/panneauaffichage/model.gltf",
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
},
|
||||
panneauclassique: {
|
||||
mapName: "panneauclassique",
|
||||
modelPath: "/models/panneauclassique/model.gltf",
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
},
|
||||
panneaufleche: {
|
||||
mapName: "panneaufleche",
|
||||
modelPath: "/models/panneaufleche/model.gltf",
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
},
|
||||
panneausolaire: {
|
||||
mapName: "panneausolaire",
|
||||
modelPath: "/models/panneausolaire/model.gltf",
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type MapInstancingAssetType = keyof typeof MAP_INSTANCING_ASSETS;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
||||
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
|
||||
import { useWind } from "@/hooks/world/useWind";
|
||||
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||
import type { VegetationInstance } from "@/world/vegetation/useVegetationData";
|
||||
|
||||
@@ -13,6 +14,7 @@ interface InstancedVegetationProps {
|
||||
scaleMultiplier: number;
|
||||
castShadow: boolean;
|
||||
receiveShadow: boolean;
|
||||
windStrength: number;
|
||||
}
|
||||
|
||||
interface MeshData {
|
||||
@@ -20,6 +22,111 @@ interface MeshData {
|
||||
material: THREE.Material;
|
||||
}
|
||||
|
||||
type WindShaderMaterial = THREE.Material & {
|
||||
userData: THREE.Material["userData"] & {
|
||||
windUniforms?: VegetationWindUniforms;
|
||||
};
|
||||
};
|
||||
|
||||
interface VegetationWindUniforms {
|
||||
time: { value: number };
|
||||
direction: { value: number };
|
||||
speed: { value: number };
|
||||
strength: { value: number };
|
||||
noiseScale: { value: number };
|
||||
}
|
||||
|
||||
function updateVegetationWindUniforms(
|
||||
uniforms: VegetationWindUniforms,
|
||||
elapsedTime: number,
|
||||
direction: number,
|
||||
speed: number,
|
||||
strength: number,
|
||||
noiseScale: number,
|
||||
): void {
|
||||
uniforms.time.value = elapsedTime;
|
||||
uniforms.direction.value = direction;
|
||||
uniforms.speed.value = speed;
|
||||
uniforms.strength.value = strength;
|
||||
uniforms.noiseScale.value = noiseScale;
|
||||
}
|
||||
|
||||
function addWindWeightAttribute(geometry: THREE.BufferGeometry): void {
|
||||
geometry.computeBoundingBox();
|
||||
|
||||
const position = geometry.getAttribute("position");
|
||||
const bounds = geometry.boundingBox;
|
||||
if (!position || !bounds) return;
|
||||
|
||||
const height = Math.max(bounds.max.y - bounds.min.y, 0.0001);
|
||||
const weights = new Float32Array(position.count);
|
||||
|
||||
for (let index = 0; index < position.count; index++) {
|
||||
const y = position.getY(index);
|
||||
const normalizedHeight = THREE.MathUtils.clamp(
|
||||
(y - bounds.min.y) / height,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
weights[index] = normalizedHeight * normalizedHeight;
|
||||
}
|
||||
|
||||
geometry.setAttribute("aWindWeight", new THREE.BufferAttribute(weights, 1));
|
||||
}
|
||||
|
||||
function applyVegetationWindMaterial(
|
||||
material: THREE.Material,
|
||||
): WindShaderMaterial {
|
||||
const windMaterial = material as WindShaderMaterial;
|
||||
const windUniforms: VegetationWindUniforms = {
|
||||
time: { value: 0 },
|
||||
direction: { value: 0 },
|
||||
speed: { value: 0 },
|
||||
strength: { value: 0 },
|
||||
noiseScale: { value: 1 },
|
||||
};
|
||||
|
||||
windMaterial.userData.windUniforms = windUniforms;
|
||||
|
||||
windMaterial.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.uVegetationWindTime = windUniforms.time;
|
||||
shader.uniforms.uVegetationWindDirection = windUniforms.direction;
|
||||
shader.uniforms.uVegetationWindSpeed = windUniforms.speed;
|
||||
shader.uniforms.uVegetationWindStrength = windUniforms.strength;
|
||||
shader.uniforms.uVegetationWindNoiseScale = windUniforms.noiseScale;
|
||||
shader.vertexShader = shader.vertexShader
|
||||
.replace(
|
||||
"#include <common>",
|
||||
`#include <common>
|
||||
attribute float aWindWeight;
|
||||
uniform float uVegetationWindTime;
|
||||
uniform float uVegetationWindDirection;
|
||||
uniform float uVegetationWindSpeed;
|
||||
uniform float uVegetationWindStrength;
|
||||
uniform float uVegetationWindNoiseScale;`,
|
||||
)
|
||||
.replace(
|
||||
"#include <begin_vertex>",
|
||||
`#include <begin_vertex>
|
||||
#ifdef USE_INSTANCING
|
||||
vec2 instanceOffset = instanceMatrix[3].xz;
|
||||
#else
|
||||
vec2 instanceOffset = vec2(0.0);
|
||||
#endif
|
||||
vec2 windDirection = vec2(cos(uVegetationWindDirection), sin(uVegetationWindDirection));
|
||||
float windPhase = dot(instanceOffset + position.xz, windDirection) * uVegetationWindNoiseScale;
|
||||
float windWave = sin(windPhase + uVegetationWindTime * uVegetationWindSpeed);
|
||||
float windGust = sin(windPhase * 0.37 + uVegetationWindTime * uVegetationWindSpeed * 0.63);
|
||||
float windOffset = (windWave * 0.65 + windGust * 0.35) * uVegetationWindStrength * aWindWeight;
|
||||
transformed.xz += windDirection * windOffset;`,
|
||||
);
|
||||
};
|
||||
|
||||
windMaterial.customProgramCacheKey = () => "vegetation-wind-v1";
|
||||
|
||||
return windMaterial;
|
||||
}
|
||||
|
||||
function extractMeshes(scene: THREE.Group): MeshData[] {
|
||||
const meshesByMaterial = new Map<
|
||||
string,
|
||||
@@ -64,9 +171,11 @@ function extractMeshes(scene: THREE.Group): MeshData[] {
|
||||
return null;
|
||||
}
|
||||
|
||||
addWindWeightAttribute(mergedGeometry);
|
||||
|
||||
return {
|
||||
geometry: mergedGeometry,
|
||||
material,
|
||||
material: applyVegetationWindMaterial(material),
|
||||
};
|
||||
})
|
||||
.filter((meshData): meshData is MeshData => meshData !== null);
|
||||
@@ -121,13 +230,16 @@ export function InstancedVegetation({
|
||||
scaleMultiplier,
|
||||
castShadow,
|
||||
receiveShadow,
|
||||
windStrength,
|
||||
}: InstancedVegetationProps): React.JSX.Element | null {
|
||||
const { scene } = useGLTF(modelPath);
|
||||
const wind = useWind();
|
||||
const terrainHeight = useTerrainHeightSampler();
|
||||
const maxAnisotropy = useThree((state) =>
|
||||
state.gl.capabilities.getMaxAnisotropy(),
|
||||
);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const windUniformsRef = useRef<VegetationWindUniforms[]>([]);
|
||||
|
||||
const meshDataList = useMemo(() => {
|
||||
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||
@@ -204,7 +316,19 @@ export function InstancedVegetation({
|
||||
}, [instancedMeshes]);
|
||||
|
||||
useEffect(() => {
|
||||
windUniformsRef.current = meshDataList
|
||||
.map(
|
||||
(meshData) =>
|
||||
(meshData.material as WindShaderMaterial).userData.windUniforms,
|
||||
)
|
||||
.filter(
|
||||
(uniforms): uniforms is VegetationWindUniforms =>
|
||||
uniforms !== undefined,
|
||||
);
|
||||
|
||||
return () => {
|
||||
windUniformsRef.current = [];
|
||||
|
||||
for (const meshData of meshDataList) {
|
||||
meshData.geometry.dispose();
|
||||
if (Array.isArray(meshData.material)) {
|
||||
@@ -218,6 +342,19 @@ export function InstancedVegetation({
|
||||
};
|
||||
}, [meshDataList]);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
for (const windUniforms of windUniformsRef.current) {
|
||||
updateVegetationWindUniforms(
|
||||
windUniforms,
|
||||
clock.elapsedTime,
|
||||
wind.direction,
|
||||
wind.speed,
|
||||
wind.strength * windStrength,
|
||||
wind.noiseScale,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (groundedInstances.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ interface VegetationChunk {
|
||||
scaleMultiplier: number;
|
||||
castShadow: boolean;
|
||||
receiveShadow: boolean;
|
||||
windStrength: number;
|
||||
centerX: number;
|
||||
centerZ: number;
|
||||
instances: VegetationInstance[];
|
||||
@@ -70,6 +71,7 @@ function createVegetationChunks(
|
||||
scaleMultiplier: config.scaleMultiplier,
|
||||
castShadow: config.castShadow,
|
||||
receiveShadow: config.receiveShadow,
|
||||
windStrength: config.windStrength,
|
||||
centerX: center.x / chunkInstances.length,
|
||||
centerZ: center.z / chunkInstances.length,
|
||||
instances: chunkInstances,
|
||||
@@ -174,6 +176,7 @@ export function VegetationSystem(): React.JSX.Element | null {
|
||||
scaleMultiplier={chunk.scaleMultiplier}
|
||||
castShadow={chunk.castShadow}
|
||||
receiveShadow={chunk.receiveShadow}
|
||||
windStrength={chunk.windStrength}
|
||||
/>
|
||||
</Suspense>
|
||||
))}
|
||||
|
||||
@@ -11,6 +11,7 @@ export const VEGETATION_TYPES = {
|
||||
scaleMultiplier: 2,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
windStrength: 0.08,
|
||||
enabled: true,
|
||||
},
|
||||
sapin: {
|
||||
@@ -19,6 +20,7 @@ export const VEGETATION_TYPES = {
|
||||
scaleMultiplier: 5,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
windStrength: 0.04,
|
||||
enabled: true,
|
||||
},
|
||||
arbre: {
|
||||
@@ -27,6 +29,7 @@ export const VEGETATION_TYPES = {
|
||||
scaleMultiplier: 1,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
windStrength: 0.06,
|
||||
enabled: true,
|
||||
},
|
||||
champdeble: {
|
||||
@@ -35,6 +38,7 @@ export const VEGETATION_TYPES = {
|
||||
scaleMultiplier: 1,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
windStrength: 0.18,
|
||||
enabled: true,
|
||||
},
|
||||
champdesoja: {
|
||||
@@ -43,6 +47,7 @@ export const VEGETATION_TYPES = {
|
||||
scaleMultiplier: 1,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
windStrength: 0.16,
|
||||
enabled: true,
|
||||
},
|
||||
champsdetournesol: {
|
||||
@@ -51,6 +56,7 @@ export const VEGETATION_TYPES = {
|
||||
scaleMultiplier: 1,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
windStrength: 0.14,
|
||||
enabled: true,
|
||||
},
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user