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