diff --git a/src/components/three/world/EcoleModel.tsx b/src/components/three/world/EcoleModel.tsx
new file mode 100644
index 0000000..75398ae
--- /dev/null
+++ b/src/components/three/world/EcoleModel.tsx
@@ -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 ;
+}
+
+useGLTF.preload(ECOLE_MODEL_PATH);
diff --git a/src/components/three/world/FermeVerticaleModel.tsx b/src/components/three/world/FermeVerticaleModel.tsx
new file mode 100644
index 0000000..44ed4bd
--- /dev/null
+++ b/src/components/three/world/FermeVerticaleModel.tsx
@@ -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 (
+
+ );
+}
+
+useGLTF.preload(FERME_VERTICALE_MODEL_PATH);
diff --git a/src/components/three/world/GenerateurModel.tsx b/src/components/three/world/GenerateurModel.tsx
new file mode 100644
index 0000000..5b5cd39
--- /dev/null
+++ b/src/components/three/world/GenerateurModel.tsx
@@ -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 ;
+}
+
+useGLTF.preload(GENERATEUR_MODEL_PATH);
diff --git a/src/components/three/world/LafabrikModel.tsx b/src/components/three/world/LafabrikModel.tsx
new file mode 100644
index 0000000..0c080b7
--- /dev/null
+++ b/src/components/three/world/LafabrikModel.tsx
@@ -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 ;
+}
+
+useGLTF.preload(LAFABRIK_MODEL_PATH);
diff --git a/src/components/three/models/generated/EcoleModel.tsx b/src/components/three/world/MergedStaticMapModel.tsx
similarity index 87%
rename from src/components/three/models/generated/EcoleModel.tsx
rename to src/components/three/world/MergedStaticMapModel.tsx
index 970d16f..f2e75ae 100644
--- a/src/components/three/models/generated/EcoleModel.tsx
+++ b/src/components/three/world/MergedStaticMapModel.tsx
@@ -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(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 (
);
}
-
-useGLTF.preload(ECOLE_MODEL_PATH);
diff --git a/src/components/three/world/TerrainModel.tsx b/src/components/three/world/TerrainModel.tsx
index 34de05c..c1f2a29 100644
--- a/src/components/three/world/TerrainModel.tsx
+++ b/src/components/three/world/TerrainModel.tsx
@@ -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(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?.();
diff --git a/src/data/world/fogConfig.ts b/src/data/world/fogConfig.ts
index edf90e6..7df1d7e 100644
--- a/src/data/world/fogConfig.ts
+++ b/src/data/world/fogConfig.ts
@@ -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,
};
diff --git a/src/hooks/three/useLoggedGLTF.ts b/src/hooks/three/useLoggedGLTF.ts
index 8e08ce3..8b535d9 100644
--- a/src/hooks/three/useLoggedGLTF.ts
+++ b/src/hooks/three/useLoggedGLTF.ts
@@ -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,
) {
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;
diff --git a/src/hooks/three/useTerrainHeight.ts b/src/hooks/three/useTerrainHeight.ts
new file mode 100644
index 0000000..5587962
--- /dev/null
+++ b/src/hooks/three/useTerrainHeight.ts
@@ -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];
+}
diff --git a/src/utils/three/optimizeGLTFScene.ts b/src/utils/three/optimizeGLTFScene.ts
new file mode 100644
index 0000000..710ebbb
--- /dev/null
+++ b/src/utils/three/optimizeGLTFScene.ts
@@ -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>;
+
+const optimizedTextures = new WeakSet();
+
+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);
+ });
+}
diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx
index 03c842c..de446e2 100644
--- a/src/world/GameMap.tsx
+++ b/src/world/GameMap.tsx
@@ -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 (
);
}
function FallbackMapNode({ node }: { node: MapNode }): React.JSX.Element {
const { position, rotation, scale } = node;
+ const normalizedScale = normalizeMapScale(scale);
return (
-
+
diff --git a/src/world/map-generated/GeneratedMapNodeInstance.tsx b/src/world/map-generated/GeneratedMapNodeInstance.tsx
index bf87b21..5f1e5bb 100644
--- a/src/world/map-generated/GeneratedMapNodeInstance.tsx
+++ b/src/world/map-generated/GeneratedMapNodeInstance.tsx
@@ -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 (
+ );
+ }
+
+ if (node.name === "fermeverticale") {
+ return (
+
+ );
+ }
+
+ if (node.name === "generateur") {
+ return (
+
+ );
+ }
+
+ if (node.name === "lafabrik") {
+ return (
+
);
diff --git a/src/world/map-generated/generatedMapModelConfig.ts b/src/world/map-generated/generatedMapModelConfig.ts
index 686d295..878daa1 100644
--- a/src/world/map-generated/generatedMapModelConfig.ts
+++ b/src/world/map-generated/generatedMapModelConfig.ts
@@ -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);
diff --git a/src/world/map-instancing/InstancedMapAsset.tsx b/src/world/map-instancing/InstancedMapAsset.tsx
index dfd695f..38dd5c6 100644
--- a/src/world/map-instancing/InstancedMapAsset.tsx
+++ b/src/world/map-instancing/InstancedMapAsset.tsx
@@ -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(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;
diff --git a/src/world/map-instancing/mapInstancingConfig.ts b/src/world/map-instancing/mapInstancingConfig.ts
index 0b8b02b..77aa728 100644
--- a/src/world/map-instancing/mapInstancingConfig.ts
+++ b/src/world/map-instancing/mapInstancingConfig.ts
@@ -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",
diff --git a/src/world/vegetation/InstancedVegetation.tsx b/src/world/vegetation/InstancedVegetation.tsx
index 847ebfc..ff8598a 100644
--- a/src/world/vegetation/InstancedVegetation.tsx
+++ b/src/world/vegetation/InstancedVegetation.tsx
@@ -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(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;
}
diff --git a/src/world/vegetation/VegetationSystem.tsx b/src/world/vegetation/VegetationSystem.tsx
index 1684a96..cb84d67 100644
--- a/src/world/vegetation/VegetationSystem.tsx
+++ b/src/world/vegetation/VegetationSystem.tsx
@@ -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 (
{visibleChunks.map((chunk) => (
@@ -154,6 +156,7 @@ export function VegetationSystem(): React.JSX.Element | null {
diff --git a/src/world/vegetation/vegetationConfig.ts b/src/world/vegetation/vegetationConfig.ts
index b279fea..a49fcec 100644
--- a/src/world/vegetation/vegetationConfig.ts
+++ b/src/world/vegetation/vegetationConfig.ts
@@ -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,