27b4a2c392
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
140 lines
3.9 KiB
TypeScript
140 lines
3.9 KiB
TypeScript
import { useMemo } from "react";
|
|
import { useGLTF } from "@react-three/drei";
|
|
import * as THREE from "three";
|
|
import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig";
|
|
import {
|
|
isInsideLaFabrikFootprint,
|
|
LA_FABRIK_FLOOR_Y,
|
|
} from "@/data/world/laFabrikConfig";
|
|
import type { Vector3Tuple } from "@/types/three/three";
|
|
import { getMapNodesByName } from "@/utils/map/loadMapSceneData";
|
|
|
|
const RAYCAST_Y = 500;
|
|
const RAYCAST_FAR = 1000;
|
|
const DOWN = new THREE.Vector3(0, -1, 0);
|
|
const DEFAULT_TERRAIN_POSITION: Vector3Tuple = [0, 0, 0];
|
|
const DEFAULT_TERRAIN_ROTATION: Vector3Tuple = [0, 0, 0];
|
|
const DEFAULT_TERRAIN_SCALE: Vector3Tuple = [1, 1, 1];
|
|
|
|
interface TerrainHeightSampler {
|
|
getHeight: (x: number, z: number) => number | null;
|
|
}
|
|
|
|
interface CachedTerrainHeightSampler {
|
|
key: string;
|
|
sampler: TerrainHeightSampler;
|
|
}
|
|
|
|
const terrainSamplerCache = new WeakMap<
|
|
THREE.Object3D,
|
|
CachedTerrainHeightSampler
|
|
>();
|
|
|
|
function createTerrainSamplerCacheKey(
|
|
position: Vector3Tuple,
|
|
rotation: Vector3Tuple,
|
|
scale: Vector3Tuple,
|
|
): string {
|
|
return `${position.join(",")}|${rotation.join(",")}|${scale.join(",")}`;
|
|
}
|
|
|
|
function createTerrainHeightSampler(
|
|
scene: THREE.Object3D,
|
|
position: Vector3Tuple,
|
|
rotation: Vector3Tuple,
|
|
scale: Vector3Tuple,
|
|
): TerrainHeightSampler {
|
|
const meshes: THREE.Mesh[] = [];
|
|
const terrainMatrix = new THREE.Matrix4().compose(
|
|
new THREE.Vector3(...position),
|
|
new THREE.Quaternion().setFromEuler(new THREE.Euler(...rotation)),
|
|
new THREE.Vector3(...scale),
|
|
);
|
|
const inverseTerrainMatrix = terrainMatrix.clone().invert();
|
|
const localOrigin = new THREE.Vector3();
|
|
const localDirection = DOWN.clone().transformDirection(inverseTerrainMatrix);
|
|
const hits: THREE.Intersection[] = [];
|
|
const raycaster = new THREE.Raycaster(
|
|
new THREE.Vector3(),
|
|
DOWN,
|
|
0,
|
|
RAYCAST_FAR,
|
|
);
|
|
|
|
scene.updateMatrixWorld(true);
|
|
scene.traverse((child) => {
|
|
if (child instanceof THREE.Mesh) {
|
|
meshes.push(child);
|
|
}
|
|
});
|
|
|
|
return {
|
|
getHeight: (x, z) => {
|
|
if (isInsideLaFabrikFootprint(x, z, 0.6)) {
|
|
return LA_FABRIK_FLOOR_Y;
|
|
}
|
|
|
|
localOrigin.set(x, RAYCAST_Y, z).applyMatrix4(inverseTerrainMatrix);
|
|
raycaster.set(localOrigin, localDirection);
|
|
hits.length = 0;
|
|
raycaster.intersectObjects(meshes, false, hits);
|
|
const hit = hits[0];
|
|
return hit?.point.applyMatrix4(terrainMatrix).y ?? null;
|
|
},
|
|
};
|
|
}
|
|
|
|
export function useTerrainHeightSampler(): TerrainHeightSampler {
|
|
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
|
|
const terrainNode = getMapNodesByName("terrain")[0];
|
|
const position = terrainNode?.position ?? DEFAULT_TERRAIN_POSITION;
|
|
const rotation = terrainNode?.rotation ?? DEFAULT_TERRAIN_ROTATION;
|
|
const scale = terrainNode?.scale ?? DEFAULT_TERRAIN_SCALE;
|
|
|
|
return useMemo(() => {
|
|
const key = createTerrainSamplerCacheKey(position, rotation, scale);
|
|
const cached = terrainSamplerCache.get(scene);
|
|
|
|
if (cached?.key === key) {
|
|
return cached.sampler;
|
|
}
|
|
|
|
const sampler = createTerrainHeightSampler(
|
|
scene,
|
|
position,
|
|
rotation,
|
|
scale,
|
|
);
|
|
terrainSamplerCache.set(scene, { key, sampler });
|
|
return sampler;
|
|
}, [position, rotation, scale, scene]);
|
|
}
|
|
|
|
export function useTerrainSnappedPosition(
|
|
position: Vector3Tuple,
|
|
): Vector3Tuple {
|
|
const terrainHeight = useTerrainHeightSampler();
|
|
|
|
return useMemo(() => {
|
|
const [x, y, z] = position;
|
|
const height = terrainHeight.getHeight(x, z);
|
|
return [x, height ?? y, z];
|
|
}, [position, terrainHeight]);
|
|
}
|
|
|
|
export function getObjectBottomOffset(
|
|
object: THREE.Object3D,
|
|
scale: Vector3Tuple = [1, 1, 1],
|
|
): number {
|
|
const bounds = new THREE.Box3().setFromObject(object);
|
|
if (bounds.isEmpty()) return 0;
|
|
|
|
return -bounds.min.y * scale[1];
|
|
}
|
|
|
|
export function normalizeMapScale(scale: Vector3Tuple): Vector3Tuple {
|
|
const [x, y, z] = scale;
|
|
const isUniform = Math.abs(x - y) < 0.001 && Math.abs(x - z) < 0.001;
|
|
return isUniform ? scale : [x, x, x];
|
|
}
|