feat: add terrain surface sampler

This commit is contained in:
Tom Boullay
2026-05-25 15:55:56 +02:00
parent 1f6d9659ed
commit b4a3545460
2 changed files with 124 additions and 0 deletions
+106
View File
@@ -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<TerrainSurfaceImageSource, ImageData>();
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),
);
}