From b4a3545460396ea5fc21525e58d363a7ecb46e27 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Mon, 25 May 2026 15:55:56 +0200 Subject: [PATCH] feat: add terrain surface sampler --- src/types/world/terrainSurface.ts | 18 ++++ src/utils/world/terrainSurfaceSampler.ts | 106 +++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 src/utils/world/terrainSurfaceSampler.ts diff --git a/src/types/world/terrainSurface.ts b/src/types/world/terrainSurface.ts index ec625c2..49f18cc 100644 --- a/src/types/world/terrainSurface.ts +++ b/src/types/world/terrainSurface.ts @@ -8,6 +8,18 @@ export type TerrainSurfaceKind = export type TerrainSurfaceRgb = readonly [number, number, number]; +export interface TerrainSurfaceUv { + u: number; + v: number; +} + +export interface TerrainSurfaceBounds { + minX: number; + maxX: number; + minZ: number; + maxZ: number; +} + export interface TerrainSurfaceColorConfig { hex: string; rgb: TerrainSurfaceRgb; @@ -16,3 +28,9 @@ export interface TerrainSurfaceColorConfig { modelPath?: string; tileSize?: number; } + +export interface TerrainSurfaceSample { + rgb: TerrainSurfaceRgb; + key: string | null; + config: TerrainSurfaceColorConfig | null; +} diff --git a/src/utils/world/terrainSurfaceSampler.ts b/src/utils/world/terrainSurfaceSampler.ts new file mode 100644 index 0000000..f76392f --- /dev/null +++ b/src/utils/world/terrainSurfaceSampler.ts @@ -0,0 +1,106 @@ +import type * as THREE from "three"; +import { TERRAIN_COLORS } from "@/data/world/terrainConfig"; +import type { + TerrainSurfaceBounds, + TerrainSurfaceRgb, + TerrainSurfaceSample, + TerrainSurfaceUv, +} from "@/types/world/terrainSurface"; +import { getTerrainColorKeyFromRgb } from "@/utils/world/terrainSurfaceColor"; + +type TerrainSurfaceImageSource = + | HTMLImageElement + | HTMLCanvasElement + | ImageBitmap; + +const imageDataCache = new WeakMap(); + +function clamp01(value: number): number { + return Math.min(Math.max(value, 0), 1); +} + +function isTerrainSurfaceImageSource( + value: unknown, +): value is TerrainSurfaceImageSource { + return ( + value instanceof HTMLImageElement || + value instanceof HTMLCanvasElement || + (typeof ImageBitmap !== "undefined" && value instanceof ImageBitmap) + ); +} + +export function createTerrainSurfaceImageData( + texture: THREE.Texture, +): ImageData | null { + if (typeof document === "undefined") return null; + + const image = texture.image as unknown; + if (!isTerrainSurfaceImageSource(image)) return null; + + const cachedImageData = imageDataCache.get(image); + if (cachedImageData) return cachedImageData; + + const width = image.width; + const height = image.height; + if (width <= 0 || height <= 0) return null; + + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + if (!context) return null; + + canvas.width = width; + canvas.height = height; + context.drawImage(image, 0, 0, width, height); + + const imageData = context.getImageData(0, 0, width, height); + imageDataCache.set(image, imageData); + return imageData; +} + +export function sampleTerrainSurfaceAtUv( + imageData: ImageData, + uv: TerrainSurfaceUv, +): TerrainSurfaceSample { + const x = Math.round(clamp01(uv.u) * (imageData.width - 1)); + const y = Math.round((1 - clamp01(uv.v)) * (imageData.height - 1)); + const index = (y * imageData.width + x) * 4; + + const rgb: TerrainSurfaceRgb = [ + imageData.data[index] ?? 0, + imageData.data[index + 1] ?? 0, + imageData.data[index + 2] ?? 0, + ]; + const key = getTerrainColorKeyFromRgb(rgb[0], rgb[1], rgb[2]); + + return { + rgb, + key, + config: key === null ? null : TERRAIN_COLORS[key], + }; +} + +export function terrainSurfaceUvFromXZ( + x: number, + z: number, + bounds: TerrainSurfaceBounds, +): TerrainSurfaceUv { + const width = bounds.maxX - bounds.minX; + const depth = bounds.maxZ - bounds.minZ; + + return { + u: width === 0 ? 0 : (x - bounds.minX) / width, + v: depth === 0 ? 0 : (z - bounds.minZ) / depth, + }; +} + +export function sampleTerrainSurfaceAtXZ( + imageData: ImageData, + x: number, + z: number, + bounds: TerrainSurfaceBounds, +): TerrainSurfaceSample { + return sampleTerrainSurfaceAtUv( + imageData, + terrainSurfaceUvFromXZ(x, z, bounds), + ); +}