简介这份资源是一份面向VR/AR初学者与独立开发者的开发脚本案例文档围绕“创建虚拟展览体验”这一典型项目给出从项目初始化、场景搭建、交互功能开发到UI音效设计、发布部署及后期维护的完整流程框架帮助读者快速理解沉浸式展览应用的构建思路。资源包内共1个doc文件约21KB内容以分步骤说明与要点提示为主涵盖开发环境配置、3D模型导入与优化、头部追踪与手势识别等交互逻辑、性能优化与兼容性测试等关键环节并附有脚本使用说明与扩展工具建议。目前已有799人学习下载适合希望系统梳理VR/AR项目开发脉络、对照搭建虚拟展览原型的读者参考也可作为课程设计或小型项目的流程模板按需扩展修改。1. 从一场线上展厅说起VR/AR 虚拟展览到底在做什么去年帮一个做工业设备的朋友搭线上展厅对方开口就要“元宇宙”预算却只够两周工期。最后落地的方案很朴素一个 WebXR 页面桌面端用鼠标拖拽看 3D 设备模型手机扫码进 AR 模式把设备“放”在真实车间地面上头显用户戴上 Pico 4 直接进沉浸式漫游。同一套资源三种入口这才是虚拟展览体验的真实形态——不是炫技而是让不同设备的观众都能看到同一批展品。VR/AR 开发脚本在这里的角色是把“场景搭建、交互逻辑、多端适配”这三件事用代码固化下来而不是靠美术在编辑器里手搓。虚拟展览的核心诉求无非几个展品模型能加载、观众能移动和交互、多人在线时状态同步、不同终端降级可用。适合谁读有前端或 Unity 基础、想接展览类项目的开发者以及需要快速验证方案的产品同学。下面按“先立概念、再跑最小场景、然后做交互、最后调优”的顺序讲清楚。2. VR/AR 虚拟展览的技术选型与最小可跑场景2.1 WebXR、Unity、Unreal 三条路怎么选做虚拟展览第一道坎是选引擎。三条主流路线差异很大选错了后面全是返工。方案上手成本多端覆盖适合场景主要坑WebXR Three.js低浏览器全覆盖头显需支持 WebXR轻量展厅、快速上线复杂模型性能吃紧Unity XR Interaction Toolkit中头显、手机 AR、PC 通吃中大型展览、需精细交互打包体积大WebGL 导出受限Unreal OpenXR高头显为主画质最好高保真展厅、影视级移动端几乎跑不动我的判断标准很简单如果展览要在微信里传播、观众随手点开就能看选 WebXR如果要做成 App 或头显内应用、交互复杂选 Unity。热搜里常出现的“pico4 vr到mr切换”本质是同一台设备在 VR 全沉浸和 MR 透视之间的模式切换Unity 的 XR 插件对这类切换支持更成熟WebXR 目前还得靠浏览器能力探测。2.2 用 Three.js 跑通第一个虚拟展厅先给一个能直接跑的最小场景加载一个展品模型加轨道控制器让观众能转着看。这是所有虚拟展览的地基。import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; import { GLTFLoader } from three/addons/loaders/GLTFLoader.js; const scene new THREE.Scene(); scene.background new THREE.Color(0x1a1a2e); // 展厅深色背景突出展品 const camera new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 1000); camera.position.set(0, 1.6, 4); // 1.6 米是成人平均视高VR 里很关键 const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(innerWidth, innerHeight); renderer.xr.enabled true; // 打开 WebXR头显才能进 document.body.appendChild(renderer.domElement); // 环境光 方向光展品没有光就是一团黑 scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1.2)); const dirLight new THREE.DirectionalLight(0xffffff, 1.5); dirLight.position.set(3, 5, 2); scene.add(dirLight); const controls new OrbitControls(camera, renderer.domElement); controls.target.set(0, 1, 0); new GLTFLoader().load(/models/exhibit.glb, (gltf) { const model gltf.scene; model.scale.set(1, 1, 1); scene.add(model); }, undefined, (err) console.error(模型加载失败, err)); renderer.setAnimationLoop(() { controls.update(); renderer.render(scene, camera); });逻辑说明renderer.xr.enabled true是 WebXR 的开关不开头显进不去。相机高度设 1.6 米是因为 VR 里观众是“站”在场景中的视高不对会晕。setAnimationLoop替代requestAnimationFrame因为 XR 会话必须用它才能正确同步帧。参数说明PerspectiveCamera的 fov 设 60 比较稳太大边缘畸变、太小有隧道感HemisphereLight强度 1.2 配合方向光 1.5是室内展厅比较通用的打光比例具体按模型材质微调。模型路径用.glb而非.gltf因为 glb 把贴图打包进单文件加载少一次请求。注意本地跑必须起 HTTP 服务直接双击 HTML 打开会因为跨域拿不到模型。用npx serve或python -m http.server都行。2.3 模型格式与性能预算展品模型是虚拟展览最重的资产。常见做法是美术在 Blender 里导出 glb面数控制在单件 5 万面以内贴图用 2K 而非 4K。一个展厅放 10 件展品总面数别超过 50 万否则中端手机直接掉帧。Draco 压缩能把模型体积压掉六七成但要在加载器里配解码器import { DRACOLoader } from three/addons/loaders/DRACOLoader.js; const draco new DRACOLoader(); draco.setDecoderPath(/draco/); // 解码器文件放本地别依赖外链 const loader new GLTFLoader(); loader.setDRACOLoader(draco);这段的意义在于压缩后的模型加载快但解码要额外 CPU低端设备上反而可能卡在解码那一下。我的经验是模型超过 20MB 才上 Draco小模型没必要。3. 虚拟展览的交互脚本拾取、漫游与展品信息3.1 射线拾取实现点击展品弹信息观众点一件展品弹出它的介绍这是展览最基本的交互。核心是射线拾取Raycaster。const raycaster new THREE.Raycaster(); const pointer new THREE.Vector2(); renderer.domElement.addEventListener(pointerdown, (e) { // 屏幕坐标转归一化设备坐标范围 -1 到 1 pointer.x (e.clientX / innerWidth) * 2 - 1; pointer.y -(e.clientY / innerHeight) * 2 1; raycaster.setFromCamera(pointer, camera); const hits raycaster.intersectObjects(scene.children, true); if (hits.length 0) { const obj hits[0].object; showInfoPanel(obj.userData.info || 暂无介绍); } }); function showInfoPanel(text) { document.getElementById(info).textContent text; document.getElementById(info).style.display block; }逻辑说明intersectObjects第二个参数true表示递归检测子对象因为 glb 模型内部是层级结构不递归点不到。userData.info是给模型挂自定义数据的标准位置美术导出时可以在 Blender 里写也可以加载后代码注入。参数说明pointerdown比click更适合 XR因为头显里没有 click 事件手柄触发的是 pointer 系列。射线检测对复杂模型有性能开销展品多时给每个模型加包围盒Box3先粗筛能省不少。3.2 第一人称漫游与传送移动展厅大了观众得能走动。桌面端用 WASD头显里用手柄摇杆或传送。传送是 VR 里最不容易晕的移动方式实现思路是射线打到地面把相机瞬移过去。const moveSpeed 0.05; const keys {}; addEventListener(keydown, (e) keys[e.code] true); addEventListener(keyup, (e) keys[e.code] false); function updateMove() { const dir new THREE.Vector3(); camera.getWorldDirection(dir); dir.y 0; dir.normalize(); // 只保留水平方向防止上下飘 const right new THREE.Vector3().crossVectors(dir, camera.up).normalize(); if (keys[KeyW]) camera.position.addScaledVector(dir, moveSpeed); if (keys[KeyS]) camera.position.addScaledVector(dir, -moveSpeed); if (keys[KeyA]) camera.position.addScaledVector(right, -moveSpeed); if (keys[KeyD]) camera.position.addScaledVector(right, moveSpeed); }逻辑说明dir.y 0是关键不置零的话观众低头看地面再前进会往地里钻。crossVectors算出右方向保证 A/D 是横移而不是斜着走。参数说明moveSpeed每帧 0.05 米60 帧下约 3 米/秒接近快走速度。VR 里这个值要调小到 0.02 左右否则晕动症明显。传送移动则要限制最大距离一般 5 米一跳太远会失去空间感。3.3 展品信息面板与多语言展览常有中英双语需求。把文案抽成 JSON按语言键取值切换时重渲染面板即可。const i18n { zh: { title: 工业机械臂, desc: 六轴协作机械臂负载 5kg }, en: { title: Industrial Arm, desc: 6-axis collaborative arm, 5kg payload } }; let lang zh; function renderInfo(key) { const data i18n[lang]; document.getElementById(info).innerHTML h3${data.title}/h3p${data.desc}/p; }逻辑说明文案和模型分离改文案不用重新导出模型。lang变量切换后调renderInfo重绘。热搜里提到的“ar pl sungtil gb字体”这类字体问题在展览里表现为中文在 3D 空间渲染时缺字解决办法是把字体转成 MSDF 位图字体用TextGeometry或 troika-three-text 渲染别直接用系统字体。4. 多端适配从手机 AR 到 Pico 4 头显的降级策略4.1 能力探测决定进哪个模式同一套展览资源要判断当前设备能跑什么。WebXR 提供了isSessionSupported来探测。async function detectMode() { if (navigator.xr) { const vrOk await navigator.xr.isSessionSupported(immersive-vr); const arOk await navigator.xr.isSessionSupported(immersive-ar); if (vrOk) return vr; if (arOk) return ar; } return desktop; // 兜底桌面或普通手机浏览器 }逻辑说明先查navigator.xr是否存在再分别探测 VR 和 AR 会话。返回结果决定 UI 显示哪个入口按钮。桌面模式就是前面讲的轨道控制器AR 模式要把相机换成设备后置摄像头背景。参数说明immersive-vr对应全沉浸头显immersive-ar对应手机 AR 或 MR 透视。Pico 4 这类设备在 VR 和 MR 之间切换时探测结果会变所以模式判断要放在进入会话前而不是页面加载时定死。4.2 手机 AR 模式的最小接入手机 AR 用immersive-ar会话把展品锚定在真实平面上。const overlay document.getElementById(ar-overlay); document.getElementById(btn-ar).addEventListener(click, async () { const session await navigator.xr.requestSession(immersive-ar, { requiredFeatures: [hit-test], // 命中测试用来找平面 optionalFeatures: [dom-overlay], domOverlay: { root: overlay } }); renderer.xr.setSession(session); });逻辑说明hit-test是 AR 里把虚拟物体放到真实桌面的前提没有它展品只能飘在空中。dom-overlay让 HTML 面板能盖在 AR 画面上否则信息面板看不见。参数说明requiredFeatures里放必须的能力缺了会话直接失败optionalFeatures放锦上添花的能力缺了也能进。这个区分很重要把 hit-test 放 optional 会导致部分设备进去后放不下展品。4.3 头显端的性能与舒适度调优头显里帧率低于 72 就会晕。几个必调项像素比限制在 1.5 以内、关闭抗锯齿改用后处理、阴影只给主展品开。renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5)); renderer.shadowMap.enabled true; renderer.shadowMap.type THREE.PCFSoftShadowMap; // 只给重点展品开阴影投射 mainExhibit.traverse((o) { if (o.isMesh) o.castShadow true; });逻辑说明setPixelRatio限制上限是因为头显分辨率高不限制会渲染两倍像素直接掉帧。阴影很吃性能全场景开阴影基本必卡只给主展品开是性价比最高的做法。参数说明PCFSoftShadowMap比基础阴影柔和但更贵中端设备可以换BasicShadowMap。热搜里“mirror vr”这类镜像需求本质是把头显画面投到屏幕上供旁观者看WebXR 里可以用renderer.xr.getCamera()拿头显相机再渲染一路到 canvas。5. 展览上线前的验证与几个提效技巧上线前必须做设备矩阵验证别只在开发机上跑通就发。下面这张表是我常用的检查清单检查项桌面 Chrome手机 SafariPico 4 浏览器模型加载通过通过通过帧率603072交互拾取鼠标触摸手柄中文渲染正常正常需位图字体进入 XR不适用AR 可进VR/MR 可进验证脚本可以写个简单的帧率探针跑在页面上实时看let frames 0, last performance.now(); function fpsProbe() { frames; const now performance.now(); if (now - last 1000) { console.log(FPS: ${frames}); if (frames 50) console.warn(帧率偏低检查模型面数或阴影); frames 0; last now; } requestAnimationFrame(fpsProbe); } fpsProbe();逻辑说明每秒统计一次帧数低于 50 就告警。这个探针在真机上比在开发机上准得多因为开发机性能往往过剩。参数说明阈值 50 是经验值头显端要提到 70。探针本身开销极小可以常驻。几个提效技巧展品模型用 LOD多级细节远处用低模贴图统一转 KTX2 压缩格式显存占用能降一半展厅灯光用烘焙贴图代替实时光照静态场景性能提升最明显。热搜里“如何不用 vr 设备看 vr 视频”这类需求对应到展览就是桌面降级模式用轨道控制器模拟漫游体验打折但覆盖人群翻几倍。最后把展品数据、文案、模型路径全部抽成一份 JSON 配置换展时只改配置不动代码这是接展览项目最省事的做法。本文还有配套的精品资源点击获取
