
1. 项目缘起与整体设计思路汉绣作为国家级非物质文化遗产其针法体系、纹样谱系和色彩规范都有一套完整的视觉语言。但现实情况是真正能接触到汉绣实物的人并不多——展馆分布有限绣品又极其脆弱光照、温湿度、触摸都会造成不可逆的损伤。我此前参与过一个地方非遗数字化项目当时就意识到用三维交互的方式把汉绣搬到线上既能解决传播半径的问题又能避免实物损耗这件事值得认真做一遍。这个项目的核心目标很明确用 Unity 搭建一个汉绣文化主题的虚拟展馆用户以第一人称或第三人称视角在馆内自由漫游走到不同展区可以查看绣品的三维模型、针法动画演示、纹样细节放大以及配套的文字解说。技术栈锁定在 Unity C#三维资产用 3D 建模工具产出后导入 Unity 做材质和光照处理。选择 Unity 而不是 Web 端 Three.js 方案主要考虑三点一是 Unity 对复杂光照和 PBR 材质的支持更成熟绣品的丝线质感需要精细的材质表现二是 C# 的工程化能力更强展馆的交互逻辑、状态管理、数据驱动加载都能写得比较干净三是后续如果要发布到桌面端或移动端Unity 的跨平台构建链路更省心。整体架构上我把系统拆成四个层次。场景层负责展馆建筑、展台、灯光和氛围交互层处理漫游控制、射线拾取、UI 响应数据层管理绣品信息、针法数据、解说文本表现层负责模型渲染、动画播放、特效反馈。这四层之间通过 C# 事件系统和 ScriptableObject 做解耦避免所有逻辑堆在一个 MonoBehaviour 里。实际做下来这种分层在后期加展品、改交互时省了大量返工时间。提示虚拟展馆类项目最容易犯的错误是一上来就堆模型结果场景卡顿、交互混乱。建议先把交互框架和数据流跑通再逐步替换美术资产。2. 核心技术点拆解与选型考量2.1 Unity 版本与渲染管线选择Unity 版本我选的是 2022 LTS。原因很直接LTS 版本稳定社区资源多URPUniversal Render Pipeline在这个版本已经相当成熟。汉绣展馆不需要 HDRP 级别的极致画质但需要稳定的帧率和良好的移动端兼容性URP 正好卡在中间。URP 的 Shader Graph 可以自定义丝线材质比如给绣品表面加一层微弱的各向异性高光模拟蚕丝的反光特性这个用内置管线做起来会麻烦很多。渲染设置里有几个关键参数需要调。阴影距离我设在 30 米左右因为展馆空间有限太远的阴影既浪费性能又看不出差别。级联阴影用 2 级就够了近距离展品用高质量阴影远处用低质量。抗锯齿开 MSAA 4x绣品的细密针脚在边缘处容易产生锯齿这个不能省。2.2 三维建模与资产导入规范绣品模型是外包给建模师做的但导入 Unity 之前我定了一套规范不然后期材质和光照会出大问题。模型单位统一用米导出 FBX 时勾选嵌入媒体和使用文件单位缩放因子保持 1。面数控制上单件绣品主体不超过 5 万三角面展台和建筑构件用 LOD 分级远景 LOD2 控制在 5000 面以内。UV 展开要求建模师按绣品实际纹理走向排布不能简单用自动展开。汉绣的针法有平针、锁针、打籽针等不同针法的纹理方向不一样UV 如果乱了贴图上去针脚方向就是错的。材质方面绣品用 Standard Shader 改的 URP Lit金属度设 0光滑度根据丝线类型在 0.3 到 0.6 之间调法线贴图用从高模烘焙出来的细节法线表现针脚的凹凸感。2.3 C# 交互框架设计交互框架的核心是三个系统漫游控制器、射线拾取系统、UI 事件总线。漫游控制器我基于 Unity 的 CharacterController 写了一套自定义方案没用现成的 FirstPersonController因为展馆里需要限制移动范围、支持平滑加速减速、还要处理上下楼梯和展台边缘的碰撞。代码结构上把输入读取、移动计算、碰撞处理拆成三个方法方便单独调试。射线拾取用 Physics.Raycast 从摄像机中心发出命中展品后触发高亮和 UI 弹出。这里有个细节绣品模型的面数高如果每帧都做射线检测会有性能开销。我的做法是给每件展品加一个低面数的碰撞体代理比如一个 BoxCollider 或简化 MeshCollider射线只检测代理层命中后再映射到实际展品。UI 事件总线用 C# 的 event 和 Action 实现展品被点击时广播一个事件UI 面板、音效系统、动画系统各自订阅互不干扰。2.4 数据驱动的内容管理展品信息如果用硬编码写在场景里后期加一件展品就要改代码、重新烘焙非常痛苦。我用 ScriptableObject 做了一套展品数据资产每件绣品对应一个 SO 文件里面存名称、年代、针法类型、解说文本、模型引用、贴图引用。展馆加载时读取一个展品列表 SO动态实例化展品并绑定数据。这样加新展品只需要在编辑器里创建 SO 文件、拖入模型和文本不用碰代码。针法动画数据也走类似思路。每套针法动画是一个 Animator Controller通过 SO 里的字符串键去 Resources 或 Addressables 里加载。Addressables 比 Resources 更适合这个场景因为展品数量可能增长Addressables 支持按需加载和远程更新不会让安装包无限膨胀。3. 实操过程与核心环节实现3.1 展馆场景搭建与光照烘焙展馆建筑我用了模块化搭建的方式墙面、地面、柱子、展台都是标准尺寸的预制体在场景里拼装。这样做的好处是改布局快而且光照烘焙时相同材质的物体可以合并处理。展馆整体是一个回字形结构中央大厅放主展品四周走廊按历史年代分展区每个展区尽头有一个多媒体互动区。光照方案上主光源用一盏方向光模拟天光色温偏暖约 4500K强度 1.2。展品重点照明用聚光灯每件展品配一盏角度 45 度强度根据展品材质在 2 到 4 之间调。环境光用 Gradient 模式天空色浅灰蓝地面反射色暖灰这样绣品在阴影里不会死黑。烘焙设置里Lightmap Resolution 设 40 texels per unit绣品展台单独用 Lightmap Parameters 提高精度到 80。烘焙时间比较长我一般晚上跑第二天检查漏光问题。注意URP 下烘焙光照需要确保所有静态物体勾选 Contribute GI动态展品如果也要接受烘焙光照需要勾选 Receive GI 并设置 Light Probes。我踩过的坑是忘了给展品加 Light Probe Group结果展品在烘焙场景里光照完全不对像贴上去的一样。3.2 漫游控制器的代码实现漫游控制器的核心逻辑我写在一个叫VisitorController的脚本里。输入用 Unity 的新 Input System支持键盘 WASD 和手柄摇杆。移动速度分三档步行 1.5 m/s快走 3 m/s奔跑 5 m/s通过 Shift 键切换。加速度用 Mathf.SmoothDamp 做平滑避免起步和停止时的突兀感。public class VisitorController : MonoBehaviour { [SerializeField] private float walkSpeed 1.5f; [SerializeField] private float runSpeed 5f; [SerializeField] private float acceleration 10f; [SerializeField] private float mouseSensitivity 2f; private CharacterController controller; private Vector3 currentVelocity; private float verticalRotation 0f; void Update() { HandleMovement(); HandleRotation(); HandleInteraction(); } private void HandleMovement() { Vector2 input moveAction.ReadValueVector2(); Vector3 moveDir transform.right * input.x transform.forward * input.y; float targetSpeed runAction.IsPressed() ? runSpeed : walkSpeed; currentVelocity Vector3.Lerp(currentVelocity, moveDir * targetSpeed, acceleration * Time.deltaTime); controller.Move(currentVelocity * Time.deltaTime); } }视角旋转用鼠标 X 轴控制水平旋转Y 轴控制垂直旋转垂直角度限制在 -60 到 60 度之间防止翻转。鼠标灵敏度默认 2在设置面板里可以调。交互检测用射线从摄像机中心发出距离 3 米命中展品层时显示交互提示。3.3 展品交互与信息展示展品交互的流程是这样的射线命中展品 - 展品高亮材质 Emission 增强- 屏幕中央显示按 E 查看详情- 按下 E 键 - 摄像机平滑移动到展品正面预设机位 - UI 面板淡入显示解说 - 按 ESC 返回漫游。摄像机移动用 DOTween 做插值时间 0.8 秒Ease 用 InOutCubic。展品高亮我写了一个Highlightable组件命中时把材质实例的 Emission 颜色设为暖金色强度从 0 渐变到 0.5离开时渐变回 0。这里要注意材质实例化的问题如果直接改 sharedMaterial 会影响所有用同一材质的展品必须用 renderer.material 创建实例但这样又会产生大量材质实例增加内存。我的做法是只对当前高亮的展品创建实例取消高亮时销毁实例用对象池管理。UI 面板用 UGUI 搭建Canvas 设 Screen Space - Camera这样 UI 能跟随摄像机有轻微的透视感。解说文本用 TextMeshPro支持富文本和自动换行。面板里还有针法动画的播放按钮点击后播放对应的 Animator 动画动画在展品模型上直接播放UI 面板半透明化避免遮挡。3.4 针法动画与特效表现针法动画是这个项目的亮点。我选了三种代表性针法做动画平针、锁针、打籽针。动画制作上平针用顶点动画模拟绣线逐步覆盖的过程锁针用路径动画展示线圈的穿插关系打籽针用粒子系统模拟线结的形成。这些动画都是在 Blender 里做好后导入 Unity 的用 Animator 控制播放。顶点动画的实现方式是在 Shader 里用一个进度参数控制顶点位移配合一张遮罩贴图决定哪些区域先显示。具体来说遮罩贴图的 R 通道存储针脚顺序Shader 里用step(progress, maskValue)判断当前顶点是否已经绣上。这个方法比逐帧动画省资源而且可以随时调整速度。// 简化版顶点动画 Shader 片段 float maskValue tex2D(_MaskTex, uv).r; float visible step(maskValue, _Progress); float3 displacedPos lerp(originalPos, targetPos, visible);打籽针的粒子系统用 Unity 的 VFX Graph 做每个粒子代表一个线结从针脚位置生成带一点随机旋转和缩放。粒子数量控制在 200 以内生命周期 0.5 秒用 GPU 事件驱动性能开销很小。3.5 性能优化与多平台适配性能优化贯穿整个开发过程。Draw Call 控制在 150 以内主要靠静态合批和 GPU Instancing。展馆建筑和展台都是静态的勾选 Static 后 Unity 自动合批。绣品模型因为要单独交互不能合批但可以用 GPU Instancing 处理相同材质的展品。纹理方面绣品贴图用 2K 分辨率建筑贴图用 1KUI 贴图用 512全部开启 Mipmap 和压缩。移动端适配是另一个重点。Pico 4 这类 VR 设备我也做了适配测试主要调整是降低阴影质量和后处理强度把 MSAA 降到 2xLightmap 分辨率减半。VR 模式下漫游控制器要改成瞬移Teleport方式避免晕动症。桌面端和移动端的构建配置分开管理用 Unity 的 Build Settings 切换平台质量设置用 Quality Settings 的等级切换。4. 常见问题与排查技巧实录4.1 模型导入后的材质与光照异常最常见的问题是模型导入后发黑或者过曝。发黑通常是法线方向反了在导入设置里勾选Calculate Normals或者让建模师检查法线朝向。过曝一般是材质的光滑度设太高URP 下光滑度超过 0.8 就会像镜子一样反射环境绣品材质控制在 0.3 到 0.6 之间比较合适。还有一个隐蔽的坑是 UV 接缝处的光照断裂。如果模型的 UV 接缝和法线接缝不重合烘焙光照时会出现明显的接缝线。解决办法是在建模软件里确保 UV 接缝和硬边法线对齐或者在 Unity 里用 Lightmap Parameters 的Push Off参数微调。4.2 射线拾取失效的排查思路射线拾取失效通常有三个原因碰撞体缺失、层级设置错误、射线距离不够。排查顺序是先用 Gizmos 画出射线确认射线方向正确然后检查展品是否有 ColliderCollider 是否在射线检测的 LayerMask 里最后检查射线距离是否覆盖到展品。我遇到过一次是展品的 Collider 被父物体的 Collider 覆盖了射线先命中了父物体导致展品无法交互。解决办法是把父物体的 Collider 去掉或者用Physics.RaycastAll取所有命中结果再筛选。4.3 漫游时的卡顿与穿模问题卡顿一般来自两个方面一是每帧的 GC 分配二是渲染批次过高。GC 问题用 Profiler 查常见的是在 Update 里 new 对象或者字符串拼接。渲染批次用 Frame Debugger 看如果 Draw Call 超过 200 就要考虑合批或减面。穿模问题在 CharacterController 上比较常见特别是快速移动时穿过薄墙。解决办法是给墙加厚度或者用Physics.CapsuleCast做预检测在移动前判断是否会穿模。4.4 常见问题速查表问题现象可能原因排查方法解决方案模型发黑法线反向检查导入设置重新计算法线光照接缝UV 与法线不齐查看烘焙贴图对齐 UV 接缝射线失效Collider 缺失Gizmos 画射线补 Collider 或改 Layer漫游卡顿GC 分配过高Profiler 查堆分配缓存对象、避免字符串拼接快速移动穿墙碰撞检测滞后减速测试加厚墙体或 CapsuleCast展品高亮不生效材质实例问题检查 renderer.material用对象池管理实例针法动画不播放Animator 状态机错误查看 Animator 窗口检查过渡条件移动端发热阴影和后处理过重真机 Profiler降阴影质量、关后处理提示排查问题时养成用 Profiler 和 Frame Debugger 的习惯不要靠猜。我见过太多人凭感觉优化结果改了半天没改到点子上。4.5 独家避坑经验第一个经验是关于 ScriptableObject 的。SO 文件在编辑器里修改后运行时不会自动同步如果展品数据在运行时被修改下次播放又会恢复。我的做法是运行时用一份 SO 的副本所有修改只作用于副本需要持久化时再写回。第二个经验是关于 Addressables 的。Addressables 的组配置很关键如果所有展品放在一个组里加载时会把整个组都拉进来内存爆炸。正确做法是按展区或按需拆组用标签Label做分类加载。第三个经验是关于 UI 的。UGUI 的 Canvas 如果频繁重建会很卡特别是展品信息面板这种动态内容多的。解决办法是把动态内容拆到独立的子 Canvas 上主 Canvas 只负责静态布局。还有 TextMeshPro 的字体图集如果解说文本包含大量生僻字要提前把字库烘焙进去不然运行时动态生成字体图集会有明显卡顿。5. 内容扩展与后续迭代方向这个项目做完之后我陆续加了一些扩展功能。一个是语音解说用 TTS 把解说文本转成音频用户可以选择听而不是看。另一个是导览模式预设几条参观路线用户跟着路线走每到一处自动播放解说。还有一个是拍照功能用户可以在展馆里截图加上展品信息水印后保存到本地。后续如果继续迭代我会考虑几个方向。一是多人漫游用 Netcode for GameObjects 做同步让多个用户同时在一个展馆里参观可以互相看到对方的位置和动作。二是 AR 模式用 AR Foundation 把绣品模型叠加到现实环境中用户可以在自己家里摆放一件汉绣作品。三是接入 AI 解说用大语言模型根据用户的提问动态生成解说内容而不是固定的文本。不过这些都是后话。就当前这个版本而言最核心的价值是把汉绣的视觉细节和针法逻辑用三维交互的方式呈现出来了。我在实际测试时发现很多年轻用户第一次看到针法动画时都会停下来看很久这种原来是这样绣出来的的反应就是这个项目最大的意义。技术只是手段让非遗被看见、被理解才是目的。