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_MODEL_PATH = "/models/terrain/model.gltf";
|
||||||
export const TERRAIN_SURFACE_COLOR_TOLERANCE = 15;
|
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_WATER_HEIGHT = 0;
|
||||||
export const TERRAIN_TILE_SIZE = 1;
|
export const TERRAIN_TILE_SIZE = 1;
|
||||||
export const GRASS_BASE_COLOR = "#1a3a1a";
|
export const GRASS_BASE_COLOR = "#1a3a1a";
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ function createTerrainSurfaceBounds(
|
|||||||
return {
|
return {
|
||||||
minX: box.min.x,
|
minX: box.min.x,
|
||||||
maxX: box.max.x,
|
maxX: box.max.x,
|
||||||
minY: box.min.y,
|
|
||||||
maxY: box.max.y,
|
|
||||||
minZ: box.min.z,
|
minZ: box.min.z,
|
||||||
maxZ: box.max.z,
|
maxZ: box.max.z,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,12 +18,17 @@ export interface TerrainSurfaceUv {
|
|||||||
export interface TerrainSurfaceBounds {
|
export interface TerrainSurfaceBounds {
|
||||||
minX: number;
|
minX: number;
|
||||||
maxX: number;
|
maxX: number;
|
||||||
minY: number;
|
|
||||||
maxY: number;
|
|
||||||
minZ: number;
|
minZ: number;
|
||||||
maxZ: number;
|
maxZ: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TerrainSurfaceProjectionConfig {
|
||||||
|
flipX: boolean;
|
||||||
|
flipZ: boolean;
|
||||||
|
offsetX: number;
|
||||||
|
offsetZ: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TerrainSurfaceColorConfig {
|
export interface TerrainSurfaceColorConfig {
|
||||||
hex: string;
|
hex: string;
|
||||||
rgb: TerrainSurfaceRgb;
|
rgb: TerrainSurfaceRgb;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as THREE from "three";
|
|||||||
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
||||||
import type {
|
import type {
|
||||||
TerrainSurfaceBounds,
|
TerrainSurfaceBounds,
|
||||||
|
TerrainSurfaceProjectionConfig,
|
||||||
TerrainSurfaceRgb,
|
TerrainSurfaceRgb,
|
||||||
TerrainSurfaceSample,
|
TerrainSurfaceSample,
|
||||||
TerrainSurfaceUv,
|
TerrainSurfaceUv,
|
||||||
@@ -14,12 +15,14 @@ type TerrainSurfaceImageSource =
|
|||||||
| ImageBitmap;
|
| ImageBitmap;
|
||||||
|
|
||||||
const imageDataCache = new WeakMap<TerrainSurfaceImageSource, ImageData>();
|
const imageDataCache = new WeakMap<TerrainSurfaceImageSource, ImageData>();
|
||||||
const DOWN = new THREE.Vector3(0, -1, 0);
|
|
||||||
|
|
||||||
function clamp01(value: number): number {
|
function clamp01(value: number): number {
|
||||||
return Math.min(Math.max(value, 0), 1);
|
return Math.min(Math.max(value, 0), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrap01(value: number): number {
|
||||||
|
return ((value % 1) + 1) % 1;
|
||||||
|
}
|
||||||
|
|
||||||
function isTerrainSurfaceImageSource(
|
function isTerrainSurfaceImageSource(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): value is TerrainSurfaceImageSource {
|
): value is TerrainSurfaceImageSource {
|
||||||
@@ -84,13 +87,27 @@ export function terrainSurfaceUvFromXZ(
|
|||||||
x: number,
|
x: number,
|
||||||
z: number,
|
z: number,
|
||||||
bounds: TerrainSurfaceBounds,
|
bounds: TerrainSurfaceBounds,
|
||||||
|
projection?: TerrainSurfaceProjectionConfig,
|
||||||
): TerrainSurfaceUv {
|
): TerrainSurfaceUv {
|
||||||
const width = bounds.maxX - bounds.minX;
|
const width = bounds.maxX - bounds.minX;
|
||||||
const depth = bounds.maxZ - bounds.minZ;
|
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 {
|
return {
|
||||||
u: width === 0 ? 0 : (x - bounds.minX) / width,
|
u,
|
||||||
v: depth === 0 ? 0 : (z - bounds.minZ) / depth,
|
v,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,31 +116,10 @@ export function sampleTerrainSurfaceAtXZ(
|
|||||||
x: number,
|
x: number,
|
||||||
z: number,
|
z: number,
|
||||||
bounds: TerrainSurfaceBounds,
|
bounds: TerrainSurfaceBounds,
|
||||||
|
projection?: TerrainSurfaceProjectionConfig,
|
||||||
): TerrainSurfaceSample {
|
): TerrainSurfaceSample {
|
||||||
return sampleTerrainSurfaceAtUv(
|
return sampleTerrainSurfaceAtUv(
|
||||||
imageData,
|
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 { useMemo } from "react";
|
||||||
|
import { TERRAIN_SURFACE_PROJECTION } from "@/data/world/terrainConfig";
|
||||||
import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData";
|
import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
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 type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData";
|
||||||
import {
|
import {
|
||||||
PATH_TILE_MAX_COUNT,
|
PATH_TILE_MAX_COUNT,
|
||||||
@@ -39,21 +40,20 @@ export function usePathTileData(): MapAssetInstance[] {
|
|||||||
terrainSurfaceData.bounds.maxZ,
|
terrainSurfaceData.bounds.maxZ,
|
||||||
PATH_TILE_SAMPLE_STEP,
|
PATH_TILE_SAMPLE_STEP,
|
||||||
);
|
);
|
||||||
const raycastY = terrainSurfaceData.bounds.maxY + 10;
|
|
||||||
|
|
||||||
for (const x of xCenters) {
|
for (const x of xCenters) {
|
||||||
for (const z of zCenters) {
|
for (const z of zCenters) {
|
||||||
if (instances.length >= PATH_TILE_MAX_COUNT) return instances;
|
if (instances.length >= PATH_TILE_MAX_COUNT) return instances;
|
||||||
|
|
||||||
const sample = sampleTerrainSurfaceAtXZFromRaycast(
|
const sample = sampleTerrainSurfaceAtXZ(
|
||||||
terrainSurfaceData.imageData,
|
terrainSurfaceData.imageData,
|
||||||
terrainSurfaceData.raycastTarget,
|
|
||||||
x,
|
x,
|
||||||
z,
|
z,
|
||||||
raycastY,
|
terrainSurfaceData.bounds,
|
||||||
|
TERRAIN_SURFACE_PROJECTION,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sample?.key !== PATH_SURFACE_KEY) continue;
|
if (sample.key !== PATH_SURFACE_KEY) continue;
|
||||||
|
|
||||||
instances.push({
|
instances.push({
|
||||||
position: [x, 0, z],
|
position: [x, 0, z],
|
||||||
|
|||||||
Reference in New Issue
Block a user