fix: center world splat camera controls
This commit is contained in:
parent
57b7292269
commit
2d76ec1629
@ -3861,11 +3861,11 @@ export default function CanvasModel3DPage() {
|
||||
{worldViewportMode === "splat" ? (
|
||||
<>
|
||||
<div className="font-medium text-zinc-200">
|
||||
{worldFirstPerson ? "WASD / 方向键可六向移动" : "鼠标拖拽会围绕场景原点旋转"}
|
||||
{worldFirstPerson ? "WASD / 方向键可六向移动" : "鼠标拖拽会围绕场景主体中心旋转"}
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
{worldFirstPerson
|
||||
? "点击画面后可锁定鼠标视角,WASD 或方向键移动,Q/R 上升,E/F 下降,Esc 退出鼠标锁定。"
|
||||
? "按住左键拖动画面转向,WASD 或方向键移动,Q/R 上升,E/F 下降。"
|
||||
: "左键拖拽旋转,滚轮缩放;开启“漫游”后可用 WASD / 方向键移动。需要切换模型时,直接点击右侧资产卡片。"}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -281,6 +281,73 @@ function buildSplatParticleCloudData(
|
||||
};
|
||||
}
|
||||
|
||||
function getPercentileValue(values: number[], percentile: number) {
|
||||
if (!values.length) return 0;
|
||||
const index = THREE.MathUtils.clamp(
|
||||
Math.floor((values.length - 1) * percentile),
|
||||
0,
|
||||
values.length - 1
|
||||
);
|
||||
return values[index] ?? 0;
|
||||
}
|
||||
|
||||
function getSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
||||
splatMesh.updateMatrixWorld(true);
|
||||
return splatMesh
|
||||
.getBoundingBox(true)
|
||||
.clone()
|
||||
.applyMatrix4(splatMesh.matrixWorld);
|
||||
}
|
||||
|
||||
function getRobustSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
||||
const packedSplats = splatMesh.packedSplats;
|
||||
const totalSplats = Math.max(0, packedSplats?.numSplats || 0);
|
||||
if (totalSplats < 32) {
|
||||
return getSplatWorldBounds(splatMesh);
|
||||
}
|
||||
|
||||
splatMesh.updateMatrixWorld(true);
|
||||
const targetCount = Math.min(totalSplats, 3200);
|
||||
const sampleStep = Math.max(Math.floor(totalSplats / targetCount), 1);
|
||||
const point = new THREE.Vector3();
|
||||
const xs: number[] = [];
|
||||
const ys: number[] = [];
|
||||
const zs: number[] = [];
|
||||
|
||||
for (let index = 0; index < totalSplats; index += sampleStep) {
|
||||
const splat = packedSplats.getSplat(index);
|
||||
if (splat.opacity < 0.025) continue;
|
||||
|
||||
point.copy(splat.center).applyMatrix4(splatMesh.matrixWorld);
|
||||
xs.push(point.x);
|
||||
ys.push(point.y);
|
||||
zs.push(point.z);
|
||||
}
|
||||
|
||||
if (xs.length < 32) {
|
||||
return getSplatWorldBounds(splatMesh);
|
||||
}
|
||||
|
||||
xs.sort((a, b) => a - b);
|
||||
ys.sort((a, b) => a - b);
|
||||
zs.sort((a, b) => a - b);
|
||||
|
||||
const robustBounds = new THREE.Box3(
|
||||
new THREE.Vector3(
|
||||
getPercentileValue(xs, 0.02),
|
||||
getPercentileValue(ys, 0.02),
|
||||
getPercentileValue(zs, 0.02)
|
||||
),
|
||||
new THREE.Vector3(
|
||||
getPercentileValue(xs, 0.98),
|
||||
getPercentileValue(ys, 0.98),
|
||||
getPercentileValue(zs, 0.98)
|
||||
)
|
||||
);
|
||||
|
||||
return robustBounds.isEmpty() ? getSplatWorldBounds(splatMesh) : robustBounds;
|
||||
}
|
||||
|
||||
function positionCameraInsideWorld({
|
||||
camera,
|
||||
controls,
|
||||
@ -296,8 +363,7 @@ function positionCameraInsideWorld({
|
||||
}): WorldCameraPose | null {
|
||||
if (!(camera instanceof THREE.PerspectiveCamera)) return null;
|
||||
|
||||
splatMesh.updateMatrixWorld(true);
|
||||
const worldBounds = splatMesh.getBoundingBox(true).clone().applyMatrix4(splatMesh.matrixWorld);
|
||||
const worldBounds = getRobustSplatWorldBounds(splatMesh);
|
||||
if (worldBounds.isEmpty()) return null;
|
||||
|
||||
const center = worldBounds.getCenter(new THREE.Vector3());
|
||||
@ -388,9 +454,24 @@ function positionCameraInsideWorld({
|
||||
Math.max(size.z * 0.08, 0.18),
|
||||
center.z
|
||||
);
|
||||
const orbitTargetPosition = anchor.clone();
|
||||
const orbitTargetPosition = center.clone();
|
||||
const orbitRadius = Math.max(size.length() * 0.5, majorSpan * 0.55, 1.2);
|
||||
const orbitDistance = THREE.MathUtils.clamp(orbitRadius * 1.85, 2.2, 14);
|
||||
const targetPosition = firstPerson ? nearTargetPosition : orbitTargetPosition;
|
||||
|
||||
if (!firstPerson) {
|
||||
cameraPosition
|
||||
.copy(targetPosition)
|
||||
.addScaledVector(horizontalAxis, orbitDistance)
|
||||
.add(
|
||||
new THREE.Vector3(
|
||||
0,
|
||||
THREE.MathUtils.clamp(size.y * 0.08, 0.18, 1.2),
|
||||
0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
camera.position.copy(cameraPosition);
|
||||
camera.lookAt(targetPosition);
|
||||
camera.updateProjectionMatrix();
|
||||
@ -746,6 +827,97 @@ function CameraMovementControls({
|
||||
return null;
|
||||
}
|
||||
|
||||
function FirstPersonLookControls({
|
||||
enabled,
|
||||
resetToken,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
resetToken?: number;
|
||||
}) {
|
||||
const { camera, gl } = useThree();
|
||||
const yawRef = useRef(0);
|
||||
const pitchRef = useRef(0);
|
||||
const draggingPointerIdRef = useRef<number | null>(null);
|
||||
const directionRef = useRef(new THREE.Vector3());
|
||||
|
||||
const syncAnglesFromCamera = React.useCallback(() => {
|
||||
camera.getWorldDirection(directionRef.current).normalize();
|
||||
yawRef.current = Math.atan2(directionRef.current.x, directionRef.current.z);
|
||||
pitchRef.current = Math.asin(
|
||||
THREE.MathUtils.clamp(directionRef.current.y, -0.98, 0.98)
|
||||
);
|
||||
}, [camera]);
|
||||
|
||||
const applyLookDirection = React.useCallback(() => {
|
||||
const pitch = THREE.MathUtils.clamp(
|
||||
pitchRef.current,
|
||||
-Math.PI / 2 + 0.08,
|
||||
Math.PI / 2 - 0.08
|
||||
);
|
||||
pitchRef.current = pitch;
|
||||
|
||||
const cosPitch = Math.cos(pitch);
|
||||
directionRef.current.set(
|
||||
Math.sin(yawRef.current) * cosPitch,
|
||||
Math.sin(pitch),
|
||||
Math.cos(yawRef.current) * cosPitch
|
||||
);
|
||||
camera.lookAt(camera.position.clone().add(directionRef.current));
|
||||
}, [camera]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
syncAnglesFromCamera();
|
||||
applyLookDirection();
|
||||
}, [applyLookDirection, enabled, resetToken, syncAnglesFromCamera]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const canvas = gl.domElement;
|
||||
const sensitivity = 0.0032;
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (event.button !== 0) return;
|
||||
if (isEditableKeyboardTarget(event.target)) return;
|
||||
syncAnglesFromCamera();
|
||||
draggingPointerIdRef.current = event.pointerId;
|
||||
canvas.setPointerCapture?.(event.pointerId);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: PointerEvent) => {
|
||||
if (draggingPointerIdRef.current !== event.pointerId) return;
|
||||
yawRef.current -= event.movementX * sensitivity;
|
||||
pitchRef.current -= event.movementY * sensitivity;
|
||||
applyLookDirection();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const stopDragging = (event: PointerEvent) => {
|
||||
if (draggingPointerIdRef.current !== event.pointerId) return;
|
||||
draggingPointerIdRef.current = null;
|
||||
canvas.releasePointerCapture?.(event.pointerId);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
canvas.addEventListener("pointerdown", handlePointerDown);
|
||||
canvas.addEventListener("pointermove", handlePointerMove);
|
||||
canvas.addEventListener("pointerup", stopDragging);
|
||||
canvas.addEventListener("pointercancel", stopDragging);
|
||||
|
||||
return () => {
|
||||
draggingPointerIdRef.current = null;
|
||||
canvas.removeEventListener("pointerdown", handlePointerDown);
|
||||
canvas.removeEventListener("pointermove", handlePointerMove);
|
||||
canvas.removeEventListener("pointerup", stopDragging);
|
||||
canvas.removeEventListener("pointercancel", stopDragging);
|
||||
};
|
||||
}, [applyLookDirection, enabled, gl.domElement, syncAnglesFromCamera]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function HotspotBillboard({
|
||||
hotspot,
|
||||
onSelect,
|
||||
@ -1049,6 +1221,10 @@ function WorldSplatScene({
|
||||
firstPerson={firstPerson}
|
||||
orbitControlsRef={orbitControlsRef}
|
||||
/>
|
||||
<FirstPersonLookControls
|
||||
enabled={interactionMode === "navigate" && firstPerson}
|
||||
resetToken={resetViewToken}
|
||||
/>
|
||||
{!firstPerson ? (
|
||||
<OrbitControls
|
||||
autoRotate={false}
|
||||
@ -1068,23 +1244,7 @@ function WorldSplatScene({
|
||||
minDistance={0.5}
|
||||
ref={handleOrbitControlsRef}
|
||||
/>
|
||||
) : (
|
||||
<OrbitControls
|
||||
dampingFactor={0.08}
|
||||
enableDamping
|
||||
enablePan={false}
|
||||
enableRotate
|
||||
enableZoom={false}
|
||||
key="world-splat-first-person"
|
||||
makeDefault
|
||||
mouseButtons={{
|
||||
LEFT: THREE.MOUSE.ROTATE,
|
||||
MIDDLE: THREE.MOUSE.ROTATE,
|
||||
RIGHT: THREE.MOUSE.ROTATE,
|
||||
}}
|
||||
ref={handleOrbitControlsRef}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<Html className="pointer-events-none" fullscreen>
|
||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user