diff --git a/src/hooks/three/useTerrainHeight.ts b/src/hooks/three/useTerrainHeight.ts index 5587962..d1b6d85 100644 --- a/src/hooks/three/useTerrainHeight.ts +++ b/src/hooks/three/useTerrainHeight.ts @@ -1,12 +1,16 @@ import { useMemo } from "react"; import { useGLTF } from "@react-three/drei"; import * as THREE from "three"; +import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig"; import type { Vector3Tuple } from "@/types/three/three"; +import { getMapNodesByName } from "@/utils/map/loadMapSceneData"; -const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf"; 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; @@ -14,8 +18,17 @@ interface TerrainHeightSampler { 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 raycaster = new THREE.Raycaster( new THREE.Vector3(), DOWN, @@ -32,17 +45,29 @@ function createTerrainHeightSampler( return { getHeight: (x, z) => { - raycaster.set(new THREE.Vector3(x, RAYCAST_Y, z), DOWN); + const localOrigin = new THREE.Vector3(x, RAYCAST_Y, z).applyMatrix4( + inverseTerrainMatrix, + ); + const localDirection = + DOWN.clone().transformDirection(inverseTerrainMatrix); + raycaster.set(localOrigin, localDirection); const hit = raycaster.intersectObjects(meshes, false)[0]; - return hit?.point.y ?? null; + 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(() => createTerrainHeightSampler(scene), [scene]); + return useMemo( + () => createTerrainHeightSampler(scene, position, rotation, scale), + [position, rotation, scale, scene], + ); } export function useTerrainSnappedPosition( diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx index 8de9b7b..c027e46 100644 --- a/src/world/GameMap.tsx +++ b/src/world/GameMap.tsx @@ -118,6 +118,7 @@ export function GameMap({ const [collisionMapNodes, setCollisionMapNodes] = useState( [], ); + const [terrainNode, setTerrainNode] = useState(null); const [mapLoaded, setMapLoaded] = useState(false); const [settledMapNodeCount, setSettledMapNodeCount] = useState(0); const mapReady = mapLoaded && settledMapNodeCount >= renderMapNodes.length; @@ -133,6 +134,7 @@ export function GameMap({ (currentStep: string) => { setRenderMapNodes([]); setCollisionMapNodes([]); + setTerrainNode(null); setMapLoaded(true); settledMapNodesRef.current.clear(); setSettledMapNodeCount(0); @@ -187,6 +189,7 @@ export function GameMap({ const modelUrl = sceneData.models.get(node.name); return { node, modelUrl: modelUrl ?? null }; }); + const loadedTerrainNode = loadedCollisionNodes[0]?.node ?? null; const missingModelCount = loadedMapNodes.filter( (mapNode) => mapNode.modelUrl === null, ).length; @@ -203,6 +206,7 @@ export function GameMap({ setRenderMapNodes(loadedMapNodes); setCollisionMapNodes(loadedCollisionNodes); + setTerrainNode(loadedTerrainNode); setMapLoaded(true); settledMapNodesRef.current.clear(); setSettledMapNodeCount(0); @@ -265,7 +269,15 @@ export function GameMap({ {isMapModelVisible("terrain", { groups, models }) ? ( - + terrainNode ? ( + + ) : ( + + ) ) : null}