feat(editor): edit hierarchical map nodes
This commit is contained in:
+12
-4
@@ -4,6 +4,7 @@ import {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
@@ -11,8 +12,9 @@ import * as THREE from "three";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
import {
|
||||
getObjectBottomOffset,
|
||||
normalizeMapScale,
|
||||
useTerrainSnappedPosition,
|
||||
useTerrainHeightSampler,
|
||||
} from "@/hooks/three/useTerrainHeight";
|
||||
import { TerrainModel } from "@/components/three/world/TerrainModel";
|
||||
import {
|
||||
@@ -356,15 +358,21 @@ function ModelInstance({
|
||||
onLoaded: () => void;
|
||||
}): React.JSX.Element {
|
||||
const { position, rotation, scale } = node;
|
||||
const snappedPosition = useTerrainSnappedPosition(position);
|
||||
const normalizedScale = normalizeMapScale(scale);
|
||||
const terrainHeight = useTerrainHeightSampler();
|
||||
const { scene } = useLoggedGLTF(modelUrl, {
|
||||
scope: "GameMap.ModelInstance",
|
||||
position: snappedPosition,
|
||||
position,
|
||||
rotation,
|
||||
scale: normalizedScale,
|
||||
});
|
||||
const sceneInstance = useClonedObject(scene);
|
||||
const groundedPosition = useMemo(() => {
|
||||
const [x, y, z] = position;
|
||||
const height = terrainHeight.getHeight(x, z);
|
||||
const bottomOffset = getObjectBottomOffset(sceneInstance, normalizedScale);
|
||||
return [x, height !== null ? height + bottomOffset : y, z] as const;
|
||||
}, [normalizedScale, position, sceneInstance, terrainHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
sceneInstance.traverse((child) => {
|
||||
@@ -379,7 +387,7 @@ function ModelInstance({
|
||||
return (
|
||||
<primitive
|
||||
object={sceneInstance}
|
||||
position={snappedPosition}
|
||||
position={groundedPosition}
|
||||
rotation={rotation}
|
||||
scale={normalizedScale}
|
||||
/>
|
||||
|
||||
@@ -130,6 +130,7 @@ function extractMeshes(scene: THREE.Group): MeshData[] {
|
||||
function setInstanceMatrices(
|
||||
instancedMesh: THREE.InstancedMesh,
|
||||
instances: MapAssetInstance[],
|
||||
geometryBottomY: number,
|
||||
): void {
|
||||
const position = new THREE.Vector3();
|
||||
const rotation = new THREE.Euler();
|
||||
@@ -145,6 +146,7 @@ function setInstanceMatrices(
|
||||
rotation.set(...instance.rotation);
|
||||
quaternion.setFromEuler(rotation);
|
||||
scale.set(...instance.scale);
|
||||
position.y += -geometryBottomY * scale.y;
|
||||
matrix.compose(position, quaternion, scale);
|
||||
instancedMesh.setMatrixAt(i, matrix);
|
||||
}
|
||||
@@ -152,6 +154,20 @@ function setInstanceMatrices(
|
||||
instancedMesh.instanceMatrix.needsUpdate = true;
|
||||
}
|
||||
|
||||
function getMeshBottomY(meshDataList: MeshData[]): number {
|
||||
let bottomY = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const meshData of meshDataList) {
|
||||
meshData.geometry.computeBoundingBox();
|
||||
const minY = meshData.geometry.boundingBox?.min.y;
|
||||
if (minY !== undefined) {
|
||||
bottomY = Math.min(bottomY, minY);
|
||||
}
|
||||
}
|
||||
|
||||
return Number.isFinite(bottomY) ? bottomY : 0;
|
||||
}
|
||||
|
||||
export function InstancedMapAsset({
|
||||
modelPath,
|
||||
instances,
|
||||
@@ -185,6 +201,7 @@ export function InstancedMapAsset({
|
||||
|
||||
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||
const meshDataList = extractMeshes(scene);
|
||||
const geometryBottomY = getMeshBottomY(meshDataList);
|
||||
const instancedMeshes = meshDataList.map((meshData, index) => {
|
||||
const instancedMesh = new THREE.InstancedMesh(
|
||||
meshData.geometry,
|
||||
@@ -192,7 +209,7 @@ export function InstancedMapAsset({
|
||||
groundedInstances.length,
|
||||
);
|
||||
|
||||
setInstanceMatrices(instancedMesh, groundedInstances);
|
||||
setInstanceMatrices(instancedMesh, groundedInstances, geometryBottomY);
|
||||
instancedMesh.castShadow = castShadow;
|
||||
instancedMesh.receiveShadow = receiveShadow;
|
||||
instancedMesh.name = `instanced-map-asset-${index}`;
|
||||
|
||||
@@ -75,6 +75,7 @@ function extractMeshes(scene: THREE.Group): MeshData[] {
|
||||
function createInstanceMatrices(
|
||||
instances: VegetationInstance[],
|
||||
scaleMultiplier: number,
|
||||
geometryBottomY: number,
|
||||
): THREE.Matrix4[] {
|
||||
const matrices: THREE.Matrix4[] = [];
|
||||
const position = new THREE.Vector3();
|
||||
@@ -90,6 +91,7 @@ function createInstanceMatrices(
|
||||
const matrix = new THREE.Matrix4();
|
||||
|
||||
position.set(...instance.position);
|
||||
position.y += -geometryBottomY * scaleMultiplier;
|
||||
rotation.set(...instance.rotation);
|
||||
quaternion.setFromEuler(rotation);
|
||||
matrix.compose(position, quaternion, scale);
|
||||
@@ -99,6 +101,20 @@ function createInstanceMatrices(
|
||||
return matrices;
|
||||
}
|
||||
|
||||
function getMeshBottomY(meshDataList: MeshData[]): number {
|
||||
let bottomY = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const meshData of meshDataList) {
|
||||
meshData.geometry.computeBoundingBox();
|
||||
const minY = meshData.geometry.boundingBox?.min.y;
|
||||
if (minY !== undefined) {
|
||||
bottomY = Math.min(bottomY, minY);
|
||||
}
|
||||
}
|
||||
|
||||
return Number.isFinite(bottomY) ? bottomY : 0;
|
||||
}
|
||||
|
||||
export function InstancedVegetation({
|
||||
modelPath,
|
||||
instances,
|
||||
@@ -130,8 +146,13 @@ export function InstancedVegetation({
|
||||
[instances, terrainHeight],
|
||||
);
|
||||
const matrices = useMemo(
|
||||
() => createInstanceMatrices(groundedInstances, scaleMultiplier),
|
||||
[groundedInstances, scaleMultiplier],
|
||||
() =>
|
||||
createInstanceMatrices(
|
||||
groundedInstances,
|
||||
scaleMultiplier,
|
||||
getMeshBottomY(meshDataList),
|
||||
),
|
||||
[groundedInstances, meshDataList, scaleMultiplier],
|
||||
);
|
||||
|
||||
const instancedMeshes = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user