Feat/map-environment #6
@@ -40,6 +40,8 @@ function createTerrainSurfaceBounds(
|
|||||||
return {
|
return {
|
||||||
minX: box.min.x,
|
minX: box.min.x,
|
||||||
maxX: box.max.x,
|
maxX: box.max.x,
|
||||||
|
minY: box.min.y,
|
||||||
|
maxY: box.max.y,
|
||||||
minZ: box.min.z,
|
minZ: box.min.z,
|
||||||
maxZ: box.max.z,
|
maxZ: box.max.z,
|
||||||
};
|
};
|
||||||
@@ -58,6 +60,7 @@ export function useTerrainSurfaceData(): TerrainSurfaceData | null {
|
|||||||
return {
|
return {
|
||||||
bounds: createTerrainSurfaceBounds(scene),
|
bounds: createTerrainSurfaceBounds(scene),
|
||||||
imageData,
|
imageData,
|
||||||
|
raycastTarget: scene,
|
||||||
};
|
};
|
||||||
}, [scene]);
|
}, [scene]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type * as THREE from "three";
|
||||||
|
|
||||||
export type TerrainSurfaceKind =
|
export type TerrainSurfaceKind =
|
||||||
| "grass"
|
| "grass"
|
||||||
| "path"
|
| "path"
|
||||||
@@ -16,6 +18,8 @@ export interface TerrainSurfaceUv {
|
|||||||
export interface TerrainSurfaceBounds {
|
export interface TerrainSurfaceBounds {
|
||||||
minX: number;
|
minX: number;
|
||||||
maxX: number;
|
maxX: number;
|
||||||
|
minY: number;
|
||||||
|
maxY: number;
|
||||||
minZ: number;
|
minZ: number;
|
||||||
maxZ: number;
|
maxZ: number;
|
||||||
}
|
}
|
||||||
@@ -38,4 +42,5 @@ export interface TerrainSurfaceSample {
|
|||||||
export interface TerrainSurfaceData {
|
export interface TerrainSurfaceData {
|
||||||
bounds: TerrainSurfaceBounds;
|
bounds: TerrainSurfaceBounds;
|
||||||
imageData: ImageData;
|
imageData: ImageData;
|
||||||
|
raycastTarget: THREE.Object3D;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
||||||
import type {
|
import type {
|
||||||
TerrainSurfaceBounds,
|
TerrainSurfaceBounds,
|
||||||
@@ -14,6 +14,7 @@ type TerrainSurfaceImageSource =
|
|||||||
| ImageBitmap;
|
| ImageBitmap;
|
||||||
|
|
||||||
const imageDataCache = new WeakMap<TerrainSurfaceImageSource, ImageData>();
|
const imageDataCache = new WeakMap<TerrainSurfaceImageSource, ImageData>();
|
||||||
|
const DOWN = new THREE.Vector3(0, -1, 0);
|
||||||
|
|
||||||
function clamp01(value: number): number {
|
function clamp01(value: number): number {
|
||||||
return Math.min(Math.max(value, 0), 1);
|
return Math.min(Math.max(value, 0), 1);
|
||||||
@@ -104,3 +105,25 @@ export function sampleTerrainSurfaceAtXZ(
|
|||||||
terrainSurfaceUvFromXZ(x, z, bounds),
|
terrainSurfaceUvFromXZ(x, z, bounds),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sampleTerrainSurfaceAtXZFromRaycast(
|
||||||
|
imageData: ImageData,
|
||||||
|
raycastTarget: THREE.Object3D,
|
||||||
|
x: number,
|
||||||
|
z: number,
|
||||||
|
raycastY: number,
|
||||||
|
): TerrainSurfaceSample | null {
|
||||||
|
const raycaster = new THREE.Raycaster(
|
||||||
|
new THREE.Vector3(x, raycastY, z),
|
||||||
|
DOWN,
|
||||||
|
);
|
||||||
|
const intersections = raycaster.intersectObject(raycastTarget, true);
|
||||||
|
const intersection = intersections.find((item) => item.uv !== undefined);
|
||||||
|
|
||||||
|
if (!intersection?.uv) return null;
|
||||||
|
|
||||||
|
return sampleTerrainSurfaceAtUv(imageData, {
|
||||||
|
u: intersection.uv.x,
|
||||||
|
v: intersection.uv.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNode
|
|||||||
import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModelConfig";
|
import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModelConfig";
|
||||||
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
|
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
|
||||||
import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig";
|
import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig";
|
||||||
|
import { PathSystem } from "@/world/paths/PathSystem";
|
||||||
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
import { logger } from "@/utils/core/Logger";
|
import { logger } from "@/utils/core/Logger";
|
||||||
@@ -257,6 +258,7 @@ export function GameMap({
|
|||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
<MapInstancingSystem />
|
<MapInstancingSystem />
|
||||||
|
<PathSystem />
|
||||||
<VegetationSystem />
|
<VegetationSystem />
|
||||||
{isMapModelVisible("terrain", { groups, models }) ? (
|
{isMapModelVisible("terrain", { groups, models }) ? (
|
||||||
<TerrainModel />
|
<TerrainModel />
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset";
|
||||||
|
import { PATH_TILE_MODEL_PATH } from "@/world/paths/pathConfig";
|
||||||
|
import { usePathTileData } from "@/world/paths/usePathTileData";
|
||||||
|
|
||||||
|
export function PathSystem(): React.JSX.Element | null {
|
||||||
|
const pathTiles = usePathTileData();
|
||||||
|
|
||||||
|
if (pathTiles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InstancedMapAsset
|
||||||
|
castShadow={false}
|
||||||
|
instances={pathTiles}
|
||||||
|
modelPath={PATH_TILE_MODEL_PATH}
|
||||||
|
receiveShadow
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TERRAIN_COLORS, TERRAIN_TILE_SIZE } from "@/data/world/terrainConfig";
|
||||||
|
|
||||||
|
export const PATH_SURFACE_KEY = "chemin";
|
||||||
|
export const PATH_TILE_MODEL_PATH = TERRAIN_COLORS.chemin.modelPath;
|
||||||
|
export const PATH_TILE_SIZE =
|
||||||
|
TERRAIN_COLORS.chemin.tileSize ?? TERRAIN_TILE_SIZE;
|
||||||
|
export const PATH_TILE_SAMPLE_STEP = 2;
|
||||||
|
export const PATH_TILE_MAX_COUNT = 1500;
|
||||||
|
export const PATH_TILE_ROTATION = [0, 0, 0] as const;
|
||||||
|
export const PATH_TILE_SCALE = [1, 1, 1] as const;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
import { sampleTerrainSurfaceAtXZFromRaycast } from "@/utils/world/terrainSurfaceSampler";
|
||||||
|
import type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData";
|
||||||
|
import {
|
||||||
|
PATH_TILE_MAX_COUNT,
|
||||||
|
PATH_SURFACE_KEY,
|
||||||
|
PATH_TILE_ROTATION,
|
||||||
|
PATH_TILE_SAMPLE_STEP,
|
||||||
|
PATH_TILE_SCALE,
|
||||||
|
} from "@/world/paths/pathConfig";
|
||||||
|
|
||||||
|
function createSampleCenters(min: number, max: number, step: number): number[] {
|
||||||
|
const start = Math.ceil(min / step) * step + step * 0.5;
|
||||||
|
const centers: number[] = [];
|
||||||
|
|
||||||
|
for (let value = start; value <= max; value += step) {
|
||||||
|
centers.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return centers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePathTileData(): MapAssetInstance[] {
|
||||||
|
const terrainSurfaceData = useTerrainSurfaceData();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!terrainSurfaceData) return [];
|
||||||
|
|
||||||
|
const instances: MapAssetInstance[] = [];
|
||||||
|
const xCenters = createSampleCenters(
|
||||||
|
terrainSurfaceData.bounds.minX,
|
||||||
|
terrainSurfaceData.bounds.maxX,
|
||||||
|
PATH_TILE_SAMPLE_STEP,
|
||||||
|
);
|
||||||
|
const zCenters = createSampleCenters(
|
||||||
|
terrainSurfaceData.bounds.minZ,
|
||||||
|
terrainSurfaceData.bounds.maxZ,
|
||||||
|
PATH_TILE_SAMPLE_STEP,
|
||||||
|
);
|
||||||
|
const raycastY = terrainSurfaceData.bounds.maxY + 10;
|
||||||
|
|
||||||
|
for (const x of xCenters) {
|
||||||
|
for (const z of zCenters) {
|
||||||
|
if (instances.length >= PATH_TILE_MAX_COUNT) return instances;
|
||||||
|
|
||||||
|
const sample = sampleTerrainSurfaceAtXZFromRaycast(
|
||||||
|
terrainSurfaceData.imageData,
|
||||||
|
terrainSurfaceData.raycastTarget,
|
||||||
|
x,
|
||||||
|
z,
|
||||||
|
raycastY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sample?.key !== PATH_SURFACE_KEY) continue;
|
||||||
|
|
||||||
|
instances.push({
|
||||||
|
position: [x, 0, z],
|
||||||
|
rotation: [...PATH_TILE_ROTATION] as Vector3Tuple,
|
||||||
|
scale: [...PATH_TILE_SCALE] as Vector3Tuple,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances;
|
||||||
|
}, [terrainSurfaceData]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user