尧图精选

Unity+3D+C#实现京绣虚拟展馆交互漫游与跨平台发布

🕒 发布时间:2026/9/18 17:29:19 📁 来源:尧图网络
前年冬天一位做非遗数字化采集的朋友给我看了一组京绣绣片的高清翻拍图——海水江崖纹里每一根盘金线的高光走向都清清楚楚连打籽针的颗粒感都能数出来。可他摊手说了一句很扎心的话照片再清楚看的人也只是在巡视没有站在展柜前的那种感觉。后来我们用Unity 3D C#搭了一套京绣宫绣文化主题的虚拟展馆交互漫游系统观众戴上头显或在浏览器里打开就能以一个正常成年人的视角推门进厅走到展柜前看绣品的正反面针脚点一下把纹样拆开看针法层次边走边听讲解。整套东西从美术资产到脚本架构全部自己啃下来中间踩的坑比预想的多得多。这篇文章就把这套系统的完整实现摊开讲一遍展馆的结构怎么定、绣品贴图怎么采、材质参数怎么调、C# 脚本怎么分层、阴影和性能怎么救、三个平台怎么打包。不管你是刚学 Unity 想找个完整项目练手还是已经做过几个小 Demo 想冲一个能交付的落地作品都能从里面抠出能直接用的东西。我会把关键代码和参数都贴出来你照着抄作业基本能跑通。1. 项目起点京绣展馆为什么必须做成可漫游的3D场景1.1 从看图到逛馆需求到底差在哪先说清楚这个项目要解决什么问题。京绣又称宫绣是以北京为中心的刺绣体系讲究平、光、齐、匀、和、顺、细、密八个字用料上多用真丝线和金银线针法有平针、套针、打籽针、盘金、钉线、施针等十几种。它的观赏特性非常特殊平视看是图案侧光看是层次凑近看是针脚。这三种观看方式传统图文网页一个都提供不了——图片永远是固定机位、固定光照观众没法自己找角度。所以需求被拆成了四条这条拆解直接决定了后面所有的技术选型自由视角观众要能自己走动、自己决定站多远、从哪个角度看这就必须有第一人称或自由视角的漫游控制。真实光照下的丝线质感金银线在不同角度下反光完全不同这要求材质走 PBR 金属工作流而不是一张死贴图。信息可展开绣品背后的针法、纹样寓意、历史源流要能点开看但不能糊在画面上挡住展品。低门槛访问不能要求观众都装几百 G 的客户端网页端和一体机都得能跑。这四条里最容易被低估的是第二条。很多虚拟展馆做出来像塑料玩具展根子就在材质没做对——真丝被做成了塑料金线被做成了黄色涂料。后面我会专门用一节讲材质参数怎么定。1.2 技术栈定版Unity 2022 LTS URP C#引擎选型上我没有纠结太久。这个项目要同时出 PC 端、WebGL 网页端和 Android 架构的 VR 一体机端跨平台能力是第一硬指标Unity 在这方面是现成的最优解其次是资源生态展馆建筑、展柜、灯光这些通用资产在资源商店里能找到大量可参考的骨架能省掉大量从零建模的时间。具体版本我用的是Unity 2022 LTS渲染管线选URP通用渲染管线理由有三条都是实际压测出来的一是 URP 对移动端和 WebGL 友好内置管线的很多效果在一体机上直接跑不动而 URP 的 Shader 复杂度可控同一个 Shader 在三个平台的降级路径清晰。二是我需要用 Shader Graph 手搓一个刺绣高光增强的着色器URP 下这件事的门槛低得多。三是 2022 LTS 的支持周期长非遗类项目往往是做完就放着两三年后还要能重新打开、重新打包用短期支持版本会埋雷。脚本语言自然是C#。这里插一句经常有人问游戏开发里 C 和 C# 怎么选。在这个项目里答案很明确C# 是 Unity 的官方脚本语言与引擎的 API 贴合度最高热重载和调试体验也最顺而虚拟展馆的逻辑层几乎全是交互、状态管理、UI 驱动这类业务代码不需要极致性能用 C 反而是在给自己找麻烦。真正吃性能的部分渲染、物理、动画Unity 底层已经是 C 了。1.3 工程目录与资源规范先立规矩再动手这一步很多人跳过然后在中后期被自己坑死。虚拟展馆的资源量比一般小游戏大得多——光贴图就上百张还有音频、动画、预制体目录一乱改一个材质要翻十层文件夹。我用的目录结构是这样的Assets/ _Project/ Art/ Architecture/ 展馆建筑、地面、天花 Furniture/ 展柜、屏风、博古架 Embroidery/ 绣品模型与贴图 Decals/ 地贴、指示牌 Audio/ BGM/ Narration/ 讲解音频 SFX/ Prefabs/ Exhibits/ 每件展品一个预制体 UI/ Scenes/ Boot.unity 启动与加载 Museum.unity 主场景 Scripts/ Player/ 漫游控制 Interaction/ 交互与拾取 UI/ Manager/ 场景管理、数据管理 Utils/ Settings/ URP Asset、Quality、Input UI/ Fonts/ Sprites/两个必须坚持的规矩。第一所有第三方资源必须先搬进_Project再引用不要直接引用 Packages 或外部目录否则换机器、清缓存的时候资源引用会集体丢失。第二所有展品的元数据不要硬编码在场景里用 ScriptableObject 存标题、描述、讲解音频引用这样后期加展品只需要新建一个资产文件不用动场景、不用重编译逻辑。这两条规矩在后面加展品、换文案的时候救了我无数次。2. 美术资产把一根金线绣出立体感2.1 展馆建筑与展陈道具的建模分寸展馆的尺度必须先定死因为漫游系统的移动速度、交互距离、LOD 层级全都依赖这个尺度。我把展厅定为长 32 米、宽 18 米、净高 4.5 米中间一条主通道宽 3.2 米两侧各排一行展柜。这个数字不是随便拍的通道宽 3.2 米两个人并排走不挤第三人通过也不用侧身接近真实展馆的舒适宽度。做窄了在 VR 里会有明显的压迫感。净高 4.5 米宫绣展品里有大幅的屏风绣高度接近 2.6 米加上展柜底座如果层高只有 3 米视觉上会非常憋屈。4.5 米能留出足够的呼吸空间。展柜高度 1.05 米这是博物馆展柜的常见台面高度观众站立时的自然视线落点在 1.5 米到 1.6 米稍微低头就能看清平放的绣片不用弯腰。建模工具上建筑结构、展柜这类硬表面我在 Blender 里做导出 FBX 给 Unity绣品本身因为需要在平面上做微起伏也是同一套流程。硬表面建模有个分寸问题不要把所有细节都做在模型上。比如展柜的木质边角线、屏风的镂空花格如果每个都做成实体几何一个展柜就能吃掉两万面。我的做法是结构轮廓用实体建模表面细节木纹、雕花的褶皱纹路全部交给法线贴图。一个展柜最终控制在 3000 面以内整个展厅的建筑和道具加起来控制在 35 万三角面剩下的大头留给绣品。提醒FBX 导出时记得勾选应用变换并且把单位统一成米。我在第一版里有一半模型单位是厘米导入 Unity 后展柜比人高两倍排查了半天才发现是导出设置的问题。2.2 京绣纹样的贴图采集与修复流程这是整个项目里最耗心力、也最能拉开品质差距的部分。绣品贴图不是简单拍张照贴上去就完事我走的是这么一条五步流程第一步基础采集。绣片在自然散射光下平铺翻拍相机保持垂直用 50mm 左右的焦段减少透视畸变逐块拍摄。翻拍时旁边放一把色卡方便后期统一色温。这里有个细节绝对不能用闪光灯直打真丝和金银线会产生大片死白高光后期怎么修都救不回来。第二步畸变校正与拼接。大幅屏风绣一张拍不全需要分块拍摄后拼接。用 Photoshop 的自动混合图层或者专门的拼接工具都行关键是拼接前先做镜头畸变校正否则接缝处会出现明显的错位。第三步去反与纹理修复。翻拍图里难免有环境反光、灰尘、织物折痕。这里要克制只修环境造成的瑕疵不要修刺绣本身的起伏和针脚。我见过有人把绣片修得像印刷品一样平整结果贴到 3D 模型上完全没有立体感白瞎了。针脚的阴影和丝线的明暗过渡要原样保留那是后面生成法线贴图的依据。第四步生成法线贴图与高度图。这一步是把平面照片变成能反光的绣面的关键。我用的方法是从修复后的图里提取明度信息转成高度图Height Map再用高度图生成法线贴图。真丝绣线的高度起伏大概在 0.3 到 0.8 毫米之间比例上非常细微所以法线强度不能给太高一般强度值在 1.5 到 2.5 之间过了就会显得像橡皮泥。第五步通道打包。把金属度、粗糙度、环境光遮蔽分别塞进贴图的不同通道减少贴图数量。URP 的 Lit Shader 支持的 Mask Map 就是 R 通道放金属度、G 通道放环境光遮蔽、A 通道放光滑度、B 通道留空。打包后一张 Mask Map 顶三张图对 WebGL 端的加载速度帮助非常明显。心得绣品的正反面都要做。京绣的背面往往能看到走线的规律和打结方式是理解针法的重要线索。背面材质可以把粗糙度调高一些做出绒面的哑光感。2.3 PBR 材质参数怎么定真丝、金线、玻璃三套配方材质参数是这个项目的品质分水岭。我一开始凭感觉调出来的东西像塑料玩具后来对着实物和实拍图反复对比才摸出下面这三套配方。它们不是标准答案但作为起手参数是靠谱的。材质类型MetallicSmoothness法线强度关键设置真丝绣线彩色0.00.42 ~ 0.552.0开启各向异性近似高光要宽而柔金银线盘金、钉线0.85 ~ 1.00.65 ~ 0.782.4必须给环境反射探针否则金线是死黑底衬丝绸0.00.30 ~ 0.381.2粗糙度高一点避免抢戏展柜玻璃0.00.92—透明队列加菲涅尔边缘反射木质展柜0.00.25 ~ 0.401.6用 AO 贴图压暗边角真丝为什么金属度是 0因为丝线本体是电介质它的亮来自表层的镜面反射和纤维内部的散射不是金属反射。很多人把丝线调成金属结果一动视角颜色就乱跳像涂了层锡纸。而金银线是真金属哪怕是仿金线也含金属颜料所以金属度必须拉满同时一定要有反射探针或天空盒提供环境信息否则金属材质在没有任何反射源的情况下渲染出来是一片黑。展柜玻璃这里有个容易被忽略的细节。URP 的透明材质如果直接设成 Alpha 混合会出现从里往外看和从外往里看排序错乱的问题。我的处理是给玻璃单独一个渲染队列并且不要让玻璃遮挡射线——玻璃所在的 Layer 要排除在交互射线的 LayerMask 之外否则观众点展品永远点不到全被玻璃挡住了。这个问题我在测试时被反馈了三次才彻底解决。2.4 模型优化LOD、图集与包围盒剔除虚拟展馆最怕的就是看着漂亮一进去卡成幻灯片。优化要在美术阶段就开始不能等打包时再补。LOD 分级是第一步。展柜做了三级近距离3 米内用全精度模型中距离3 到 8 米用一半面数远距离8 米外用一个简化的长方体加贴图。绣品因为本身面数不高只做了两级。切换阈值不要设得太近否则观众走动时会看到模型跳变很出戏。图集合并是第二步。展厅里几十个展柜如果每个用一张独立贴图Draw Call 会很难看。我把所有木质展柜的木纹贴图合成一张 2048 的图集所有绣品的小配饰贴图合成另一张合并后 Draw Call 直接降了一半以上。合并图集有个副作用是贴图分辨率被平均化了所以核心展品的大图要单独留出来不要塞进图集。关于包围盒这里展开说一个很多人会混淆的点。Unity 里Renderer.bounds和Mesh.bounds是两个不同的东西Mesh.bounds是模型在本地空间的包围盒永远跟着模型走Renderer.bounds是世界空间的、加入了所有渲染器包括 SkinnedMeshRenderer 变形后的包围盒。视锥剔除用的是世界空间的包围盒。如果你发现某个物体明明在视野里却被剔除了八成是包围盒算错了——常见原因是给物体挂了一个带 Animator 的骨骼动画或者手动改过Mesh.bounds但没同步更新。还有一种情况是包围盒过大导致剔除失效给展柜挂了一个范围很大的粒子特效粒子的包围盒会把整个展柜的剔除范围撑大结果远处的展柜全在渲染。处理办法是把粒子单独拆成子物体或者给粒子设置Renderer.forceRenderingOff配合手动剔除。3. C# 脚本层交互漫游系统的四根柱子3.1 漫游控制器CharacterController 方案与防穿墙细节漫游控制器是整个系统的地基地基不稳后面全部白搭。方案上我在CharacterController和Rigidbody之间选了前者理由是展馆漫游需要的是确定性的移动不是物理真实的移动。用 Rigidbody 会遇到惯性滑行、碰到展柜被弹开、卡在角落抖动这些问题观众在虚拟展馆里被弹出展柜是很糟糕的体验。CharacterController 自带胶囊体碰撞、台阶自动抬升、坡度限制控制感干净得多。下面是我实际在用的控制器去掉了项目专用的部分保留了核心逻辑using UnityEngine; [RequireComponent(typeof(CharacterController))] public class MuseumWalker : MonoBehaviour { [Header(移动参数)] public float walkSpeed 2.2f; // 常规步速接近真人逛展 public float runSpeed 4.2f; // 长按 Shift 加速 public float smoothTime 0.12f; // 速度过渡时间太小会顿挫 public float gravity -20f; public float groundedStick -2f; // 贴地吸附力 [Header(视角参数)] public Transform headPivot; public float sensitivity 2.2f; public float pitchLimit 78f; CharacterController cc; Vector3 planarVel; Vector3 planarVelRef; float pitch; float verticalVel; void Awake() { cc GetComponentCharacterController(); SetCursor(true); } void Update() { Look(); Move(); if (Input.GetKeyDown(KeyCode.Escape)) SetCursor(false); if (Input.GetMouseButtonDown(0) Cursor.lockState ! CursorLockMode.Locked) SetCursor(true); } void SetCursor(bool locked) { Cursor.lockState locked ? CursorLockMode.Locked : CursorLockMode.None; Cursor.visible !locked; } void Look() { if (Cursor.lockState ! CursorLockMode.Locked) return; float mx Input.GetAxis(Mouse X) * sensitivity; float my Input.GetAxis(Mouse Y) * sensitivity; // 左右转身影响整个身体上下只转头 transform.Rotate(Vector3.up, mx, Space.Self); pitch Mathf.Clamp(pitch - my, -pitchLimit, pitchLimit); headPivot.localEulerAngles new Vector3(pitch, 0f, 0f); } void Move() { float h Input.GetAxisRaw(Horizontal); float v Input.GetAxisRaw(Vertical); Vector3 wish (transform.right * h transform.forward * v).normalized; bool running Input.GetKey(KeyCode.LeftShift); float target running ? runSpeed : walkSpeed; // 用 SmoothDamp 做加减速避免一按就走、一松就停的机械感 planarVel Vector3.SmoothDamp(planarVel, wish * target, ref planarVelRef, smoothTime); if (cc.isGrounded verticalVel 0f) verticalVel groundedStick; verticalVel gravity * Time.deltaTime; Vector3 motion planarVel; motion.y verticalVel; cc.Move(motion * Time.deltaTime); } }几个参数是反复调出来的。walkSpeed定 2.2 米每秒是因为真人逛展馆的速度大约在 1.8 到 2.5 米每秒之间太快会让人错过展品太慢会让人烦躁。smoothTime给 0.12 秒是个平衡点低于 0.08 会出现明显的顿挫感高于 0.2 会有踩在冰上的漂移感。防穿墙有两个必须做的设置。一是 CharacterController 的Skin Width建议设为Radius的 8% 到 10%太小容易在角落卡住太大人物会浮在墙外。二是场景里的所有墙体、展柜必须挂MeshCollider并勾选 Convex 或换成 BoxCollider千万别用没勾 Convex 的 MeshCollider 当静态碰撞体——那种情况下物理引擎会把整个网格当成一个巨大的凹形碰撞体移动时会出现莫名其妙的推挤。展厅的墙我用的是 BoxCollider 拼出来的视觉模型和碰撞体分离这样碰撞检测既准又快。3.2 展品交互射线拾取、高亮与信息面板交互的核心是我看向哪件展品它就应该有反应。我用的是从摄像机中心发出的射线检测配合 Layered 分层using UnityEngine; using UnityEngine.Events; public class ExhibitInteractor : MonoBehaviour { public Camera eye; public float maxDistance 3.5f; // 超过这个距离不响应 public LayerMask interactMask; // 排除玻璃、地面、墙体 public UnityEventstring onFocusChanged; public UnityEventExhibitInfo onOpen; ExhibitInfo current; void Update() { ExhibitInfo hit null; Ray ray new Ray(eye.transform.position, eye.transform.forward); if (Physics.Raycast(ray, out RaycastHit info, maxDistance, interactMask, QueryTriggerInteraction.Collide)) { hit info.collider.GetComponentInParentExhibitInfo(); } if (hit ! current) { if (current ! null) current.SetHighlight(false); current hit; if (current ! null) current.SetHighlight(true); onFocusChanged?.Invoke(current ! null ? current.title : string.Empty); } if (current ! null Input.GetMouseButtonDown(0)) onOpen?.Invoke(current); } }maxDistance给 3.5 米是有讲究的。太短比如 1.5 米会逼着观众贴着展柜在 VR 里会触发越界感太长比如 8 米会出现在通道中间就能点到远处展品的情况失去了走近观看的意义。3.5 米差不多是站在展柜前一步的距离。高亮效果我一开始想用描边方案但描边在 URP 里要么改写 Shader 做两个 Pass要么用后处理都会带来额外的性能开销。后来改成更轻的方案在 Shader 里加一个可控的_OutlineWidth属性通过 MaterialPropertyBlock 动态修改不生成材质实例也不会打断合批。using UnityEngine; [RequireComponent(typeof(Collider))] public class ExhibitInfo : MonoBehaviour { public string title; [TextArea(3, 8)] public string description; public AudioClip narration; public Renderer highlightTarget; static readonly int OutlineWidthID Shader.PropertyToID(_OutlineWidth); MaterialPropertyBlock mpb; void Awake() mpb new MaterialPropertyBlock(); public void SetHighlight(bool on) { if (highlightTarget null) return; highlightTarget.GetPropertyBlock(mpb); mpb.SetFloat(OutlineWidthID, on ? 1.6f : 0f); highlightTarget.SetPropertyBlock(mpb); } }关键提醒用 MaterialPropertyBlock 而不是直接改renderer.material._OutlineWidth。后者会在运行时创建一个材质实例每个展品一份不仅吃内存还会破坏静态合批展品一多帧率直接掉。这个坑我第一版就踩了后来用 Frame Debugger 才看出 Draw Call 暴涨的原因。信息面板用 TextMeshPro 做Canvas 模式选 World Space 贴在展品侧后方或者用 Screen Space - Camera 固定在屏幕一侧。我选的是后者原因是 World Space 的面板在近距离看会很大、远距离看不清而固定屏幕侧栏的阅读体验稳定和讲解音频配合也自然。面板打开时不要暂停漫游观众可以边听边绕着展品走这个自由度很重要。3.3 自动导览与导航网格自由漫游满足了探索欲但不是所有观众都愿意自己找路尤其是第一次进来的人。所以我加了一个自动导览模式观众点跟团参观镜头会沿预设路线自动移动每到一件重点展品就停下讲解讲完继续走。实现上有两条路。一条是烘焙 NavMesh用 NavMeshAgent 驱动一个导游角色摄像机跟随。这条路灵活能绕开临时障碍但需要烘焙导航网格而且展厅里的展柜都要标记为 Navigation Static 并设置好可行走区域工作量大。另一条是手打关键帧路线用插值移动摄像机。这条路简单可控缺点是摄像机可能穿模。我最后用的是混合方案主路线用关键帧插值展柜前的停留点用 NavMesh 做局部寻路这样既保证了主要路径的顺畅又能在展品附近做微调。关键帧插值用的是带缓入缓出的 Catmull-Rom 样条避免直线段之间的生硬转折。// 沿样条路径移动t 为 0~1 的进度 Vector3 GetPointOnPath(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { float t2 t * t; float t3 t2 * t; return 0.5f * ((2f * p1) (-p0 p2) * t (2f * p0 - 5f * p1 4f * p2 - p3) * t2 (-p0 3f * p1 - 3f * p2 p3) * t3); }小地图我用的是最省性能的方案一台正交相机垂直向下只渲染地面的简化和展品图标 Layer输出到一张 512x512 的 RenderTexture界面上用 RawImage 显示。相机跟随玩家的 XZ 坐标移动旋转保持固定不跟随玩家转向这样地图方向永远是稳定的观众不会看晕。如果要做成地图跟着转的效果把正交相机的 Y 轴旋转同步到玩家朝向即可但我实测下来固定朝向更好用。3.4 声音、氛围与 UI 分层虚拟展馆的沉浸感有一半是声音给的。我的音频分三层环境层是持续播放的展厅底噪音量压得很低主要作用是填满完全无声带来的空洞感。空间层是每件展品挂的 3D 音源讲解音频开启时用AudioSource.spatialBlend 1加一点混响观众转身时声音会跟着变化这个细节很加分。交互层是按钮点击、面板开合这类 UI 音效用 2D 播放不参与空间衰减。混响用 Reverb Zone 做展厅整体给一个类似大厅的混响预设靠近展柜的小范围区域切到更干的混响模拟走近了说话更清楚的听感。这个效果的参数别调猛混响时间超过 2.5 秒就会显得像在山洞里。UI 分层上我分了三个 CanvasHUD 层准星、提示文字、小地图始终显示面板层展品详情、讲解控制按需开关系统层加载进度、设置菜单在最上层。三个 Canvas 的 Sort Order 分别是 0、10、100这样不用改层级关系就能保证遮挡顺序正确。别把所有 UI 塞进一个 Canvas任何一个元素变化都会导致整个 Canvas 重建性能上很不划算。4. 光影与性能让人愿意多待十分钟4.1 烘焙还是实时混合光照的取舍光照方案是这个项目里最难权衡的一点。全烘焙的静态光照好看、性能好但观众走近展柜时绣品上的光照不会变化金属线的反光会显得很假全实时光照真实但一体机和 WebGL 根本跑不动多光源阴影。我最后用的是混合光照展厅主光、顶灯、环境光全部烘焙到 Lightmap这是性能的大头展柜内部的关键展品加一盏实时的点光源只照这一小块区域并开启 Light Probe 让绣品上的金属反射随视角变化。这样既保住了 90% 的性能又让重点展品活了起来。烘焙参数上有几个关键值Lightmap 分辨率用每单位 20 到 40 像素太低了墙面会出现明显的块状太高了烘焙时间和贴图体积爆炸Lightmap Padding 给 4 到 8 像素避免相邻展品的烘焙信息互相渗色环境光遮蔽强度给 0.8 到 1.2能明显增强展柜与地面的接触感。提醒烘焙前一定要把不需要参与烘焙的物体比如特效粒子、UI标记为 Contribute GI 关闭否则它们会把光照信息印到 Lightmap 上而且烘焙时间会大幅增加。4.2 Unity 阴影问题排查清单阴影是虚拟展馆最容易出问题的地方也是看起来专业不专业的分水岭。我把实际遇到过的问题整理成了一张排查表现象常见原因处理方式阴影边缘出现锯齿状断线阴影贴图分辨率不足提高 Shadow Resolution或缩短 Shadow Distance物体表面出现条纹状自阴影深度偏差设置不当调大 Normal Bias或在 URP 中开启 Shadow Bias 微调远处物体完全没有阴影超出阴影距离增大 Shadow Distance同时降低阴影级联数控制开销移动的展品阴影抖动级联切换过于敏感调整 Cascade 分割比例或给展品单独设置阴影玻璃展柜投出不透明的黑块透明材质默认投射阴影在材质上关闭 Cast Shadows或改用 Dither 阴影阴影边缘一圈明显的亮边阴影偏移过大减小 Bias 值配合提高阴影贴图分辨率这里重点说玻璃那一行。透明物体的阴影在 URP 里如果直接投射会变成不透明的黑色块整个展柜看起来像块黑板非常难看。解决办法是在 Renderer 组件上把 Cast Shadows 设为 Off然后在需要玻璃有一点影子的地方用一张带抖动的阴影贴图Dither做近似。这个技巧在很多教程里都不提但它是玻璃展柜能不能看的关键。4.3 性能预算与 Profiler 实测做跨平台项目必须先把性能预算定下来不然后期只能靠删东西来救。我定的预算表是这样的指标PC 端目标一体机目标WebGL 目标帧率60 FPS72 FPS30 FPS三角面数≤ 300 万≤ 80 万≤ 60 万Draw Call≤ 350≤ 150≤ 120实时光源≤ 4≤ 2≤ 1贴图总显存≤ 1.5 GB≤ 400 MB≤ 200 MB阴影贴图20481024关闭或 512实测下来最大的开销不在模型面数而在Draw Call 和实时光源的阴影。我用了几个手段把 Draw Call 压下来所有静态物体标记 Batching Static 走静态合批展厅里的重复道具同一款展柜的十几个副本用 GPU Instancing先把材质的 Enable GPU Instancing 勾上展品图标、装饰线这类小物件全部走图集合并。观察性能我用的是 Unity Profiler 加 Frame Debugger 的组合。Profiler 看总体趋势和 CPU 瓶颈Frame Debugger 逐帧拆解 Draw Call能精确看到是哪个物体在偷偷多渲染一次。我第一次跑 Frame Debugger 的时候发现有个不起眼的装饰灯模型在每一帧都单独占了一个 Draw Call因为它用了和其他灯不同的材质实例改成共享材质后直接省了十几个 Call。心得把 Profiler 的 Deep Profile 打开会大幅拖慢运行速度只能用来定位哪个函数慢不能用来测真实帧率。测真实性能时关掉 Deep Profile看 Rendering Profiler 里的 Camera.Render 和 Render.OpaqueGeometry 两个耗时项。5. 常见问题速查表与踩坑记录5.1 交互类问题问题一点不中展品。十个有八个是 LayerMask 配错了。检查三件事展品的 Collider 是否在同一层级射线检测的 LayerMask 是否包含该层级射线上是否有透明物体玻璃、雾气、UI 的世界空间画布挡住了。玻璃挡射线这个坑我前面提过展厅里最典型。问题二鼠标移出窗口后视角还在转或者转不动了。这是光标锁定状态管理的问题。我一开始只在 Awake 里锁了一次结果按 Esc 解锁后没有恢复逻辑。后来加了显式的锁定/解锁函数并且在窗口失去焦点时OnApplicationFocus强制解锁避免焦点切回来时视角乱飞。问题三高亮效果在多个展品之间串台。原因是 HighlightTarget 引用错了物体或者几个展品共用了同一个 Renderer。每件展品必须有自己的 Renderer 引用并且材质实例不要共享——共享材质的物体如果其中一个改了属性另一个会跟着变。用 MaterialPropertyBlock 的话就没这个问题因为它改的是实例数据不影响共享材质。问题四面板打开后无法关闭或者关掉后再也打不开。经典的 UI 状态管理混乱。我的做法是给面板做一个状态机Closed / Opening / Open / Closing所有打开和关闭的请求都通过状态机转发不做直接的SetActive。加了状态机之后这类 bug 基本消失了。5.2 渲染类问题材质发灰、没有反射感。九成是场景里没有反射源。URP 里金属材质如果没有 Reflection Probe 或天空盒环境反射是黑的金属就变成黑色。展厅里放几个反射探针重点展品区域单独放一个效果立竿见影。绣品在特定角度下闪白边。这是法线贴图强度过高加上高光过于集中的结果。把法线强度降到 2.0 以下同时把 Smoothness 调低 0.05 到 0.1闪白边基本就没了。贴图在远处出现摩尔纹。这是常见的走样问题。解决办法是给贴图设置各向异性过滤或者在 URP Asset 里开启 Mipmap 混合。注意 mipmap 会让贴图显存增加约三分之一WebGL 端要谨慎。WebGL 端画面整体偏暗。WebGL 的默认色彩空间和光照设置与 PC 端不一致。检查项目设置里的 Color Space 是否为 Linear以及 URP Asset 里的 HDR 和色调映射设置是否在两个平台保持一致。5.3 打包类问题WebGL 打包后打开一片空白。先看浏览器控制台八成是加载失败。常见原因是压缩格式不对——Unity 的 WebGL 默认用 Brotli 或 Gzip 压缩如果服务器没有配置对应的内容编码浏览器就解不开。检查方法是在浏览器网络面板里看.wasm和.data文件的状态码如果是 200 但内容乱码就是压缩头没配对。一体机上跑起来发热严重、掉帧。除了前面说的性能优化还有一个容易被忽略的点一体机是电池供电性能是动态调节的。建议把目标帧率明确设成 72 并开启固定帧率模式让系统知道你的性能需求避免它自作主张降频。同时把渲染分辨率做个降级配置宁可稍微糊一点也要保住稳定帧率——在 VR 里掉帧比糊更难受。PC 端打包后跑在别人电脑上黑屏。检查两点一是显卡驱动版本二是 URP 的 Shader Variant 是否都包含在构建里了。URP 有大量的 Shader 变体如果构建时被剥离掉运行时就会用粉色错误材质显示。在 URP Asset 里勾选保留必要变体或者用一个材质变体收集资产把用到的材质全部收集起来参与构建。6. 发布落地PC、WebGL 与一体机的三套适配6.1 资源压缩与加载策略三个平台的资源策略完全不同共用一套资源包一定会出问题。我最终做成了三套 Quality 配置加一套资源分级核心展品的高清贴图和法线贴图只在 PC 端加载全分辨率版本一体机降一半WebGL 再降一半。做法是把不同分辨率的版本分别打包成 AssetBundle运行时按平台加载对应版本。加载策略上启动时只加载展厅建筑和第一批展品观众往前走的时候后台异步预加载下一区域的资源。这样首屏打开时间从 40 多秒压到了 8 秒左右。提醒WebGL 端要注意内存上限。浏览器给单页面的内存通常有限制资源一次性全加载很容易崩。宁可多做几次小加载也不要一次加载一大包。6.2 各平台参数差异表配置项PC 端一体机WebGL渲染管线预设URP BalancedURP PerformantURP Performant抗锯齿MSAA 4xMSAA 2x关闭阴影软阴影 2048硬阴影 1024关闭后处理Bloom 色调映射 AOBloom弱关闭贴图最大尺寸409620481024各向异性过滤8x4x1x目标帧率607230后处理这块我做了取舍。Bloom 对金银线的高光表现很有帮助PC 端一定要开一体机上开弱档主要为了保帧率WebGL 直接关掉用材质本身的高光来补。环境光遮蔽在 PC 端开能明显增强展柜底部的体积感其他平台关掉。6.3 展馆上线后的真实反馈上线之后收到了一些挺有意思的反馈其中两条让我印象最深。一条是有位做刺绣的老师说她在系统里第一次看清了盘金线是怎么盘出弧度的——平面照片里那只是一条粗金线在 3D 场景里侧着看能明显看到金线是绕着针脚一圈一圈压过去的立体结构。这说明把绣品做成 3D 是值得的它提供的不是更清楚的照片而是照片给不了的观看角度。另一条是有观众反馈走一会儿就晕。排查下来是两个原因一是帧率不稳偶尔掉到 40 帧以下在 VR 里这是大忌二是转身速度没有上限鼠标灵敏度设得太高。后来我把帧率稳定放在第一优先级同时给转向速度加了一个顺滑的限速晕动感明显改善。在虚拟漫游里稳定性永远比画面华丽更重要。整套系统做下来我最大的体会是虚拟展馆这类项目的难点从来不在把模型放进场景而在那些看不见的地方——材质参数、光照烘焙、Draw Call、光标状态、碰撞体的 Skin Width。这些细节每一个单独看都很小但叠加起来就是像玩具和像真展馆的差别。另一个体会是美术和程序一定要在同一个尺度体系里工作模型单位、移动速度、交互距离、LOD 阈值这四个数字必须一开始就商量好并写进规范文档后期改任何一个都会连带影响其他三个。如果你的项目也准备做类似的东西我建议先花两天时间把手绣实物摆在面前用不同的光照条件看它把它到底长什么样这件事吃透再动手建模和调材质这一步省不了。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →