免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Unity 3D+C#核雕虚拟展馆交互漫游与性能优化实战

Unity 3D+C#核雕虚拟展馆交互漫游与性能优化实战 核雕这门手艺我第一次近距离看是在一个做红木家具的朋友店里。老师傅拿出一颗橄榄核用放大镜看上面刻着一整条船船上有窗有帘帘子还能推动。那颗核不到三厘米长摆在桌上跟花生米差不多大。当时我就想这东西要是拍成照片贴在网上基本没人看得清也没人有耐心看。后来有做非遗展陈的朋友找过来问能不能用 Unity 3D 做一个核雕主题的虚拟展馆让观众能自己走进去、走近看、转着看还能听讲解。这篇就把我从头到尾踩过的坑、做过的取舍、跑通过的参数按实操顺序写下来。核心围绕Unity 3D C#这一套技术栈展开讲的是核雕文化主题虚拟展馆的交互漫游怎么做包含资产处理、场景搭建、光照烘焙、交互代码、性能优化和打包发布的完整链条。适合已经会一点 C# 基础、想往 3D 展陈方向转的开发者也适合手里有非遗内容、需要落地一个可交互线上展馆的策划和美术。看完你至少能自己搭出一个能走、能看、能点、能听的基础版本并且知道哪些地方最容易被卡住。1. 先把需求说清楚核雕展馆这件事到底难在哪1.1 核雕的小与展馆的大之间的核心矛盾做这个项目最先要解决的不是技术问题而是尺度问题。核雕的物理尺寸通常在一到四厘米之间橄榄核、桃核、杏核各有不同题材从十八罗汉到核舟、园林、山水细节密度极高。一粒橄榄核上的罗汉面部可能只有两毫米宽但眉毛、衣褶、手指都要交代清楚。而虚拟展馆的访客视角默认是一米七左右的成年人身高如果按真实比例把核雕摆在展柜里观众站在展柜前看到的就是一粒芝麻。这不是美术问题是空间叙事问题。我的处理办法是把真实拆成两层。第一层是展馆空间本身按真实尺度走走廊宽度、展柜高度、门洞尺寸全部按现实比例来让观众的身体感知是正常的。第二层是展品本身做放大陈列把核雕放大到三十到五十厘米放进独立的陈列台旁边配一个实物比例的等比小模型作为参照。这样观众既能看清细节又能通过对比理解它原本有多小。这个放大参照的双层结构是整个展馆设计里最关键的一个决定后面所有的模型精度、光照强度、相机参数都是围绕它来定的。注意展品放大倍数不要一刀切。直径一厘米的珠串类核雕放大到二十厘米左右比较舒服而核舟、园林这类横向展开的作品放大到四十到六十厘米更有叙事感。倍数过大反而会暴露扫描模型的精度不足观众一看就知道是假的。1.2 技术选型为什么是 Unity 3D C# 而不是别的方案选 Unity 3D 配 C#理由很实际。第一是跨端成本低同一个工程可以出 Windows 桌面版给展厅大屏用出 Android 版给平板做移动导览出 WebGL 版挂在网页上做线上入口这三套需求在同一个工程里靠质量分级就能覆盖不需要维护三份代码。第二是 C# 这套语言对做过工具类开发的人很友好。我之前做过一段时间的 C# 上位机用 EasyModbus 读 PLC 寄存器、用 OPC UA 客户端读传感器数据那套事件回调、委托、异步的写法在 Unity 里基本能直接迁移过来尤其是委托和事件系统在做展品点击回调、讲解音频播放完成通知这类逻辑时非常顺手。第三是 Unity 的编辑器扩展能力。核雕展品的讲解文案、朝代、材质、尺寸这类信息是结构化的我用了 ScriptableObject 加自定义的编辑器面板来管理美术和文案不用碰代码就能录数据这个效率提升在项目中期非常明显。至于渲染管线这个项目我选的是 URP 而不是内置管线也不是 HDRP。原因有三个目标是移动端和 WebGLURP 的包体和性能更合适URP 的 Shader Graph 做木头、包浆、玻璃这类材质可视化的效率很高光照烘焙的流程比 HDRP 简单不需要的光追效果在这个项目里也用不上。如果你之前在纠结游戏开发里 C 和 C# 的区别用一句话说清楚C 管性能的底C# 管逻辑的快Unity 把渲染和物理的底层用 C 封好了你写 C# 就是在调度这些能力。这个项目里我一行 C 都没写全部时间花在资产和交互上这就是选 Unity 的收益。1.3 工程目录与模块拆分的实际做法项目一开始我没做模块化所有脚本丢在 Assets/Scripts 下面到第三周就乱了。后来按 Assembly Definition 拆成了五个程序集Core基础工具、事件总线、单例管理、Exhibit展品数据与交互、Visitor访客控制器与相机、UI信息面板、小地图、设置菜单、Build打包与质量分级配置。拆开的好处是改 UI 不会触发整个工程重新编译迭代速度大概快了三分之一而且各个模块的依赖关系一目了然后来加了新人接手也能很快定位。目录结构上我把美术资源和代码严格分开放。Assets/Art 下面分 Models、Textures、Materials、Audio、Lightmaps 五个子目录Assets/Scenes 下面是主场景和各个测试场景。这里有个细节经验光照贴图一定要放在独立目录并且纳入版本管理Unity 烘焙出来的 Lightmap 是二进制文件一旦丢失重新烘焙要几个小时尤其是展柜玻璃多、探针密的场景。源文件我用 Git LFS 管理模型和贴图都设了 LFS 追踪规则不然仓库几个月就膨胀到几个 G。2. 核雕数字资产从实物到可用的实时模型2.1 采集方式的选择与取舍核雕模型的来源有两条路三维扫描和手工建模。我先说结论这个项目最终用的是扫描打底、手工修饰。扫描的好处是形准尤其核雕表面的刀痕走向、镂空结构、镂空内部的通道这些手工建模极难还原。但扫描的原始数据有两个致命问题一是面数极高一个橄榄核扫出来动辄两三百万面二是表面噪声大木质的细微起伏和扫描噪点混在一起直接减面会糊成一片。我的做法是把扫描模型作为高频细节源用高模烘焙法线贴图到低模上。高模保留下来不删只用于烘焙。低模手工重新拓扑核雕的主结构其实不复杂圆雕的罗汉是一个柱状主体加四肢头部核舟是一艘船体加舱室拓扑的重点是把刀刃形成的转折线保留下来因为这些转折线是识别核雕工艺的关键特征。这里提醒一句扫描的环境很重要核雕表面有包浆反光不均匀用白光扫描仪容易在弧面产生过曝我后来改用带偏振滤镜的补光灯才把表面纹理扫干净。2.2 减面、UV与法线贴图的具体参数面数预算我是这么分的单个核心展品也就是观众能拉近仔细看的那几件控制在八万到十五万三角面普通陈列展品控制在五千到两万面展馆建筑和家具这类环境模型单个不超过三千面。这个预算不是拍脑袋来的是按目标平台反推的。Android 中端平板在 URP 下能稳定跑六十帧的三角面总量大概在八十万到一百二十万之间加上剔除后每帧实际提交的面数会少很多所以总量按一百万留余量。法线贴图的烘焙参数我踩过坑。第一次用的是默认的 4K 输出加 16 位精度结果导入后贴图占了将近两百兆移动端直接崩。后来改成主展品用 2K、普通展品用 1K法线精度用 8 位加抖动视觉上几乎看不出差别。烘焙时的 Ray Distance 要根据高低模的偏差来设核雕这种细节密集的模型Ray Distance 设太大会把相邻结构的法线串过去我最后用的是模型包围盒对角线的百分之二左右配合 Cage 手动调整效果比较稳。UV 的展开有个核雕特有的问题镂空结构的内壁。核舟的窗、罗汉的衣褶间隙这些内壁在 UV 上很容易被忽略展品一旦旋转到侧面就会看到没有贴图的纯色面。我的处理是把内壁单独展到 UV 的边角区域贴一张统一的木质底色加噪点牺牲一点细节换取无破绽。2.3 橄榄核的材质与包浆质感怎么做核雕的材质是这个项目里我花时间最多的地方因为像不像几乎全靠它。橄榄核的材质核心是三层底色、刀痕、包浆。底色是深黄褐色但不均匀向阳面和凹陷处色差明显。刀痕是法线贴图负责的前面已经说过。包浆是这一层最容易被做假的地方很多人直接用高光加一点反射就完事看起来像塑料。我的做法是在 URP 的 Lit Shader 基础上做了一个变体用 Smoothness Map 控制不同区域的光泽度。包浆厚的地方也就是经常被手盘到的凸起部位Smoothness 高接近零点八刀痕深处和新刻的部分Smoothness 低零点二到零点三。这张 Smoothness Map 不是随便涂的我是用高模的曲率图Curvature Map作为基础凸起区域自动提亮凹陷区域压暗再手工修一遍。这样渲染出来的核雕转动的时候高光会沿着凸起流动非常接近真品被手盘过的状态。另外补一个次表面散射的近似处理。橄榄核薄的地方在强光下会有微弱的透光感我在 Shader 里加了一个简单的背光透射项用视线和法线的点积控制透射强度数值调到零点一五左右就够了过了就成玉石了。提示包浆的光泽不要用金属度去做核雕终究是木质金属度设为零靠 Smoothness 和法线表现质感一旦给了金属度整体会发灰发脏这是我在预览时第一时间就能看出来的问题。2.4 纹理规范与导入设置贴图规范我定了一套硬性标准。主展品的 Base Color 用 2K法线用 2KSmoothness 和 AO 合到一张 1K 的通道贴图里R 通道放 SmoothnessG 通道放 AO这样能省一半显存。普通展品全部降到 1K。导入设置里有一个必改项把 sRGB 关掉的非颜色贴图要单独标记法线贴图要选 Normal Map 类型并且勾选 Create from Grayscale 的反向处理否则法线会整体翻掉模型看起来像被从里面照亮。纹理压缩在移动端用 ASTC 6x6桌面端用 BC7WebGL 用 DXT5 加一个降级。这个配置我是在 Quality Settings 里按平台分的不用手改。这里有个细节ASTC 的块大小会直接影响包体和画质6x6 是比较平衡的选择如果包体紧张可以降到 8x8视觉上在七寸屏幕上基本察觉不到。3. 展馆空间与光照让微雕细节真正可读3.1 尺度换算与展陈动线设计展馆的空间我参考了传统园林的移步换景思路不是一个大平层摆一排柜子而是用几段短廊和转折把展区串起来。每一个展区有一个主题进门第一区是核雕的历史和工具第二区是橄榄核雕的人物题材第三区是核舟与园林题材最后是一个可以坐下来听讲解的影音区。这样观众的动线是自然流动的不会一进门就被信息糊脸。尺度上走廊净宽我用了二点四米展柜之间的通道留一点五米这个宽度是让人能自然错身又不会觉得空旷的临界值。展柜玻璃面到展品的距离控制在二十到三十厘米太近了观众转身会撞到视觉上的玻璃太远了高光反射会挡住展品。展台高度用了八十五厘米这是站着看最舒服的高度如果考虑儿童和轮椅用户可以在旁边加一个可调视角的观察椅功能用按键把相机降到一米二这个功能在展厅场景里非常实用。3.2 URP光照烘焙参数与Light Probe布点光照是这个项目的性能大头。我全程用烘焙光照实时阴影只留给访客自己的投影而且用的是一个很轻的 Blob Shadow 方案而不是实时阴影贴图。烘焙参数上主展品所在的展区光照贴图密度给到每单位六十到八十 texel环境区域降到二十到三十。光照贴图的尺寸单张上限设成 2048超过就拆多张否则移动端采样会很慢。Light Probe 的布点决定了动态物体和展品在场景里的受光是否自然。我的经验是探针不要均匀撒要贴着光和面的交界处加密。展柜玻璃边缘、射灯照下来的明暗分界、走廊两个光源之间的过渡带这些地方探针间距设零点五米其他区域一米到一点五米。布完之后一定要用场景视图里的探针可视化检查一遍看有没有探针落在墙壁内部那种探针会采集到错误的颜色导致展品在某个位置突然变绿或者变暗。3.3 展柜玻璃、反射探针与光照细节展柜玻璃是展陈类项目的经典难题。用完全透明的材质观众会看到玻璃后面什么都没有因为实时反射算不出来用高反射材质整个展区会被反射成一片白。我的方案是玻璃做成半透明加低强度反射同时在场馆里放几个烘焙好的 Reflection Probe位置放在每个展区的中心偏上尺寸覆盖整个展区。这样玻璃上反射出来的是烘焙好的场景看起来有内容成本又是零。玻璃的透明排序也是坑。展品在玻璃后面玻璃在观众前面如果渲染队列设置不对展品会被玻璃的透明层覆盖掉。处理办法是把玻璃材质的 Render Queue 调到 Transparent 之后并且给玻璃加一个不写深度的设置同时展品材质保持 Opaque。如果展品本身有透明部件比如核雕里偶尔会配的丝线那就要单独处理我一般给这类部件单独一个渲染层按距离手动排。4. 交互漫游的代码实现4.1 第一人称访客控制器怎么写才跟手访客控制器我用了 CharacterController 而不是刚体原因是展馆里不需要物理碰撞的拟真感需要的是稳定和跟手。刚体做第一人称很容易在斜面和台阶上抖动CharacterController 的 Move 方法直接给位移行为可预测得多。输入我用的是新版 Input System因为它能同时支持键鼠、手柄和触屏不用自己写三套适配。核心代码结构是这样using UnityEngine; using UnityEngine.InputSystem; [RequireComponent(typeof(CharacterController))] public class VisitorController : MonoBehaviour { [Header(移动)] [SerializeField] private float walkSpeed 2.2f; [SerializeField] private float runSpeed 4.4f; [SerializeField] private float acceleration 12f; [Header(视角)] [SerializeField] private Transform cameraRoot; [SerializeField] private float lookSensitivity 0.08f; [SerializeField] private float pitchLimit 78f; [Header(头部起伏)] [SerializeField] private float bobAmplitude 0.032f; [SerializeField] private float bobFrequency 7.5f; private CharacterController controller; private Vector2 moveInput; private Vector2 lookInput; private Vector3 velocity; private float pitch; private float bobTimer; private Vector3 cameraLocalOrigin; private float currentSpeed; private void Awake() { controller GetComponentCharacterController(); cameraLocalOrigin cameraRoot.localPosition; } public void OnMove(InputAction.CallbackContext ctx) moveInput ctx.ReadValueVector2(); public void OnLook(InputAction.CallbackContext ctx) lookInput ctx.ReadValueVector2(); private void Update() { HandleLook(); HandleMove(); HandleBob(); } private void HandleLook() { pitch - lookInput.y * lookSensitivity; pitch Mathf.Clamp(pitch, -pitchLimit, pitchLimit); cameraRoot.localRotation Quaternion.Euler(pitch, 0f, 0f); transform.Rotate(Vector3.up, lookInput.x * lookSensitivity); } private void HandleMove() { Vector3 wish transform.right * moveInput.x transform.forward * moveInput.y; if (wish.sqrMagnitude 1f) wish.Normalize(); float targetSpeed moveInput.y 0.1f IsRunning() ? runSpeed : walkSpeed; currentSpeed Mathf.Lerp(currentSpeed, wish.magnitude * targetSpeed, acceleration * Time.deltaTime); if (controller.isGrounded velocity.y 0f) velocity.y -2f; velocity.y -9.81f * Time.deltaTime; Vector3 motion wish.normalized * currentSpeed Vector3.up * velocity.y; controller.Move(motion * Time.deltaTime); } private bool IsRunning() Keyboard.current ! null Keyboard.current.leftShiftKey.isPressed; }移动速度这里我调过很多次。走路二点二米每秒跑步四点四米每秒这个比例接近真人。视角灵敏度零点零八是配合鼠标 DPI 一千六调的如果你的鼠标 DPI 不一样这个值要改。头部起伏这块是个小细节但很关键起伏幅度零点零三二米、频率七点五这两个数是让走动时有轻微的身体感但幅度一旦超过零点零五就会晕。我测试时找了几个人试晕动反应最明显的就是起伏幅度过大和视角加速度太陡所以视角旋转我特意用了直接映射而不是加阻尼看似不那么高级但实际体验更稳。4.2 射线拾取与展品高亮反馈展品的点击拾取用的是 Physics.Raycast从屏幕中心发射距离限制三点二米这个距离是手臂加一步的合理范围。射线只打展品所在的 Layer避免误打到玻璃和墙壁。拾取的高亮我用的是 URP 的 Renderer Feature 做描边原理是把选中的展品再画一遍用一层外扩的纯色看起来就是一圈轮廓光。using UnityEngine; public class ExhibitInteractor : MonoBehaviour { [SerializeField] private Camera viewCamera; [SerializeField] private float maxDistance 3.2f; [SerializeField] private LayerMask exhibitMask; [SerializeField] private ExhibitPanel panel; [SerializeField] private CrosshairView crosshair; private ExhibitView hovered; private ExhibitView selected; private void Update() { ExhibitView hit Probe(); if (hit ! hovered) { hovered?.SetHighlight(false); hovered hit; hovered?.SetHighlight(true); crosshair.SetState(hovered ! null); } if (hovered ! null InteractPressed()) { selected hovered; panel.Open(selected.Data); } } private ExhibitView Probe() { Ray ray new Ray(viewCamera.transform.position, viewCamera.transform.forward); if (Physics.Raycast(ray, out RaycastHit info, maxDistance, exhibitMask, QueryTriggerInteraction.Collide)) { return info.collider.GetComponentInParentExhibitView(); } return null; } private bool InteractPressed() { var mouse UnityEngine.InputSystem.Mouse.current; var touch UnityEngine.InputSystem.Touchscreen.current; bool pressed (mouse ! null mouse.leftButton.wasPressedThisFrame) || (touch ! null touch.primaryTouch.press.wasPressedThisFrame); return pressed; } }GetComponentInParent这个写法是有意为之的。核雕模型的碰撞体如果直接加在整体上镂空区域的射线会穿透所以我给展品加了一层简化碰撞体父物体挂 ExhibitView 脚本射线打到子碰撞体再往上找脚本这样镂空结构不会出现点击不到的情况。4.3 展品数据用ScriptableObject驱动展品的讲解信息我全部放在 ScriptableObject 里一个展品一个 Asset。这么做的好处是不用碰代码文案改完直接生效而且打包时这些数据是预处理的运行时零解析成本。using UnityEngine; [CreateAssetMenu(menuName Hediao/Exhibit Data, fileName Exhibit_)] public class ExhibitData : ScriptableObject { [Header(基本信息)] public string id; public string displayName; public string dynasty; public string material; public string sizeText; public string artisan; [Header(说明)] [TextArea(3, 12)] public string description; public string[] craftTags; [Header(多媒体)] public AudioClip narration; public Sprite thumbnail; public string[] relatedIds; [Header(观察参数)] [Range(1f, 3f)] public float focusDistance 1.4f; [Range(0f, 60f)] public float initialYaw 0f; }relatedIds这个字段是做展品之间的关联用的讲解面板底部会显示相关展品点一下就跳到下一件。这个功能在展馆里很实用核雕的题材是互相呼应的比如核舟和园林题材之间就有传承关系。数据录入我用了一个自定义的 EditorWindow把当前选中的展品列表做成表格支持批量编辑和批量校验比如检查有没有漏填讲解音频、有没有重复 id这个工具比手点 Inspector 快太多特别是展品数量超过五十件之后。4.4 楼层小地图用Render Texture方案实现小地图这件事Unity 3D 里有好几种做法最简单的是用一个正交相机从正上方往下拍把画面渲染到一个 Render Texture 上然后显示在 UI 的 RawImage 里。我用的就是这套因为展馆是单层为主、局部有夹层的结构俯视视角足够表达。如果展馆是多层结构就要按楼层切换做法是给每一层准备一个正交相机的拍摄层掩码用楼层按钮切换可见层。using UnityEngine; using UnityEngine.UI; public class MiniMapController : MonoBehaviour { [SerializeField] private Camera mapCamera; [SerializeField] private RectTransform marker; [SerializeField] private RectTransform mapRoot; [SerializeField] private Transform visitor; [SerializeField] private float worldRadius 18f; private void LateUpdate() { Vector3 local visitor.position - mapRoot.position; Vector2 uv new Vector2(local.x / (worldRadius * 2f), local.z / (worldRadius * 2f)); marker.anchoredPosition new Vector2(uv.x * mapRoot.rect.width, uv.y * mapRoot.rect.height); marker.localRotation Quaternion.Euler(0f, 0f, -visitor.eulerAngles.y); } }小地图的相机我设成正交Size 设成十八意味着地图显示的是直径三十六米的区域刚好覆盖整个展馆。相机高度放在十五米只拍地面层和展台。有几个坑要说清楚小地图相机的 Culling Mask 一定要排除访客自己、UI 层和特效层否则地图上会看到一个跟着你转的大圈小地图的更新频率不用每帧我改成每两帧更新一次肉眼完全看不出延迟但能省下一点开销地图上的标记用 UI 旋转而不是 3D 物体旋转性能更好。4.5 特写观察模式的设计与实现看核雕最重要的功能其实是特写。观众点开一件展品之后要能把它转到任意角度、拉到最近看刀痕。我实现的方式是打开面板的同时切换到一条独立的观察相机相机围绕展品中心做轨道旋转滚轮控制距离展品本身不动或者做微小的自转。这样做而不是让访客走得更近是因为展柜有物理限制而且人走太近容易产生视角穿模。观察相机的轨道半径我用focusDistance控制默认一点四米最小可以拉到零点三五米这个距离下核雕的刀痕在屏幕上能占到半屏。旋转用四元数做垂直方向限制在正负六十度避免转到展品底下去。这里有个手感细节拖拽旋转的时候要加一个速度平滑松手之后有一点点惯性衰减千万不要做无摩擦的匀速旋转那种感觉像在转一个与你不相干的物体代入感会差很多。5. 性能优化与多端发布5.1 Draw Call、批处理与剔除的实操展馆场景里最容易失控的就是 Draw Call。一个展区如果有五十件展品每件展品有三四个材质球加起来就是一百多到两百个 Draw Call加上建筑、家具、灯光图标很容易冲到四百以上。我的优化顺序是这样先把同材质的静态物体用 Static Batching 合批把展馆建筑和固定家具全部标记为 Static再把展品的材质合并我要求美术把所有核雕贴图展在一张图集上材质只用同一个靠 UV 区分这样所有展品理论上可以合批。遮挡剔除Occlusion Culling一定要烘焙但参数要压。默认的 Smallest Occluder 设得太小会让烘焙时间爆炸我把最小遮挡物尺寸设成展柜的尺寸也就是一米左右Smallest Hole 设成零点三米这样既能剔除掉墙后看不见的展区又不会把核雕这种小物件算进去。剔除烘焙完之后一定要在场景里用可视化面板走一圈检查有没有本该被剔除的区域没剔掉通常出现在镂空隔断和玻璃展柜附近需要手动补几个遮挡体。5.2 Addressables资源管理与内存控制资源加载我用的是 Addressables把展馆按展区分成几个 Group访客走到某个展区才异步加载该区的展品资源离开一定距离后释放。这个策略对移动端特别重要因为核雕模型和四 K 贴图加起来内存占用很高全部常驻会导致中低端设备直接闪退。内存上限我按设备分了档中低端安卓限制在一点二 G高端安卓和 iOS 限制在两点五 G桌面端不限制。加载时机上有个技巧不要在观众刚走到展区边界就开始加载那样会有明显的卡顿要提前一个展区的距离预加载同时用一个低模的占位模型先顶上等真模型加载完再替换视觉上有个渐变观众感知不到。占位模型我用的是展品的简化版本面数只有百分之五看起来远看完全一样。音频资源也要管。讲解音频我用的是流式加载Streaming不常驻内存播放时从磁盘读代价是有几十毫秒的启动延迟所以按下播放按钮之后先播一个很短的提示音再开始正式音频把这几十毫秒藏起来。5.3 移动端与WebGL的分级策略同一套工程出多个版本我用的是 Quality Settings 加一个自定义的配置 Asset。桌面端开全部特效光照贴图用最高分辨率抗锯齿用 MSAA 四倍移动端关掉实时阴影、降光照贴图采样、抗锯齿改成 FXAA同时把渲染分辨率缩放到百分之八十五WebGL 版本最省关掉所有后处理和反射探针纹理全部降一档并且把背包大小限制在能用内存加载整个场景的范围内。WebGL 有个比较容易踩的坑就是不能在主线程上做长时间计算否则整个页面会卡死。我遇到过一次展品数据初始化时的解析卡顿原因是把所有展品的 JSON 一次性反序列化。后来改成用 Addressables 分帧加载每次加载五件中间用协程让出时间片页面就顺了。协程这个工具在 WebGL 上比多线程可靠得多因为 WebGL 本身不支持真正的多线程用 Task 反而容易出问题。6. 常见问题排查实录6.1 模型与材质类的典型问题问题最多的一类出现在模型导入之后。最典型的是模型看起来发灰法线方向反了原因是扫描模型在导出时坐标系的 Y 轴方向和 Unity 不一致需要在导入设置里改一下烘焙轴向或者让美术在导出时统一成 Y 轴向上。第二类问题是 UV 接缝处的黑线通常是 UV 边缘留白不够我要求展品在 UV 边距留至少四像素法线贴图留八像素导入时把 Mipmap 的边界处理好这个问题基本就消失了。第三类是镂空内部发黑。核雕的镂空结构多光照贴图烘焙时如果密度不够内部会一片死黑。处理办法是把镂空区域的展品单独设一个光照贴图密度更高的 Lightmap Parameters或者在展柜里补一个很弱的发光面作为环境补光这个补光的强度调到零点一就够了多了会破坏整体的光影层次。6.2 交互与输入系统的问题新版 Input System 和旧版很容易冲突如果导入的某个包里带了旧的 Input 代码可能会出现既收不到输入又不报错的情况。排查方法是看 Project Settings 里的 Active Input Handling 设成了哪一种如果设成两者都支持那就要小心两套系统抢同一个按键。我的建议是整个工程统一用新版把旧版的 Input Manager 配置清空。触屏和鼠标的兼容也有坑。移动端上点击 UI 面板的时候射线会从屏幕中心打出结果就是用户点面板按钮的同时触发了场景里的展品交互。这个问题的根因是没有判断点击位置是否落在 UI 上。解决办法是在输入处理前用 EventSystem 的IsPointerOverGameObject判断一下如果指针在 UI 上就直接跳过场景交互。这个判断在移动端要用触摸的 fingerId不是默认的无参版本。6.3 构建与打包阶段的问题打包阶段的问题通常集中在资源和平台设置上。Android 上最常见的是纹理压缩格式没选对导致贴图在设备上糊成马赛克或者反过来包体大到装不下。我的配置是 ASTC、按设备分级、打包前用一个脚本检查所有贴图的格式和尺寸不符合规范的直接报错中断构建这个脚本后来帮我省了无数次返工。WebGL 上最常见的是内存溢出。浏览器对内存的限制比原生平台严得多尤其是移动浏览器。我在 WebGL 版本里把展品分辨率全部降一档纹理上限设到一 K同时把音频全部转成流式包体从最初的一百八十兆压到了七十兆左右。还有一点WebGL 的首次加载时间要控制好我用的是压缩加载加一个进度条加载过程中显示核雕的历史介绍文字把等待变成阅读时间用户流失会少很多。6.4 常见问题速查表现象最可能的原因处理办法展品整体发灰、像从内部发光法线贴图通道方向错误检查法线导入类型与绿通道翻转设置镂空内部全黑光照贴图密度不足单独提高该展品的光照参数或补弱光源点击展品无反应射线被玻璃或碰撞体阻挡分层遮挡、给展品加独立简化碰撞体触屏点 UI 时误触场景未判断指针是否在 UI 上用 IsPointerOverGameObject 提前拦截移动端运行几分钟后闪退纹理常驻内存超限用 Addressables 分区加载并释放小地图上出现跟随圆圈相机 Culling Mask 未排除访客调整掩码排除玩家层与特效层WebGL 页面卡死主线程长时间计算拆分成协程分帧执行玻璃后面的展品看不见透明渲染队列顺序错误调整 Render Queue玻璃不写深度7. 内容表达与后续可扩展的方向7.1 讲解内容的组织与音频处理技术搭完之后展馆能不能留住人其实取决于内容怎么讲。核雕这个题材有个特点作品本身很小故事却很大一颗核上刻一条船背后是整部文学的典故。我的处理是把讲解文字按三层组织第一层是一句话概括进门看板上的大字三秒读完第二层是三百字以内的说明点在展品上弹出来第三层是音频讲解一到两分钟配合展品自动旋转观众愿意听就能听完整。音频的处理有个小技巧讲解音频开头不要直接讲先留一点环境音比如木工刻刀的声音把观众拉进氛围里再开始讲。音频的响度要统一我用的是负十六 LUFS 的响度标准不同讲解员录的声音大小差异很大如果不在后期统一观众每换一件展品就要调一次音量体验会很碎。7.2 从单馆扩展到多馆的技术预留这个项目做下来我最大的一个感受是数据一定要和场景解耦。展馆的建筑、灯光、展台这些是场景资源展品和讲解是数据资源两者通过一个配置表关联。这样以后要新增展区美术只做场景策划只配数据程序基本不用动。我在工程里预留了一个展馆配置的 ScriptableObject里面记录了每个展区的场景名、加载地址、展品 id 列表扩展的时候只要加一条记录。另一个预留是展品的观察参数已经数据化了focusDistance、initialYaw这些都在 Asset 里将来如果要做不同尺寸的展馆展品不用重新调。我还留了一个接口是做展品对比的可以同时把两件核雕放在一个观察空间里左右旋转对比这在讲解同一题材不同艺人的时候会非常有用比如同是核舟题材不同年代的处理手法差别很大放在一起看比单独讲十句话都清楚。我个人在实际项目里体会最深的一点是这类文化主题的虚拟展馆技术只是门槛真正决定成败的是你对内容的理解深度。我为了让模型准跑了三趟老师傅的工作室看他怎么下刀、怎么选核、怎么抛光这些观察最后都变成了材质参数和光照设计里的判断依据。那些参数不是从文档里查来的是从实物上看来的。如果你也准备做类似的项目建议在动手写第一行代码之前先拿真东西仔细看两天看过和没看过做出来的东西完全是两个层次。
返回列表