
简介这份文档面向已具备一定引擎基础、希望切入VR/AR展览场景的开发者以Unity或Unreal Engine为主线给出从项目初始化、场景搭建到交互功能、UI音效、发布部署与后期维护的完整流程框架。内容涵盖开发环境与SDK配置、分辨率与帧率设定、3D模型导入及场景灯光材质调整以及头部追踪、手势识别等交互逻辑的编写与真机调试并延伸到3D建模、纹理编辑、音频处理等配套工具和延迟加载、LOD等性能优化思路。资源包共1个doc文件约21KB以流程说明与要点清单为主便于按模块查阅和二次改写。已有807人浏览学习。读者可据此快速理清虚拟展览项目的开发顺序、关键参数与测试发布节点减少从零摸索的成本也可作为团队内部流程模板或课程案例参考。1. 虚拟展览体验不是拖模型VR/AR 开发脚本案例的六个落地关口把展厅模型拖进引擎、挂个漫游相机虚拟展览体验就算成了真到一体机里跑先翻车的往往是尺度、帧率和交互射线。这份 VR/AR 开发脚本案例把项目初始化、场景搭建、交互、UI 音效、构建和维护拆成可执行关口核心是一套流程骨架和 C# 脚本片段不是成品模板。它适合有基础脚本能力、要在短周期内搭出可演示原型的人也适合作为 VR/AR 软件/插件选型时的检查清单。下面按落地顺序拆先钉参数基线再谈展陈效果。拿到脚本包后别急着替换模型先把空场景跑通后面每一步才有对照。2. 项目初始化与参数基线目录、渲染管线、帧率先钉死2.1 工程目录与版本锁定虚拟展厅项目最常见的返工不是模型不好看而是目录混放导致构建时把源文件、缓存、测试资产一起打进去。我一般先把工程目录固定成“构建资产”和“源资产”两条线源文件留在Art/Source最终进包的模型放Art/Exhibits脚本按交互、UI、验证分层。这样做的好处是资产审计脚本有稳定路径可查换人接手也不会把测试模型误挂到场景里。virtual-exhibition/ Assets/ Art/ Source/ # 建模源文件不进构建 Exhibits/ # 最终展品模型命名带编号 Environment/ # 展厅壳体、地面、导览标识 Audio/ Music/ Ambience/ SFX/ Materials/ Scripts/ Interaction/ UI/ Validation/ Settings/ Docs/ asset-manifest.csv # 资产清单编号、路径、面数、纹理上限 device-matrix.md # 目标设备档位与验收项 Packages/目录定完紧接着锁版本。VR/AR 软件/插件升级最容易改输入 API 和模型导入默认值编辑器里看着正常出包后手柄射线可能直接失效。我的做法是把引擎主版本、XR 插件、输入系统、模型导入器、音频中间件各写一行锁定项任何一项变更都必须跑对应回归。版本号不用写死到具体数字写清“主版本.次版本.补丁”的锁定规则即可。组件锁定项变更前必须跑的回归引擎主版本主版本.次版本.补丁空场景基线帧率、构建冒烟XR 插件主版本.次版本手柄射线、手势识别、头部追踪输入系统主版本.次版本UI 点击、展品拾取、返回键模型导入器默认设置快照抽 3 个展品看尺度、材质、碰撞体音频中间件主版本3D 衰减、总线音量、爆音检查资产清单不要只写文件名至少保留asset_id、path、type、triangle、texture_max、status六个字段。展品多的时候靠肉眼查“这个模型是不是 20 万面”很不靠谱清单配合后面的审计脚本才能把预算压住。提示工程路径不要带中文、空格和特殊符号XR 插件在部分构建链路里对路径很敏感出问题时连日志都难读。2.2 渲染管线与色彩空间选择虚拟展览的场景复杂度通常集中在展品材质、透明展柜和灯光上渲染管线选型要先看目标设备。移动一体机优先前向渲染灯光数量少、透明层数可控PC 串流可以上延迟渲染和后处理但别把桌面端的习惯直接搬到一体机。常见的翻车是移动端开了实时全局光照、屏幕空间反射和泛光编辑器预览挺好看戴上一体机三分钟就开始掉帧。色彩空间我一般强制线性纹理的 sRGB 标记按用途区分颜色贴图开 sRGB法线、粗糙度、金属度贴图关 sRGB。模型发黑不一定是灯光暗先查色彩空间和纹理标记比盲目加灯有效。抗锯齿在 VR 里优先 MSAA后处理 AA 容易把远处展品文字糊成一片如果 UI 文字仍然虚先提高渲染缩放再考虑换 AA 方案。参数移动一体机PC 串流说明渲染管线前向前向或延迟移动端少用重后处理色彩空间线性线性颜色贴图开 sRGB立体渲染单通道立体单通道立体避免双通道重复提交抗锯齿MSAA 4xMSAA 4x文字优先保清晰实时阴影1~2 个主光2~4 个阴影距离别覆盖全展厅后处理关闭或极简按需泛光、反射慎用透明展柜是另一个坑。玻璃、亚克力、水面这类材质每多一层叠加填充率就往上走。移动端我一般把透明层数压在 3 层以内展柜玻璃用单层透明加边缘高光不追求真实折射。材质排序也要在场景里实测编辑器里顺序对不代表头显里不闪。2.3 目标帧率、分辨率与单眼渲染参数帧率预算要按毫秒算不能只写“目标 90 帧”。72Hz 每帧约 13.9ms90Hz 约 11.1ms120Hz 只有 8.3ms。脚本里设置目标帧率只是上限实际能不能稳要看渲染缩放、灯光、材质和 Draw Call。我的习惯是给目标帧率留 20% 余量目标 90Hz 的场景按 72Hz 的预算做资产真机上才有波动空间。using System; public enum DeviceTier { MobileStandalone, PCStreaming } public interface IRenderBaseline { void SetTargetFrameRate(int hz); void SetRenderScale(float scale); void SetSinglePassInstanced(bool enabled); void SetMSAA(int samples); } public sealed class RenderBaseline { public void Apply(IRenderBaseline api, DeviceTier tier) { switch (tier) { case DeviceTier.MobileStandalone: api.SetTargetFrameRate(72); api.SetRenderScale(0.85f); // 先降渲染分辨率再谈后处理 api.SetSinglePassInstanced(true); api.SetMSAA(4); break; case DeviceTier.PCStreaming: api.SetTargetFrameRate(90); api.SetRenderScale(1.0f); // 桌面端可先保清晰度 api.SetSinglePassInstanced(true); api.SetMSAA(4); break; } } }这段代码是引擎无关的基线骨架落地时把IRenderBaseline映射到实际引擎的渲染设置接口。SetTargetFrameRate控制上限SetRenderScale控制单眼渲染分辨率缩放SetSinglePassInstanced控制立体渲染是否单通道提交SetMSAA控制抗锯齿采样数。移动端把渲染缩放压到 0.85 是常用起点如果展品文字仍然可读可以再降到 0.8如果文字明显糊优先回到 0.9 并减少实时灯光而不是直接加后处理。单眼分辨率不要照搬显示器参数按头显推荐分辨率和渲染缩放一起算。展陈场景里远处信息牌的文字可读性比边缘抗锯齿更重要所以 UI 文字尽量用大字号、短行宽别把一段说明塞进 0.3 米宽的面板里。帧率、分辨率、渲染缩放这三个参数要一起调单独改一个很容易出现“帧率够了但画面糊”或“画面清楚了但头晕”的来回拉扯。3. 场景搭建与资产管线模型尺度、三层布光与性能预算3.1 模型导入的尺度、轴向与碰撞体展品模型进引擎前先在建模侧把单位统一成米、Y 轴向上、正面朝向一致。不要到了引擎里再用非均匀缩放把模型拉成“看起来差不多”非均匀缩放会让法线和碰撞体一起变形后面射线拾取容易点不中。展品高度我一般控制在 0.2~3.0 米超过这个区间的展品要么拆件要么单独做展台否则观众视线高度对不上。using System.Collections.Generic; using System.Numerics; public readonly struct Bounds3 { public readonly Vector3 Size; public Bounds3(Vector3 size) { Size size; } } public sealed class AssetAudit { public Liststring Check(string assetPath, Bounds3 bounds, int triangleCount) { var warnings new Liststring(); if (!assetPath.Contains(/Exhibits/)) warnings.Add(展品应放在 Art/Exhibits 下源文件放 Art/Source); var h bounds.Size.Y; if (h 0.2f || h 3.0f) warnings.Add($展品高度 {h:F2}m 超出 0.2~3.0m 展陈区间); if (triangleCount 80000) warnings.Add($三角面 {triangleCount} 超过单展品 8 万预算); return warnings; } }这段审计代码放在编辑器批处理里跑导入前先扫一遍路径、高度和面数。0.2~3.0m是常规展陈区间实际项目可以按展品类型调80000是单展品三角面预算的经验值移动端展品多的时候还要往下压。碰撞体不要直接用高面数网格展品用简单盒体或胶囊体包一层边缘留 5% 左右的外扩点选更稳但别外扩到穿墙。导入后还要查材质槽和纹理尺寸。一个展品挂 8 个材质、每张纹理 4K移动端很容易被填充率拖死。常见做法是同类展品共用材质实例纹理主体 1024、细节 2048透明贴图单独压。模型轴向不对时先在建模侧修不要在引擎里旋转 90 度后继续叠父子层级后面做动画和碰撞会越来越乱。3.2 灯光与材质避免“塑料感”的三层布光虚拟展厅的灯光不需要模拟真实太阳三层布光就够用环境光打底、主光定方向、重点光突出展品。环境光用低强度天空光或环境贴图主光用一盏带阴影的定向光重点光只照核心展品别让每盏灯都开阴影。移动端实时阴影距离压到 8~12 米远处展品用烘焙光照或顶点光补。层级作用移动端建议常见误用环境光打底、避免死黑低强度环境贴图强度过高导致材质发灰主光定方向、投影1 盏带阴影定向光多盏主光互相打架重点光展品高光2~3 盏无阴影点光每盏都开阴影自发光信息牌、灯带少量、低强度泛光叠太多材质方面非金属展品金属度压在 0~0.2粗糙度 0.4~0.8法线强度 0.5~1.0。金属度拉满又没有环境反射模型会像塑料粗糙度太低展品边缘全是高光头显里容易闪。透明展柜用单层透明加边缘高光别开折射如果必须做多层玻璃先把透明层数记进性能预算表后面统一查。3.3 展陈布局与性能预算布局先定动线再摆模型。主通道留 1.5~2.0 米展品间距不小于 1.2 米信息牌放在展品侧前方 30~45 度视线高度按 1.5 米左右调。别把展品围成一圈让观众原地转 360 度头部追踪频繁大幅度转动更容易晕。导览动线用地面箭头或灯带提示比在空气里飘一串文字更稳。性能预算要写成表不能只靠“感觉差不多”。下面的数字是经验基线不是硬标准但拿来当第一版验收线够用。项目移动一体机PC 串流检查方式单帧三角面≤ 15 万≤ 80 万帧调试器Draw Call≤ 120≤ 400帧调试器实时灯光≤ 2 个带阴影≤ 4 个带阴影灯光面板材质数≤ 40≤ 120资产审计主体纹理10242048导入设置透明叠加≤ 3 层≤ 6 层场景巡查展品数量多的时候把不重要的展品做成低模加贴图近距离再切高模或者按展区做可见性分组观众走到哪个区才加载哪组。别一上来就全场景常驻移动端内存和填充率都扛不住。布局阶段就把性能预算表贴在场景里每加一个展品勾一次比出包后拿帧调试器到处找原因省事得多。4. 交互脚本与空间 UI射线拾取、手势状态机、音频总线4.1 输入抽象层把手柄、手势、注视统一成三条射线交互插件换版本最怕业务脚本里到处写插件 API。我的做法是先做一层输入抽象手柄射线、手势射线、注视射线都输出同一种Ray业务层只认射线和选择事件。这样后面换交互插件或者同时支持手柄和手势不用把展品拾取脚本重写一遍。using System.Numerics; public readonly struct Ray { public readonly Vector3 Origin; public readonly Vector3 Direction; public Ray(Vector3 origin, Vector3 direction) { Origin origin; Direction direction; } } public interface IInputProvider { bool TryGetRay(out Ray ray); // 手柄或手势指向 bool TryGetGaze(out Ray gaze); // 头部注视 bool TryGetPinch(out float strength); // 捏合强度 0~1 bool TryGetSelect(out bool pressed, out bool released); }TryGetRay负责指向射线TryGetGaze负责注视射线TryGetPinch输出 0~1 的捏合强度TryGetSelect输出按下和抬起边沿。业务层不要直接读插件原始数据先在这层做死区和防抖。射线长度我一般设 12 米展厅内够用UI 层和导览包围盒要放在忽略列表里否则射线会先打到面板背面。4.2 展品拾取与信息面板120ms 防抖和 250ms 防误触展品拾取不是“射线打中就直接弹窗”。头显里手会抖手势识别也会抖直接弹窗会导致信息面板反复开关。我的做法是射线命中后先高亮高亮稳定 120ms 才允许选择选择按下后再等 250ms 防误触。这样既能保留即时反馈又不会让面板闪来闪去。public interface IExhibitTarget { void SetHighlight(bool on); void OpenInfo(); } public interface IExhibitRegistry { IExhibitTarget Raycast(Ray ray, float maxDistance); } public sealed class ExhibitPicker { private readonly IInputProvider _input; private readonly IExhibitRegistry _registry; private IExhibitTarget _hover; private float _hoverTime; public ExhibitPicker(IInputProvider input, IExhibitRegistry registry) { _input input; _registry registry; } public void Tick(float dt) { if (!_input.TryGetRay(out var ray)) { ClearHover(); return; } var hit _registry.Raycast(ray, 12f); // 12m 拾取上限 if (hit ! _hover) { ClearHover(); _hover hit; _hoverTime 0f; } if (_hover null) return; _hoverTime dt; _hover.SetHighlight(_hoverTime 0.12f); // 120ms 高亮防抖 if (_input.TryGetSelect(out var pressed, out _) pressed _hoverTime 0.25f) { _hover.OpenInfo(); // 250ms 防误触 } } private void ClearHover() { if (_hover ! null) _hover.SetHighlight(false); _hover null; _hoverTime 0f; } }maxDistance设 12 米超过展厅尺度就会点到墙后0.12f是高亮防抖低于这个值手抖会闪0.25f是选择防误触手势捏合刚触发时容易连点。信息面板打开后我一般把拾取射线临时降到 UI 层避免面板后面的展品继续响应。面板关闭再恢复不然观众会感觉“点了一下后面好几个展品都亮了”。4.3 手势状态机hover、dwell、confirm、cooldown手势交互比手柄更容易误触因为捏合、松开、悬停之间没有物理按键的明确边沿。用状态机把Idle → Hover → Dwell → Confirm → Cooldown拆开阈值写进常量调参时不用满脚本找数字。public enum GestureState { Idle, Hover, Dwell, Confirm, Cooldown } public sealed class GestureStateMachine { private const float HoverFilter 0.15f; // 150ms 悬停过滤 private const float DwellToConfirm 0.8f; // 800ms 注视确认 private const float Cooldown 0.3f; // 300ms 冷却 private GestureState _state GestureState.Idle; private float _timer; public GestureState Tick(float dt, bool hasTarget, bool pinchReleased) { _timer dt; switch (_state) { case GestureState.Idle: if (hasTarget) { _state GestureState.Hover; _timer 0f; } break; case GestureState.Hover: if (!hasTarget) { _state GestureState.Idle; _timer 0f; } else if (_timer HoverFilter) { _state GestureState.Dwell; _timer 0f; } break; case GestureState.Dwell: if (!hasTarget) { _state GestureState.Idle; _timer 0f; } else if (pinchReleased || _timer DwellToConfirm) { _state GestureState.Confirm; _timer 0f; } break; case GestureState.Confirm: _state GestureState.Cooldown; _timer 0f; break; case GestureState.Cooldown: if (_timer Cooldown) { _state GestureState.Idle; _timer 0f; } break; } return _state; } }HoverFilter过滤手势插件输出的抖动DwellToConfirm是注视确认时间Cooldown防止连续触发。手势识别插件输出不稳定时先加过滤不要靠降低灵敏度硬压。头部追踪这边相机不要被脚本强制平移转向用瞬移或 30 度 snap turn观众自己转头最稳。导览移动时加一层轻微暗角能减少视觉到前庭的冲突但别做长时间全屏黑幕。4.4 空间 UI 与音频总线别把面板焊在眼前空间 UI 最常见的问题是面板每帧LookAt头部观众稍微转头面板就跟着甩几分钟就晕。我的做法是面板放在距离 1.5 米左右角度死区 15 度超出死区再延迟 0.2 秒跟随而且只跟水平方向不跟俯仰。文字用大字号、短行宽主标题视角高度不小于 1.2 度正文不小于 0.6 度。参数建议值说明面板距离1.2~2.0m太近压迫太远看不清水平跟随死区15°小转头不触发跟随跟随延迟0.2s避免面板甩动面板宽度≤ 60° 视场超过就要分页正文最小视角0.6°再小头显里读不清按钮最小尺寸2.5°手势点选容错音频按总线分层Master、Music、Ambience、SFX、UI 各一组背景音乐压到 -18dB 左右环境声 -24dBUI 提示音 -12dB展品解说单独走一条 ducking 总线。3D 音效最小距离 1 米、最大距离 15 米滚降用对数别用线性线性衰减在展厅里听起来忽大忽小。透明展柜和音频没关系但玻璃反射声如果做太多观众会以为展品在漏音环境声保持一层就够。5. 避坑与排查五个翻车现场和我的后悔药5.1 渲染与资产类现象展品模型发黑换灯光也没用。原因通常不是灯不够而是色彩空间或纹理 sRGB 标记错了。颜色贴图没开 sRGB线性空间里会偏暗法线贴图开了 sRGB光照方向会怪。解决顺序是先查项目色彩空间再查三张核心贴图的 sRGB 标记最后才调灯光强度。材质发灰、金属感不对也先看金属度和粗糙度不要一上来加环境反射。现象透明展柜边缘闪烁展品一会儿在前面一会儿在后面。原因是透明材质排序和深度写入冲突多层透明叠加时更明显。解决是把展柜玻璃压成单层透明关闭不必要的深度写入透明层数控制在 3 层以内如果必须多层把内层展品改成不透明材质加边缘光。展柜别用折射移动端填充率扛不住头显里也容易晕。5.2 交互与输入类现象手柄射线在展品边缘漂移点选经常穿到墙后。原因多是碰撞体比视觉模型大太多或者射线忽略列表没配好。展品碰撞体留 5% 外扩就够别用高面数网格直接当碰撞体射线要忽略 UI 层、导览包围盒和展柜玻璃。拾取上限从 12 米往下压到 8 米也能减少穿墙但会牺牲远处展品的可点性先修碰撞体再调距离。现象手势交互频繁误触观众手一抬就弹面板。原因是手势插件原始输出有抖动业务层直接拿捏合边沿触发。解决是加 150ms 悬停过滤、250ms 选择防误触和 300ms 冷却把状态机阈值写进常量。另一个常见原因是面板打开后射线没切到 UI 层后面的展品继续响应看起来像“一次手势点了三个东西”。面板打开时暂停展品拾取关闭再恢复。5.3 构建与音频类现象编辑器里一切正常构建后一体机黑屏或手柄没输入。先查场景列表和 XR 插件初始化顺序再看输入系统配置有没有被打包剥离。常见原因是构建时只勾了空场景或者插件初始化脚本挂在编辑器专用目录里。解决是把启动场景、XR 运行时初始化、输入配置写进出包检查表构建后先跑空场景冒烟能看到地面、能转头、手柄射线能出现再进正式展厅。音频总线没初始化也会导致构建后无声但黑屏优先查场景和插件。6. 发布前验证与性能回归三组检查跑完再出包出包前我固定跑三组回归空场景基线、单展品压力、全展品导览。空场景看启动、转头、手柄射线和 UI 点击单展品压力把面数最高、材质最多的展品单独拉出来看帧率和内存全展品导览按真实动线走两圈记录每个展区的帧率低点。三组都过再谈发布。回归组检查项通过线空场景基线启动、转头、射线、UI目标帧率稳定 60 秒单展品压力最高面数展品、透明展柜帧率不低于目标 80%全展品导览三个展区动线、音频切换无连续掉帧、无爆音构建冒烟场景列表、插件初始化一体机不黑屏、输入可用#!/usr/bin/env bash # 出包前静态检查文档、清单、未清理标记 set -e test -f Docs/device-matrix.md || { echo 缺少设备矩阵; exit 1; } test -f Docs/asset-manifest.csv || { echo 缺少资产清单; exit 1; } if grep -R TODO_FIX Assets/Scripts /dev/null 21; then echo 存在未清理标记; exit 1 fi echo 静态检查通过这个脚本只做静态检查不替代真机回归但能拦住“文档没更新、资产清单漏展品、脚本里还留着调试标记”这类低级问题。性能回归的数据记在device-matrix.md里每次出包对比上一版掉 10% 以上就停下来查。从那以后我每次出包前都强制走一遍空场景和全展品导览不再靠“编辑器里看着挺顺”下判断。希望帮到你。本文还有配套的精品资源点击获取