资讯动态

Unity+3D+C#:羌绣虚拟展馆交互漫游系统实战

发布时间:2026/9/19 2:55:42 来源:尧图企业网站定制
去年冬天接了个活给一家地方非遗文化馆做线上展陈主题是羌族刺绣。需求方最初的想法很朴素拍一段全景视频配个网页能转着看就行。真做起来才发现行不通——观众想看的是绣片上那些盘绕的羊角花、太阳纹、云云纹视频里一放大就糊成一片色块针脚的走向、丝线的走向全丢了。方案推翻重来最后落地成一套基于Unity 3D C#的虚拟展馆交互漫游系统。说得直白点观众在桌面端或浏览器里打开用键盘鼠标或者手指像逛真馆子一样走进一个个展厅走到展柜前能点开把每一片绣品转到眼前看清楚。这套东西的核心价值不在炫技而在于把静态的、只能隔着玻璃看的绣品变成可以无限接近、可以任意角度端详的数字展品。适合谁看做数字文博、做展厅可视化、做VR看房类项目的开发者都能直接抄走大半哪怕是刚学Unity的学生照着把漫游控制器和热点交互这两块啃下来也足够撑起一个像样的课程设计。下面我把整个项目从选型到打包的链路拆开讲重点讲那些文档里不会写、只有真做过才知道的地方。1. 为什么这个展馆用Unity而不是网页3D引擎来扛1.1 羌绣题材对渲染能力提出的三个硬指标先说清楚这个题材难在哪不然选型就是拍脑袋。羌绣的视觉特征有三个特别吃性能的点。第一是丝线的高光——真正的手工绣线是蚕丝捻成的在灯下有明显的方向性反光转向不同角度绣面会有一层流动的亮带。这不是简单的金属度贴图能糊弄过去的材质得做出各向异性。第二是织物的微表面结构粗布底料的经纬纹理、绣线堆叠的厚度感都是几何层面的细节靠一张平面贴图会显得像贴纸。第三是展陈照明博物馆里为了护文物用的是低照度、高显色的定向光这种光照环境下色彩对比强烈黑底配红黄绿很容易过曝。这三个指标叠加起来意味着引擎必须在实时PBR、自定义Shader、烘焙光照这三件事上都有成熟方案。我当时拿三张高清绣片照片做了个测试场景分别在三套方案里跑结果一目了然。1.2 Unity、Three.js、Unreal三条路的取舍账先排除掉的是纯网页方案。Three.js确实轻打开网页就出画面但它的问题在于资产管线太原始——一个稍微复杂的展厅场景模型、贴图、光照全部要靠手写代码挂载团队里三个人协作两周后代码就没人敢动了。而且它的阴影和全局光照基本要靠自己补做室内展馆这种对光影氛围要求极高的场景性价比太低。Unreal的画质没得说Lumen一开展厅的光影氛围几乎是电影级。但我们这活儿的交付形态有三种桌面端、浏览器端、还有一套给馆内触摸大屏用的安卓端。Unreal打包WebGL的体积和兼容性都不理想安卓端对中低端设备也不友好。更现实的是团队配置——三个人里两个是C#背景没人写过UE的C学习成本压不住。最后落到Unity。不是因为它是最好的而是它是在画质、跨平台、团队上手成本三者之间最均衡的。C#写逻辑手脚快URP管线在移动端和WebGL的表现在同级别里最稳Shader Graph拖节点就能调出织物材质美术不用等程序。下面这张表是我们内部决策时真实拉过的对比。维度Three.jsUnity URPUnreal上手门槛低但复杂场景需自研中C#友好高C与蓝图双线室内光影表现需自研弱烘焙Lightmap成熟最强Lumen开箱即用自定义材质手写GLSLShader Graph可视化材质编辑器强大WebGL打包体积最小中等可优化偏大兼容有坑移动端表现取决于设备稳定可分级中低端吃力多端复用一套资产需重新适配基本可直接复用需针对平台调整1.3 我最终锁定的技术栈与版本选择版本这块我踩过一次亏必须说。一开始图新鲜用了某个较新的预览版结果打包WebGL时IL2CPP的编译优化报了一堆莫名其妙的错折腾两天没搞定果断退回Unity 2022.3 LTS。LTS版本的最大好处不是新功能多而是社区里的坑基本都被人踩过了搜得到答案。一个做交付项目的人最怕的不是遇到问题是遇到问题搜不到人问。渲染管线选URP不选HDRP。HDRP画质确实更高但它对移动端和WebGL基本是关闭的等于自己把交付形态砍掉一半。URP虽然上限没那么高但通过烘焙光照加精心调材质展厅的质感完全够用实测在大屏上观众根本分不出来。其余几个关键依赖输入系统用新版的Input System为了同时兼容键盘鼠标和触摸屏老Input Manager写双套逻辑太脏纹理压缩统一走ASTC给移动端、BC7给桌面端代码版本用Git LFS托管模型和贴图不然仓库几个G同事拉一次代码得等半小时。提示URP的版本一定要和Unity主版本严格对应。我见过有人手动装了个不匹配的URP包结果所有材质变成粉色排查半天才发现是版本错位。用Package Manager里自带的那个版本最省事。2. 从一张羌绣照片到可漫游的展厅资产管线怎么搭2.1 建模精度的够用边界别把绣片当成雕塑来做刚入行时我有个毛病觉得面数越高越专业。这个项目里差点栽进去——美术同学按参考照片把一片巴掌大的绣品建了两万面十几片绣品加展厅结构一凑单帧三角面数直接冲到八十万中低端安卓机打开就掉到十几帧。后来全部返工。虚拟展馆的建模精度要按观看距离来分配不是按物体本身的复杂度。我最后定的规则是建筑结构墙面、地面、柱子、展台单个构件控制在2000面以内用平铺贴图代替细节几何。展柜与家具3000到5000面边角倒角保留因为观众会凑近看。绣品挂轴1500面以内主要靠法线贴图表现织物质感几何上只做轻微的弯曲起伏模拟布料自然垂坠。特写级的绣片允许到8000面但整个场景里最多三片只在重点展项区域出现。这个分配的底层逻辑是视锥剔除和LOD的成本。远处的墙面做再精细也看不见把面数预算留给观众真正会凑近的东西才是划算的。2.2 纹样还原从照片到PBR材质的处理链羌绣纹样的数字还原是这项目里最费功夫的部分也是最出彩的部分。流程大致是这样先拍高清原片我用的是微距镜头加偏振片偏振片是为了压掉丝线的杂散反光拍出来的底图才干净然后进图像软件做三件事——去光影、校色、切分图层。去光影这一步新手最容易忽略。拍出来的照片带着现场灯光的方向性明暗如果直接当贴图用等于把当时的灯光焊死在材质上进引擎后一打新光就脏。做法是用高反差保留加频率分离把低频的光影信息抽掉只保留高频的纹理细节。切分图层是为了做PBR。一张照片至少拆成三层BaseColor纯色底、Normal/Height从灰度推算织物的凹凸、Roughness丝线区域粗糙度低底布区域粗糙度高。丝线那部分我还单独做了一张遮罩交给Shader去叠加各向异性高光。处理阶段目的关键操作常见错误原片拍摄拿到无污染素材微距偏振均匀柔光用手机闪光灯直拍反光炸掉去光影剥离环境光频率分离保留高频直接调曲线纹理一起压平校色还原真实色色卡比对注意黑底偏色凭肉眼调屏幕色差导致偏色图层切分生成PBR通道拆Base/Normal/Rough全都塞进一张图引擎认不出通道打包降低Draw Call按URP规则合并通道打包顺序错材质全乱2.3 展厅动线规划先在纸上画平面再进引擎这一步我强烈建议别跳过。很多开发者拿到需求就开始搭场景搭到一半发现观众走两步就撞墙或者重点展项藏在角落里没人看得到。展厅的本质是空间叙事得先有动线再有模型。我的做法是先在方格纸上画平面图标出入口、序厅、主展区、重点展项、互动区、出口这几个节点然后画观众的行进路线。羌绣这个题材我是按历史源流—纹样分类—工艺流程—当代创新四段式布局的观众沿着一条环形动线走不走回头路每个展区之间用半开放的隔断引导方向避免一眼望穿整个展厅导致失去探索感。有两点经验值得说。第一入口宽度至少留2.5米太窄会产生压迫感虚拟漫游里的空间感受比现实更敏感因为没有真实的声学反馈。第二重点展项要放在动线的第二个转弯之后太靠前观众还没进入状态太靠后又会疲劳。这个尺度感是我做完三个展馆项目后总结出来的不绝对但可以当起点。3. C#漫游控制器从能走到走得舒服3.1 CharacterController和刚体的选择依据漫游控制器这块第一个决策就是用CharacterController还是Rigidbody。我选的是CharacterController理由很具体刚体方案需要自己处理摩擦力、重力累积、地面检测稍有偏差就会出现贴墙抖动或者下坡时滑行加速的诡异体验。CharacterController虽然是个简化模型但它自带斜坡限制、台阶高度、地面贴合配一个简单的重力累积就够了对于展馆这种室内、平地为主、没有物理交互需求的场景它是最省心的。代价也有就是它不响应物理力。如果后面想做推开一扇门这种效果得额外加触发器去伪造。这个项目里没有这种需求所以就无所谓。下面是控制器的核心逻辑骨架我把关键参数都标了注释using UnityEngine; [RequireComponent(typeof(CharacterController))] public class HallWalker : MonoBehaviour { [Header(移动参数)] public float walkSpeed 2.8f; // 步行速度接近真人闲逛 public float runSpeed 5.5f; // 加速长按Shift触发 public float gravity -18f; // 比真实重力大落地更干脆 public float jumpHeight 0.4f; // 展馆里其实用不太上 [Header(视角参数)] public Transform cameraPivot; public float mouseSensitivity 2.2f; public float minPitch -75f; public float maxPitch 75f; private CharacterController controller; private Vector3 velocity; private float pitch; void Start() { controller GetComponentCharacterController(); Cursor.lockState CursorLockMode.Locked; } void Update() { HandleLook(); HandleMove(); } 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, minPitch, maxPitch); cameraPivot.localEulerAngles new Vector3(pitch, 0f, 0f); } void HandleMove() { float x Input.GetAxis(Horizontal); float z Input.GetAxis(Vertical); Vector3 dir transform.right * x transform.forward * z; dir Vector3.ClampMagnitude(dir, 1f); // 斜向不能更快 float speed Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed; controller.Move(dir * speed * Time.deltaTime); if (controller.isGrounded velocity.y 0) velocity.y -2f; velocity.y gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }这里有两个细节值得抠。一是dir要 clamp 到长度1不然玩家同时按 W 和 D斜向速度会是单方向的1.41倍走起来感觉像在滑冰。二是isGrounded时把Y速度强行压到-2而不是0。这是CharacterController的经典技巧——留一点向下的速度让它持续贴合地面否则走下坡或者台阶边缘会出现弹跳。3.2 相机控制鼠标视角与触屏输入的双套输入方案桌面端用鼠标转视角很顺但同一套代码搬到安卓大屏上就废了。我最后是用Input System的Action Map把两套输入抽象掉逻辑层只接收视角增量和移动向量两个值具体是鼠标给的还是手指给的控制器不关心。触屏这块有两个坑。第一是单指拖动转视角、双指缩放或平移需要判断触摸点数量并做状态机切换不能简单地把第一根手指当鼠标。第二是触摸拖动要有阻尼手指离开后视角不能立即停要有一点惯性衰减否则在大屏上操作会感觉很硬。我给的阻尼系数是0.88每帧衰减实测下来手感接近主流移动端第一人称游戏。还有个很实际的细节移动端必须有虚拟摇杆。直接让玩家点屏幕移动方向体验很差。我用了一个简单的双区域方案——左半屏任意位置按下生成摇杆、拖动移动右半屏拖动转视角。这个方案不需要美术出UI图纯代码画两个圆就行省事且不挡视线。3.3 碰撞体、台阶与扶手走起来才会发现的问题场景搭好、控制器写完第一次跑测试的时候问题全冒出来了。第一个问题是台阶。展馆入口做了三级台阶每级15厘米CharacterController的stepOffset默认是0.3理论上能上去但实际走起来会卡顿——因为台阶的碰撞体是三个独立Box角色在边缘处会反复触发地面检测。解法是把台阶的碰撞体合并成一个斜面视觉上还是台阶碰撞上是一个斜坡。第二个是扶手的碰撞。我一开始给栏杆加了碰撞体结果观众一走近就被卡住因为栏杆的碰撞体和参观动线的距离太近。后来把栏杆碰撞体全部改成触发器Trigger只用于偶尔的提示不参与物理阻挡。第三个是墙面厚度。虚拟展厅的墙如果只是一层单面片观众贴过去会看到穿帮——视线穿过墙面看到后面的东西。所有墙体必须是双面或者带厚度的Box厚度至少10厘米。这个坑我在第一个项目里踩过观众反馈说展厅漏风。注意CharacterController的skinWidth默认是0.08这个值太小会导致贴墙时抖动太大又会让角色悬空。我的经验是设成半径的10%左右对于一个半径0.3的胶囊体0.03到0.04比较舒服。这个值没有标准答案得在场景里实际走一圈调。4. 交互层设计让观众真的看懂羌绣4.1 射线拾取热点与展品信息面板漫游只是基础真正让展馆有价值的是交互。观众走到展柜前视线对准某片绣品屏幕上出现一个提示圈点击后弹出信息面板显示这件绣品的名称、纹样含义、针法类型、年代。这套逻辑的核心是从相机中心发射射线做拾取而不是用鼠标位置——因为漫游状态下鼠标是被锁定的屏幕中心点才是视线焦点。void CheckInteraction() { Ray ray new Ray(cam.position, cam.forward); if (Physics.Raycast(ray, out RaycastHit hit, interactRange, interactLayer)) { var target hit.collider.GetComponentExhibitHotspot(); if (target ! currentHotspot) { ClearHotspot(); currentHotspot target; currentHotspot.OnFocusEnter(); // 显示提示圈 } } else { ClearHotspot(); } }这里的关键设计是interactLayer。我把可交互物体单独放一个Layer射线只检测这一层避免射线打到墙面上误触发。另外interactRange设成3米左右太远会让玩家在展厅另一头就能点到展品失去了走近观看的仪式感太近又要求玩家几乎贴到玻璃上操作别扭。信息面板本身也有讲究。我最初做的是全屏遮罩式面板结果发现观众看完信息后关掉面板会丢失空间感不知道自己刚才站在哪。后来改成半透明的侧边卡片占据屏幕右侧三分之一观众既能读信息又能看到展品还在正前方。这个小改动让用户测试里的迷失感反馈下降了非常多。4.2 纹样放大观察与360度旋转的实现绣品这种题材光看整体是不够的观众真正想琢磨的是针法细节。所以我做了一个放大观察模式点击展品上的细看按钮相机平滑推近到预设的特写机位展品模型原地缓慢自转观众可以拖动鼠标进一步旋转和拉近。技术上有两个点要处理好。一是相机过渡的插值不能用瞬间位移要用一个带缓动的插值我用了SmoothStep加时间曲线大约1.2秒完成太快会晕太慢会失去耐心。过渡期间要临时禁用漫游控制器否则鼠标输入会同时驱动相机和控制器打成一团。二是模型的旋转轴心必须把绣品的Pivot设到它的几何中心不然旋转的时候会围绕某个奇怪的角落转看起来像甩来甩去。为了让放大后依然清晰重点绣品的贴图用了4K分辨率配合Mipmap设置得当远处不会因为过采样而闪烁近处又能展示细节。这里有个取舍4K贴图很吃显存我的做法是只给三件重点展品用4K其余全部2K整体显存占用控制在一个中端手机能接受的范围。4.3 引导式漫游与自由漫游的切换逻辑观众分两类一类想自己逛一类希望被带着走。我做了两套模式用状态机管理切换。自由漫游就是前面讲的那套控制器。引导式漫游则是预设一条相机路径用Dotween或者自己写个样条插值相机沿着节点平滑移动每到一站自动停下、弹出该展项的介绍观众点下一站继续。两种模式之间切换时要做三件事把相机从路径上解绑、把控制器重新启用、把相机姿态对齐到当前位置否则会出现视角瞬移。这个对齐特别容易被忽略我第一次做的时候切换回自由模式相机直接弹到了零点观众吓一跳。引导模式的路径节点我建议手工摆放而不是程序生成。因为展厅的观看体验是有节奏的——在重点展项前要多停留在过渡走廊要加快通过这种节奏感程序算不出来。5. 光照、后处理与性能实时渲染里的绣品质感5.1 展厅光源布局与Lightmap烘焙博物馆的照明是门学问虚拟展馆里同样。我的布光原则是整体低照度、重点高显色环境光压得很低靠天花板上的轨道射灯打亮展品每一件绣品上方至少有一盏聚光灯色温偏中性4500K左右避免暖光让红黄变橙。这种戏剧性的布光反而比均匀照明更耐看也更符合真实现场的氛围。光源一多实时阴影就算是废了。所以全部走烘焙——把静态的光影信息烘到Lightmap里运行时零开销画质还比实时软阴影好。烘焙有几个参数必须调对Lightmap Resolution我设的是每单位40 texel太密烘得慢太疏会有光照漏出、Ambient Occlusion开启能强化展柜角落的阴影层次、Padding至少4防止相邻UV块之间的光照串色。至于动态物体比如观众自己、偶尔飘动的布幔用Light Probe采样周围光照避免它们和烘焙环境格格不入。5.2 让丝线发光的Shader小技巧前面提到的各向异性高光是这个项目的视觉记忆点值得单独说。URP自带的Lit Shader不支持各向异性所以我用Shader Graph自己搭了一个。核心思路是用切线方向代替法线来做高光计算让高光沿着丝线的走向拉成一条带而不是一个圆点。具体做法是在Shader Graph里取Tangent Space的切线和视线方向做点积再做一个合适的幂次运算得到的高光范围再和一张丝线遮罩相乘只让绣线区域亮起来。参数上Smoothness设在0.75到0.85之间太高会像塑料太低又没有丝光感各向异性强度0.6左右这个值在不同绣品上要微调因为不同针法的丝线走向不一样。一个小技巧给这部Shader加一个微弱的时间扰动让高光带极其缓慢地流动。真人在看绣品时会微微移动头部视觉上高光就会流动静态渲染里加一点这种扰动会让人产生这东西是活的的错觉非常加分。幅度一定要小大了就变成廉价特效了。5.3 帧率兜底LOD、遮挡剔除与贴图压缩交付前一定要做性能兜底不然在开发机上跑得飞起到用户的中端手机上就变幻灯片。我做了这几件事LOD分级每件展品做三级模型距离10米、20米、35米分别切换远处的绣品直接用一个带贴图的片面代替。这一项把渲染面数砍掉了将近六成。遮挡剔除Occlusion Culling展厅是分隔空间大量的展品在墙背后是看不见的。烘焙一套遮挡数据帧率在复杂区域能提升一倍以上。贴图压缩桌面端BC7移动端ASTC把4K的贴图压到原来的四分之一大小肉眼几乎无损。后处理慎用Bloom和景深很吃性能。我只保留了轻微的Bloom和一个色差把动态模糊、SSAO这些关掉了。展馆场景其实不需要太重的后处理画面干净反而更衬绣品的色彩。优化项收益帧率提升副作用是否保留LOD分级约60%面数下降远处细节略降保留遮挡剔除复杂区翻倍烘焙耗时保留贴图压缩显存下降75%极近距离轻微糊保留轻量后处理提升约30%无保留实时阴影关闭提升约40%需烘焙补光保留6. 打包发布与实测三种交付形态的差异6.1 桌面端、WebGL与移动端的画质档位设计一套资产三种形态最容易犯的错是一套参数打天下。我的做法是用Quality Settings做三档预设然后在启动时根据运行平台自动切换。桌面端开满还有余量上4K输出WebGL压到中档贴图降一半、后处理砍掉一半移动端根据设备型号再做一次分级低端机连Bloom都关掉。需要特别说的是WebGL的体积。浏览器打开超过30MB用户流失率会急剧上升。我最终把WebGL包压到了22MB左右主要靠三件事贴图强制压缩、模型LOD之外再删一轮不可见面、把不用的Unity内置模块在打包设置里勾掉。那些不知道有没有用的引擎模块默认全勾着一个个关掉能省好几兆。6.2 移动端触摸大屏的几个真机坑馆里那台触摸大屏用的是安卓系统我在真机上遇到几个模拟器里根本看不出来的问题。第一是长时间运行的内存泄漏。展馆大屏是常年开着的运行半天后帧率会缓慢下降。排查发现是每次打开信息面板都动态加载了纹理但关闭时没释放。改成对象池复用之后连续跑48小时内存曲线才平稳。第二是触摸精度。大屏的触摸是红外感应的边缘区域有几十毫秒的响应延迟。观众点击细看按钮时如果按在边缘经常没反应。解法是把所有可点区域的判定范围放大约30%视觉上按钮大小不变但热区扩大。第三是屏幕息屏。这个坑最搞笑——安卓大屏默认几分钟不操作就息屏开会演示到一半黑屏。代码里加一句Screen.sleepTimeout SleepTimeout.NeverSleep就解决了但不知道的人能查一下午。6.3 交付后的用户反馈与后续迭代方向系统上线之后收了一批真实反馈有几个点挺出乎意料。观众提得最多的不是画质不够好而是**找不到厕所在哪——虚拟展馆里也需要环境导览得有俯视小地图和我在哪的定位标记。还有个反馈是希望有讲解**文字信息看得慢很多观众更愿意听。后来我加了一套文字转语音的旁白用户点击展品时可以听。再往后如果要迭代我会考虑两个方向一是把绣品的工艺流程做成可交互的动画比如让观众拖动进度条看一根线怎么一步步绣成一片花这比静态展示的传达效率高得多二是引入轻量的AR预览让观众在家里就能用手机把一件绣品摆到自己的桌子上看。这两块技术上都成熟主要看内容制作的成本。最后分享一个在展馆类项目里反复验证过的心得别追求把每一个角落都做得同样精细。观众的注意力是有限的把八成的精力砸在动线上的三五个重点展项上剩下的地方用环境音、灯光和简单的几何引导情绪就够了。这个项目里真正让人停下来看超过一分钟的只有四片绣品但它们撑起了整个展馆的记忆点。做数字展陈本质上和做实体展陈一样是做取舍的活儿不是做堆料的活儿。

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

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

免费获取报价