资讯动态

Unity+C#构建京绣虚拟展馆:3D建模与交互漫游实战

发布时间:2026/9/18 11:11:00 来源:尧图企业网站定制
去年冬天我在一个非遗展馆里站了四十分钟就为了看清一件京绣蟒袍下摆的针脚走向。玻璃柜的反光、顶部的射灯、后退一米就完全糊掉的细节最后我掏出手机放大拍了张照片回家对着屏幕看了半天,依然分不清哪一针是盘金、哪一针是打籽。那次之后我就一直想动手做一件事用 Unity 加 3D 建模加 C#把京绣宫绣这种对细节极度挑剔的手工艺搬进一个可以自由走、可以凑近看、可以听讲解的虚拟展馆里。这套东西不是做一个炫技的 Demo而是要能让普通观众在浏览器或者一体机里真正看懂一件绣品贵在哪也能让做非遗数字化的人直接把这套骨架拿去改。这篇文章我会把这套交互漫游系统从需求拆解、三维资产制作、Unity 工程搭建、中文讲解与 UI、灯光烘焙与性能预算一直到踩过的坑完整讲一遍。适合正在做博物馆数字化、非遗展示、虚拟展厅的 Unity 开发者也适合只是想把 C# 和 3D 建模真正串起来做点东西的人。下面所有的方案和参数都是我在这个项目里实际跑通、并且在不同设备上验证过的路径。1. 京绣展馆为什么值得用实时3D重做一遍1.1 隔着玻璃看绣品观众到底损失了什么京绣最值钱的地方恰恰是最难被物理展陈还原的三样东西针脚的层次、丝线的光泽、金线的立体走向。这三样都有一个共同点——它们只在特定距离和特定角度下才成立。观众站在展柜前最佳观赏距离大概在 30 到 50 厘米但展柜玻璃会反光、保安会提醒你不要贴太近、展签上的字离得远看不清。结果就是大部分人看两分钟就走记住的只有很精致三个字。更麻烦的是环境光的干扰。绣品的丝线是各向异性材质光泽会随着观察角度变化专业展陈需要用低照度、定向的洗墙光才能把这种变化表现出来。但普通展厅为了保证整体亮度往往用大面积泛光丝线的高光被冲平盘金绣的金线看起来就像贴了一层黄纸。这不是策展水平问题而是物理空间的硬约束。虚拟展馆能绕过这些约束但不是因为它更高级而是因为它把观赏这件事的变量全部交还给了观众距离可以随便调光源可以按绣品类型切换想看针脚就把镜头推到 5 厘米想看整体纹样就拉远到 3 米。这套逻辑听上去像游戏实际上和文物数字化的研究需求是同一套——可重复观察、可量化对比、可留存记录。1.2 把需求拆成技术清单这个展馆长什么样在动手写第一行 C# 之前我把需求列成了一张表因为虚拟展馆这个词太笼统不拆开就会做成一堆孤立的特效。下面是我最终确定的清单也是后面所有技术选型的依据。需求具体表现对应技术点自由漫游第一人称走位、可上下楼、不穿墙CharacterController、碰撞体分层展品聚焦点击绣品拉近并显示讲解射线拾取、相机插值、数据驱动自动导览一键跟随路线参观NavMesh、路点、镜头运镜细节观察放大到针脚级别不糊高精度贴图、法线叠加、LOD中文讲解语音加字幕同步音频时间轴、动态字体图集多端运行桌面、一体机、网页渲染管线统一、贴图压缩策略内容可更新换展品不改代码ScriptableObject 加 JSON 配置拆完这张表你会发现真正难的不是做个展馆而是在同一套资源上同时满足近看要清晰和远看要流畅。这两个目标天生冲突所有优化工作本质上都是在调和这个矛盾。我在需求阶段还额外加了一条自己的私心必须能看懂一件绣品的工艺分类。所以我给每件展品都定义了工艺标签盘金、打籽、平针、锁绣等点击标签会高亮绣品上对应的区域。这个功能技术上不难就是在贴图上多做一张遮罩图但它把看热闹变成了看门道是整个项目的价值锚点。2. 从一张平绣照片到可交互展品京绣三维资产的制作链路2.1 展厅本体模块化建模和网格预算的底线展馆建筑的部分反而是最容易被做崩的。很多人的第一反应是我要做一个恢弘的中式大殿然后在 Blender 里堆了几十万面进 Unity 一看帧率直接腰斩。我的做法是先定网格预算再开始建模顺序反了就得返工。具体到数值整个展厅的静态建筑面数控制在 15 万三角面以内单件展品在最高 LOD 下不超过 3 万面绣品本身基本是平面面数主要花在装裱框和展台的雕花上。之所以定这个数是因为目标机型里有一体机它的 GPU 填充率和顶点吞吐都有限超过这个量级再叠加烘焙光照掉帧是必然的。建模层面我用的是模块化拼装柱子、斗拱、窗棂、地砖各做一套基础件然后在场景里复制旋转。这样做有两个好处一是可以用 GPU Instancing 把重复件的 Draw Call 压下来二是后期改配色只要换材质不用重新导模型。斗拱这类细节件我做了一个高模和一个低模高模只用来烘焙法线实际进场景的是低模加法线贴图视觉上几乎看不出差别。提示中式建筑的木构件有大量重复建模时一定要提前想好哪些件可以复用。我见过一个同类项目光窗棂就做了 40 个独立模型最后光是这一项就吃掉了 200 多个 Draw Call。2.2 绣品建模翻拍贴图、法线叠加与针脚可见的实现这部分是整个项目的核心也是我花时间最多的地方。绣品本质上是二维的所以没必要真的去建三维的针脚几何那是自找麻烦。我的方案是高精度平面加多层贴图。第一层是翻拍贴图。有条件就用中画幅或者高像素微单加微距镜头在可控光源下做平铺翻拍分辨率至少做到 8192 长边因为最终要放大到针脚级别。翻拍时一定注意两点一是用偏振镜压掉丝线的镜面反光否则贴图上会出现大面积死白二是做色彩校准放置色卡后期在 Unity 里用颜色校正曲线还原。丝线的颜色偏差一旦大了整个展品的质感就全垮了。第二层是法线贴图。这一步是把平变成鼓的关键。我没有用软件从照片硬算而是手动做了一层高度图把每一簇丝线的走向画成灰度渐变再用高度图转法线。这么做比自动生成麻烦但结果可控——自动生成的法线会在绣品边缘产生大量噪点看起来像发霉。第三层是粗糙度和金属度。丝线用较高的粗糙度变化范围0.25 到 0.6金线单独做一张遮罩金属度拉到 0.8 以上、粗糙度压到 0.15 左右。这样在同样的光照下金线会比其他丝线亮出明显的层次。第四层是工艺标签遮罩。一张 RGBA 图每个通道对应一类工艺点击标签时用 Shader 做区域高亮。这个设计让展品从单纯的好看变成了可读。2.3 丝绸与金线的材质URP下的一套可控方案材质是决定像不像的最后一公里。我在 URP 下没有直接用 Lit而是写了一个基于 Lit 修改的自定义 Shader主要加了三样东西各向异性高光、边缘柔化、可切换的光照模式。各向异性高光是丝线的命门。标准 Lit 的高光是圆形扩散的看起来像塑料丝线的高光沿纤维方向拉长才是正确的表现。我在 Shader 里加了一个切线方向的控制参数用它来模拟丝线的走向。这个参数不需要和真实针脚完全对齐只要大方向对大脑就会自动接受。边缘柔化是为了解决近距离观察时的锯齿感。绣品贴图放大到极限时边缘会出现硬边加一点基于视角的柔化能明显改善观感。光照模式切换则是为了满足不同展示场景比如整体参观用柔和的全局光细节观察时切到单向硬光让针脚的起伏更明显。注意自定义 Shader 一定要在目标机型上跑一遍。我在编辑器里调得很满意导到一体机上发现各向异性计算量比预期高最后把参数降了一档才稳住帧率。3. Unity工程骨架的搭建管线、场景组织与漫游控制3.1 版本与渲染管线的选择理由版本我选的是 Unity 2022 LTS。理由很朴素这个项目要交付给别人维护LTS 的插件兼容性最稳遇到问题也最容易搜到答案。技术预览版的某些新特性确实诱人但一个需要长期运营的展馆项目稳定压倒一切。渲染管线用的是 URP不是 HDRP。HDRP 的材质表现力确实更强尤其是半透明和体积光但它对硬件的要求也更高一体机和网页端基本无缘。URP 在画质和性能之间的平衡点刚好落在我的目标区间内而且它对移动端的支持成熟后面要往一体机上移植时改动量最小。场景组织上我做了拆分而不是把整个展馆塞进一个 SceneBoot场景只放启动逻辑和配置加载常驻不卸载。Hall_Main主展厅静态建筑和灯光。Hall_Detail细节展区加载高清绣品资源。Exhibit_XXX单件展品的独立场景按需异步加载。这样做的好处是显存占用可控。观众在主展厅闲逛时高清绣品贴图根本不在内存里点进去才加载。异步加载我用Addressables做配合一个加载进度提示体验上不会有明显卡顿。3.2 漫游控制器CharacterController与轨道模式的切换漫游控制我试过三套方案最后留下的是 CharacterController。Rigidbody 加胶囊碰撞在楼梯和门槛上很容易打滑或者卡住而 CharacterController 的移动是自己算的虽然要手写重力但可控性高得多。核心逻辑不复杂关键是几个容易忽略的参数public class RoamController : MonoBehaviour { public float walkSpeed 2.2f; // 步行速度展馆内不宜过快 public float runSpeed 4.0f; public float gravity -9.81f; public float groundCheckOffset -0.15f; public float stepOffset 0.35f; // 台阶高度对应展厅的实际台阶 private CharacterController cc; private Vector3 velocity; private bool isGround; void Update() { isGround Physics.CheckSphere( transform.position Vector3.up * groundCheckOffset, 0.25f, LayerMask.GetMask(Ground)); if (isGround velocity.y 0) velocity.y -2f; velocity.y gravity * Time.deltaTime; float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 move transform.right * h transform.forward * v; float speed Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed; cc.Move((move * speed velocity) * Time.deltaTime); } }walkSpeed这个值我调了很久。定 2.2 是因为展馆的观赏节奏比游戏慢走太快观众根本来不及看展品会变成跑图。stepOffset必须和展厅实际台阶高度一致我一开始没改默认值结果观众在台阶前反复横跳上不去。另外我加了一个轨道模式切换到聚焦状态时控制器停用相机绕着展品中心做球面插值。这两个模式的切换用状态机管理避免两套逻辑同时写 Transform 导致抖动。3.3 交互拾取与展品聚焦视图拾取用射线检测但有几个细节决定了手感的顺滑程度。第一射线的最大距离不能设太远否则观众在展厅另一头也能点中展品体验很怪我设为 4 米。第二要用 LayerMask 过滤只检测展品层不要把地面和墙壁也算进去。第三聚焦过程必须用插值而不是瞬移我用的是一段 0.6 秒的球面插值同时把相机的 Field of View 从 60 收到 45视觉上会有一个推近的感觉。聚焦状态下的相机位置不是随便定的而是每件展品在配置里预先存好一个观察点位和注视点。这是因为不同绣品的观赏角度差异很大盘金绣要斜着看才有立体感平针绣正面看纹样最清楚自动算出来的位置往往不是最佳观赏位。提示交互不只是鼠标点击一体机上要用射线控制器网页端要处理鼠标和触摸两套输入。我封装了一层 InputAdapter把输入源统一成射线的起点和方向上层逻辑就不用改。3.4 自动导览NavMesh和路点该怎么配合自动导览一开始我想直接用 NavMesh 让虚拟导游自己走过去后来发现效果不好——NavMesh 算出来的路径是最短路径不是最佳观赏路径它会贴着墙走还会错过重要的展品。最后改成路点驱动加 NavMesh 修正手动摆放主要观赏点NavMesh 只负责在点与点之间做局部避障。这个方案的好处是节奏可控。每个路点可以配置停留时长、镜头朝向、是否触发讲解音频。观众开启导览后相机会按照预设的节奏移动到某件绣品前停 20 秒同时播放对应的讲解讲完自动走向下一个点。中途观众随时可以按键接管控制权接管后导览暂停而不是退出再次按键可以继续。这种可打断、可续播的设计我是从实际参观习惯推出来的。观众听讲解听到一半想自己看看细节如果导览直接退出他得从头再走一遍体验很差。4. 中文讲解、字幕与UI最容易翻车的部分4.1 TextMeshPro的中文图集内存问题中文在 Unity UI 里一直是个麻烦事。TextMeshPro 默认用静态图集需要你预先指定用到的字符一旦漏字就是方块。京绣的讲解文本里有大量生僻字和工艺术语靠手工列字符表根本不可能覆盖全。我的做法是静态图集加动态图集混合把常用汉字预先生成到静态图集里保证启动后的第一屏不会出现延迟同时开一个动态图集兜底遇到静态图集里没有的字运行时补进去。这个配置在 Font Asset 的属性面板里就能设关键是动态图集的最大尺寸和数量要控制住。我第一次做的时候把动态图集上限设成了 4096结果跑了一个小时内存涨了 200 多兆。原因是图集只增不减看完所有展品后所有出现过的字都留在图集里。后来我把单片图集压到 1024允许最多 4 片同时在切换展区时主动调用图集清理内存才稳定下来。注意动态图集清理有个坑清完之后屏幕上正在显示的文字会变成方块。所以清理时机一定要选在场景切换的黑屏或者加载页期间不能随时清。4.2 语音讲解与字幕的时序对齐语音和字幕对齐是看起来简单、做起来烦的典型。我用的是音频时间轴驱动字幕的方案每段讲解音频按句子切分成若干时间区间运行时读AudioSource.time落在哪个区间就显示哪句。这么做比每句话单独做一个音频文件然后顺序播放要稳因为后者的句间停顿完全靠代码控制稍微有点音频加载延迟就会错位。时间轴方案的缺点是前期标注麻烦需要人工听音频把每句的起止时间记下来。我的偷懒办法是先用语音识别工具跑一遍自动打轴再人工校对效率能提高三倍以上。还有一个细节字幕的出现和消失不能是硬切。我加了一个 0.2 秒的淡入淡出视觉上柔和很多。另外字幕的换行要按语义断句不能让它自动按宽度折行否则会出现盘金和绣被拆到两行的情况。4.3 分辨率适配与VR模式下的UI重排UI 适配我用的是 Canvas Scaler 加锚点组合。桌面端按 1920x1080 设计Scaler 设为Scale With Screen Size匹配模式选 0.5保证横竖比例变化时文字不会变形。但这套逻辑在一体机上完全不适用因为 VR 里的 UI 是空间化的不存在屏幕分辨率这个概念。所以我的 UI 分了两套平面 UI 用于桌面和网页端世界空间 UI 用于一体机。两者共享同一份数据但布局完全独立。世界空间的讲解面板我放在了观察点位的左前方约 1.2 米处高度与视线齐平字号换算下来相当于 1 米外看 32 号字这个尺寸在实际体验里最舒服既不用刻意转头也不会挡住展品。5. 灯光烘焙与性能预算让展馆在笔记本和一体机上都能跑5.1 光照烘焙策略与光照贴图UV的准备展馆的灯光几乎全部是烘焙的只有展品的局部补光用了实时灯。这么选是因为动态全局光照在目标机型上开销太大而展馆是静态场景烘焙完全够用。烘焙之前有一件事必须做就是准备光照贴图 UV。Unity 里的模型如果直接用导入时的 UV 去烘焙会出现大面积重叠和接缝发黑。正确做法是在建模软件里展一套专用的第二套 UV或者在 Unity 里用Generate Lightmap UVs自动生成。自动生成对简单几何体够用但斗拱这种复杂件一定要手动展否则接缝会非常难看。烘焙参数上我踩过一个坑Lightmap Resolution设得太高我一开始设了 60烘焙时间从 20 分钟涨到 3 小时但画质提升几乎看不出来。后来按区域分开设置地面和墙面用 15 到 20斗拱和雕花用 40烘焙时间回到 40 分钟左右效果反而更好。提示烘焙前一定要把不参与烘焙的物体比如展品、可动部件标记为Contribute GI关闭否则它们会在墙上留下奇怪的阴影。5.2 Draw Call、遮挡剔除与LOD的取舍性能优化里我优先级排得最清楚的一件事是先砍 Draw Call再谈其他。桌面端超过 1500 个 Draw Call 就开始有压力一体机上 500 以上就是危险区。砍 Draw Call 的手段按性价比排序材质合并把能共用一张贴图的物体合到同一个材质。展厅的木质结构本来有 12 个材质合并贴图后压到 3 个。GPU Instancing所有重复的柱子、地砖、展柜都开这个选项Draw Call 直接按实例数摊薄。静态合批不动的小物件合并网格代价是内存占用上升要权衡。遮挡剔除展馆是隔间结构墙体天然遮挡开启 Occlusion Culling 后站在一个展区里时其他区域的物体全部被剔除效果非常明显。LOD 我只给展品做了两级。展厅建筑本身是静态的配合遮挡剔除已经够用做 LOD 收益不大。展品的 LOD0 用 8K 贴图LOD1 降到 2K切换距离设在 2.5 米。为什么是 2.5 米因为这是观众从看整体切换到看细节的心理分界线再近就该给高清贴图了。5.3 贴图压缩与显存预算移动端的硬约束移动端的显存是真的紧这一点在桌面端做开发时完全感觉不到。我的做法是按分辨率分层指定压缩格式。贴图类型桌面端移动/一体机说明绣品反照率BC7 / 4KASTC 8x8 / 2K精度影响质感不能压太狠法线贴图BC5 / 4KASTC 6x6 / 2K法线压缩错误会产生噪点遮罩图工艺标签BC1 / 1KASTC 10x10 / 1K只有区域信息可以狠压建筑贴图BC1 / 2KASTC 10x10 / 1K不需要高精度光照贴图BC6H / 2KASTC HDR / 1K必须是 HDR 格式除了压缩格式还有一个很多人忽略的点是贴图的 Read/Write 开关。默认关闭一旦打开Unity 会在内存里额外保存一份可读副本显存翻倍。我检查工程时发现有三分之一的高清贴图被误开了这个选项关掉之后一体机的内存占用直接降了 300 多兆。6. 踩坑复盘几个卡了我一整天的具体问题6.1 阴影贴图引发的条纹与自阴影痤疮展厅的斗拱区域出现了一道道横向的条纹远看像百叶窗。这个问题我排查了大半天最后定位到是阴影贴图分辨率和 Bias 配置不匹配导致的自阴影痤疮。排查过程是这样的先怀疑是烘焙光照贴图的问题重新烘焙了一遍条纹消失了一半说明确实和烘焙有关然后关掉实时阴影条纹完全消失锁定问题在实时阴影接着把 Directional Light 的 Shadow Resolution 从 Medium 提到 High条纹变细但没有消失说明是 Bias 的问题。最后把Shadow Bias从默认的 0.05 调到 0.12、Normal Bias调到 0.4条纹彻底消失。这个过程的经验是自阴影痤疮和光照贴图接缝的视觉表现很像但排查顺序不一样。如果是条纹跟着相机移动多半是实时阴影如果是固定在几何体边缘多半是光照贴图 UV 问题。6.2 玻璃展柜的半透明排序玻璃展柜用的是半透明材质展品在柜子里结果出现了展品被玻璃盖住一半的诡异现象而且随着相机移动遮挡关系还会跳变。这是半透明渲染排序的经典问题Unity 按物体中心到相机的距离排序但玻璃和展品的位置关系是嵌套的按中心排序必然出错。解决方案有两层。第一层是让玻璃不写深度但正确排序把玻璃材质的 Render Queue 设为 Transparent并关掉ZWrite。第二层也是更可靠的一层是给展品单独指定渲染队列让它在玻璃之前渲染。我最终用的是第二种因为展品数量可控手动指定队列最稳。如果展柜数量很多还有个办法是把玻璃换成伪透明材质不做真正的 Alpha 混合而是用折射贴图加轻微的高光视觉上像玻璃但走的是不透明渲染流程完全没有排序问题。我在细节展区就用的这个方案。6.3 WebGL构建与桌面端的资源读取差异桌面端跑得好好的东西构建成网页版之后直接打不开。排查下来有三个原因。第一是资源读取方式。桌面端可以用File.ReadAllBytes直接读文件网页端没有本地文件系统必须走UnityWebRequest或者把资源打进包体。我原来那套从外部加载展品配置的逻辑在网页端全部改成从StreamingAssets走网络请求。第二是线程。网页端不支持多线程任何用了System.Threading的代码都会直接报错。我的异步加载逻辑原来是开线程读文件的改成协程之后才正常。第三是包体大小。8K 贴图打进去包体直接 400 多兆加载页转了快一分钟。最后我把网页版单独做了一套资源最高 2K 贴图配合压缩包体降到 90 多兆加载时间压到 20 秒以内。注意网页版和桌面版最好从一开始就分成两套构建配置不要等到最后再改否则要动的代码会非常多。7. 把展品做成数据内容可扩展的工程化思路7.1 ScriptableObject加JSON的双层配置展馆项目最怕的是换一件展品就要改代码重新打包。所以我从一开始就把展品定义成数据。核心结构用 ScriptableObject方便在编辑器里可视化编辑和引用资源[CreateAssetMenu(fileName Exhibit, menuName JingXiu/Exhibit)] public class ExhibitData : ScriptableObject { public string exhibitId; public string displayName; public string craftType; // 盘金、打籽、平针等 public Texture2D albedo; public Texture2D normalMap; public Texture2D craftMask; // RGBA 分通道标记工艺区域 [TextArea] public string description; public AudioClip narration; public Vector3 cameraPos; // 预设观察点 public Vector3 lookAtPos; public float focusFov 45f; public ListSubtitleLine subtitles; } [System.Serializable] public struct SubtitleLine { public float startTime; public float endTime; public string text; }同时我在StreamingAssets里放了一份 JSON内容和 ScriptableObject 一致。运行时优先读 JSON本地没有就回落到内置的 SO 数据。这么做是为了让展馆运营方可以自己改讲解词、调观察点位改完直接把 JSON 推上去不用重新构建。7.2 这套骨架还能往哪长做到这里展馆的基本盘已经稳了但我实际用下来觉得还有几个方向值得继续加。一个是工艺对比模式。现在的展馆是一件一件看但京绣的价值很大程度上在于工艺之间的差异。我后来加了一个并排对比功能可以把盘金绣和平针绣放在同一光照下左右对照观众能直观看到金线的反光和丝线的哑光是完全两回事。另一个是纹样的结构拆解。京绣的纹样有很强的对称性和重复规律我尝试过用程序化的方式把一件绣品的纹样拆成基础单元观众点某个单元其余相同的单元一起高亮。这个功能技术上是复用工艺遮罩那套逻辑把遮罩换成纹样单元图就行。最后是参观记录导出。我加了一个很轻的功能观众可以给自己喜欢的展品打标记参观结束时导出成一张图包含展品缩略图和名称。这个功能本身价值不大但它显著提高了观众的停留时间因为有了要收集的动机之后人会不自觉地多看几件。我个人在整套项目里最深的体会是非遗数字化最大的难点从来不是渲染技术而是你到底想让观众看懂什么。技术选型、贴图精度、光照方案这些都能查文档解决但这件绣品最值得被看见的那一针在哪里只能靠你反复看、反复问手艺人才能找到答案。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价