feat(collision): include static map models in octree
This commit is contained in:
@@ -30,6 +30,10 @@ export function isRuntimeSingleMapNode(node: MapNode): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isRuntimeCollisionMapNode(node: MapNode): boolean {
|
||||
return node.name === "terrain" || isRuntimeSingleMapNode(node);
|
||||
}
|
||||
|
||||
export function isEditorVisibleMapNode(node: MapNode): boolean {
|
||||
return !isRuntimeStructureMapNode(node.name) && node.type !== "Mesh";
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import { logger } from "@/utils/core/Logger";
|
||||
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
||||
import {
|
||||
getTerrainMapNode,
|
||||
isRuntimeCollisionMapNode,
|
||||
isRuntimeSingleMapNode,
|
||||
} from "@/utils/map/mapRuntimeClassification";
|
||||
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
|
||||
@@ -178,7 +179,7 @@ export function GameMap({
|
||||
return { node, modelUrl: modelUrl ?? null };
|
||||
});
|
||||
const loadedCollisionNodes = sceneData.mapNodes
|
||||
.filter((node) => node.name === "terrain")
|
||||
.filter(isRuntimeCollisionMapNode)
|
||||
.map((node) => {
|
||||
const modelUrl = sceneData.models.get(node.name);
|
||||
return { node, modelUrl: modelUrl ?? null };
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
@@ -11,6 +12,11 @@ import * as THREE from "three";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
|
||||
import {
|
||||
getObjectBottomOffset,
|
||||
normalizeMapScale,
|
||||
useTerrainHeightSampler,
|
||||
} from "@/hooks/three/useTerrainHeight";
|
||||
import { WorldBoundsCollision } from "@/world/collision/WorldBoundsCollision";
|
||||
import type { MapNode } from "@/types/editor/editor";
|
||||
import type { OctreeReadyHandler } from "@/types/three/three";
|
||||
@@ -27,6 +33,8 @@ interface ResolvedGameMapCollisionNode {
|
||||
modelUrl: string;
|
||||
}
|
||||
|
||||
type TerrainHeightSampler = ReturnType<typeof useTerrainHeightSampler>;
|
||||
|
||||
interface GameMapCollisionProps {
|
||||
buildOctree?: boolean;
|
||||
mapReady: boolean;
|
||||
@@ -47,8 +55,6 @@ interface CollisionErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
const MAP_COLLISION_NODE_NAMES = new Set(["terrain"]);
|
||||
|
||||
class CollisionErrorBoundary extends Component<
|
||||
CollisionErrorBoundaryProps,
|
||||
CollisionErrorBoundaryState
|
||||
@@ -88,9 +94,7 @@ class CollisionErrorBoundary extends Component<
|
||||
function isCollisionNode(
|
||||
mapNode: GameMapCollisionNode,
|
||||
): mapNode is ResolvedGameMapCollisionNode {
|
||||
return (
|
||||
mapNode.modelUrl !== null && MAP_COLLISION_NODE_NAMES.has(mapNode.node.name)
|
||||
);
|
||||
return mapNode.modelUrl !== null;
|
||||
}
|
||||
|
||||
export function GameMapCollision({
|
||||
@@ -105,6 +109,7 @@ export function GameMapCollision({
|
||||
const settledCollisionNodesRef = useRef(new Set<number>());
|
||||
const loadedNotifiedRef = useRef(false);
|
||||
const [settledCollisionNodeCount, setSettledCollisionNodeCount] = useState(0);
|
||||
const terrainHeight = useTerrainHeightSampler();
|
||||
const collisionNodes = nodes.filter(isCollisionNode);
|
||||
const collisionReady =
|
||||
mapReady && settledCollisionNodeCount >= collisionNodes.length;
|
||||
@@ -188,6 +193,7 @@ export function GameMapCollision({
|
||||
node={mapNode.node}
|
||||
modelUrl={mapNode.modelUrl}
|
||||
onLoaded={() => handleCollisionNodeSettled(index)}
|
||||
terrainHeight={terrainHeight}
|
||||
/>
|
||||
</Suspense>
|
||||
</CollisionErrorBoundary>
|
||||
@@ -201,19 +207,30 @@ function CollisionModelInstance({
|
||||
node,
|
||||
modelUrl,
|
||||
onLoaded,
|
||||
terrainHeight,
|
||||
}: {
|
||||
node: MapNode;
|
||||
modelUrl: string;
|
||||
onLoaded: () => void;
|
||||
terrainHeight: TerrainHeightSampler;
|
||||
}): React.JSX.Element {
|
||||
const { position, rotation, scale } = node;
|
||||
const normalizedScale = normalizeMapScale(scale);
|
||||
const { scene } = useLoggedGLTF(modelUrl, {
|
||||
scope: "GameMapCollision.ModelInstance",
|
||||
position,
|
||||
rotation,
|
||||
scale,
|
||||
scale: normalizedScale,
|
||||
});
|
||||
const sceneInstance = useClonedObject(scene);
|
||||
const collisionPosition = useMemo(() => {
|
||||
if (node.name === "terrain") return position;
|
||||
|
||||
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;
|
||||
}, [node.name, normalizedScale, position, sceneInstance, terrainHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
onLoaded();
|
||||
@@ -222,9 +239,9 @@ function CollisionModelInstance({
|
||||
return (
|
||||
<primitive
|
||||
object={sceneInstance}
|
||||
position={position}
|
||||
position={collisionPosition}
|
||||
rotation={rotation}
|
||||
scale={scale}
|
||||
scale={normalizedScale}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ const DEFAULT_KEYS: Keys = {
|
||||
jump: false,
|
||||
};
|
||||
|
||||
const PLAYER_COLLISION_ITERATIONS = 3;
|
||||
const PLAYER_FLOOR_NORMAL_MIN = 0.5;
|
||||
|
||||
interface PlayerControllerProps {
|
||||
octree: Octree | null;
|
||||
spawnPosition: Vector3Tuple;
|
||||
@@ -300,16 +303,21 @@ export function PlayerController({
|
||||
capsule.current.translate(_translateVec);
|
||||
|
||||
if (octree) {
|
||||
const result = octree.capsuleIntersect(capsule.current);
|
||||
onFloor.current = false;
|
||||
|
||||
if (result) {
|
||||
onFloor.current = result.normal.y > 0;
|
||||
for (let index = 0; index < PLAYER_COLLISION_ITERATIONS; index++) {
|
||||
const result = octree.capsuleIntersect(capsule.current);
|
||||
if (!result) break;
|
||||
|
||||
if (!onFloor.current) {
|
||||
const vn = result.normal.dot(velocity.current);
|
||||
velocity.current.addScaledVector(result.normal, -vn);
|
||||
} else {
|
||||
const isFloorCollision = result.normal.y > PLAYER_FLOOR_NORMAL_MIN;
|
||||
onFloor.current ||= isFloorCollision;
|
||||
const normalVelocity = result.normal.dot(velocity.current);
|
||||
|
||||
if (!isFloorCollision && normalVelocity < 0) {
|
||||
velocity.current.addScaledVector(result.normal, -normalVelocity);
|
||||
}
|
||||
|
||||
if (isFloorCollision) {
|
||||
velocity.current.y = Math.max(0, velocity.current.y);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user