ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Unity与C#的瓯绣3D虚拟展馆漫游系统实现

基于Unity与C#的瓯绣3D虚拟展馆漫游系统实现 前几年接手过几个地方非遗文化数字化的活儿说实话一开始我是拒绝的。因为这类项目十有八九最后做成了一个能点的电子画册——几张高清图加一段文字说明再配点背景音乐交差完事。但瓯绣这个题材不太一样它是那种你必须凑近了、侧着光看才能看明白门道的工艺。绣线的丝光随着角度变化针脚的走向层层叠叠平面的照片根本压榨不出这种东西的味道。所以我给自己定了个目标不做画册做一座能走进去、能蹲下来看细节的3D虚拟展馆。这篇就把整个基于Unity、3D技术和C#脚本层的实现思路、踩坑过程和我自己总结出来的经验原原本本写一遍给同在做文化数字化、虚拟展馆、交互漫游系统的朋友当个参考。1. 从能点的画册到能走的展馆这个项目到底要解决什么我先说说立项时的判断。非遗数字化项目最常见的失败模式就是把线下展陈的思维直接搬到线上一张大图挂在中间两边放说明文字用户点一下翻页。这套东西在网页时代勉强能用但它有个致命问题——用户没有在场感。你没法感受到瓯绣作品的实际尺幅没法判断一根绣线到底多细更没法理解一幅绣品上针脚疏密带来的质感差异。漫游系统要解决的恰恰是这个在场感缺失的问题。1.1 瓯绣这门手艺的展示难点究竟在哪瓯绣是浙江温州的传统刺绣工艺讲究的是以针代笔、以线代墨针法上有齐针、接针、滚针、施针、打籽针等多种变化题材多取自花鸟、山水、人物。它的展示难点有三个层面。第一是尺度感一幅大幅绣品动辄一米见方绣面上一根线条可能只占几毫米没有参照物的情况下照片会让人误判大小。第二是材质感绣线是蚕丝材质具有强烈且方向性的高光同一块绣面换一个观察角度明暗关系完全不一样这是照片永远表达不了的。第三是工艺层级一件绣品是底布、多层绣线叠加出来的立体结构不是一张平整的贴图只有从侧面看才能看出堆叠的厚度。这三个难点决定了如果只做平面展示相当于把这个项目最核心的价值全丢了。所以我在方案里坚持用真实的3D空间来承载展品让用户能绕着走、能俯身看、能把镜头怼到绣面上。1.2 为什么是漫游而不是轮播轮播图式的交互本质上是一种被动接收用户只能沿着设计者规定好的路径一张张看下去无法建立空间记忆。而我想要的是主动探索。漫游这种形态的好处在于它把展馆的空间结构交给了用户自己理解——序厅在哪、工艺区在哪、代表作品展墙在哪用户走一遍就能在脑子里形成一张地图。这种空间记忆会显著提升信息留存这也是实体展馆几百年来一直沿用的逻辑。虚拟展馆要做的就是把这套逻辑在数字世界里复现出来同时去掉实体场馆里那些限制比如你可以瞬间飞到任何一件展品面前可以从任何角度看这在现实中是做不到的。明确了这个核心目标之后后面所有的技术选型、资产制作、交互设计都是围绕让用户在3D空间里舒适地看绣品这一件事来展开的。2. 技术栈定盘Unity加C#在这个场景里凭什么胜出确定要做3D漫游之后引擎选型就成了第一个要拍板的事。当时摆在桌面上的一共有三个候选基于Three.js的纯网页方案、Unity、以及Unreal。很多人第一反应会觉得网页方案最轻量、传播最方便但我最终选了Unity加C#的路线这个决定不是拍脑袋是逐条比出来的。2.1 三种技术路线的横向对比我把当时评估的维度整理成了下面这张表这几个维度基本能覆盖展馆项目的全部诉求。评估维度Three.js网页方案Unity C#Unreal开发语言JavaScriptC#C/蓝图高精度绣面材质表现中PBR需要手工接高标准PBR管线成熟很高但对本项目过剩编辑器可视化搭场景弱基本靠代码布置强所见即所得强访问便利性极好浏览器直开WebGL可发布需处理包体包体大网页发布不友好二次开发与迭代速度中高C#上手快低编译慢多端扩展头显/桌面弱强一套工程多端发布强Three.js的优点非常明确零安装、打开网页就能看。但它的短板同样明显——缺乏成熟的可视化编辑器展馆里几百个物件的摆放、灯光、碰撞体全靠代码写迭代效率极低。而且要在网页端做高质量PBR材质和烘焙光照需要手工拼接各种渲染管线对一个以文化展示为主、团队里美术偏多的项目来说成本太高。Unreal的表现力确实最强但对这个项目来说是杀鸡用牛刀。瓯绣展馆不需要电影级的实时渲染需要的是稳定的漫游体验和细腻的材质表现Unreal的编译速度和包体大小反而会拖累迭代。2.2 C#脚本层在展馆系统里到底承担什么选Unity的核心原因之一是C#这门语言在业务逻辑层的开发效率。虚拟展馆听起来是渲染和美术的活但真正的工作量大头在交互逻辑上。C#在这个项目里主要承担四类工作漫游控制器处理WASD移动、鼠标视角、手柄和触屏输入的统一抽象、展品交互系统射线拾取、悬停高亮、详情面板弹出、数据驱动层把展品信息从配置里读进来绑定到对应的3D物件上改内容不用改代码、以及多端适配层根据运行平台切换输入方式和画质档位。举个例子我把展品的所有信息都放在一个ScriptableObject列表里每件展品用唯一的ID对应一个场景中的物件。这样运营方后期想改某件绣品的文字说明只要在编辑器里填表格就行完全不需要程序员介入。这种数据和表现分离的设计思路是C#这个层级最能发挥价值的地方也是Three.js方案里要靠后端接口才能勉强做到的。3. 瓯绣展品的3D资产生产链路从实物到可交互模型引擎定好之后最难啃的骨头来了——绣品模型的制作。这是整个项目的技术核心也是最容易翻车的地方。一件平面的刺绣作品做成3D模型听起来简单实际上要考虑的问题比做一个人物角色还多。3.1 实物采集扫描和拍照建模怎么选采集环节我试过两种主流方案。第一种是结构光扫描用结构光相机对绣品做逐面扫描优点是能拿到比较准的几何表面缺点是结构光对高反光表面非常不友好。绣线的丝质高光会导致扫描点云出现大量空洞和噪声特别是滚针这类反光强烈的针法区域扫出来的数据基本没法用。第二种是基于多视角照片的摄影测量建模用一圈照片反算出表面几何和纹理对反光有一定的容错只要拍摄时打匀光、避免直射高光就能拿到可用的纹理。最终我采用的是混合方案几何结构上用摄影测量拿大形绣面的高频细节不依赖几何而是靠法线和置换贴图来还原。原因很简单——绣线的凹凸本身尺度太小如果真把它做成几何一个绣面会产生几十万个面性能直接崩掉。用贴图假造凹凸是性价比最高的做法。提示拍摄绣品时一定用柔光箱或者漫反射光源避免出现硬阴影和镜面高光斑点否则反算出来的纹理上会留下洗不掉的亮斑。3.2 绣面材质的PBR还原丝光、底布和法线材质是这个项目最花心思的部分。瓯绣的视觉特征可以拆成三块底布是一层偏哑光的织物绣线是一层带方向性高光的丝质材料两者叠在一起还有厚度差异。我在Unity里用的是标准PBR管线重点调三个参数。第一个是Smoothness光滑度。绣线区域给到0.6到0.75之间底布区域压到0.2左右这样侧光一扫绣线会有明显的高光流动底布则保持沉稳。第二个是法线贴图的强度。针脚的方向性很强齐针是一排排平行的线条施针则是斜向交错。法线贴图必须按照这些真实针脚的走向来做不能随便找一张布料法线糊上去否则近看就是一片乱七八糟的噪点。我的做法是让美术对照实物照片手工绘制针脚的走向图再转成法线。第三个是各向异性高光。绣线的丝光其实是各向异性的——沿着线的方向高光弱垂直方向高光强。Unity标准着色器不直接支持各向异性我通过自定义着色器叠加了一层方向性的高光计算来实现代价是DrawCall会多一些但近景观察时的质感提升非常明显。3.3 模型减面与拓扑整理展馆里如果放了二三十件绣品每件模型的面数必须严格控制。我的经验值是单件展品的主体网格控制在五千到两万面之间远看的大幅挂墙绣品甚至可以压到两千面以下靠法线贴图撑细节。减面的时候有个坑要注意不要用自动减面工具直接糊那样会导致绣品的边框和装裱结构扭曲。正确做法是把绣品拆成装裱框架和绣面两部分分别处理框架结构规整可以用硬边低模绣面是平面可以用高密度网格保留轮廓再统一减面。展品类型建议面数场景位置观察距离大幅挂墙绣品2000-8000主展墙中远距离中型立式展品8000-20000独立展台中近距离小型可把玩绣片20000-40000互动体验台近距离4. 展馆空间动线与漫游手感导航、碰撞和视角设计模型都准备好了接下来是搭建空间和设计漫游体验。这个环节直接决定了用户是逛得舒服还是逛得晕头转向。我见过很多虚拟展馆项目空间做得金碧辉煌但一走起来就头晕、卡墙、掉出场景体验极差。漫游的第一原则是让用户感觉不到系统的存在。4.1 展馆平面布局与参观动线我给瓯绣展馆设计的是一条环形动线从入口序厅开始依次经过历史沿革区、针法工艺区、代表作品区最后到互动体验区和文创区形成闭环。环形动线的好处是不会让用户产生我是不是走过头了的焦虑逛完一圈自然回到起点附近。空间尺度上有个反直觉的点虚拟展馆的通道要比实体展馆更宽。实体里人走路有真实的惯性参照而虚拟漫游里用户对速度的感知是失真的通道太窄很容易频繁撞墙。我的经验值是主通道宽度至少三米展品之间的间隔至少两米给人留出足够的视角调整空间。4.2 第一人称漫游的移动与视角实现移动方式上我提供了两套方案第一人称自由漫游和轨道环绕观察。自由漫游适合逛展轨道环绕适合看单品。自由漫游的控制器用简单的CharacterController就能实现下面是一段核心逻辑的简化版本。public class VisitorController : MonoBehaviour { public float moveSpeed 3.0f; public float mouseSensitivity 2.0f; private CharacterController controller; private float pitch 0f; private float gravity -9.81f; private Vector3 velocity; void Start() { controller GetComponentCharacterController(); Cursor.lockState CursorLockMode.Locked; } void Update() { // 视角旋转 float mouseX Input.GetAxis(Mouse X) * mouseSensitivity; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity; pitch - mouseY; pitch Mathf.Clamp(pitch, -80f, 80f); transform.Rotate(Vector3.up * mouseX); Camera.main.transform.localRotation Quaternion.Euler(pitch, 0f, 0f); // 移动 float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 move transform.right * h transform.forward * v; controller.Move(move * moveSpeed * Time.deltaTime); // 重力 if (controller.isGrounded velocity.y 0) velocity.y -2f; velocity.y gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }有一点必须强调鼠标Y轴也就是俯仰角一定要做角度钳制我限制了±80度避免用户把头翻到完全倒过来导致画面失控。这个约束看似小但没有它用户猛拉鼠标的时候体验会非常糟糕。4.3 碰撞体与导航网格的取舍碰撞体这块展馆里可交互的物件多如果每个都挂精细的Mesh Collider物理开销会很大。我的做法是分层处理墙壁、地板、大型展台这些静态物件用Box Collider简单又高效只有需要精确交互的小型展品才用Mesh Collider而且要用减面后的低模碰撞网格。这样既保证了漫游时不会被卡住也保证了交互拾取的精度。至于导航网格NavMesh我一开始想用它给展馆加个自动导览功能让虚拟向导带着用户逛。后来发现展馆是开放动线自动寻路的意义不大反而增加了烘焙和维护成本就砍掉了。这里分享一个判断标准只有当空间复杂、路径明确时导航网格才值得投入像这种环形开放展馆用户自由漫游体验更好。5. 展品交互层点选、悬停与详情面板漫游能让用户走进来但真正传递信息的是展品交互层。用户在展馆里的核心行为就是靠近一件绣品点一下看它是什么。这个交互链路听起来简单要做好却有不少门道。5.1 基于包围盒和射线的拾取方案拾取的本质是从摄像机发出一根射线检测它撞到了哪个展品。Unity里最直接的做法是Physics.Raycast配合Collider。但这里有个性能陷阱如果场景里有几十上百个带碰撞体的物件每帧对每个物件都做一次精细检测开销会累积起来。我用了两级筛选。第一级用Renderer的包围盒做粗略排除——每帧计算相机视锥和每个Renderer包围盒的关系只有进入视锥的物件才参与后续检测。第二级再用射线做精确命中判断。这样即使展品多每帧真正参与射线检测的也只有视野内的那几个。void Update() { if (Input.GetMouseButtonDown(0)) { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f, exhibitLayer)) { ExhibitItem item hit.collider.GetComponentExhibitItem(); if (item ! null) infoPanel.Show(item.data); } } }注意最后那个exhibitLayer参数我用LayerMask把射线限制在展品层这样地面和墙壁再厚也不会被误判成展品交互逻辑干净很多。5.2 悬停高亮与描边效果光有点选还不够用户得先知道这个东西是可以点的。我加了悬停高亮鼠标移到展品上时物件轮廓会亮起。实现上有两种主流方案一种是给物件加一层描边材质一种是缩放放大。描边的视觉效果更专业但代价是要多渲染一遍模型。考虑到性能我的折中是只对当前悬停的单个物件做描边同一时刻描边对象唯一开销完全可接受。悬停检测的频率也不用每帧都跑我设成每两帧检测一次肉眼几乎感知不到延迟CPU压力却降了一半。5.3 详情面板与图文数据绑定点开展品之后弹出的详情面板是信息传递的最后一公里。这里我踩过一个坑一开始把面板的每个文字都硬编码在预制体里结果后期运营改文案得一件件去改预制体效率极低。后来我把所有展品数据抽成了配置表用ID绑定。[CreateAssetMenu(fileName ExhibitData, menuName Ouxiu/ExhibitData)] public class ExhibitData : ScriptableObject { public string id; public string title; [TextArea] public string description; public Sprite[] detailImages; public float scaleReference; // 实际尺寸用于比例提示 }特别说一下scaleReference这个字段。因为是虚拟展馆用户对展品实际大小没概念我在面板里加了一条比例标尺根据这个字段动态显示本作品实际宽度约 120 厘米。这么一个小小的设计让用户对绣品的真实尺幅有了直观认知是漫游系统相比平面展示的一个独有优势。6. 视觉表现和性能的拉扯阴影、光照与分辨率前面几节讲的都是功能这一节讲讲性能。虚拟展馆最容易出现的翻车不是功能不全而是卡。用户对流畅度的容忍度极低一旦掉帧再精美的场景也会被嫌弃。性能和表现之间的平衡是贯穿整个开发周期的持续工作。6.1 阴影问题的根因与烘焙策略我遇到的第一个大坑就是阴影。展馆里灯光多如果全开实时阴影GPU直接吃不消尤其是多光源投影叠加的时候帧率断崖式下跌。而且实时阴影在远处容易出现锯齿和闪烁也就是俗称的阴影痤疮。我的解决方案是以烘焙为主实时为辅。整个展馆的静态光照——包括主光、环境光、展柜底光——全部烘焙到Lightmap里一次性烤好运行时零开销且效果细腻。只有少数需要动态变化的部分比如用户走近展品时脚下的一圈高亮才用实时阴影而且给它单独设置了很短的阴影距离。还有几个具体参数值得记录阴影贴图分辨率不用盲目拉高主光用2048、补光用1024就够了展馆这种尺度再高也很难看出差别阴影级联数用4级能覆盖从近到远的过渡阴影距离压到三十米以内超出这个范围的物件根本不投影——反正用户看不到远景的阴影细节白白浪费算力。6.2 LOD、批处理与遮挡剔除除开阴影模型数量也是性能大头。展馆里几十件绣品加上建筑结构如果不做优化DrawCall能轻松上到几百。我用了三板斧。第一是静态合批。展馆的墙壁、地板、装饰这些不会移动的物件材质相同或相近的全部合并成批次渲染DrawCall能降下一大截。第二是LOD分级。每件展品准备高、中、低三档模型根据相机距离自动切换。远看用低模凑近自动切高模用户几乎无感知但远处那几十件展品的渲染压力大幅下降。第三是遮挡剔除。展馆有墙体很多展品是藏在墙后面的遮挡剔除能把它们提前剔除出渲染队列。烘焙好遮挡数据之后相机看不到的物件就不会被提交渲染。优化手段主要收益适用对象静态合批降低DrawCall墙壁、地板、装饰LOD分级降低面数与渲染压力所有展品遮挡剔除剔除不可见物件被墙体遮挡的展品光照烘焙免除实时阴影开销静态场景光照6.3 分辨率与UI适配多端运行意味着分辨率不固定UI适配必须提前考虑。我用的是Unity的Canvas Scaler参照分辨率设成1920×1080匹配模式用Match Width Or Height匹配值给0.5这样在不同宽高比的屏幕上UI布局都不会变形。详情面板这种需要精确排版的界面我进一步用了锚点自适应布局保证文字框能跟着屏幕拉伸。GPU方面有个要提的点包体里我把贴图的压缩格式按平台区分。PC端用高质量压缩保证绣面细节移动和网页端用更激进的压缩格式换取加载速度。同一张绣面纹理不同平台的显存占用能差出好几倍这个优化对移动端和网页端尤为重要。7. 多端发布PC、网页与头显端的适配取舍这个项目最终要做多端发布PC端给展厅大屏和深度体验用网页端给线上传播用头显端给沉浸式体验用。一套工程多端发布是所有Unity项目既爱又恨的地方——代码能复用但性能和输入方式差异巨大。7.1 WebGL打包与服务器部署网页端是传播的主力我用Unity的WebGL平台打包。这里有个绕不开的环节服务器配置。Unity WebGL导出的产物包含.wasm、.data、.js等文件如果服务器没配置对应的MIME类型浏览器会拒绝加载页面直接白屏。我在IIS上部署时需要在配置里补上这些扩展名的映射。system.webServer staticContent remove fileExtension.wasm / mimeMap fileExtension.wasm mimeTypeapplication/wasm / remove fileExtension.data / mimeMap fileExtension.data mimeTypeapplication/octet-stream / remove fileExtension.unityweb / mimeMap fileExtension.unityweb mimeTypeapplication/octet-stream / /staticContent /system.webServer除了MIME类型还要开启压缩。Unity打包时可以选择gzip或Brotli压缩配合服务器端的压缩模块包体体积能降一半以上。另外包体的加载进度一定要做可视化——网页端初次加载动辄几十兆用户盯着白屏几十秒会直接关掉加个和瓯绣主题一致的加载动画等待体验会好很多。7.2 头显端的适配要点头显端我评估过主流的几款安卓系头显。VR模式下的核心约束是帧率必须稳定在72以上一旦掉帧用户会立刻产生眩晕感。这对渲染优化提出了更高要求我的做法是在VR模式下强制启用最低画质档——关掉大部分实时阴影、降低LOD切换距离、减少后处理效果用表现力换帧率。输入方式上VR用的是手柄射线交互和PC的鼠标射线逻辑几乎一样只是把射线源从鼠标位置改成了手柄朝向。这也是我一开始坚持把交互逻辑抽象成射线拾取这个统一模型的好处换输入设备时改动量很小。7.3 轻量端的现实取舍至于更轻量的传播场景比如在社交平台上做个小程序版本我的判断是要大幅简化不要想着把完整展馆塞进去。轻量端只保留几个核心展品的环绕观察就够了漫游和复杂交互都砍掉。原因很实在轻量端的性能和包体限制太苛刻硬塞完整展馆的结果就是又卡又慢还不如做个精致的单品展示体验。做多端适配最容易犯的错误就是试图让每个端都有完整体验结果哪个端都做不好。8. 开发中踩过的坑和一份经验清单写到这里该讲的核心系统基本都覆盖了。最后这部分我想把整个项目里踩过的坑单独拎出来因为这些东西在官方文档里基本找不到全是一次次调试熬出来的。8.1 材质和光照相关的教训第一个坑是绣面反光过头。刚开始调材质的时候为了突出丝光效果把Smoothness拉得太高结果整个绣面在强光下变成一片白色的镜面纹理全被淹没。后来把高光区域收窄、底布区域压暗让高光只出现在绣线脊线上画面才清爽起来。这个教训是丝光要点到为止不是越亮越好。第二个坑是烘焙光照和动态物件打架。有些展品我加了轻微的旋转动画结果发现它旋转的时候投在展柜上的烘焙阴影纹丝不动显得非常假。解决办法是把这类动态展品单独标记为动态光照探针接收对象让它的光照跟着变化虽然烘焙成本增加了但避免了穿帮。8.2 交互和性能相关的教训射线拾取的层级泄漏是个隐蔽的坑。有次发现点地面的砖块也能弹出展品详情排查了半天才发现是碰撞体层级没设对地面误入了展品层。后来我把所有可交互物件的层级统一管理用脚本在初始化时自动校验杜绝了这类问题。远距离物件的DrawCall浪费也是常见问题。展馆远端的小展品在用户还看不清的时候就已经全量渲染了白白吃性能。解决办法就是前面说的LOD加遮挡剔除组合拳把近处的渲染预算省下来给真正需要看清的展品。8.3 给同类文化数字化项目的几点建议第一文物和工艺美术类的模型细节假不了。用户可能不懂PBR原理但他能一眼看出绣面假不假。宁可多花时间做针脚法线和材质也不要在资产上偷工减料。第二交互要克制。展馆不是游戏不需要花哨的特效和复杂的玩法。用户进来是看绣品的所有交互都应该服务于看清、看懂这两件事多余的动画和音效只会分散注意力。第三数据一定要和表现分离。文化项目的内容后期改动频繁如果每改一段文字都要程序员参与项目交付后会非常痛苦。把数据抽成配置是让项目能长期维护的关键。第四多端发布前先明确每个端的定位。PC端做深度、网页端做传播、头显端做沉浸各自有各自的优化重点不要一套参数走到底。我个人在这个项目里最深的一点体会是虚拟展馆做得再炫本质还是个文化容器。技术只是把瓯绣这门手艺端到用户面前的手段真正让用户记住的还是绣面上那些细密的针脚和丝线的光泽。所以每次在性能优化和表现力之间纠结的时候我都会问自己一句这个取舍会不会让用户看不清一根绣线如果会那就不砍。这套判断标准帮我在无数个技术决策里找到了方向也希望能给正在做类似项目的你一点参考。
返回列表