fix(terrain): map surface colors with configurable projection
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user