diff --git a/src/hooks/world/useTerrainSurfaceData.ts b/src/hooks/world/useTerrainSurfaceData.ts index 4a2725d..87b43bc 100644 --- a/src/hooks/world/useTerrainSurfaceData.ts +++ b/src/hooks/world/useTerrainSurfaceData.ts @@ -40,6 +40,8 @@ function createTerrainSurfaceBounds( return { minX: box.min.x, maxX: box.max.x, + minY: box.min.y, + maxY: box.max.y, minZ: box.min.z, maxZ: box.max.z, }; @@ -58,6 +60,7 @@ export function useTerrainSurfaceData(): TerrainSurfaceData | null { return { bounds: createTerrainSurfaceBounds(scene), imageData, + raycastTarget: scene, }; }, [scene]); } diff --git a/src/types/world/terrainSurface.ts b/src/types/world/terrainSurface.ts index ca64927..258b27d 100644 --- a/src/types/world/terrainSurface.ts +++ b/src/types/world/terrainSurface.ts @@ -1,3 +1,5 @@ +import type * as THREE from "three"; + export type TerrainSurfaceKind = | "grass" | "path" @@ -16,6 +18,8 @@ export interface TerrainSurfaceUv { export interface TerrainSurfaceBounds { minX: number; maxX: number; + minY: number; + maxY: number; minZ: number; maxZ: number; } @@ -38,4 +42,5 @@ export interface TerrainSurfaceSample { export interface TerrainSurfaceData { bounds: TerrainSurfaceBounds; imageData: ImageData; + raycastTarget: THREE.Object3D; } diff --git a/src/utils/world/terrainSurfaceSampler.ts b/src/utils/world/terrainSurfaceSampler.ts index f76392f..f04f718 100644 --- a/src/utils/world/terrainSurfaceSampler.ts +++ b/src/utils/world/terrainSurfaceSampler.ts @@ -1,4 +1,4 @@ -import type * as THREE from "three"; +import * as THREE from "three"; import { TERRAIN_COLORS } from "@/data/world/terrainConfig"; import type { TerrainSurfaceBounds, @@ -14,6 +14,7 @@ type TerrainSurfaceImageSource = | ImageBitmap; const imageDataCache = new WeakMap(); +const DOWN = new THREE.Vector3(0, -1, 0); function clamp01(value: number): number { return Math.min(Math.max(value, 0), 1); @@ -104,3 +105,25 @@ export function sampleTerrainSurfaceAtXZ( 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, + }); +} diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx index aa579b7..4588fe3 100644 --- a/src/world/GameMap.tsx +++ b/src/world/GameMap.tsx @@ -24,6 +24,7 @@ import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNode import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModelConfig"; import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem"; import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig"; +import { PathSystem } from "@/world/paths/PathSystem"; import { VegetationSystem } from "@/world/vegetation/VegetationSystem"; import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading"; import { logger } from "@/utils/core/Logger"; @@ -257,6 +258,7 @@ export function GameMap({ ))} + {isMapModelVisible("terrain", { groups, models }) ? ( diff --git a/src/world/paths/PathSystem.tsx b/src/world/paths/PathSystem.tsx new file mode 100644 index 0000000..ef50e4d --- /dev/null +++ b/src/world/paths/PathSystem.tsx @@ -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 ( + + ); +} diff --git a/src/world/paths/pathConfig.ts b/src/world/paths/pathConfig.ts new file mode 100644 index 0000000..76627f2 --- /dev/null +++ b/src/world/paths/pathConfig.ts @@ -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; diff --git a/src/world/paths/usePathTileData.ts b/src/world/paths/usePathTileData.ts new file mode 100644 index 0000000..56d3ae2 --- /dev/null +++ b/src/world/paths/usePathTileData.ts @@ -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]); +}