资讯详情

Unity 3D+C#湘绣虚拟展馆交互漫游开发实战

📅 2026/9/18 11:58:26 | 华诺云谱 👁 阅读
Unity 3D+C#湘绣虚拟展馆交互漫游开发实战
去年秋天接了个活甲方是非遗保护中心需求说得很直白把湘绣的经典绣品和针法工艺做成一个能用键鼠或触摸走进去逛的线上展馆。翻译成技术语言就是基于 Unity 3D C# 做一套湘绣文化主题虚拟展馆交互漫游系统。听上去像3D建模加贴图再加个走路脚本的常规项目真动手才发现坑全在细节绣品的丝光质感还原不到位整个展馆立刻退化成贴了图片的白盒子漫游手感一旦发飘观众三分钟就想关页面。这篇就把我这一版从策展逻辑、资产处理、漫游控制、交互拾取、光照烘焙到打包上线踩过的路完整摊开讲一遍代码和参数都给到能直接抄的程度。1. 先把展厅翻译成Unity工程结构动手建场景之前我花了整整两天做一件看起来不产出任何资产的事把策展大纲拆成坐标和层级。这一步做扎实后面能省掉的返工远比这两天多。湘绣这个题材有自己的叙事节奏——从历史溯源到针法工艺再到狮虎题材、双面全异绣这些绝活最后是传承人影像区观众的心理动线是走进去、被吸引、停下来看细节、带着印象离开。Unity 里的空间结构必须服务这条动线而不是反过来让策展去迁就建模方便。1.1 定漫游方式再定场景尺度第一人称、第三人称、还是俯视定点这个选择直接决定场景尺度和摄像机高度必须先定。湘绣展品是平面绣片和立轴为主细节要靠贴近看所以最终选了第一人称自由漫游 关键展品定点聚焦的混合模式自由走动能保留逛的感觉靠近展柜按提示键切到聚焦视角摄像机缓缓推到绣面正前方观众看针脚。尺度按真实尺寸走1 Unity 单位 1 米这是最容易后续被坑的地方。层高定 3.8 米主展线通道净宽 2.6 米展柜高 0.95 米、进深 0.6 米观众视点高度 1.65 米。为什么死磕真实尺度因为摄像机移动速度、碰撞体半径、光照衰减、贴图密度全都跟尺度绑死。我见过把场景建成 10 倍大的项目结果只能靠把移动速度调到 12 m/s 来追回手感走起来像在太空飘观众第一反应就是晕。观众步行速度按真实步速取1.3 m/s按住 Shift 加速到 2.4 m/s。这个值不是拍脑袋真人展厅里的舒适步速大约 1.0 到 1.5 m/s取中位数再略微上调是因为虚拟环境缺少真实的脚下触感和周边视差感知速度会比实际值低 15% 左右。1.2 目录与场景切分工程目录我习惯按运行时模块而不是资产类型来分找东西比按类型分快得多Assets/ _Project/ Art/ # 模型、材质、贴图 Exhibits/ # 展品专用资产 Architecture/ # 建筑与展柜 Audio/ Scripts/ Core/ # 单例、事件总线、存档 Data/ # ScriptableObject 定义 Player/ # 漫游控制 Interaction/ # 拾取与高亮 UI/ # 面板、小地图 Editor/ # 自定义工具 Settings/ # URP Asset、Input Actions Scenes/ Boot.unity MainHall.unity Data/ # 展品数据资产场景切分上我没有用一个大场景装完而是 Boot 场景常驻负责初始化、音频总线、Addressables 预热和加载界面MainHall 装展厅主体。湘绣展品有 60 多件如果全塞进一个场景光照贴图会撑到几 GB烘焙时间直奔小时级。我的做法是把针法工艺区和传承人影像区做成独立的子场景用 Addressables 远程加载观众走到区域入口时才流式载入。1.3 用 ScriptableObject 把展品数据抽出来这是我认为整个项目里性价比最高的一个决定。展品名称、年代、尺寸、工艺说明、针法标签、音频讲解、模型引用全部做成 ScriptableObject 资产场景里只放一个空 GameObject 挂ExhibitBehaviour引用对应数据。using UnityEngine; [CreateAssetMenu(fileName Exhibit_, menuName 湘绣/展品数据)] public class ExhibitData : ScriptableObject { [Header(基础信息)] public string exhibitId; public string displayName; [TextArea(3, 6)] public string description; [Header(工艺标签)] public string[] stitchTags; // 如掺针、鬅毛针、游针 public string era; public string[] sizeCm; // 长宽 [Header(资源引用)] public GameObject prefab; public AudioClip narration; public Sprite thumbnail; }好处有三点。第一运营方改文案不用碰场景我直接把 SO 资产的编辑权限交给他们改完发新的资产包即可。第二批量做数据校验很简单——写个 Editor 脚本扫一遍所有ExhibitData检查exhibitId是否重复、prefab是否为空、音频是否超过 3 分钟。我吃过一次亏上线前发现两件展品的 ID 撞了详情面板随机显示错误内容就是靠这个校验扫出来的。第三做多语言时只需要复制一份 SO 换文案代码层完全不用动。2. 绣品还原丝光质感决定这个项目的成败虚拟展馆和普通建筑漫游最大的区别在于观众来这里不是看墙是看展品。湘绣的核心视觉特征是丝线的方向性反光和针脚的立体起伏一朵花瓣在不同角度看会明暗流转。如果只把绣品扫描图当普通贴图贴在平面上观众转头时高光一动不动立刻就会觉得这不是绣的是打印的。这块我迭代了三轮才满意。2.1 从高清扫描图到 PBR 材质的完整处理链路甲方给的素材是 600 DPI 的平面扫描图直接扔进 Unity 有三个问题色彩偏灰、缺少立体感、边缘有扫描噪点。我的处理链路是这样的在图像软件里先做色彩还原。扫描仪自带的白平衡会压暗深色丝线用色阶把黑场拉到 12 左右、白场拉到 240 左右让桑蚕丝的高饱和色重新出来。湘绣很讲究色彩艳丽而不俗这一步压过头就俗了宁可保守。生成法线贴图。用高度图软件把扫描图的亮度通道转成法线强度控制在 0.6 到 0.9 之间。强度调太高绣面会像搓衣板太低就没有针脚的起伏感。制作粗糙度贴图。这是我花时间最多的一步。丝线在顺纤维方向反光强、垂直方向反光弱所以我用扫描图做底叠加 45 度方向的斜纹噪声让粗糙度呈现方向性差异。Unity 标准光照模型本身不支持各向异性但用这张粗糙度贴图加上一张细节法线视觉上能做到七成像。输出AO 贴图。从高度图算强度 0.4 左右主要用来加深针脚交叠处的暗部绣面的层次立刻就不一样了。2.2 让绣面立起来的三个实战技巧这部分是纯经验文档里不会写。技巧一给绣面加极浅的置换或曲面。完全平的面在掠射角下会露出马脚。我给每件绣品加了 1 到 2 毫米的曲面起伏模拟绣布被绷架撑起、丝线堆叠后的张力。幅度大了会穿帮2 毫米是安全值。技巧二展柜玻璃别用纯光滑材质。玻璃的反射率调到 0.06 到 0.12粗糙度给 0.05再加一层极淡的指纹或灰尘贴图。完全干净的玻璃在实时渲染里反而假会让观众觉得展柜不存在破坏沉浸感。技巧三用两盏灯而不是一盏灯照绣面。一盏主光从斜上方 45 度打一盏弱补光从另一侧 30 度打强度是主光的 0.3 倍。两盏灯在丝线上形成的明暗交界就是观众眼里的丝光。单盏灯打出来的绣面会死板。这里要注意展柜内部如果用了点光源记得把 Shadow Type 设为 No Shadows 或者用极小的阴影距离否则实时光影会把性能吃光。2.3 纹理导入设置与显存预算60 多件展品每张 4K 贴图按 RGBA 未压缩算是 64 MB六十件就是 3.8 GB直接爆显存。我的取舍规则展品类型贴图尺寸压缩格式Mipmap说明主展品狮虎、双面绣2048ASTC 6x6开允许贴近观看一般展品1024ASTC 8x8开距离 1 米外无差别缩略图512ASTC 8x8关UI 用不需要 mip建筑与展柜1024ASTC 8x8开重复使用做图集有一点必须提醒贴图压缩后一定要在目标设备上实机确认编辑器里看到的预览是解压后的效果。我之前给一件双面异色绣用了 ASTC 8x8编辑器里看不出问题打包到中端平板上绣线的渐变出现了明显的色带换成 6x6 才解决。所以主展品一律给到 6x6别省这点显存。3. 漫游控制手感的成败全在这几个参数漫游系统是整个项目里观众接触时间最长的一块也是最容易能用但不好用的一块。我把移动逻辑重构了两次第一次用 Rigidbody 加力被自己在展柜边角卡住的问题折磨了两天最后换成 CharacterController 才收工。3.1 为什么最终选了 CharacterController 而不是 RigidbodyRigidbody 的物理模拟很真实但对展馆这种精确走位场景是负担。观众在展柜夹缝里转身物理引擎会算出一堆反作用力人物会被弹开或者贴墙打滑。CharacterController 是运动学控制器移动完全由代码决定配合OnControllerColliderHit处理滑动走位干净可控还能直接拿到isGrounded判断落地。参数上我调了很久最终固化为public class PlayerWalker : MonoBehaviour { [SerializeField] private CharacterController controller; [SerializeField] private float walkSpeed 1.3f; [SerializeField] private float runSpeed 2.4f; [SerializeField] private float rotationSpeed 12f; [SerializeField] private float gravity -18f; private Vector3 velocity; private float currentSpeed; public void Move(Vector2 input, bool running, float deltaTime) { // input 来自 Input System已归一化 Vector3 dir new Vector3(input.x, 0f, input.y); dir Vector3.ClampMagnitude(dir, 1f); currentSpeed running ? runSpeed : walkSpeed; Vector3 motion dir * currentSpeed; if (!controller.isGrounded) velocity.y gravity * deltaTime; else if (velocity.y 0f) velocity.y -2f; // 轻微下压保证贴地稳定 Vector3 total motion Vector3.up * velocity.y; controller.Move(total * deltaTime); if (dir.sqrMagnitude 0.001f) { Quaternion target Quaternion.LookRotation(dir); transform.rotation Quaternion.Slerp( transform.rotation, target, rotationSpeed * deltaTime); } } }关键参数解释slopeLimit设 45 度超过就不让走避免观众爬上展台stepOffset设 0.3 米能自然迈过 30 厘米以内的台阶skinWidth设 0.08 米太小会在墙角抖动太大会让人悬空center的 y 值设 0.9对应胶囊体中心。重力我用 -18 而不是 -9.81因为真实重力在虚拟环境里显得飘加大一倍左右落地感更扎实。3.2 输入系统一套代码同时接键鼠和触摸新 Input System 的好处是同一份 Action 资产能映射到不同设备。我在Settings下建了一个PlayerControls.inputactions定义了Move、Look、Interact、Sprint四个 ActionMove 绑定 WASD 和左摇杆Look 绑定鼠标位移和右摇杆/触摸拖动Interact 绑定 E 键和屏幕点按。切换设备时PlayerInput组件会自动切换 Control Scheme不需要写 if 判断平台。这里踩过一个坑触摸屏上滑动手势会同时触发 Look 和 UI 的点击导致点展品时视角先转一下。解决办法是给 UI 的 Canvas 加GraphicRaycaster运行时光标位置在 UI 上时禁用 Look。用EventSystem.current.IsPointerOverGameObject()判断不够可靠因为它在触摸的同一帧可能返回 false稳妥做法是监听IPointerEnterHandler/IPointerExitHandler自己维护一个 UI 遮挡标志。3.3 摄像机防穿墙和视野调优摄像机跟随直接用子物体挂上去最省事但观众一转身摄像机就会穿进展柜或墙壁。我的做法是从视点位置向摄像机目标位置做SphereCast碰到碰撞体就把摄像机拉到碰撞点前方 0.2 米private float ResolveCameraDistance(Vector3 origin, Vector3 dir, float desired) { if (Physics.SphereCast(origin, 0.15f, dir, out RaycastHit hit, desired, obstacleLayer, QueryTriggerInteraction.Ignore)) { return Mathf.Max(0.3f, hit.distance - 0.2f); } return desired; }视野角FOV我最终定在 65 度。这是反复测试的结果FOV 太高75 以上走起来速度快但画面边缘拉伸严重观众容易晕太低50 以下在狭窄展线里会有压抑感。65 度配上 1.3 m/s 的步速长时间逛下来疲劳感最低。另外强烈建议关掉摄像机的运动模糊展厅里的移动模糊会让绣品细节糊成一团。4. 交互拾取让观众点得到并且点得准漫游只是把人带进来真正让观众停留的是交互。展馆里观众最自然的动作就是走到一件绣品前想看看它叫什么、什么年代、用了什么针法。这套拾取加反馈的机制做得顺不顺直接决定观众愿意点几次。4.1 射线检测的三个常见问题展品拾取用Physics.Raycast从摄像机中心发射测到展品层就触发选中。听起来简单实际有三个坑坑一是射线打到了展柜玻璃。玻璃的碰撞体挡住了后面的绣品点上去没反应。解决方式是给所有玻璃的碰撞体加独立 Layer射线的 LayerMask 里排除掉。这样玻璃能挡人也仍然透明。坑二是远处展品永远点不到。射线距离设成了 100 米结果观众在序厅就能点到最后一件绣品。我加了距离限制和角度限制距离不超过 4 米射线与展品法线的夹角超过 60 度不响应。这两条一加走近才能看的策展意图就自然实现了。坑三是需要每帧都发射线吗。不需要。我用 0.1 秒的间隔轮询命中目标变化时才更新高亮状态CPU 开销几乎为零。这一点在移动端很关键每帧一次RaycastAll在低端机上会造成明显的帧率波动。4.2 展品高亮的三种方案取舍方案实现方式优点缺点适用材质属性块换色MaterialPropertyBlock 修改 Emission零 GC、性能最好只适合简单发光大量展品同时高亮轮廓描边第二个 Pass 或 Renderer Feature视觉明确移动端开销明显少量核心展品高亮遮罩贴图叠加一层自发光遮罩材质效果可控、能吃美术资源需要额外贴图主展品推荐主展品我最终选第三种预生成一张只含边缘和针脚轮廓的自发光贴图选中时把自发光强度从 0 平滑升到 1.5用协程做 0.25 秒过渡。因为湘绣本身的针脚就是天然的轮廓线用遮罩贴图高亮时亮起来的正好是针法本身观众看到的是这朵花是用掺针绣的被点亮而不是一个生硬的光圈。4.3 详情面板的数据加载与呈现节奏选中展品后弹出面板内容分三层名称与年代、针法标签、说明文字下方是播放讲解按钮。这里有个设计上的小坚持——文字不要一次全部显示。我把说明文字做成逐行淡入每行间隔 0.15 秒配上打字音效。实测观众在动画期间会自然地阅读直接铺满一屏反而没人看。讲解音频用AudioSource.PlayOneShot播放同时把展品的自发光做呼吸节奏让观众感知到正在讲解的是这一件。面板关闭时要记得停音频、还原自发光、取消选中状态这三个清理动作一个都不能少。我最早漏了停音频结果观众关闭面板后讲解还在后台念下一件展品的讲解叠上来体验非常糟糕。5. URP 渲染配置与光照烘焙渲染管线用 URP 而不是内置管线主要原因是移动端和 WebGL 的适配性好而且后处理的管理更集中。但 URP 带来的迁移成本不小尤其是光照烘焙这一块参数没调好会出现明显的漏光和光斑。5.1 URP 资产的关键设置我创建了两个 URP AssetURP_PC和URP_Mobile。PC 端开 HDR、开 MSAA 4x、阴影距离 30 米、阴影级联 4 级移动端关 HDR、MSAA 2x、阴影距离 15 米、级联 2 级。切换靠 Quality Settings 关联不同的 URP Asset打包时一份代码两个配置。有几个参数特别值得调Additional Lights设为 Per Pixel但 Max Additional Lights 限制在 4。展柜里的补光如果不做数量限制一进针法区就会因为几十个点光源同时参与着色而掉帧。Soft Shadows打开展品在墙面上的投影边缘如果太硬会显得很廉价。Shadow Resolution主光给 2048移动端 1024。这是画质和性能之间最划算的一档。Color Grading用 ACES 色调映射。湘绣的红色和金色饱和度很高默认的 Neutral 映射会过曝ACES 的滚降更自然。5.2 光照贴图与 Light Probe 的实用配置展馆的光照我采用Baked 为主 少量实时光的混合方案。建筑、展柜、墙面全部标记为 Static参与烘焙展品是动态物体只能接收 Light Probe 的光照。所有展品上方我按 2 米间距铺了 Light Probe Group确保观众走近时展品受到的间接光照是连续变化的不会出现走到某处突然变亮的跳变。烘焙参数上Lightmap Resolution 给 15 texels/unitLightmap Padding 给 4Lightmap Size 上限设 2048。为什么会漏光绝大概率是墙面模型没有生成 Lightmap UV2。在模型的导入设置里勾上 Generate Lightmap UVs或者把 Lightmap Scale 调到 0.5 到 1 之间做微调。我遇到过一次展厅角落有一条亮缝排查半天最后发现是两面墙的模型接缝处 N 值没有对齐缝隙处的间接光穿了过去把两面墙合成一个模型后问题消失。5.3 后处理的克制使用后处理很容易上头加着加着整个画面就糊了。我的展馆只用了四项Bloom 强度 0.3让绣线的丝光溢出一点点、Vignette 强度 0.25把注意力收拢到画面中心、Color Grading 做基础校正、抗锯齿用 SMAA比 TAA 更省且不会因为观众快速转头产生鬼影。镜头畸变、景深、运动模糊这三项我全部关掉。原因是这个场景的观众会大量时间近距离看静态展品景深会让边缘的绣品发虚运动模糊会在转头时糊掉细节都是负收益。这里有个经验后处理的每一项都要问它对观众理解展品有帮助吗答不上来就关掉。6. 小地图导航与自动巡游路线展馆面积不算特别大但不熟悉路线的观众还是会绕晕尤其是针法工艺区和传承人影像区在侧厅很多人逛完主厅就走了。小地图和自动巡游这两套机制本质上是在做降低探索成本。6.1 小地图的 RenderTexture 方案与性能注意小地图用第二台正交摄像机Culling Mask 只渲染地面、墙体和展品图标层输出到一张 256x256 的 RenderTextureUI 上用 RawImage 显示外面套一层圆形遮罩。摄像机跟随玩家的 XZ 位置平移动不旋转这样地图方向感是稳定的观众不用额外学一套地图随人物转的逻辑。性能上有两点必须注意。第一正交摄像机的深度要设得比主摄像机小并且关掉它的 Audio Listener否则场景里会有两个监听器导致音频异常。第二RenderTexture 的目标帧率独立设置——我把它降到 15 fps 更新用协程控制摄像机每 1/15 秒拍一次画面。人眼对小地图的更新频率不敏感这一改动在主摄像机满帧运行时能省掉大约 8% 的 GPU 开销。6.2 用 NavMesh 做自动巡游路线有些观众不想自己走希望一键跟着看。这条自动巡游路线我是用 NavMeshAgent 实现的但做了两点定制速度限制为 1.2 m/s接近标准步速到点后停留 8 秒摄像机缓缓转向展品并播放讲解。巡游路线的点我用一个Transform数组预先排好代码里顺序下发SetDestination用OnDestinationReached判断到达。NavMesh 烘焙时把展台和展柜标记为 Not Walkable把人群可通行区域留够 1.5 米宽通道。有个细节值得说自动巡游时不要禁用玩家的输入而是让玩家任意操作就能退出巡游。我最初的做法是巡游期间锁定输入观众发现走不了会慌张以为程序卡死了。改成按任意移动键即退出巡游体验顺畅很多。6.3 展品标记与快速传送小地图上的展品标记做成可点击的图标点击后展开缩略图列表选中就传送过去。传送用CharacterController的enabled false后直接改transform.position再enabled true避免控制器把传送当碰撞处理。传送落点要特别注意——不能直接传送到展品正前方那样摄像机可能已经在墙体内部。我在每件展品的背面预留了一个空 GameObject 作为落点参考落点距离展品 1.8 米、朝向展品传送后摄像机自动转向展品。这套传送落点的规划设计比写传送代码本身花的时间还多。7. 打包上线与真实踩坑记录前面几节讲的是应该怎么做这一节讲的是我实际怎么被坑的。这几个问题都不是靠看文档解决的全靠一行行排查出来的写出来希望能帮你省点时间。7.1 中文显示TextMeshPro 字体图集必须自己生成这是最经典的坑。TextMeshPro 默认的字体资产只含 ASCII 字符所有中文全是方块。正确做法是在 Window TextMeshPro Font Asset Creator 里用一份中文字体源文件生成字体图集Character Set 选 Custom Characters把项目里所有可能出现的汉字导进去。问题是汉字太多全量三千五百常用字做一张 4096x4096 的图集SDF 渲染后大概 30 MB。我的处理是按展区分组生成多张图集——序厅、工艺区、传承区各一张每张只放该区用到的字加起来反而更省。另外记得把Atlas Population Mode设为 Dynamic让它运行时按需补充字符防止遗漏。7.2 WebGL 平台的流式加载与文件读取如果要做网页版WebGL 下Application.streamingAssetsPath是不能用File.ReadAllBytes直接读的这是一个必踩的坑。我的展品音频本来放在 StreamingAssets 里用 File 读PC 端跑得好好的发布到 WebGL 后全部静音。排查了很久才意识到WebGL 下这个路径实际是一个 URL必须用UnityWebRequest异步加载。后来我干脆把所有需要动态加载的资产都迁到 Addressables用统一的异步接口处理两端行为就一致了。顺便说一句Addressables 的 Group 划分要按加载时机而不是资产类型来分。我把主厅常驻展品、侧厅流式展品、UI 素材分成三个 Group配置不同的加载策略首包大小从 180 MB 压到了 62 MB。7.3 一套固定下来的排查顺序被坑多了之后我总结出一套固定的排查顺序遇到打开了但不对的问题按这个顺序查能解决九成先看 Console 有没有报错和警告尤其是 Shader 相关的粉色材质提示这通常意味着 URP 迁移时 Shader 没换。批量修复用 Render Pipeline Converter或者选中材质手工把 Shader 改成 URP 版本的 Lit。再看 Layer 和 Culling Mask交互失效、小地图缺东西、阴影丢失十有八九是层级没配对上。然后看光照贴图Baked 模式下如果物体没标记 Static它会带着实时光照渲染视觉上表现就是这个柜子怎么和别的不一样亮。最后看平台差异编辑器里正常、真机不对的优先怀疑纹理压缩和路径读取。性能问题用 Profiler 定位别猜。我一度以为卡顿是展品面数高Profiler 一开发现是 UI 的 Canvas 每帧重绘把静态的展品信息面板拆到独立 Canvas 后帧率直接回升。这套顺序的价值在于它把随机试探变成了有序收敛。展馆类项目涉及的东西太多没有一套固定流程很容易在无关的地方浪费一整天。最后分享一个我自己一直在用的小技巧给每件展品在场景里都挂一个空的Anchor子物体位置就放在展品正中心稍微靠前一点的地方。所有跟展品相关的逻辑——聚焦摄像机的目标点、小地图图标的挂载点、传送落点的朝向参考——全部引用这个 Anchor而不是引用展品模型本身。这样一来模型怎么换、怎么调逻辑引用都不会断模型替换的成本几乎降到了零。做这类文化主题展馆展品资产更新是常态这个习惯帮我省下的返工时间远超过当初多花的那半小时。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。