1. 项目缘起与整体设计思路飞机场漫游展馆系统说白了就是让用户在虚拟的机场航站楼里自由走动、观看展品、了解机场运作流程的一套三维交互程序。我第一次接触这类需求是在一个航空科普项目里客户想要一个能放在展厅大屏上、也能打包成桌面应用让参观者自己操作的东西。当时评估了几条路线纯Web三维方案加载慢、大场景卡顿明显UE画面好但团队C#积累更深、迭代速度更快最后敲定Unity加C#这套组合核心原因就三个字——可控性。Unity的组件化架构让场景里的每个物件都能独立挂脚本航站楼这种功能分区明确的空间特别适合这种模式。值机岛、安检口、登机廊桥、行李转盘每一块都可以做成独立模块后期要改某个区域的交互逻辑不用动其他部分。C#作为主语言语法严谨、生态成熟配合Unity的MonoBehaviour生命周期写起状态机来很顺手。至于标题里提到的“3D”我理解是强调三维场景的深度构建包括模型、材质、光照、后处理这一整套视觉管线而不是某个具体工具的名字。这个系统解决的核心问题是把物理机场里不方便频繁实地参观的区域用三维方式还原出来让用户在零风险、零成本的前提下完成漫游体验。适合谁来参考我觉得三类人最对口一是做数字展馆、科普教育的开发者二是想入门Unity三维交互的学生或转行者三是需要做机场可视化演示的技术团队。哪怕你之前只写过C#控制台程序跟着思路走也能把框架搭起来。整体设计上我采用的是“场景分层加载模块化交互第一人称漫游”的架构。场景按功能划分为航站楼主体、停机坪、塔台、地下行李通道四个大区每个区做成独立的Scene或Prefab用异步加载切换。交互模块抽成通用组件比如可交互物体基类、信息面板管理器、路径导航器这样新增展品时只需要挂组件、填参数不用重复写逻辑。漫游方式选了第一人称控制器因为展馆场景需要沉浸感第三人称会削弱“身处其中”的体验。下面我把这套东西拆开从场景搭建到代码实现再到踩坑记录完整过一遍。2. 场景搭建与三维资产处理要点2.1 航站楼空间的模块化拆分逻辑拿到机场的CAD图纸或参考照片后第一件事不是急着建模而是做空间拆分。我的习惯是按“用户动线”来切从入口大厅开始经过值机区、安检区、候机区、登机口最后到停机坪。每个区域单独建一个Unity场景或者做成同一个场景里的不同父物体。这样做的好处是加载时只渲染当前区域和相邻区域远处用低模或直接剔除帧率能稳住。具体操作上我在Hierarchy里建了这么几层_Environment放静态建筑结构_Props放可交互展品_Lighting放灯光和反射探针_Nav放导航网格和路径点。每个区域用空物体做根节点命名规范是Area_Terminal、Area_Apron这种。模型导入时统一缩放因子设为1因为建模软件里通常按米为单位Unity默认也是米乱改缩放会导致物理碰撞和导航出问题。注意机场这种大跨度空间千万别把所有模型塞进一个场景。我试过一次全量加载编辑器里直接卡死后来改成Addressables按需加载才顺畅。2.2 模型优化与材质合并的实操细节机场模型的面数很容易失控尤其是座椅、行李车、指示牌这些重复物件。我的处理流程是先在Blender或3ds Max里做减面把看不见的内部结构删掉比如航站楼外墙的内侧、座椅的底面。然后导入Unity后用Mesh Baker或自写的合并脚本把相同材质的静态物体合并成一个Mesh。这一步能把Draw Call从几千降到几百效果立竿见影。材质方面机场场景大量使用玻璃、金属、抛光地面这些都需要反射。我用了Reflection Probe配合Lightmap玻璃用Standard Shader的透明模式金属度调高、光滑度拉满。但要注意移动端或低配设备上实时反射很吃性能所以我在Quality Settings里做了分级高配开实时反射中配用烘焙的Cubemap低配直接关反射改用假高光。还有一个细节是纹理图集。机场指示牌、广告牌上的文字和图标如果每张单独一张贴图显存占用会爆炸。我把它们按区域打成2048x2048的图集UV重新排布这样十几种指示牌共用一张贴图显存直接省了八成。2.3 光照烘焙与实时阴影的取舍机场航站楼通常有大面积玻璃幕墙自然光进来后阴影很柔和。Unity的实时全局光照Realtime GI效果不错但烘焙时间长、显存占用高。我的做法是静态建筑和地面用Baked GI动态物体比如移动的行李车、人物用Light Probe采样环境光只有主角和少数关键物体开实时阴影。烘焙参数上Lightmap Resolution我设成每单位20到30 texel太高了烘焙慢、显存大太低了阴影糊。Lightmap Padding留4个像素防止相邻面块渗色。环境光遮蔽AO一定要开机场的角落、座椅下方、廊桥连接处有了AO层次感完全不一样。实操心得烘焙前把所有静态物体的Static标记勾上包括Contribute GI和Batching Static。有一次我漏勾了几个柱子结果它们不参与烘焙阴影直接穿模排查了半天。3. 第一人称漫游控制器的核心实现3.1 角色控制器的参数计算与配置第一人称漫游的流畅度八成取决于CharacterController的参数。我用的标准配置是身高1.75米半径0.3米Slope Limit设45度Step Offset设0.35米。为什么是这些数身高对应真实视角高度半径保证不会卡进墙缝Slope Limit决定能爬多陡的坡Step Offset决定能迈多高的台阶。机场里有很多自动扶梯和缓坡45度足够覆盖。移动速度分两档步行2米每秒按住Shift加速到5米每秒。加速度用Mathf.SmoothDamp做平滑不要直接赋值否则起步和停止会很生硬。跳跃高度设0.5米重力用-9.81乘一个系数我一般用-15让下落感更干脆。public class FirstPersonController : MonoBehaviour { public float walkSpeed 2.0f; public float runSpeed 5.0f; public float jumpHeight 0.5f; public float gravity -15.0f; private CharacterController controller; private Vector3 velocity; private float currentSpeed; void Update() { float targetSpeed Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed; currentSpeed Mathf.SmoothDamp(currentSpeed, targetSpeed, ref speedVelocity, 0.1f); Vector3 move transform.right * Input.GetAxis(Horizontal) transform.forward * Input.GetAxis(Vertical); controller.Move(move * currentSpeed * Time.deltaTime); if (controller.isGrounded velocity.y 0) velocity.y -2f; if (Input.GetButtonDown(Jump) controller.isGrounded) velocity.y Mathf.Sqrt(jumpHeight * -2f * gravity); velocity.y gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }3.2 摄像机视角与鼠标灵敏度的调校摄像机挂在角色头部位置本地坐标设(0, 1.6, 0)因为眼睛大概在身高1.75米的1.6米处。鼠标X轴控制角色左右旋转Y轴控制摄像机上下俯仰。俯仰角度要Clamp在-80度到80度之间不然会翻转。鼠标灵敏度是个玄学参数我默认设2.0但在设置面板里开放调节。有个细节不同分辨率下同样的灵敏度感受不一样所以我在Start里根据Screen.width做了一次归一化。另外鼠标锁定用Cursor.lockState CursorLockMode.Locked按Esc解锁方便用户切出去。踩过的坑早期版本我没做俯仰限制测试时鼠标往上一甩视角直接翻过去整个场景倒过来用户直接晕了。后来加了Clamp才解决。3.3 碰撞检测与防穿模处理CharacterController自带碰撞但默认的Skin Width是0.08在狭窄的登机廊桥里容易卡住。我把它调到0.05同时把Min Move Distance设成0避免微小移动被忽略。另外机场里有很多薄片物体比如玻璃隔断、指示牌它们的碰撞体如果用Mesh Collider会很耗性能我统一换成Box Collider手动调整大小贴合。防穿模还有一个技巧在角色脚下发射一条向下的射线检测地面距离如果超过一定阈值就强制拉回。这在上下楼梯和扶梯时特别有用防止角色悬空或陷进地面。4. 展馆交互系统的模块化设计4.1 可交互物体的统一接口设计展馆里能交互的东西很多点击展品弹出介绍、靠近指示牌显示路线、走到登机口触发登机动画。如果每个都写一套逻辑代码会乱成麻。我的方案是定义一个IInteractable接口所有可交互物体实现它。public interface IInteractable { string GetInteractionPrompt(); void OnInteract(); bool CanInteract(); }然后写一个InteractionManager在Update里从摄像机发射射线检测前方物体有没有实现IInteractable。有的话显示提示文字按E键触发OnInteract。这样新增展品时只需要挂一个继承IInteractable的脚本填上提示文字和交互逻辑其他什么都不用改。4.2 信息面板的弹出与数据驱动展品介绍面板我用的是UI Toolkit比传统UGUI更灵活。面板内容不硬编码在场景里而是存在ScriptableObject或JSON里每个展品对应一个ID交互时根据ID去查数据。这样做的好处是后期改文案不用重新打包直接替换JSON文件就行。面板弹出动画用DoTween做从下方滑入加淡入0.3秒完成。关闭时反向播放。要注意的是面板打开时要把角色移动禁用否则用户一边看介绍一边乱走体验很割裂。4.3 路径导航与寻路系统的实现机场很大用户容易迷路。我加了一个导航功能用户点击地图上的目标点系统计算路径并生成指引箭头。底层用Unity的NavMesh把航站楼地面烘焙成导航网格障碍物座椅、柜台、柱子标记为Not Walkable。路径计算用NavMesh.CalculatePath得到一系列拐点后用LineRenderer画一条发光的线同时在角色前方生成一个箭头粒子指向下一个拐点。箭头用对象池管理避免频繁Instantiate造成GC。注意NavMesh烘焙时台阶和扶梯要单独处理。我的做法是在扶梯两端各放一个Off Mesh Link让导航网格能跨过去否则路径会绕远。5. 性能优化与多平台适配经验5.1 Draw Call与批处理的实战调优机场场景的Draw Call是性能大头。除了前面说的材质合并我还用了GPU Instancing处理重复物件。比如候机区的座椅几百把椅子用同一个Mesh和Material开Instancing后Draw Call从几百降到1。开启方式很简单在材质面板勾上Enable GPU Instancing代码里用Graphics.DrawMeshInstanced批量绘制。静态批处理Static Batching也要开但要注意它会把合并后的Mesh常驻内存场景太大反而吃显存。我的经验是小物件用静态批处理大物件用GPU Instancing动态物件用动态批处理三者结合。5.2 LOD与遮挡剔除的配置策略LODLevel of Detail对机场这种大场景是刚需。我给每个复杂模型做了三级LOD近处高模、中距离中模、远处低模。LOD Bias在Quality Settings里根据设备性能动态调整低配设备把Bias调大让低模更早出现。遮挡剔除Occlusion Culling能大幅减少不可见物体的渲染。烘焙时把场景按房间分区设置好Occluder和Occludee。有个坑是玻璃幕墙如果设成Occluder会把后面的东西全剔掉但玻璃是透明的用户能看到后面。所以透明物体不能当Occluder我一般用不透明的柱子、墙体来做遮挡。5.3 多平台打包的注意事项这个系统我打包过Windows桌面端、Android平板和WebGL三个版本。Windows最省心直接Build就行。Android要注意纹理压缩格式用ASTC比ETC2质量好。WebGL最麻烦内存限制严格我把场景拆得更碎用Addressables远程加载首包只放入口大厅。实操心得WebGL下不能用System.Threading所有异步操作都得用协程或UniTask。我早期用了Task.Run打包后直接报错排查了好久才换成协程。6. 常见问题排查与避坑速查6.1 模型导入与显示异常排查问题现象可能原因解决方法模型发黑法线反了或材质丢失在建模软件里翻转法线检查材质球是否导入模型太大/太小缩放因子不对导入设置里把Scale Factor改成1或建模时统一单位纹理模糊Mipmap或压缩设置不当关闭Mipmap或提高压缩质量UI纹理设成Sprite模型闪烁Z-Fighting调整摄像机近裁剪面或给重叠面加微小偏移6.2 交互失效与碰撞问题排查交互失效最常见的原因是射线没打中。排查步骤先在Scene视图里看Collider有没有正确包裹物体再看LayerMask有没有把该层排除最后检查InteractionManager的射线距离是不是太短。我遇到过展品在玻璃后面射线先打到玻璃导致展品无法交互。解决办法是把玻璃的Layer设成Ignore Raycast或者用RaycastAll取所有命中再筛选。碰撞问题里角色卡在墙角是最烦的。除了调Skin Width还可以在角色周围加几个辅助碰撞体做成胶囊形减少卡角概率。6.3 性能骤降的定位与解决帧率突然掉先用Profiler看是CPU还是GPU瓶颈。CPU瓶颈通常是脚本Update太多或GC频繁解决办法是合并Update、用对象池、避免在Update里new对象。GPU瓶颈一般是Draw Call太高或Shader太复杂用Frame Debugger逐个看找出耗时的渲染批次。我印象最深的一次场景里有个行李转盘上面几十个行李箱每个都挂了独立脚本做旋转CPU直接飙红。后来改成用一个脚本统一控制所有行李箱的Transform帧率从30回到60。7. 个人实操体会与后续扩展方向这套系统从第一版跑通到最终交付前后迭代了四个大版本。最大的体会是三维展馆项目的难点不在写代码而在“空间感”的把控。你在编辑器里看着比例正常的走廊戴上头显或全屏漫游时可能觉得窄得压抑。所以每做完一个区域一定要用第一人称实际走一遍用真实尺度去感受。另一个体会是数据驱动比硬编码省心太多。展品信息、导航点、交互提示全部抽成配置文件后后期改需求只需要改JSON不用重新编译。这个习惯我后来带到了所有项目里。后续如果继续扩展我会往两个方向走一是加入多人漫游让参观者能看到彼此适合线上展会场景二是接入语音导览用TTS把展品介绍读出来解放用户双手。这两个方向技术上都不难Unity的Netcode和第三方TTS插件都能搞定关键还是看实际需求要不要上。