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 { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
|
||||
import { isGeneratedMapModelName } from "@/data/world/generatedMapModelConfig";
|
||||
import { hasMapOctreeCollisionBox } from "@/data/world/octreeCollisionConfig";
|
||||
import { getMapSingleModelScaleMultiplier } from "@/data/world/mapInstancingConfig";
|
||||
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
|
||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||
@@ -115,6 +116,9 @@ export function GameMap({
|
||||
const [collisionMapNodes, setCollisionMapNodes] = useState<LoadedMapNode[]>(
|
||||
[],
|
||||
);
|
||||
const [proxyCollisionMapNodes, setProxyCollisionMapNodes] = useState<
|
||||
MapNode[]
|
||||
>([]);
|
||||
const [terrainNode, setTerrainNode] = useState<MapNode | null>(null);
|
||||
const [mapLoaded, setMapLoaded] = useState(false);
|
||||
const [settledMapNodeCount, setSettledMapNodeCount] = useState(0);
|
||||
@@ -134,6 +138,7 @@ export function GameMap({
|
||||
(currentStep: string) => {
|
||||
setRenderMapNodes([]);
|
||||
setCollisionMapNodes([]);
|
||||
setProxyCollisionMapNodes([]);
|
||||
setTerrainNode(null);
|
||||
setMapLoaded(true);
|
||||
settledMapNodesRef.current.clear();
|
||||
@@ -191,6 +196,10 @@ export function GameMap({
|
||||
const modelUrl = sceneData.models.get(node.name);
|
||||
return { node, modelUrl: modelUrl ?? null };
|
||||
});
|
||||
const loadedProxyCollisionNodes = sceneData.mapNodes.filter(
|
||||
(node) =>
|
||||
node.type === "Object3D" && hasMapOctreeCollisionBox(node.name),
|
||||
);
|
||||
const loadedTerrainNode = getTerrainMapNode(sceneData.mapNodes);
|
||||
const repairMissionAnchors = getRepairMissionMapAnchors(
|
||||
sceneData.mapNodes,
|
||||
@@ -211,6 +220,7 @@ export function GameMap({
|
||||
|
||||
setRenderMapNodes(loadedMapNodes);
|
||||
setCollisionMapNodes(loadedCollisionNodes);
|
||||
setProxyCollisionMapNodes(loadedProxyCollisionNodes);
|
||||
setTerrainNode(loadedTerrainNode);
|
||||
setRepairMissionAnchors(repairMissionAnchors);
|
||||
setMapLoaded(true);
|
||||
@@ -285,6 +295,7 @@ export function GameMap({
|
||||
buildOctree={buildOctree}
|
||||
mapReady={mapReady}
|
||||
nodes={collisionMapNodes}
|
||||
proxyNodes={proxyCollisionMapNodes}
|
||||
onLoaded={onLoaded}
|
||||
onLoadingStateChange={onLoadingStateChange}
|
||||
onOctreeReady={onOctreeReady}
|
||||
|
||||
+168
-10
@@ -17,9 +17,23 @@ import {
|
||||
normalizeMapScale,
|
||||
useTerrainHeightSampler,
|
||||
} 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 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 { logModelLoadError } from "@/utils/three/modelLoadLogger";
|
||||
|
||||
@@ -39,6 +53,7 @@ interface GameMapCollisionProps {
|
||||
buildOctree?: boolean;
|
||||
mapReady: boolean;
|
||||
nodes: readonly GameMapCollisionNode[];
|
||||
proxyNodes: readonly MapNode[];
|
||||
onLoaded?: (() => void) | undefined;
|
||||
onLoadingStateChange?: SceneLoadingChangeHandler | undefined;
|
||||
onOctreeReady: OctreeReadyHandler;
|
||||
@@ -101,6 +116,7 @@ export function GameMapCollision({
|
||||
buildOctree = true,
|
||||
mapReady,
|
||||
nodes,
|
||||
proxyNodes,
|
||||
onLoaded,
|
||||
onLoadingStateChange,
|
||||
onOctreeReady,
|
||||
@@ -111,8 +127,23 @@ export function GameMapCollision({
|
||||
const [settledCollisionNodeCount, setSettledCollisionNodeCount] = useState(0);
|
||||
const terrainHeight = useTerrainHeightSampler();
|
||||
const collisionNodes = nodes.filter(isCollisionNode);
|
||||
const collisionSourceCount =
|
||||
collisionNodes.length + proxyNodes.length + CHARACTER_IDS.length;
|
||||
const collisionReady =
|
||||
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(() => {
|
||||
if (loadedNotifiedRef.current) return;
|
||||
@@ -144,14 +175,14 @@ export function GameMapCollision({
|
||||
useOctreeGraphNode(
|
||||
groupRef,
|
||||
handleOctreeReady,
|
||||
collisionReady ? collisionNodes.length : 0,
|
||||
buildOctree && collisionReady && collisionNodes.length > 0,
|
||||
collisionRebuildKey,
|
||||
buildOctree && collisionReady && collisionSourceCount > 0,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady) return;
|
||||
|
||||
if (collisionNodes.length === 0) {
|
||||
if (collisionSourceCount === 0) {
|
||||
notifyLoaded();
|
||||
return;
|
||||
}
|
||||
@@ -171,6 +202,7 @@ export function GameMapCollision({
|
||||
}, [
|
||||
buildOctree,
|
||||
collisionNodes.length,
|
||||
collisionSourceCount,
|
||||
collisionReady,
|
||||
mapReady,
|
||||
notifyLoaded,
|
||||
@@ -180,6 +212,18 @@ export function GameMapCollision({
|
||||
return (
|
||||
<group ref={groupRef} visible={false}>
|
||||
{mapReady ? <WorldBoundsCollision /> : null}
|
||||
{mapReady
|
||||
? proxyNodes.map((node, index) => (
|
||||
<MapCollisionBoxProxy
|
||||
key={`proxy-collision-${index}`}
|
||||
node={node}
|
||||
terrainHeight={terrainHeight}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
{mapReady ? (
|
||||
<CharacterCollisionProxies terrainHeight={terrainHeight} />
|
||||
) : null}
|
||||
{mapReady
|
||||
? collisionNodes.map((mapNode, index) => (
|
||||
<CollisionErrorBoundary
|
||||
@@ -253,11 +297,125 @@ function CollisionModelInstance({
|
||||
}, [onLoaded]);
|
||||
|
||||
return (
|
||||
<primitive
|
||||
object={sceneInstance}
|
||||
position={collisionPosition}
|
||||
rotation={rotation}
|
||||
scale={normalizedScale}
|
||||
/>
|
||||
<>
|
||||
<primitive
|
||||
object={sceneInstance}
|
||||
position={collisionPosition}
|
||||
rotation={rotation}
|
||||
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