1. 项目缘起与整体设计思路汴绣也叫宋绣是中国传统刺绣工艺中极具代表性的一支2006年被列入国家级非物质文化遗产名录。它的针法细腻、色彩层次丰富代表作《清明上河图》绣品长卷在业内知名度很高。但现实问题是真正能到开封本地展馆看汴绣的人并不多传统展馆受限于地理位置、开放时间、展品保护要求传播效率很低。我做的这个项目就是用Unity加C#配合3D建模和UGUI搭一个汴绣文化主题的虚拟展馆让用户在浏览器或桌面端就能自由漫游、近距离观看绣品细节、点击展品查看图文介绍。这个项目的核心目标有三个第一空间漫游用户以第一人称或第三人称视角在展馆内自由行走、转向、上下楼梯第二展品交互靠近绣品时出现提示点击后弹出详细介绍面板包含绣品名称、针法分类、历史背景、高清细节图第三跨平台发布优先支持WebGL让用户打开浏览器就能访问同时保留Windows桌面端构建能力方便后续做线下大屏展示。为什么选Unity而不是Three.js或者原生WebGL这是我在动手前反复权衡的一个点。Three.js做轻量级3D展示确实方便但汴绣展馆涉及大量精细模型、多光源烘焙、UI交互逻辑、音频解说、后期处理这些在Unity里有成熟的管线支持。C#的强类型特性和Unity的组件化架构让展品交互逻辑写起来很干净——每个展品挂一个脚本通过接口统一管理后期加新展品只需要拖拽配置。WebGL发布方面Unity 2022 LTS之后的WebGL构建质量已经能满足展馆类应用的需求加载时间通过AssetBundle拆分可以压到可接受范围。整体架构我分成四层场景层负责展馆建筑、展品模型、灯光、反射探针交互层处理射线检测、UI事件、漫游控制数据层用ScriptableObject存储展品信息方便非程序员编辑发布层处理WebGL和桌面端的构建配置差异。这个分层不是一开始就定好的是我在做到第三个展品时发现代码越来越乱才回头重构的。经验告诉我展馆类项目一定要提前把数据层抽出来否则后期改一个展品描述要翻遍场景里所有脚本。2. 展馆场景搭建与3D建模要点2.1 展馆空间布局的设计逻辑汴绣展馆的空间布局我参考了真实博物馆的动线设计但做了简化。整个展馆分为序厅、主展厅、针法体验区、文创区四个部分。序厅放一幅大型汴绣屏风作为视觉焦点主展厅沿墙布置六个展位每个展位放一幅代表性绣品针法体验区用动画演示不同针法的走线逻辑文创区展示汴绣衍生品。为什么这么分因为虚拟展馆最大的问题是用户容易迷路或者失去方向感。真实展馆有地面导引、有工作人员、有空间尺度感虚拟环境里这些都没有。我的做法是用视觉锚点加动线约束序厅的屏风是第一个锚点主展厅的环形灯带是第二个锚点每个展位上方有统一的编号灯箱。用户即使乱走抬头看到编号灯箱就知道自己在哪。动线约束方面我用碰撞体把非参观区域封死用户只能沿着设计好的路径走减少迷路概率。展馆尺寸方面主展厅我设定为24米长、12米宽、5米高。这个尺寸是试出来的太小了显得局促绣品细节看不清太大了用户走起来累WebGL端渲染压力也大。5米层高是为了给顶部灯带和反射探针留空间同时避免压抑感。地面用深色木纹材质墙面用浅米色这样绣品的色彩能跳出来。2.2 绣品模型的制作流程与优化汴绣绣品的3D模型是这个项目里最花时间的部分。真实绣品是平面的但直接贴一张图会显得很假没有立体感。我的做法是先用高清扫描或翻拍获取绣品正面图像然后在Blender里建一个带轻微起伏的平面网格把图像作为基础色贴图再用法线贴图模拟丝线的纹理走向。具体步骤是这样的第一步获取绣品高清图分辨率至少4096×4096因为汴绣的针脚很细分辨率不够放大就糊了。第二步在Blender里建一个细分平面用雕刻工具按照绣品的实际起伏轻轻推拉比如人物面部稍微凸起背景稍微凹陷。第三步导出法线贴图和AO贴图。第四步在Unity里用Standard Shader或者URP的Lit Shader把基础色、法线、AO、粗糙度贴图接上。粗糙度贴图很关键丝线反光和底布反光不一样我用灰度图区分丝线部分粗糙度低一点底布部分粗糙度高一点。模型面数控制方面每幅绣品的网格控制在5000面以内。这个数字是权衡的结果面数太低起伏感出不来面数太高WebGL端加载慢。5000面配合法线贴图视觉效果已经足够。展馆建筑本身的面数控制在3万面以内主要靠光照贴图和反射探针来提升质感而不是堆面数。注意绣品贴图导入Unity时一定要把Texture Type设为DefaultsRGB勾选Mip Maps开启Max Size设成4096。如果设成Sprite或者关闭Mip Maps远处看会有闪烁。2.3 光照方案与烘焙策略光照是展馆氛围的核心。我试过三种方案实时全局光照、烘焙光照、混合光照。最后选了烘焙光照为主、实时补光为辅的方案。原因很简单WebGL端实时全局光照性能吃不消而展馆类场景光照是静态的烘焙一次就能一直用。具体配置主光源用一盏Directional Light模拟天光强度0.3色温偏冷。展品重点照明用Spot Light每幅绣品上方一盏强度1.2角度45度色温偏暖。环境光用Gradient模式天空色浅蓝赤道色米白地面色深棕。反射探针放在主展厅中央分辨率256影响范围覆盖整个展厅。烘焙设置里Lightmap Resolution我设的是每单位20 texel这个值试过15和4015细节不够40烘焙太慢且文件太大。Lightmap Size最大2048因为WebGL对纹理尺寸有限制。Ambient Occlusion开启Max Distance设1.5这样展品和墙面的接触阴影会更自然。踩过的坑一开始我没开Baked GI的Compressed模式烘焙出来的Lightmap占了几百MBWebGL加载直接卡死。后来改成Compressed质量损失很小体积降到十分之一。另外展品的自发光部分比如灯箱要用Emission材质并且标记为Baked否则烘焙后灯箱不亮。3. C#交互逻辑与UGUI界面实现3.1 漫游控制器的实现细节漫游控制器我写了一个FirstPersonController核心逻辑包括移动、转向、碰撞检测、重力模拟。移动用CharacterController组件不用Rigidbody因为CharacterController对台阶和斜坡的处理更平滑不需要额外写物理逻辑。关键参数移动速度3米每秒奔跑速度6米每秒鼠标灵敏度2.0重力-9.81跳跃高度1.2米。这些数值是反复调出来的。移动速度3米每秒接近真人步行速度太快了用户来不及看展品太慢了逛完整个展馆要十分钟以上。鼠标灵敏度2.0是默认值但我在设置面板里留了调节选项因为不同用户习惯差异很大。代码结构上我把输入处理放在Update里物理移动放在FixedUpdate里。为什么因为CharacterController.Move应该在FixedUpdate里调用保证物理帧率稳定而鼠标输入在Update里读取保证响应及时。这个细节很多教程不讲但实际做的时候如果全放Update里低帧率下移动会抖动。void Update() { // 鼠标视角 float mouseX Input.GetAxis(Mouse X) * mouseSensitivity; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity; cameraPitch - mouseY; cameraPitch Mathf.Clamp(cameraPitch, -80f, 80f); cameraTransform.localRotation Quaternion.Euler(cameraPitch, 0f, 0f); transform.Rotate(Vector3.up * mouseX); // 键盘输入缓存 moveInput new Vector3(Input.GetAxis(Horizontal), 0f, Input.GetAxis(Vertical)); } void FixedUpdate() { // 移动 Vector3 move transform.right * moveInput.x transform.forward * moveInput.z; controller.Move(move * currentSpeed * Time.fixedDeltaTime); // 重力 velocityY gravity * Time.fixedDeltaTime; controller.Move(Vector3.up * velocityY * Time.fixedDeltaTime); }碰撞检测方面展馆的墙壁、展台、栏杆都加了Box Collider展品本身用Mesh Collider但设成Convex减少性能开销。用户和展品之间保持0.5米的交互距离太近了会穿模太远了点击不到。3.2 展品交互的射线检测与UI触发展品交互的核心是射线检测。我从摄像机中心发一条射线距离2.5米检测层设为Interactable。如果射线击中展品就在屏幕中心显示一个准星提示同时展品高亮。点击鼠标左键触发展品详情面板。这里有个细节射线检测不能每帧都做否则性能浪费。我的做法是每0.1秒检测一次用InvokeRepeating或者自己写计时器。0.1秒的间隔用户感知不到延迟但性能节省很明显。展品高亮我用的是MaterialPropertyBlock不直接改材质。为什么因为如果直接改sharedMaterial所有用同一材质的展品都会亮如果改material会创建材质实例增加内存。MaterialPropertyBlock可以在不创建新材质的情况下改单个Renderer的属性这是Unity里做高亮的正确姿势。void HighlightObject(GameObject obj, bool state) { Renderer renderer obj.GetComponentRenderer(); MaterialPropertyBlock block new MaterialPropertyBlock(); renderer.GetPropertyBlock(block); block.SetFloat(_Highlight, state ? 1f : 0f); renderer.SetPropertyBlock(block); }详情面板用UGUI做Canvas设成Screen Space - Overlay分辨率自适应。面板内容包括展品名称、针法分类、年代、尺寸、简介、高清图。高清图用RawImage加载方式用Addressables异步加载避免一次性把所有图都读进内存。3.3 UGUI界面的布局与适配UGUI的适配我踩过不少坑。最开始用Constant Pixel Size结果在4K屏上UI小得看不清在手机上又大得溢出。后来改成Scale With Screen Size参考分辨率1920×1080Match设0.5这样横竖屏切换时UI比例比较均衡。主界面布局左上角是展馆名称和Logo右上角是设置按钮和帮助按钮底部中间是操作提示WASD移动、鼠标转向、左键交互屏幕中心是准星。详情面板从右侧滑入占屏幕宽度的40%背景半透明黑色文字用TextMeshPro字体大小24行距1.2。设置面板里我放了几个选项鼠标灵敏度滑块、音量滑块、画质等级下拉框、全屏开关。画质等级分低中高三档低档关闭阴影和反射中档开阴影关反射高档全开。这个设置对WebGL端特别重要因为不同用户的显卡性能差异很大。提示UGUI的Canvas Scaler在WebGL端要注意浏览器窗口大小变化时如果Match值设得不对UI会错位。建议在Canvas上加一个脚本监听分辨率变化动态调整Match值。4. WebGL发布与性能优化实战4.1 WebGL构建配置与加载优化WebGL发布是这个项目最折腾的部分。Unity的WebGL构建默认配置出来的包很大加载慢用户体验差。我做了几轮优化把初始加载体积从80MB压到了25MB左右。优化手段一代码剥离。Player Settings里把Managed Stripping Level设成HighStrip Engine Code勾选。这样Unity会把没用到的引擎代码删掉体积能减不少。但要注意如果用了反射或者动态加载剥离可能会误删需要加link.xml保护。优化手段二纹理压缩。WebGL端支持ASTC、ETC2、DXT我选了ASTC 6x6因为它在移动端和桌面端浏览器都支持压缩比也不错。绣品贴图单独设成ASTC 4x4保证细节。所有贴图的Max Size根据实际需要设不要无脑4096。优化手段三AssetBundle拆分。展馆建筑、绣品模型、音频、UI图集分成不同的Bundle初始只加载序厅和主展厅针法体验区和文创区按需加载。这样首屏加载时间从15秒降到了5秒左右。优化手段四压缩格式。Publishing Settings里Compression Format选Brotli比Gzip压缩率高但需要服务器支持。如果服务器不支持就选Gzip。Decompression Fallback勾选兼容性更好。4.2 渲染性能调优与帧率稳定WebGL端的渲染性能是另一个大问题。我实测下来在集成显卡的笔记本上默认设置只能跑20帧左右优化后能稳定在45帧以上。优化手段一批处理。展馆里大量重复的墙面、地板、灯箱用Static Batching合并。动态物体用GPU Instancing。Draw Call从300多降到了80左右。优化手段二LOD。绣品模型做了两级LOD近距离用高模远距离用低模。展馆建筑也做了LOD远处只渲染轮廓。LOD Bias设成1.5过渡距离根据实际测试调整。优化手段三阴影。WebGL端阴影很吃性能我把阴影距离从50米降到20米阴影分辨率从2048降到1024软阴影改成硬阴影。视觉上损失不大性能提升明显。优化手段四后处理。Bloom、AO、Color Grading这些后处理在WebGL端很贵我只保留了Color GradingBloom用假发光代替AO直接烘焙到Lightmap里。优化项优化前优化后提升幅度Draw Call3207875%三角面数180万65万64%初始加载体积80MB25MB69%首屏加载时间15s5s67%平均帧率22fps48fps118%4.3 浏览器兼容性与移动端适配WebGL在不同浏览器上的表现差异很大。Chrome和Edge支持最好Firefox次之Safari在Mac上还行在iOS上限制较多。我做了几件事来保证兼容性第一检测WebGL支持。如果浏览器不支持WebGL 2.0自动降级到WebGL 1.0。如果都不支持显示提示页面建议用户换浏览器。第二移动端适配。手机屏幕小操作方式不同我加了一套虚拟摇杆和触摸视角控制。虚拟摇杆用UGUI的Joystick组件触摸视角用Input.touches读取。移动端画质默认设成低档关闭阴影和反射。第三全屏适配。WebGL Canvas默认不是全屏的我加了一个全屏按钮调用Screen.fullScreen。同时监听窗口大小变化动态调整Canvas的Render Mode和Camera的aspect。注意iOS Safari对WebGL的内存限制很严超过256MB可能会崩溃。所以移动端一定要控制纹理内存大图用AssetBundle按需加载用完及时Unload。5. 常见问题与排查技巧实录5.1 模型导入与材质显示异常问题一绣品模型导入后发黑。这个我遇到过好几次原因通常是法线方向反了或者材质球的Shader选错了。排查步骤先看Scene视图里模型是不是黑的如果是检查Blender导出时有没有勾选Recalculate Normals再看材质球URP项目里要用Universal Render Pipeline/Lit不要用Standard。问题二贴图模糊。检查贴图的Max Size和Compression设置。绣品贴图设成4096Compression设成None或者高质量。另外Filter Mode设成TrilinearAniso Level设成4远处看会清晰很多。问题三模型穿模。展品和展台之间穿模通常是模型原点不对。在Blender里把模型原点设到底部中心导出时Apply Transform。Unity里摆放时用Vertex Snapping对齐。5.2 交互逻辑失效与UI错位问题一射线检测不到展品。检查三件事展品的Layer是不是Interactable射线检测的LayerMask有没有包含这个Layer展品的Collider是不是被禁用了。我遇到过展品用了Mesh Collider但没勾Convex射线检测不到勾上就好了。问题二点击展品没反应。检查EventSystem是不是存在UGUI的Button有没有被其他UI遮挡。WebGL端还要注意如果Canvas的Render Mode是World Space射线检测逻辑不一样要用Physics Raycaster。问题三UI在不同分辨率下错位。Canvas Scaler的Match值要调横屏设0.5竖屏设0或1。另外锚点要设对左上角的元素锚左上右下角的锚右下不要全锚中心。5.3 WebGL加载失败与运行报错问题一WebGL构建后打开白屏。最常见的原因是服务器MIME类型没配好。.wasm文件要配application/wasm.data文件要配application/octet-stream。如果用的是IIS要在web.config里加mimeMap。问题二加载到90%卡住。通常是AssetBundle加载失败。检查Bundle的URL路径对不对跨域有没有配好。WebGL端不能用File协议打开必须用HTTP服务器。问题三运行时报内存不足。WebGL默认内存是256MB可以在Player Settings里调大但不要超过512MB否则浏览器可能崩溃。优化方法是及时卸载不用的AssetBundle纹理用Crunch压缩。问题现象可能原因排查方法解决方案模型发黑法线反向/Shader错误检查Blender导出设置和材质Shader重算法线/换URP Lit贴图模糊Max Size太小/压缩过度查看贴图导入设置调大Max Size/关压缩射线检测失效Layer/Collider问题检查LayerMask和Collider勾Convex/改LayerUI错位Canvas Scaler配置错误检查Match和锚点调Match/重设锚点WebGL白屏MIME类型未配查看浏览器控制台配wasm和data的MIME加载卡住AssetBundle路径错误查看Network面板修正URL/配跨域5.4 实操心得与避坑清单做了这个项目之后我整理了几条血泪经验。第一条尽早确定发布平台。我一开始按桌面端思路做用了很多实时阴影和后处理后来要转WebGL全部推倒重来。如果一开始就定WebGL很多决策会不一样。第二条数据层一定要抽出来。展品信息用ScriptableObject存不要硬编码在脚本里。我做到第五个展品时改一个描述要翻三个脚本后来重构花了两天。第三条性能优化要贯穿始终。不要等做完了再优化那时候改起来成本很高。每加一个展品就检查一下Draw Call和面数控制在预算内。第四条多设备测试。我在自己的台式机上跑得很流畅拿到笔记本上一测帧率掉到20。后来借了几台不同配置的电脑测试才把画质等级调好。第五条备份工作流。Unity项目文件多Git管理要配好.gitignore和LFS。我丢过一次场景文件因为没有及时提交重做了半天。这个项目后续还可以扩展的方向加入语音解说用Unity的Audio Mixer做空间音频加入多语言支持用Localization Package加入VR模式适配Pico等设备让用户真正“走进”展馆。不过这些都是后话先把当前版本打磨好再说。