fix: correct world splat orientation and movement
This commit is contained in:
parent
021f497970
commit
07bf4a293c
@ -6,6 +6,10 @@ import { Canvas, type ThreeEvent, useFrame, useThree } from "@react-three/fiber"
|
||||
import { SparkRenderer, SplatMesh as SparkSplatMesh } from "@sparkjsdev/spark";
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
Box,
|
||||
Crosshair,
|
||||
Globe,
|
||||
@ -29,6 +33,16 @@ type World3DPickResult = {
|
||||
point: [number, number, number];
|
||||
};
|
||||
|
||||
type MovementCommand =
|
||||
| "forward"
|
||||
| "backward"
|
||||
| "left"
|
||||
| "right"
|
||||
| "up"
|
||||
| "down";
|
||||
|
||||
type MovementState = Record<MovementCommand, boolean>;
|
||||
|
||||
type World3DViewerProps = {
|
||||
mode: World3DViewMode;
|
||||
worldUrl?: string | null;
|
||||
@ -58,6 +72,32 @@ const DEFAULT_MESH_SETTINGS: Model3DViewerSettings = {
|
||||
wireframe: false,
|
||||
};
|
||||
|
||||
const EMPTY_MOVEMENT_STATE: MovementState = {
|
||||
forward: false,
|
||||
backward: false,
|
||||
left: false,
|
||||
right: false,
|
||||
up: false,
|
||||
down: false,
|
||||
};
|
||||
|
||||
const KEY_TO_MOVEMENT: Partial<Record<string, MovementCommand>> = {
|
||||
KeyW: "forward",
|
||||
ArrowUp: "forward",
|
||||
KeyS: "backward",
|
||||
ArrowDown: "backward",
|
||||
KeyA: "left",
|
||||
ArrowLeft: "left",
|
||||
KeyD: "right",
|
||||
ArrowRight: "right",
|
||||
KeyQ: "up",
|
||||
KeyR: "up",
|
||||
PageUp: "up",
|
||||
KeyE: "down",
|
||||
KeyF: "down",
|
||||
PageDown: "down",
|
||||
};
|
||||
|
||||
function getSemanticFacts(metadata?: Record<string, unknown> | null) {
|
||||
if (!metadata) return [];
|
||||
|
||||
@ -118,24 +158,115 @@ function WorldEmptyState({
|
||||
);
|
||||
}
|
||||
|
||||
function FirstPersonMovement({ enabled }: { enabled: boolean }) {
|
||||
function createMovementState(): MovementState {
|
||||
return { ...EMPTY_MOVEMENT_STATE };
|
||||
}
|
||||
|
||||
function DirectionPad({
|
||||
onCommandChange,
|
||||
}: {
|
||||
onCommandChange: (command: MovementCommand, active: boolean) => void;
|
||||
}) {
|
||||
const pressHandlers = (command: MovementCommand) => ({
|
||||
onMouseDown: () => onCommandChange(command, true),
|
||||
onMouseUp: () => onCommandChange(command, false),
|
||||
onMouseLeave: () => onCommandChange(command, false),
|
||||
onTouchStart: () => onCommandChange(command, true),
|
||||
onTouchEnd: () => onCommandChange(command, false),
|
||||
onTouchCancel: () => onCommandChange(command, false),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto rounded-2xl border border-white/10 bg-black/55 p-2 shadow-2xl backdrop-blur-sm">
|
||||
<div className="mb-2 text-center text-[10px] text-zinc-400">方向移动</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div />
|
||||
<button
|
||||
className="flex h-10 w-10 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-zinc-100 transition hover:bg-white/10"
|
||||
type="button"
|
||||
{...pressHandlers("forward")}
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="flex h-10 min-w-[3.25rem] items-center justify-center rounded-xl border border-white/10 bg-white/5 px-2 text-xs text-zinc-100 transition hover:bg-white/10"
|
||||
type="button"
|
||||
{...pressHandlers("up")}
|
||||
>
|
||||
上
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="flex h-10 w-10 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-zinc-100 transition hover:bg-white/10"
|
||||
type="button"
|
||||
{...pressHandlers("left")}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex h-10 items-center justify-center rounded-xl border border-dashed border-white/10 bg-white/[0.03] px-2 text-[10px] text-zinc-500">
|
||||
前后左右
|
||||
</div>
|
||||
<button
|
||||
className="flex h-10 w-10 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-zinc-100 transition hover:bg-white/10"
|
||||
type="button"
|
||||
{...pressHandlers("right")}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div />
|
||||
<button
|
||||
className="flex h-10 w-10 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-zinc-100 transition hover:bg-white/10"
|
||||
type="button"
|
||||
{...pressHandlers("backward")}
|
||||
>
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="flex h-10 min-w-[3.25rem] items-center justify-center rounded-xl border border-white/10 bg-white/5 px-2 text-xs text-zinc-100 transition hover:bg-white/10"
|
||||
type="button"
|
||||
{...pressHandlers("down")}
|
||||
>
|
||||
下
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CameraMovementControls({
|
||||
enabled,
|
||||
firstPerson,
|
||||
virtualMovement,
|
||||
orbitControlsRef,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
firstPerson: boolean;
|
||||
virtualMovement: MovementState;
|
||||
orbitControlsRef: React.MutableRefObject<any>;
|
||||
}) {
|
||||
const { camera } = useThree();
|
||||
const pressedKeysRef = useRef<Record<string, boolean>>({});
|
||||
const pressedKeysRef = useRef<MovementState>(createMovementState());
|
||||
const directionRef = useRef(new THREE.Vector3());
|
||||
const rightRef = useRef(new THREE.Vector3());
|
||||
const upRef = useRef(new THREE.Vector3());
|
||||
const moveRef = useRef(new THREE.Vector3());
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
pressedKeysRef.current = {};
|
||||
pressedKeysRef.current = createMovementState();
|
||||
return;
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
pressedKeysRef.current[event.code] = true;
|
||||
const movement = KEY_TO_MOVEMENT[event.code];
|
||||
if (!movement) return;
|
||||
pressedKeysRef.current[movement] = true;
|
||||
};
|
||||
const handleKeyUp = (event: KeyboardEvent) => {
|
||||
pressedKeysRef.current[event.code] = false;
|
||||
const movement = KEY_TO_MOVEMENT[event.code];
|
||||
if (!movement) return;
|
||||
pressedKeysRef.current[movement] = false;
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
@ -151,23 +282,51 @@ function FirstPersonMovement({ enabled }: { enabled: boolean }) {
|
||||
if (!enabled) return;
|
||||
|
||||
const pressed = pressedKeysRef.current;
|
||||
const moveSpeed = pressed.ShiftLeft || pressed.ShiftRight ? 6.5 : 3.4;
|
||||
const hasMovement =
|
||||
Object.values(pressed).some(Boolean) || Object.values(virtualMovement).some(Boolean);
|
||||
if (!hasMovement) return;
|
||||
|
||||
const moveSpeed = 3.8;
|
||||
const distance = moveSpeed * delta;
|
||||
|
||||
camera.getWorldDirection(directionRef.current);
|
||||
directionRef.current.normalize();
|
||||
directionRef.current.setY(0).normalize();
|
||||
rightRef.current.crossVectors(directionRef.current, camera.up).normalize();
|
||||
upRef.current.copy(camera.up).normalize();
|
||||
|
||||
if (pressed.KeyW) camera.position.addScaledVector(directionRef.current, distance);
|
||||
if (pressed.KeyS) camera.position.addScaledVector(directionRef.current, -distance);
|
||||
if (pressed.KeyA) camera.position.addScaledVector(rightRef.current, -distance);
|
||||
if (pressed.KeyD) camera.position.addScaledVector(rightRef.current, distance);
|
||||
if (pressed.KeyQ) camera.position.addScaledVector(upRef.current, distance);
|
||||
if (pressed.KeyE) camera.position.addScaledVector(upRef.current, -distance);
|
||||
moveRef.current.set(0, 0, 0);
|
||||
|
||||
if (pressed.forward || virtualMovement.forward) {
|
||||
moveRef.current.add(directionRef.current);
|
||||
}
|
||||
if (pressed.backward || virtualMovement.backward) {
|
||||
moveRef.current.addScaledVector(directionRef.current, -1);
|
||||
}
|
||||
if (pressed.left || virtualMovement.left) {
|
||||
moveRef.current.addScaledVector(rightRef.current, -1);
|
||||
}
|
||||
if (pressed.right || virtualMovement.right) {
|
||||
moveRef.current.add(rightRef.current);
|
||||
}
|
||||
if (pressed.up || virtualMovement.up) {
|
||||
moveRef.current.add(upRef.current);
|
||||
}
|
||||
if (pressed.down || virtualMovement.down) {
|
||||
moveRef.current.addScaledVector(upRef.current, -1);
|
||||
}
|
||||
|
||||
if (moveRef.current.lengthSq() === 0) return;
|
||||
|
||||
moveRef.current.normalize().multiplyScalar(distance);
|
||||
camera.position.add(moveRef.current);
|
||||
|
||||
if (!firstPerson && orbitControlsRef.current?.target) {
|
||||
orbitControlsRef.current.target.add(moveRef.current);
|
||||
orbitControlsRef.current.update();
|
||||
}
|
||||
});
|
||||
|
||||
if (!enabled) return null;
|
||||
if (!enabled || !firstPerson) return null;
|
||||
return <PointerLockControls makeDefault />;
|
||||
}
|
||||
|
||||
@ -218,6 +377,9 @@ function WorldSplatScene({
|
||||
onLoadStateChange?: (state: "ready" | "error") => void;
|
||||
}) {
|
||||
const renderer = useThree((state) => state.gl);
|
||||
const orbitControlsRef = useRef<any>(null);
|
||||
const [virtualMovement, setVirtualMovement] =
|
||||
useState<MovementState>(createMovementState);
|
||||
const semanticFacts = useMemo(
|
||||
() => getSemanticFacts(semanticsMetadata),
|
||||
[semanticsMetadata]
|
||||
@ -243,6 +405,7 @@ function WorldSplatScene({
|
||||
|
||||
useEffect(() => {
|
||||
splatMesh.frustumCulled = false;
|
||||
splatMesh.rotation.x = Math.PI;
|
||||
let cancelled = false;
|
||||
splatMesh.initialized
|
||||
.then(() => {
|
||||
@ -263,9 +426,19 @@ function WorldSplatScene({
|
||||
|
||||
const groundPlaneOffset =
|
||||
typeof semanticsMetadata?.ground_plane_offset === "number"
|
||||
? semanticsMetadata.ground_plane_offset
|
||||
? -semanticsMetadata.ground_plane_offset
|
||||
: null;
|
||||
|
||||
const handleDirectionChange = (command: MovementCommand, active: boolean) => {
|
||||
setVirtualMovement((prev) => {
|
||||
if (prev[command] === active) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[command]: active,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: ThreeEvent<PointerEvent>) => {
|
||||
if (interactionMode !== "hotspot") return;
|
||||
event.stopPropagation();
|
||||
@ -298,6 +471,12 @@ function WorldSplatScene({
|
||||
onSelect={onHotspotSelect}
|
||||
/>
|
||||
))}
|
||||
<CameraMovementControls
|
||||
enabled={interactionMode === "navigate"}
|
||||
firstPerson={firstPerson}
|
||||
orbitControlsRef={orbitControlsRef}
|
||||
virtualMovement={virtualMovement}
|
||||
/>
|
||||
{!firstPerson ? (
|
||||
<OrbitControls
|
||||
autoRotate={false}
|
||||
@ -307,9 +486,15 @@ function WorldSplatScene({
|
||||
makeDefault
|
||||
maxDistance={16}
|
||||
minDistance={0.5}
|
||||
ref={orbitControlsRef}
|
||||
/>
|
||||
) : (
|
||||
<FirstPersonMovement enabled />
|
||||
<OrbitControls
|
||||
enablePan={false}
|
||||
enableRotate={false}
|
||||
enableZoom={false}
|
||||
ref={orbitControlsRef}
|
||||
/>
|
||||
)}
|
||||
<Html className="pointer-events-none" fullscreen>
|
||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
||||
@ -336,13 +521,20 @@ function WorldSplatScene({
|
||||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="rounded-2xl border border-white/10 bg-black/55 px-3 py-2 text-[11px] text-zinc-300 backdrop-blur-sm">
|
||||
{firstPerson ? "WASD 移动 · 鼠标观察 · Shift 加速 · Esc 退出" : "拖拽旋转 · 滚轮缩放 · 原生 SPZ 渲染"}
|
||||
{firstPerson
|
||||
? "方向键 / WASD 前后左右 · Q/E 或 PgUp/PgDn 上下 · 鼠标观察 · Esc 退出"
|
||||
: "拖拽旋转 · 滚轮缩放 · 方向键 / 按钮可做六向移动"}
|
||||
</div>
|
||||
<div className="flex items-end gap-3">
|
||||
{interactionMode === "navigate" ? (
|
||||
<DirectionPad onCommandChange={handleDirectionChange} />
|
||||
) : null}
|
||||
{(firstPerson || interactionMode === "hotspot") && (
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-white/10 bg-black/55 text-zinc-100 shadow-xl backdrop-blur-sm">
|
||||
<Crosshair className="h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(firstPerson || interactionMode === "hotspot") && (
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-white/10 bg-black/55 text-zinc-100 shadow-xl backdrop-blur-sm">
|
||||
<Crosshair className="h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user