fix(terrain): map surface colors with configurable projection

This commit is contained in:
Tom Boullay
2026-05-25 17:40:01 +02:00
parent 235a38f67b
commit 417afdc1d5
5 changed files with 42 additions and 37 deletions
+6
View File
@@ -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";
-2
View File
@@ -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,
};
+7 -2
View File
@@ -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;
+23 -27
View File
@@ -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<TerrainSurfaceImageSource, ImageData>();
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,
});
}
+6 -6
View File
@@ -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],