恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Unity 3D+C#苏绣虚拟展馆交互漫游与材质性能优化
首页
资讯中心
/
Unity 3D+C#苏绣虚拟展馆交互漫游与材质性能优化
Unity 3D+C#苏绣虚拟展馆交互漫游与材质性能优化
发布时间:2026/9/18 11:11:37
从苏州本地一个非遗数字化项目说起。去年团队接到一个需求:把苏绣的经典作品搬进一个能自由走动的3D展厅,观众用鼠标键盘或平板就能在展厅里逛,走到哪件绣品前就弹出对应的讲解。技术栈在合同里写死了——Unity 3D C#,交付形式是 Windows 可执行程序加一个轻量网页版本。这套苏绣文化主题虚拟展馆交互漫游系统前后折腾了两个多月,从白盒搭景、绣品材质还原、第一人称漫游控制器,到一个能显示楼层和展位的小地图,中间返工过三次。下面把我实际做下来的完整思路和踩过的坑写清楚,包括为什么某些地方必须那样做,以及哪些参数是调了很多遍才定下来的。不管你是刚接触Unity 3D的初学者,还是已经在写C#业务逻辑、想转到交互漫游方向的开发者,这篇都能拿去对照着复现。1. 苏绣题材为什么值得做成可漫游的3D展馆先说清楚这个项目的性质。它不是游戏,没有战斗、没有任务、没有分数,唯一的交互目标是看得清、走得顺、找得到。这三点决定了后面所有技术选型的取向:性能要让位于画质,画质要让位于绣品细节的真实还原,而绣品细节又必须服务于观众的观看体验。1.1 苏绣展陈在数字世界里的三道坎第一道坎是细节密度。一幅双面绣屏,观众在实体展馆里会凑近到20厘米去看针脚的走向、丝线的光泽、正反两面图案的差异。如果在3D场景里只贴一张2048的贴图,凑近就是一片模糊。这意味着贴图分辨率、法线贴图的精度、材质的各向异性都要认真对待,不能用做手游的那套能省则省。第二道坎是光线的敏感性。苏绣用的蚕丝线,在侧光下会呈现非常明显的方向性反光,一束光打过去,绣面上一部分亮一部分暗,针脚的立体感全靠这个。而如果场景里用的是普通的漫反射材质,绣品会变成一块死板的平面,完全失去神韵。这是这个项目里最花时间的一部分,后面第3节会展开。第三道坎是空间比例的真实感。虚拟展馆最容易犯的毛病是做得太大、太空,观众走进去像站在体育馆里。真实的展馆通道宽度大约2.4到3米,展柜离墙60到80厘米,人眼高度在1.6到1.7米之间。这些数字如果不对,走起来就会有一种说不出的别扭。1.2 为什么是Unity 3D C#,而不是别的方案当时也考虑过用UE做,最后否掉的原因很实际:团队里三个人,两个熟悉C#,一个做前端。UE的蓝图系统虽然上手快,但一旦逻辑复杂起来,蓝图会变成一团线,维护成本很高;而C#写展品数据加载、交互状态机、导航逻辑这一类业务代码,效率要高得多。另一条路是纯Web3D,用Three.js。这条路在打开浏览器就能看这个诉求上确实有优势,但绣品的材质还原是硬伤——没有成熟的URP级渲染管线,各向异性反射、次表面散射这些东西实现起来成本极高。所以最终方案是:Unity 3D做主体,负责高质量的漫游体验;Web版本作为降级方案,压缩贴图、砍掉部分特效,只保留基本漫游和讲解。提示:如果项目预算是一次性做完不再维护,那选型可以激进一点;但如果后续还要加新展品、换新展厅,一定要把数据配置和渲染逻辑解耦,否则每次加一幅绣品都要重新连材质,那是灾难。2. 展馆空间的第一版白盒:动线、比例与碰撞边界这个项目我做的第一件事不是建模型,而是用Cube在场景里搭了一个白盒——只有几何体、没有材质、没有贴图,用来验证空间比例和漫游动线。这一步花了两天,但省掉了后面至少两周的返工。2.1 动线设计决定了场景的骨架苏绣展馆的动线我设计成回字形:观众从入口进入,沿着外围走廊顺时针走一圈,中间是一个天井式的挑空中庭,放一幅大型双面绣作为视觉焦点。这样做的理由是这样的人流不会对撞,而且中庭的挑空能让空间不显得压抑。具体到尺寸,我定的参数如下:空间要素实际尺寸说明主通道宽度3.0 m两人并排通过不局促展柜离墙距离0.7 m便于近距离观看层高4.2 m留出顶部灯槽和挑空感中庭挑空宽度6.0 m放大型双面绣刚好观众默认眼高1.65 m按亚洲成年人平均值取单件展品观察距离0.8 ~ 1.5 m触发交互的合理范围这些数字不是拍脑袋来的。我拿卷尺去本地一个美术馆量了一圈:通道的实际宽度、展柜的进深、人站在画前的习惯距离,全部记录下来,回来等比换算。这一步看似笨,但效果非常明显——测试的时候同事第一句话是这里走起来像真的馆。2.2 模块化搭建与场景拼装白盒验证通过之后,进入正式建模阶段。我没有一块一块地建墙,而是把展厅拆成几个可复用的模块:标准展墙单元(3米宽)、转角单元、展柜单元、门窗单元、顶部灯槽单元。每个模块单独建,单独展开UV,然后在Unity里拼。这样做的收益有两块。一是批次合并友好:同一种材质的模块在场景里可以走静态合批,Draw Call数量能压下来。二是后期改布局成本低:如果某一面墙想加长,直接多拼一个标准单元就行,不用重新处理UV,也不会出现贴图拉伸。展墙的UV我用的是三平面映射(Box Projection)的思路,因为在URP里如果墙面贴图出现拉伸,观众一眼就能看出来,而模块化拼接恰好能规避这个问题。2.3 碰撞体不是随便加的这是新手最容易忽略的地方。展馆里需要碰撞的东西只有三类:地面、墙面、展柜外壳。其它装饰物(比如挂饰、屏风花纹、天花板造型)一律不给碰撞体。原因很直接:每个Mesh Collider都要参与物理计算,场景里有几百个装饰件的时候,帧率会掉得很明显。我把可漫游区域用一面面Box Collider围起来,形成一个封闭的笼子,观众永远走不出这个笼子,也永远不会穿进展柜里。注意:如果你的展柜需要观众走近时玻璃自动变透明,那玻璃就必须要有一个Trigger Collider来检测玩家距离,但不要给物理碰撞。Trigger和Collider要分清楚。3. 绣品材质的还原是整件事成败的关键绣品是这个项目的灵魂,材质做砸了整个馆就没意义了。这一节写得细一些,因为它是区别于普通展厅项目的核心难点。3.1 绣品贴图的采集:别用手机随手拍最早的贴图是同事用手机对着绣品拍的,结果一塌糊涂:闪光灯打在缎面上形成一片白斑,颜色也偏得厉害。后来改成两种方式采集。一种是现场棚拍:带两盏柔光灯,45度侧打,相机固定在三脚架正对绣品,用偏振镜消除缎面反光。拍出来的图颜色准、纹理清晰,后续能直接裁切。另一种是交叉偏振法:在镜头前加一块偏振镜,同时在灯前也加一块,两块偏振片旋转到互相垂直,这样能把缎面那种镜面反光的成分全部滤掉,只留下丝线本身的颜色和纹理。这个方法拍出来的图偏暗,但后期在Photoshop里提亮还原后,细节保留得最好。采回来的图我做了三件事:裁切到实际绣品的比例、去色偏(用白平衡卡校准)、生成一张法线贴图。法线贴图的生成方法是把彩色图转成灰度,再用高通滤波提取纹理,最后转成法线。这一步是为了让针脚的立体感保留下来。3.2 URP Lit Shader下的蚕丝参数URP的Lit Shader里能调的参数就那么几个,但每个参数对绣品的影响不一样。我实测下来的一组参数如下:参数缎面底布绣线部分说明Base Map裁切图裁切图分辨率不低于 4096Metallic0.00.05蚕丝不是金属,最多给一点点Smoothness0.450.6绣线比底布更亮一些Normal Map弱强度 0.3强度 0.8针脚立体感主要靠它Anisotropy0.60.75URP需要开启各向异性Occlusion无无绣品本身没有AO,不要硬加这里要重点说的是各向异性(Anisotropy)。URP默认的Lit Shader不带各向异性,需要切换到Lit Shader的Specular Highlights Anisotropic变体,或者用Shader Graph自己搭一个各向异性节点。蚕丝线是一根一根平行的,光线沿丝线方向反射和垂直方向反射强度不一样,这个效果做出来之后,绣面在侧光下有明显的流光感,非常接近真实。如果不用各向异性,退一步的方案是在法线贴图里做文章:把针脚的方向性做进法线的切线方向,配合较低的粗糙度,也能看出一点方向感,但效果差一截。3.3 展柜玻璃的反射处理展厅里展柜的玻璃是另一个麻烦。如果用完全透明的材质,观众凑近的时候看不到自己的反射,会觉得这个玻璃是假的。如果用真实的反射探针(Reflection Probe),每个展柜一个,渲染开销又太大。我采用的折中方案是用一张**预渲染的立方体贴图(Cubemap)**作为全局反射源,配合一层很淡的Fresnel叠加,玻璃在边缘处反射强、正视处接近透明。这样既有玻璃的质感,开销也可控。方形展柜的朝向不同,反射方向会略有差异,但因为展厅整体色调统一,视觉上不容易穿帮。提示:如果展厅里玻璃展柜数量超过十组,一定要用共享的Cubemap,不要每个展柜配独立的反射探针,不然后面优化的坑等着你。4. C#角色控制器:把逛展的手感做出来漫游手感决定了观众愿不愿意在馆里多待。这一节讲控制器,包括为什么不用Rigidbody、视角参数怎么定、以及双端输入怎么处理。4.1 为什么用CharacterController而不是Rigidbody这是一个被问过很多次的选择题。Rigidbody走的是物理模拟,好处是能撞、能推、能滑;但展馆里不需要物理玩法,观众不会去撞墙,更不会去推展柜。Rigidbody的代价是:每次移动都要参与物理更新,地面检测、斜坡阻挡、台阶攀爬都要额外处理,而且最要命的是——它会惯性滑行,观众松开按键之后还会往前滑一小段,这在逛展场景里是明显的负面体验。CharacterController则是指哪走哪,位移是直接算出来的,松手就停,没有惯性。它自带斜坡处理和台阶检测,不需要额外的物理参与,性能也更友好。所以这个项目里选择的是CharacterController。4.2 视角与控制的核心脚本下面是我实际使用的第一人称漫游脚本,做了简化处理,把跟项目无关的逻辑去掉了。using UnityEngine; [RequireComponent(typeof(CharacterController))] public class VisitorWalker : MonoBehaviour { [Header(移动参数)] public float walkSpeed 1.7f; // 逛展速度,比常规FPS慢很多 public float runSpeed 3.2f; // 按住Shift加速 public float gravity -14f; // 稍微加大重力,落地更干脆 public float stepDown 0.3f; // 台阶下沉容差 [Header(视角参数)] public Transform cameraPivot; // 挂在角色下的摄像机枢轴 public float mouseSensitivity 2.0f; public float pitchMin -65f; // 低头极限 public float pitchMax 65f; // 抬头极限 private CharacterController controller; private Vector3 velocity; private float pitch; void Awake() { controller GetComponentCharacterController(); } void Update() { HandleLook(); HandleMove(); } // 处理鼠标/触摸视角旋转 private void HandleLook() { float mx Input.GetAxis(Mouse X) * mouseSensitivity; float my Input.GetAxis(Mouse Y) * mouseSensitivity; // 水平旋转作用在角色身上,垂直旋转只作用在摄像机枢轴上 transform.Rotate(Vector3.up * mx); pitch - my; pitch Mathf.Clamp(pitch, pitchMin, pitchMax); cameraPivot.localEulerAngles new Vector3(pitch, 0f, 0f); } // 处理移动 private void HandleMove() { float h Input.GetAxisRaw(Horizontal); float v Input.GetAxisRaw(Vertical); Vector3 dir transform.right * h transform.forward * v; dir.y 0f; if (dir.sqrMagnitude 1f) dir.Normalize(); // 斜向不加速 float speed Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed; Vector3 horizontal dir * speed; if (controller.isGrounded velocity.y 0f) { velocity.y -2f; // 贴地,防止浮空累积 } velocity.y gravity * Time.deltaTime; controller.Move((horizontal velocity) * Time.deltaTime); } }这段代码里有几个值得说明的细节。速度为什么是1.7而不是5。常规FPS游戏里玩家移动速度在4到6之间,但那是为了战斗和躲闪。逛展不需要快,1.7米每秒差不多是真人走路稍快的速度,配合1.65米的眼高,空间尺度的真实感就出来了。我在测试时让同事盲测,1.7和2.5两个版本,大多数人说1.7更像在走。视角俯仰限制在±65度。如果放开到±90度,观众抬头时会看到天花板之外的空白,低头时会看到脚下的穿模,都会破坏沉浸感。限制到65度刚好够看清展品的全貌,也不会暴露场景的边界。速度归一化那一行很重要。如果同时按下W和D,不归一化的话合速度是单方向速度的1.414倍,走起来会莫名其妙地快一拍。这个细节在写代码的时候很多人会漏掉。4.3 双端输入方案这个项目要在 PC 和平板两端跑,输入方式完全不同:PC用键鼠,平板用触摸摇杆加滑动转视角。我的做法是把输入抽象成一层接口:public interface IMoveInput { Vector2 GetMoveDir(); Vector2 GetLookDelta(); bool IsRunning(); }PC 端实现一套,读取Input.GetAxisRaw;移动端实现一套,摇杆用OnDrag累加偏移。角色控制器只依赖接口,不关心输入从哪来。这样切换平台的时候,逻辑代码一行都不用改。提示:平板的触摸视角建议加一个很小的死区(dead zone),否则手指没动的时候视角会缓慢漂移,玩起来非常难受。5. 展品交互:从射线检测到讲解面板的数据流观众走到绣品前,点击它,弹出的面板显示名称、年代、尺寸、针法、作者和一段讲解文字。这套流程看起来简单,但数据怎么存、怎么查、怎么和3D对象挂钩,有很多讲究。5.1 交互对象的分层我把所有可交互的展品放在一个专门的 Layer 上,叫Interactable。然后射线检测的时候只检测这个层:private int interactMask; void Start() { interactMask LayerMask.GetMask(Interactable); } void Update() { if (Physics.Raycast(cam.position, cam.forward, out RaycastHit hit, 4.0f, interactMask)) { var item hit.collider.GetComponentInParentExhibitItem(); if (item ! currentItem) { SetHighlight(currentItem, false); currentItem item; SetHighlight(currentItem, true); } if (Input.GetMouseButtonDown(0)) { DetailPanel.Instance.Show(item.Data); } } else if (currentItem ! null) { SetHighlight(currentItem, false); currentItem null; } }把检测层单独划出来,好处是不会误触到墙、地、装饰件,射线距离控制在4米,观众必须走近才能触发,远处乱点不会弹出面板。高亮用的是 Outline 效果——在绣品后面叠一层放大的轮廓网格,或者用 URP Renderer Feature 做边缘描边。我选的是前者,因为描边Feature在低端平板上开销偏大。5.2 展品数据用 ScriptableObject 还是 CSV一开始我把数据写在 CSV 里,理由是方便策划用 Excel 编辑。后来发现两个问题:一是 CSV 里的中文路径在打包后经常读不到,二是每加一个展品都要改 CSV 又要保证路径没错,很容易出错。换成ScriptableObject之后就好多了。每一件绣品对应一个.asset文件,里面存:名称、年代、尺寸、针法、作者、讲解正文、正反面贴图引用、交互碰撞体尺寸。这些字段在 Inspector 里可以直接拖拽引用资源,不会出现路径错误,而且能配合 AssetBundle 做分资源打包。字段名类型用途displayNamestring面板标题erastring年代,如清末sizeVector2实际尺寸(厘米)stitchstring针法,如平针、乱针authorstring绣娘/绣庄descriptionTextArea讲解正文frontTexTexture2D正面高清图backTexTexture2D背面高清图(双面绣用)panelAnchorTransform讲解面板弹出的锚点讲解正文我建议控制在150到250字之间。太短说不清,太长观众不愿意读。事实上一段300字以上的讲解,90%的人会直接关掉面板。5.3 面板 UI 用 World Space 还是 Screen Space这是另一个需要想清楚的问题。Screen Space 的面板固定在屏幕上,不随视角移动,优点是文字始终清晰、不会被遮挡;World Space 的面板挂在展品旁边,像是悬浮在空中的说明牌,沉浸感更好但文字容易糊。我的选择是混合:讲解文字用 Screen Space Overlay,保证可读性;同时在展品旁边放一个小的 World Space 铭牌,显示作品名和年代,增加真实感。这样既不影响阅读,也不破坏空间感。6. 楼层小地图与展品导航小地图这个需求在立项时没写进合同,是后期评审会上临时加的。原因是测试的时候有人反馈走进去就迷路了,不知道自己在哪一层、哪一边。这一节讲小地图的实现。6.1 正交相机方案小地图最直接的做法是在场景上方挂一个正交相机,从顶部垂直往下拍,把拍到的画面渲染到一张 Render Texture 上,再把这个 Render Texture 贴到 UI 的 RawImage 上。核心配置如下:public class MinimapCamera : MonoBehaviour { public Transform player; public float mapHeight 25f; public Vector2 mapBounds new Vector2(30f, 22f); void LateUpdate() { if (player null) return; // 相机紧跟玩家,但只跟随 XZ 平面 Vector3 p player.position; transform.position new Vector3(p.x, p.y mapHeight, p.z); // 相机始终朝下 transform.rotation Quaternion.Euler(90f, player.eulerAngles.y, 0f); } }注意最后那行旋转——把相机绕 Y 轴跟着玩家的朝向转,得到的是旋转地图(玩家图标固定朝上,地图转);如果只让相机朝下不跟朝向,得到的是固定地图(地图不动,玩家图标转)。苏绣展馆用的是固定地图,因为展厅是对称的,固定地图更容易建立空间记忆。6.2 玩家图标的朝向映射地图固定之后,玩家图标需要根据角色朝向旋转。这里有个容易搞混的地方:角色在场景里绕Y轴旋转的角度,映射到小地图上需要做一次符号翻转。float y player.eulerAngles.y; playerIcon.localRotation Quaternion.Euler(0f, 0f, -y);为什么是负号?因为场景里的顺时针旋转,在俯视地图上是逆时针的。这个符号第一次调的时候卡了我半小时,拿一个已知方向反复测试才确认。6.3 展品标记与快速传送小地图上还标了所有展品的位置,用不同颜色的点区分:主展品用深红,普通展品用浅灰,洗手间和出口用蓝色。这些点的位置是在编辑器里手动摆的,没有用自动生成——因为自动生成需要给每个展品算世界坐标到地图坐标的映射,听起来简单,但一旦展厅布局改了,所有点位都要重算。手动摆的话,布局改了点也跟着挪,更直观。点击地图上的展品点可以快速传送过去。传送在展馆场景里是合理的,因为逛展不像游戏,观众有时候就是想直接去看某一件作品,不想走回头路。实现方式是直接改CharacterController的位置:public void TeleportTo(Transform target) { controller.enabled false; // 先禁用,否则位置会被覆盖 transform.position target.position target.forward * 1.2f; transform.rotation Quaternion.LookRotation(-target.forward); controller.enabled true; // 再启用 }注意:CharacterController在启用状态下直接改transform.position是无效的,必须先把enabled设为 false,移动完再设回 true。这是一个非常隐蔽的坑,症状是代码执行了但人没动。7. URP管线配置与展厅光照URP 的配置看着简单,但展馆这种大空间 少量精贵展品的场景,光照方案要仔细选。7.1 静态光照用烘焙,动态光源只用一盏展厅里的灯光绝大多数是静态的——顶部灯槽、墙面射灯、地面导引灯,这些位置固定、亮度固定,全部走Baked模式烘焙进光照贴图(Lightmap)。只有一盏主光作为方向光,走Mixed模式,给整个空间一个统一的明暗基础,同时保留动态阴影能力。这样做的好处是运行时几乎没有实时光照开销,帧率稳定。代价是烘焙时间长,而且每次改布局都要重新烘。我的经验是把烘焙参数里的Lightmap Resolution设在 20 到 30 texels 每单位之间,再低会糊,再高烘焙时间受不了。7.2 展品单独打光这里有个矛盾:如果把展品也纳入整体烘焙,那它的光就固定死了,侧光的方向和强度都不能根据观看角度变化,各向异性的流光效果就出不来。所以展品的打光是独立的:每件绣品前面挂一盏Spot Light,范围很小(角度20度,距离3米),只照亮这一件展品,走Realtime模式。为了避免实时光源过多,我给这些灯做了距离剔除——超出一定距离自动关掉。展厅里同时亮着的展品灯不超过4盏,性能完全可控。光源类型模式数量说明顶部环境光Baked全部基础照明,烘进Lightmap主方向光Mixed1提供初始明暗走向展品射灯Realtime≤4随距离动态开关中庭补光Baked2提升挑空区域层次7.3 后处理不要贪多URP 的后处理里,我用到的只有三项:Bloom(让绣线的高光有微弱的光晕)、Color Grading(整体偏暖一点点,贴合丝线的色调)、Vignette(极轻微的暗角,收拢视线)。其余的 Depth of Field、Motion Blur、Chromatic Aberration 全部关掉——展馆不是电影,观众需要看清每一处细节,景深会让远处的展品模糊,反而添乱。Bloom 的阈值建议设在 1.1 到 1.3 之间,强度不超过 0.3。超过这个值,缎面的反光会糊成一片白,绣线纹理全丢。8. 性能优化与交付打包做完了功能,真正的考验是在别人的电脑上跑不跑得起来。8.1 优化的优先级顺序优化要按收益排序,不要上来就乱调。我的处理顺序是:合批 贴图压缩 LOD 剔除。静态合批是最划算的:展厅里所有不动的墙、地、顶、展柜,全部勾选Batching Static,同材质的网格会被合并成少数几个 Draw Call。这一个操作让场景的 Draw Call 从800多降到120以下。贴图压缩是第二位的。所有墙面、地面的贴图用 ASTC 或 DXT 压缩,只有绣品贴图保留高质量(因为它们是视觉焦点)。一张4096的绣品贴图在未压缩时约64MB,压缩后约8MB,场景里10件绣品就是80MB的差距。LOD用在展柜和装饰件上,远处自动切换到低模版本。视锥剔除让相机看不到的东西不渲染。优化前后的对比大致是这样:指标优化前优化后Draw Call810115纹理内存1.4 GB320 MB平均帧率(中端台式机)3488平均帧率(平板)18528.2 打包时的几个坑第一个坑是中文字体。Unity 默认的 Arial 字体不包含中文字库,讲解面板里的中文会全部显示成方块。必须自己导入一个中文字体,并且设置好 Fallback 列表。我用的是思源黑体,把常用的几千个汉字做成字体图集打包,避免整个字库全量打入导致包体暴涨。第二个坑是着色器变体。URP 的项目打包时如果不做 Shader Variant 剔除,包里会带上大量用不到的着色器变体,包体可能多出几十MB。在 Project Settings 的 Graphics 里开启 Shader Stripping,把用不到的变体去掉。第三个坑是光照贴图不随包走。烘焙好的 Lightmap 如果没有正确关联到场景,打包后场景会变成一片全黑或者全亮。检查方式是打包后在目标机器上跑一遍,如果光照和编辑器里不一样,那就是光照贴图丢了,需要在 Lighting 设置里确认 Lightmap 已经保存。8.3 交付物清单最后交付的东西包括:Windows 可执行程序、WebGL 降级版本、展品 ScriptableObject 资源包、一份操作说明(给展馆的工作人员用)、一份资源更新指南(后续加展品怎么操作)。其中操作说明和更新指南是容易被忽略但非常关键的——展馆的工作人员不会写代码,但他们需要能自己改讲解文字、加新的绣品。所以我专门做了一个简单的配置工具,让他们可以在不打开Unity的情况下修改讲解内容。9. 这一路踩过的具体坑写完流程,再单独把这几个月踩过的几个坑列一下,这些都是当时卡了很久、后面才知道原因的。第一个坑:第一人称视角下摄像机穿进墙里。原因是CharacterController的半径设成了0.3,而摄像机的近裁剪面是0.05,角色贴墙走的时候摄像机就穿出去了。解决办法是把胶囊半径调到0.4,近裁剪面调到0.1,并在相机上加一层很薄的防穿墙后处理。更彻底的方案是在相机位置做射线检测,撞到墙就把相机往前推,但这个方案会导致视角抖动,最后没用。第二个坑:触摸屏上的视角漂移。平板测试时,手指放在屏幕上不动,视角会缓慢转动。排查后确认是触摸输入的delta在手指静止时仍有微小读数,累积起来就变成了漂移。解决方法是加死区:两帧之间的delta模长小于0.02时直接归零。第三个坑:小地图的 Render Texture 导致内存超标。最初小地图用的是1024×1024的Render Texture,后来发现这个尺寸在低端设备上占显存很明显。改成512×512,视觉上几乎看不出差别。第四个坑:展品高亮在背面也亮。用轮廓网格做高亮的时候,轮廓是全方向的,从背面看也能看到高亮的边。改成了在脚本里判断相机和展品法线的夹角,夹角大于90度就关闭高亮,只在高亮朝向观众的一侧显示。第五个坑:打包后中文讲解变成问号。这个就是前面说的字库问题。当时在编辑器里一切正常,打包到别人的电脑上就全变问号。原因是我用的字体文件是编辑器机器的系统字体,没有打进包里。换成项目内的字体资源文件后解决。第六个坑:展柜玻璃在平板上有明显色带。原因是渲染精度的设置。URP Asset 里的色深选项默认是自动,在部分移动设备上会降到较低精度,渐变区域就出现色带。强制开启高精度后解决,但要接受一点性能损耗。提示:如果后续要扩展这个展馆,最值得先做的一件事是把展品数据和漫游逻辑彻底解耦。我现在加一件新绣品,只需要新建一个 ScriptableObject、拖两张图、在场景里摆一个点位,五分钟就能完成,不用碰一行代码。这个设计一开始多花了半天,后面省下的时间远不止半天。从最初那版走路会穿墙、绣品像糊了一层灰的白盒,到最后能在普通笔记本上流畅逛完一圈、绣面在侧光下能看出丝线流光,整个过程最有价值的经验其实就一句:把空间比例先做对,把光打好,材质和交互才有意义。比例错了,再好的贴图也是白搭;光打错了,再贵的绣品也是死板一块。