上传文件至「/」
This commit is contained in:
+212
@@ -0,0 +1,212 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>3D 高斯专业全景漫游</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 规范的本地 Import Map,彻底切断对外部 CDN 的依赖 -->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "./libs/three.module.js",
|
||||
"@mkkellogg/gaussian-splats-3d": "./libs/gaussian-splats-3d.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="module">
|
||||
import * as THREE from 'three';
|
||||
import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
|
||||
import ProgressText from './libs/ProgressText.js';
|
||||
import DragFPSControls from './libs/DragFPSControls.js'; // 1. 引入我们的定制控制器
|
||||
import SettingsPanel from './libs/SettingsPanel.js'; // <--- 新增引入这一行
|
||||
|
||||
// --- 核心调度逻辑:精准识别移动端触屏设备 ---
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || (navigator.maxTouchPoints > 0);
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const modelName = params.get('m');
|
||||
if (!modelName) {
|
||||
document.body.innerHTML = '<h2 style="color:white; text-align:center; font-family:sans-serif; margin-top:20%;">缺少模型参数</h2>';
|
||||
throw new Error("缺少模型参数");
|
||||
}
|
||||
// ==========================================
|
||||
// 👑 架构升级:真正的工业级格式嗅探 (网络探针)
|
||||
// 彻底告别报错,全自动精准识别服务器文件!
|
||||
// ==========================================
|
||||
let modelUrl = '';
|
||||
|
||||
if (modelName.toLowerCase().endsWith('.ply') || modelName.toLowerCase().endsWith('.ksplat')) {
|
||||
// 场景 A:如果你在网址明确写了后缀,直接使用,不废话。
|
||||
modelUrl = '/models/' + modelName;
|
||||
} else {
|
||||
// 场景 B:你只传了名字,发射探针去服务器查岗!
|
||||
console.log(`🔍 正在自动嗅探 [ ${modelName} ] 的真实文件格式...`);
|
||||
try {
|
||||
// 发送轻量级 HEAD 请求,问服务器:这个名字的 .ksplat 存在吗?
|
||||
const probeResponse = await fetch('/models/' + modelName + '.ksplat', { method: 'HEAD' });
|
||||
|
||||
if (probeResponse.ok) {
|
||||
modelUrl = '/models/' + modelName + '.ksplat';
|
||||
console.log('✅ 嗅探成功:锁定为 .ksplat 格式');
|
||||
} else {
|
||||
modelUrl = '/models/' + modelName + '.ply';
|
||||
console.log('✅ 嗅探完成:未发现 ksplat,回退使用 .ply 格式');
|
||||
}
|
||||
} catch (error) {
|
||||
// 万一网络波动,兜底给最原始的 ply
|
||||
modelUrl = '/models/' + modelName + '.ply';
|
||||
}
|
||||
}
|
||||
|
||||
// 你的完美坐标系配置
|
||||
const cameraEyeHeight = 1.6;
|
||||
// ==========================================
|
||||
// 【核心修改】:工业级优雅降级 —— 能力检测
|
||||
// 绝不猜测环境,直接测试系统底层是否支持共享内存 API
|
||||
// ==========================================
|
||||
const hasSharedMemory = typeof SharedArrayBuffer !== 'undefined';
|
||||
|
||||
// ==========================================
|
||||
// 👑 架构升级:工业级设备嗅探器
|
||||
// 完美区分:普通手机 vs 电脑 vs 真正的 VR 头显
|
||||
// ==========================================
|
||||
const isVRHeadset = /Pico|Quest|Oculus|VR/i.test(navigator.userAgent);
|
||||
const isPureMobile = isMobile && !isVRHeadset; // 只有纯手机才是 true
|
||||
|
||||
const viewer = new GaussianSplats3D.Viewer({
|
||||
'cameraUp': [0, 1, 0], // 保持你截图里的设置,维持正确的重力方向
|
||||
'initialCameraPosition': [0, cameraEyeHeight, 3],
|
||||
'initialCameraLookAt': [0, cameraEyeHeight, -2],
|
||||
|
||||
'sharedMemoryForWorkers': hasSharedMemory,
|
||||
|
||||
// 【核心修复1】:只有纯手机,才开启手指触控。PC 和 VR 坚决不用。
|
||||
'useBuiltInControls': isPureMobile,
|
||||
|
||||
// 【核心修复2】:如果是纯手机,彻底阉割 VR 模块,防止陀螺仪抢夺手指触控权!
|
||||
'webXRMode': isPureMobile ? undefined : GaussianSplats3D.WebXRMode.VR
|
||||
});
|
||||
|
||||
window.history.replaceState(null, '', '?m=' + modelName);
|
||||
|
||||
// 3. 如果是 PC 端,挂载并启动我们自己的控制器
|
||||
let customControls = null;
|
||||
if (!isMobile) {
|
||||
customControls = new DragFPSControls(viewer.camera, document.body);
|
||||
customControls.connect();
|
||||
|
||||
// 2. 实例化 UI 设置面板,并建立单向数据流通信
|
||||
new SettingsPanel(customControls.moveSpeed, (newSpeed) => {
|
||||
// 当 UI 面板发生拖拽时,无缝将新参数注入到底层控制器中
|
||||
customControls.setMoveSpeed(newSpeed);
|
||||
});
|
||||
}
|
||||
|
||||
const progressUI = new ProgressText();
|
||||
|
||||
viewer.addSplatScene(modelUrl, {
|
||||
'progressiveLoad': true,
|
||||
// 【新增】:将模型沿 Z 轴翻转 180 度 (四元数写法)
|
||||
// 如果你发现翻转后上下对了,但是“左右镜像”反了,请把这行改成 'rotation': [1, 0, 0, 0] (沿 X 轴翻转)
|
||||
'rotation': [0, 0, 1, 0],
|
||||
// 【新增】:模型翻转后被甩到了空中,我们通过位置偏移把它按回地面!
|
||||
// 数组代表 [X左右偏移, Y上下推拉, Z前后移动]
|
||||
'position': [0.5, 0, 0],
|
||||
'onProgress': (percent) => {
|
||||
progressUI.update(percent);
|
||||
if (percent >= 99.8) {
|
||||
progressUI.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
viewer.start();
|
||||
|
||||
// 准备一个空的 VR 座舱静默待命,平时绝对不干扰原系统
|
||||
const xrRig = new THREE.Group();
|
||||
xrRig.name = "PlayerXRRig";
|
||||
|
||||
// ==========================================
|
||||
// 👇 XR 生命周期:动态挂载与拆卸 👇
|
||||
// ==========================================
|
||||
if (viewer.renderer && viewer.renderer.xr) {
|
||||
|
||||
// 监听 1:进入 VR 沉浸模式
|
||||
viewer.renderer.xr.addEventListener('sessionstart', () => {
|
||||
console.log("🥽 进入 VR:动态挂载物理座舱,接管相机!");
|
||||
|
||||
let targetScene = null;
|
||||
for (const key in viewer) {
|
||||
if (viewer[key] instanceof THREE.Scene) {
|
||||
targetScene = viewer[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetScene) {
|
||||
targetScene.add(xrRig);
|
||||
xrRig.add(viewer.camera);
|
||||
|
||||
// 这里的偏移量保持你之前调试好的数值 (比如 0, -3.2, 0)
|
||||
xrRig.position.set(0, 0, 1);
|
||||
}
|
||||
|
||||
if (customControls) customControls.enabled = false;
|
||||
});
|
||||
|
||||
// 监听 2:退出 VR 模式,回到网页
|
||||
viewer.renderer.xr.addEventListener('sessionend', () => {
|
||||
console.log("🖥️ 退出 VR:拆卸座舱,相机完璧归赵。");
|
||||
|
||||
let targetScene = null;
|
||||
for (const key in viewer) {
|
||||
if (viewer[key] instanceof THREE.Scene) {
|
||||
targetScene = viewer[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetScene) {
|
||||
targetScene.add(viewer.camera); // 相机还给原生场景
|
||||
}
|
||||
|
||||
if (customControls) {
|
||||
customControls.enabled = true;
|
||||
// 重置水平滚转角,防止画面歪斜
|
||||
const targetQuaternion = viewer.camera.quaternion.clone();
|
||||
targetQuaternion.setFromAxisAngle(viewer.camera.up, 0);
|
||||
customControls.targetQuaternion.copy(targetQuaternion);
|
||||
}
|
||||
});
|
||||
}
|
||||
// ==========================================
|
||||
// 👆 XR 生命周期结束 👆
|
||||
// ==========================================
|
||||
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("加载失败:", e);
|
||||
document.body.innerHTML = `<h2 style="color:#ff6b6b; text-align:center; font-family:sans-serif; margin-top:20%;">模型加载失败</h2>`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user