feat(world): add octree collision proxies
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
export interface OctreeCollisionBox {
|
||||||
|
center: Vector3Tuple;
|
||||||
|
size: Vector3Tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MapOctreeCollisionBox extends OctreeCollisionBox {
|
||||||
|
bottomY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MAP_OCTREE_COLLISION_BOXES = {
|
||||||
|
immeuble1: {
|
||||||
|
center: [-0.0308, 5.8389, 0],
|
||||||
|
size: [17.2522, 11.6098, 9.2668],
|
||||||
|
bottomY: 0.034,
|
||||||
|
},
|
||||||
|
maison1: {
|
||||||
|
center: [0, 1.3638, 0.0536],
|
||||||
|
size: [2.7813, 3.022, 2.8609],
|
||||||
|
bottomY: -0.1472,
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, MapOctreeCollisionBox>;
|
||||||
|
|
||||||
|
export const LA_FABRIK_INTERIOR_COLLISION_BOXES = [
|
||||||
|
{
|
||||||
|
center: [-6.9351, 2.278, -0.0001],
|
||||||
|
size: [0.2, 1.94, 3.711],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
center: [0.8026, 0.719, -3.639],
|
||||||
|
size: [4.346, 1.108, 1.181],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
center: [-5.8519, 0.9362, 2.5742],
|
||||||
|
size: [1.67, 1.551, 2.566],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
center: [-2.0627, 1.4875, -1.2243],
|
||||||
|
size: [0.691, 0.723, 0.687],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
center: [-3.5502, 1.4378, -1.2485],
|
||||||
|
size: [1.055, 0.657, 0.563],
|
||||||
|
},
|
||||||
|
] as const satisfies readonly OctreeCollisionBox[];
|
||||||
|
|
||||||
|
export const CHARACTER_OCTREE_COLLISION_BOX = {
|
||||||
|
center: [0, 0.875, 0],
|
||||||
|
size: [0.62, 1.75, 0.62],
|
||||||
|
} as const satisfies OctreeCollisionBox;
|
||||||
|
|
||||||
|
export function hasMapOctreeCollisionBox(
|
||||||
|
name: string,
|
||||||
|
): name is keyof typeof MAP_OCTREE_COLLISION_BOXES {
|
||||||
|
return name in MAP_OCTREE_COLLISION_BOXES;
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import { useMapLodModelPath } from "@/hooks/world/useMapLodModelPath";
|
|||||||
import { GameMapCollision } from "@/world/GameMapCollision";
|
import { GameMapCollision } from "@/world/GameMapCollision";
|
||||||
import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
|
import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
|
||||||
import { isGeneratedMapModelName } from "@/data/world/generatedMapModelConfig";
|
import { isGeneratedMapModelName } from "@/data/world/generatedMapModelConfig";
|
||||||
|
import { hasMapOctreeCollisionBox } from "@/data/world/octreeCollisionConfig";
|
||||||
import { getMapSingleModelScaleMultiplier } from "@/data/world/mapInstancingConfig";
|
import { getMapSingleModelScaleMultiplier } from "@/data/world/mapInstancingConfig";
|
||||||
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
|
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
@@ -115,6 +116,9 @@ export function GameMap({
|
|||||||
const [collisionMapNodes, setCollisionMapNodes] = useState<LoadedMapNode[]>(
|
const [collisionMapNodes, setCollisionMapNodes] = useState<LoadedMapNode[]>(
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
const [proxyCollisionMapNodes, setProxyCollisionMapNodes] = useState<
|
||||||
|
MapNode[]
|
||||||
|
>([]);
|
||||||
const [terrainNode, setTerrainNode] = useState<MapNode | null>(null);
|
const [terrainNode, setTerrainNode] = useState<MapNode | null>(null);
|
||||||
const [mapLoaded, setMapLoaded] = useState(false);
|
const [mapLoaded, setMapLoaded] = useState(false);
|
||||||
const [settledMapNodeCount, setSettledMapNodeCount] = useState(0);
|
const [settledMapNodeCount, setSettledMapNodeCount] = useState(0);
|
||||||
@@ -134,6 +138,7 @@ export function GameMap({
|
|||||||
(currentStep: string) => {
|
(currentStep: string) => {
|
||||||
setRenderMapNodes([]);
|
setRenderMapNodes([]);
|
||||||
setCollisionMapNodes([]);
|
setCollisionMapNodes([]);
|
||||||
|
setProxyCollisionMapNodes([]);
|
||||||
setTerrainNode(null);
|
setTerrainNode(null);
|
||||||
setMapLoaded(true);
|
setMapLoaded(true);
|
||||||
settledMapNodesRef.current.clear();
|
settledMapNodesRef.current.clear();
|
||||||
@@ -191,6 +196,10 @@ export function GameMap({
|
|||||||
const modelUrl = sceneData.models.get(node.name);
|
const modelUrl = sceneData.models.get(node.name);
|
||||||
return { node, modelUrl: modelUrl ?? null };
|
return { node, modelUrl: modelUrl ?? null };
|
||||||
});
|
});
|
||||||
|
const loadedProxyCollisionNodes = sceneData.mapNodes.filter(
|
||||||
|
(node) =>
|
||||||
|
node.type === "Object3D" && hasMapOctreeCollisionBox(node.name),
|
||||||
|
);
|
||||||
const loadedTerrainNode = getTerrainMapNode(sceneData.mapNodes);
|
const loadedTerrainNode = getTerrainMapNode(sceneData.mapNodes);
|
||||||
const repairMissionAnchors = getRepairMissionMapAnchors(
|
const repairMissionAnchors = getRepairMissionMapAnchors(
|
||||||
sceneData.mapNodes,
|
sceneData.mapNodes,
|
||||||
@@ -211,6 +220,7 @@ export function GameMap({
|
|||||||
|
|
||||||
setRenderMapNodes(loadedMapNodes);
|
setRenderMapNodes(loadedMapNodes);
|
||||||
setCollisionMapNodes(loadedCollisionNodes);
|
setCollisionMapNodes(loadedCollisionNodes);
|
||||||
|
setProxyCollisionMapNodes(loadedProxyCollisionNodes);
|
||||||
setTerrainNode(loadedTerrainNode);
|
setTerrainNode(loadedTerrainNode);
|
||||||
setRepairMissionAnchors(repairMissionAnchors);
|
setRepairMissionAnchors(repairMissionAnchors);
|
||||||
setMapLoaded(true);
|
setMapLoaded(true);
|
||||||
@@ -285,6 +295,7 @@ export function GameMap({
|
|||||||
buildOctree={buildOctree}
|
buildOctree={buildOctree}
|
||||||
mapReady={mapReady}
|
mapReady={mapReady}
|
||||||
nodes={collisionMapNodes}
|
nodes={collisionMapNodes}
|
||||||
|
proxyNodes={proxyCollisionMapNodes}
|
||||||
onLoaded={onLoaded}
|
onLoaded={onLoaded}
|
||||||
onLoadingStateChange={onLoadingStateChange}
|
onLoadingStateChange={onLoadingStateChange}
|
||||||
onOctreeReady={onOctreeReady}
|
onOctreeReady={onOctreeReady}
|
||||||
|
|||||||
@@ -17,9 +17,23 @@ import {
|
|||||||
normalizeMapScale,
|
normalizeMapScale,
|
||||||
useTerrainHeightSampler,
|
useTerrainHeightSampler,
|
||||||
} from "@/hooks/three/useTerrainHeight";
|
} from "@/hooks/three/useTerrainHeight";
|
||||||
|
import {
|
||||||
|
CHARACTER_CONFIGS,
|
||||||
|
CHARACTER_IDS,
|
||||||
|
type CharacterId,
|
||||||
|
} from "@/data/world/characters/characterConfig";
|
||||||
|
import {
|
||||||
|
CHARACTER_OCTREE_COLLISION_BOX,
|
||||||
|
LA_FABRIK_INTERIOR_COLLISION_BOXES,
|
||||||
|
MAP_OCTREE_COLLISION_BOXES,
|
||||||
|
hasMapOctreeCollisionBox,
|
||||||
|
type OctreeCollisionBox,
|
||||||
|
} from "@/data/world/octreeCollisionConfig";
|
||||||
|
import { getMapModelScaleMultiplier } from "@/data/world/mapInstancingConfig";
|
||||||
|
import { useCharacterDebugStore } from "@/managers/stores/useCharacterDebugStore";
|
||||||
import { WorldBoundsCollision } from "@/world/collision/WorldBoundsCollision";
|
import { WorldBoundsCollision } from "@/world/collision/WorldBoundsCollision";
|
||||||
import type { MapNode } from "@/types/map/mapScene";
|
import type { MapNode } from "@/types/map/mapScene";
|
||||||
import type { OctreeReadyHandler } from "@/types/three/three";
|
import type { OctreeReadyHandler, Vector3Tuple } from "@/types/three/three";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
|
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
|
||||||
|
|
||||||
@@ -39,6 +53,7 @@ interface GameMapCollisionProps {
|
|||||||
buildOctree?: boolean;
|
buildOctree?: boolean;
|
||||||
mapReady: boolean;
|
mapReady: boolean;
|
||||||
nodes: readonly GameMapCollisionNode[];
|
nodes: readonly GameMapCollisionNode[];
|
||||||
|
proxyNodes: readonly MapNode[];
|
||||||
onLoaded?: (() => void) | undefined;
|
onLoaded?: (() => void) | undefined;
|
||||||
onLoadingStateChange?: SceneLoadingChangeHandler | undefined;
|
onLoadingStateChange?: SceneLoadingChangeHandler | undefined;
|
||||||
onOctreeReady: OctreeReadyHandler;
|
onOctreeReady: OctreeReadyHandler;
|
||||||
@@ -101,6 +116,7 @@ export function GameMapCollision({
|
|||||||
buildOctree = true,
|
buildOctree = true,
|
||||||
mapReady,
|
mapReady,
|
||||||
nodes,
|
nodes,
|
||||||
|
proxyNodes,
|
||||||
onLoaded,
|
onLoaded,
|
||||||
onLoadingStateChange,
|
onLoadingStateChange,
|
||||||
onOctreeReady,
|
onOctreeReady,
|
||||||
@@ -111,8 +127,23 @@ export function GameMapCollision({
|
|||||||
const [settledCollisionNodeCount, setSettledCollisionNodeCount] = useState(0);
|
const [settledCollisionNodeCount, setSettledCollisionNodeCount] = useState(0);
|
||||||
const terrainHeight = useTerrainHeightSampler();
|
const terrainHeight = useTerrainHeightSampler();
|
||||||
const collisionNodes = nodes.filter(isCollisionNode);
|
const collisionNodes = nodes.filter(isCollisionNode);
|
||||||
|
const collisionSourceCount =
|
||||||
|
collisionNodes.length + proxyNodes.length + CHARACTER_IDS.length;
|
||||||
const collisionReady =
|
const collisionReady =
|
||||||
mapReady && settledCollisionNodeCount >= collisionNodes.length;
|
mapReady && settledCollisionNodeCount >= collisionNodes.length;
|
||||||
|
const characterCollisionSignature = useCharacterDebugStore((state) =>
|
||||||
|
CHARACTER_IDS.map((id) => {
|
||||||
|
const character = state.characters[id];
|
||||||
|
return [
|
||||||
|
...character.position,
|
||||||
|
...character.rotation,
|
||||||
|
...character.scale,
|
||||||
|
].join(",");
|
||||||
|
}).join("|"),
|
||||||
|
);
|
||||||
|
const collisionRebuildKey = collisionReady
|
||||||
|
? `${collisionNodes.length}:${collisionSourceCount}:${characterCollisionSignature}`
|
||||||
|
: "pending";
|
||||||
|
|
||||||
const notifyLoaded = useCallback(() => {
|
const notifyLoaded = useCallback(() => {
|
||||||
if (loadedNotifiedRef.current) return;
|
if (loadedNotifiedRef.current) return;
|
||||||
@@ -144,14 +175,14 @@ export function GameMapCollision({
|
|||||||
useOctreeGraphNode(
|
useOctreeGraphNode(
|
||||||
groupRef,
|
groupRef,
|
||||||
handleOctreeReady,
|
handleOctreeReady,
|
||||||
collisionReady ? collisionNodes.length : 0,
|
collisionRebuildKey,
|
||||||
buildOctree && collisionReady && collisionNodes.length > 0,
|
buildOctree && collisionReady && collisionSourceCount > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapReady) return;
|
if (!mapReady) return;
|
||||||
|
|
||||||
if (collisionNodes.length === 0) {
|
if (collisionSourceCount === 0) {
|
||||||
notifyLoaded();
|
notifyLoaded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -171,6 +202,7 @@ export function GameMapCollision({
|
|||||||
}, [
|
}, [
|
||||||
buildOctree,
|
buildOctree,
|
||||||
collisionNodes.length,
|
collisionNodes.length,
|
||||||
|
collisionSourceCount,
|
||||||
collisionReady,
|
collisionReady,
|
||||||
mapReady,
|
mapReady,
|
||||||
notifyLoaded,
|
notifyLoaded,
|
||||||
@@ -180,6 +212,18 @@ export function GameMapCollision({
|
|||||||
return (
|
return (
|
||||||
<group ref={groupRef} visible={false}>
|
<group ref={groupRef} visible={false}>
|
||||||
{mapReady ? <WorldBoundsCollision /> : null}
|
{mapReady ? <WorldBoundsCollision /> : null}
|
||||||
|
{mapReady
|
||||||
|
? proxyNodes.map((node, index) => (
|
||||||
|
<MapCollisionBoxProxy
|
||||||
|
key={`proxy-collision-${index}`}
|
||||||
|
node={node}
|
||||||
|
terrainHeight={terrainHeight}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
{mapReady ? (
|
||||||
|
<CharacterCollisionProxies terrainHeight={terrainHeight} />
|
||||||
|
) : null}
|
||||||
{mapReady
|
{mapReady
|
||||||
? collisionNodes.map((mapNode, index) => (
|
? collisionNodes.map((mapNode, index) => (
|
||||||
<CollisionErrorBoundary
|
<CollisionErrorBoundary
|
||||||
@@ -253,11 +297,125 @@ function CollisionModelInstance({
|
|||||||
}, [onLoaded]);
|
}, [onLoaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<primitive
|
<primitive
|
||||||
object={sceneInstance}
|
object={sceneInstance}
|
||||||
position={collisionPosition}
|
position={collisionPosition}
|
||||||
rotation={rotation}
|
rotation={rotation}
|
||||||
scale={normalizedScale}
|
scale={normalizedScale}
|
||||||
/>
|
/>
|
||||||
|
{node.name === "lafabrik" ? (
|
||||||
|
<group
|
||||||
|
name="lafabrik-interior-collision-proxies"
|
||||||
|
position={collisionPosition}
|
||||||
|
rotation={rotation}
|
||||||
|
scale={normalizedScale}
|
||||||
|
>
|
||||||
|
{LA_FABRIK_INTERIOR_COLLISION_BOXES.map((box, index) => (
|
||||||
|
<CollisionBox key={`lafabrik-interior-${index}`} box={box} />
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollisionBox({ box }: { box: OctreeCollisionBox }): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<mesh position={box.center}>
|
||||||
|
<boxGeometry args={box.size} />
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScaledMapNodeScale(node: MapNode): Vector3Tuple {
|
||||||
|
const baseScale = normalizeMapScale(node.scale);
|
||||||
|
const scaleMultiplier = getMapModelScaleMultiplier(node.name);
|
||||||
|
|
||||||
|
return [
|
||||||
|
baseScale[0] * scaleMultiplier,
|
||||||
|
baseScale[1] * scaleMultiplier,
|
||||||
|
baseScale[2] * scaleMultiplier,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function MapCollisionBoxProxy({
|
||||||
|
node,
|
||||||
|
terrainHeight,
|
||||||
|
}: {
|
||||||
|
node: MapNode;
|
||||||
|
terrainHeight: TerrainHeightSampler;
|
||||||
|
}): React.JSX.Element | null {
|
||||||
|
const collisionBox = hasMapOctreeCollisionBox(node.name)
|
||||||
|
? MAP_OCTREE_COLLISION_BOXES[node.name]
|
||||||
|
: null;
|
||||||
|
const normalizedScale = useMemo(() => createScaledMapNodeScale(node), [node]);
|
||||||
|
const position = useMemo(() => {
|
||||||
|
const [x, y, z] = node.position;
|
||||||
|
if (!collisionBox) return [x, y, z] satisfies Vector3Tuple;
|
||||||
|
|
||||||
|
const height = terrainHeight.getHeight(x, z);
|
||||||
|
const bottomOffset = -collisionBox.bottomY * normalizedScale[1];
|
||||||
|
|
||||||
|
return [x, (height ?? y) + bottomOffset, z] satisfies Vector3Tuple;
|
||||||
|
}, [collisionBox, node.position, normalizedScale, terrainHeight]);
|
||||||
|
|
||||||
|
if (!collisionBox) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group
|
||||||
|
name={`${node.name}-octree-collision-proxy`}
|
||||||
|
position={position}
|
||||||
|
rotation={node.rotation}
|
||||||
|
scale={normalizedScale}
|
||||||
|
>
|
||||||
|
<CollisionBox box={collisionBox} />
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CharacterCollisionProxies({
|
||||||
|
terrainHeight,
|
||||||
|
}: {
|
||||||
|
terrainHeight: TerrainHeightSampler;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{CHARACTER_IDS.map((id) => (
|
||||||
|
<CharacterCollisionProxy
|
||||||
|
key={`character-collision-${id}`}
|
||||||
|
id={id}
|
||||||
|
terrainHeight={terrainHeight}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CharacterCollisionProxy({
|
||||||
|
id,
|
||||||
|
terrainHeight,
|
||||||
|
}: {
|
||||||
|
id: CharacterId;
|
||||||
|
terrainHeight: TerrainHeightSampler;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const config = CHARACTER_CONFIGS[id];
|
||||||
|
const state = useCharacterDebugStore((store) => store.characters[id]);
|
||||||
|
const position = useMemo(() => {
|
||||||
|
const [x, y, z] = state.position;
|
||||||
|
const height = terrainHeight.getHeight(x, z);
|
||||||
|
|
||||||
|
return [x, height ?? y, z] satisfies Vector3Tuple;
|
||||||
|
}, [state.position, terrainHeight]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group
|
||||||
|
name={`${config.id}-octree-collision-proxy`}
|
||||||
|
position={position}
|
||||||
|
rotation={state.rotation}
|
||||||
|
>
|
||||||
|
<CollisionBox box={CHARACTER_OCTREE_COLLISION_BOX} />
|
||||||
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user