From 417afdc1d5a9c518e938369ee9eb22ba37d3d163 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Mon, 25 May 2026 17:40:01 +0200 Subject: [PATCH] fix(terrain): map surface colors with configurable projection --- src/data/world/terrainConfig.ts | 6 +++ src/hooks/world/useTerrainSurfaceData.ts | 2 - src/types/world/terrainSurface.ts | 9 ++++- src/utils/world/terrainSurfaceSampler.ts | 50 +++++++++++------------- src/world/paths/usePathTileData.ts | 12 +++--- 5 files changed, 42 insertions(+), 37 deletions(-) diff --git a/src/data/world/terrainConfig.ts b/src/data/world/terrainConfig.ts index 4cd6eb6..d4bb151 100644 --- a/src/data/world/terrainConfig.ts +++ b/src/data/world/terrainConfig.ts @@ -2,6 +2,12 @@ import type { TerrainSurfaceColorConfig } from "@/types/world/terrainSurface"; export const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf"; export const TERRAIN_SURFACE_COLOR_TOLERANCE = 15; +export const TERRAIN_SURFACE_PROJECTION = { + flipX: false, + flipZ: true, + offsetX: 0, + offsetZ: 0, +}; export const TERRAIN_WATER_HEIGHT = 0; export const TERRAIN_TILE_SIZE = 1; export const GRASS_BASE_COLOR = "#1a3a1a"; diff --git a/src/hooks/world/useTerrainSurfaceData.ts b/src/hooks/world/useTerrainSurfaceData.ts index 87b43bc..2b7c348 100644 --- a/src/hooks/world/useTerrainSurfaceData.ts +++ b/src/hooks/world/useTerrainSurfaceData.ts @@ -40,8 +40,6 @@ 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, }; diff --git a/src/types/world/terrainSurface.ts b/src/types/world/terrainSurface.ts index 258b27d..4f0dc4d 100644 --- a/src/types/world/terrainSurface.ts +++ b/src/types/world/terrainSurface.ts @@ -18,12 +18,17 @@ export interface TerrainSurfaceUv { export interface TerrainSurfaceBounds { minX: number; maxX: number; - minY: number; - maxY: number; minZ: number; maxZ: number; } +export interface TerrainSurfaceProjectionConfig { + flipX: boolean; + flipZ: boolean; + offsetX: number; + offsetZ: number; +} + export interface TerrainSurfaceColorConfig { hex: string; rgb: TerrainSurfaceRgb; diff --git a/src/utils/world/terrainSurfaceSampler.ts b/src/utils/world/terrainSurfaceSampler.ts index f04f718..c80fc2e 100644 --- a/src/utils/world/terrainSurfaceSampler.ts +++ b/src/utils/world/terrainSurfaceSampler.ts @@ -2,6 +2,7 @@ import * as THREE from "three"; import { TERRAIN_COLORS } from "@/data/world/terrainConfig"; import type { TerrainSurfaceBounds, + TerrainSurfaceProjectionConfig, TerrainSurfaceRgb, TerrainSurfaceSample, TerrainSurfaceUv, @@ -14,12 +15,14 @@ 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); } +function wrap01(value: number): number { + return ((value % 1) + 1) % 1; +} + function isTerrainSurfaceImageSource( value: unknown, ): value is TerrainSurfaceImageSource { @@ -84,13 +87,27 @@ export function terrainSurfaceUvFromXZ( x: number, z: number, bounds: TerrainSurfaceBounds, + projection?: TerrainSurfaceProjectionConfig, ): TerrainSurfaceUv { const width = bounds.maxX - bounds.minX; const depth = bounds.maxZ - bounds.minZ; + let u = width === 0 ? 0 : (x - bounds.minX) / width; + let v = depth === 0 ? 0 : (z - bounds.minZ) / depth; + + if (projection?.flipX) { + u = 1 - u; + } + + if (projection?.flipZ) { + v = 1 - v; + } + + u = wrap01(u + (projection?.offsetX ?? 0)); + v = wrap01(v + (projection?.offsetZ ?? 0)); return { - u: width === 0 ? 0 : (x - bounds.minX) / width, - v: depth === 0 ? 0 : (z - bounds.minZ) / depth, + u, + v, }; } @@ -99,31 +116,10 @@ export function sampleTerrainSurfaceAtXZ( x: number, z: number, bounds: TerrainSurfaceBounds, + projection?: TerrainSurfaceProjectionConfig, ): TerrainSurfaceSample { return sampleTerrainSurfaceAtUv( imageData, - terrainSurfaceUvFromXZ(x, z, bounds), + terrainSurfaceUvFromXZ(x, z, bounds, projection), ); } - -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/paths/usePathTileData.ts b/src/world/paths/usePathTileData.ts index 56d3ae2..b024dd0 100644 --- a/src/world/paths/usePathTileData.ts +++ b/src/world/paths/usePathTileData.ts @@ -1,7 +1,8 @@ import { useMemo } from "react"; +import { TERRAIN_SURFACE_PROJECTION } from "@/data/world/terrainConfig"; import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData"; import type { Vector3Tuple } from "@/types/three/three"; -import { sampleTerrainSurfaceAtXZFromRaycast } from "@/utils/world/terrainSurfaceSampler"; +import { sampleTerrainSurfaceAtXZ } from "@/utils/world/terrainSurfaceSampler"; import type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData"; import { PATH_TILE_MAX_COUNT, @@ -39,21 +40,20 @@ export function usePathTileData(): MapAssetInstance[] { 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( + const sample = sampleTerrainSurfaceAtXZ( terrainSurfaceData.imageData, - terrainSurfaceData.raycastTarget, x, z, - raycastY, + terrainSurfaceData.bounds, + TERRAIN_SURFACE_PROJECTION, ); - if (sample?.key !== PATH_SURFACE_KEY) continue; + if (sample.key !== PATH_SURFACE_KEY) continue; instances.push({ position: [x, 0, z],