拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity 3D + C#实现盆景文化虚拟展馆交互漫游系统开发全解析

Unity 3D + C#实现盆景文化虚拟展馆交互漫游系统开发全解析

你们有没有想过,把动辄需要逛一两个小时的盆景展,整个搬进电脑里,让观众自己在里面随便走、随便看?我最近做了一套基于Unity 3D + C#实现的盆景文化主题虚拟展馆交互漫游系统,就是解决这件事的。盆景这东西很特殊,线下展场地有限、展品大多是活的植物经不起折腾、很多精品又不让人近距离碰,文化传播非常受限。把展馆做成虚拟场景后,观众可以自由走动,点开每盆展品看文化介绍、树种、树形流派,甚至还能转着看各个角度。这个项目适合想上手Unity的开发者、做数字文博展示的朋友,也适合想把传统文化做成互动内容的团队参考。下面我按实际开发的顺序,把整个系统拆开讲一遍。

1. 懂盆景的人怎么把“意境”翻译给引擎:项目定位与整体架构

1.1 一盆二景三几架:先理解盆景艺术的空间逻辑

做虚拟展馆之前,我特意补了一段时间的盆景知识。如果你连“一盆二景三几架”都不清楚,做出来的大概率不是盆景展馆,而是“绿植超市”。

盆景艺术的空间构成其实非常清晰:一盆是盆钵,二景是景(树木、山石、水景的组合),三几架是摆放盆钵的几架。景是主体,盆是载体,几架是展示底座,三者共同构成一件完整的盆景作品。我在搭三维场景时,就把这个逻辑直接映射成了三层结构:

  • 底层:盆钵与几架,负责形态和材质质感;
  • 中层:植物主干、枝片、山石、水景,负责视觉主体和空间构图;
  • 上层:留白与背景,负责“意境”。

“意境”这个词听起来玄,翻译到引擎里其实就是构图和光影。一盆悬崖式黑松,如果背景是白墙、顶光是均匀的,观众不会有任何情绪反馈;但给它一束斜射的侧光、背后留一片阴影、盆底放一点青苔点缀,整个味道就出来了。所以场景搭建不是简单摆模型,而是要先把每件展品当成“画”来布置。

1.2 为什么是Unity 3D + C#,而不是Web技术栈或UE

项目启动时我也调研过Three.js、Babylon.js这类Web 3D方案,甚至考虑过要不要用UE来做。最后定在Unity 3D + C#,原因很实际:

  • Web 3D方案要做复杂的展馆交互逻辑,开发节奏偏慢,而且C#在Unity里写业务逻辑,面向对象的组织方式明显比纯JS更顺手;
  • 项目体量没有大到需要UE那种大场景、超写实渲染的程度,Unity在移动端适配和WebGL导出上更成熟,后续要挂到官网或展厅触摸屏上都很方便;
  • C#生态里有大量现成的UI、音视频、JSON解析、序列化等库,开发速度最快,一个小团队完全够用。

另外说个题外话,C#的委托和事件机制在这个项目里帮了大忙。展品被点击之后,要同时触发UI面板更新、音频播放、日志统计,如果用“A调用B、B调用C”的硬编码方式,改一处就崩三处。我后来改成简单的事件总线,展品只发一个“OnExhibitClicked”事件,订阅方自己去响应,后面要加新交互就非常轻松。这也是我建议做Unity项目的同学尽早养成的好习惯:能用事件解耦的地方,就别写死引用。

1.3 系统模块划分与场景组织

整个系统的代码结构大概是这样的:

Scripts/ Player/ PlayerController.cs // 第一人称漫游控制 CameraFollow.cs // 相机平滑跟随 Interaction/ ExhibitRaycaster.cs // 射线点击检测 ExhibitInfoPanel.cs // 展品信息面板 ExhibitViewer.cs // 观赏模式(旋转缩放) ZoneTrigger.cs // 展区触发播报 AudioManager.cs // 全局音效管理 Data/ ExhibitItem.cs // 展品数据模型 ExhibitConfigLoader.cs // XML配置加载 UI/ MainMenuController.cs // 主菜单 SettingsPanel.cs // 设置面板

场景组织采用的是“入口序厅—树木盆景展区—山水盆景展区—微型盆景与几架展区—尾厅”的一条主线动线。入口序厅放文化介绍墙和总体导览;三个展区分别聚焦不同盆景形式;尾厅做互动留言和背景资料播放。

这样安排的逻辑很简单:线下展馆本来就是有动线设计的,观众跟着动线走才不会漏掉内容。虚拟展馆虽然可以自由乱走,但依然应该给观众一个引导性的路径,否则很多人会在第一个展台前发懵。后面各种UI加载、数据读取、音频播报,都跟着这条动线来组织。

2. 展馆场景搭建:从展台布局到光影氛围的工序复盘

2.1 展馆空间与参观动线设计

展馆不是一个空荡荡的大盒子,我把它做成了“厅中套厅”的结构。入口是一个半开放序厅,透过隔断能看到中央主展台的灯光,观众会自然往前走;主展台放一盆大型水旱盆景,作为整个展馆的视觉焦点;左右两侧分别是树木盆景区和山水盆景区,中间用半通透的木格栅隔开,形成“移步换景”的效果。

空间设计上有一个很关键的细节:不要一进去就看到全部展品。人的心理是,看得到全部就会失去探索欲,而且没有遮挡的空间会让漫游目标感缺失。我用Box Collider做区域触发,观众走近某个展区入口时,自动触发一段简短的语音导览,比如“您现在进入的是山水盆景展区,这里主要展示以山石水景为主的盆景形式,代表作有……”这样即使没有人工讲解,观众也有被“带节奏”的感觉。

展台高度也考虑过。现实中盆景展的几架高度通常和观赏者视线平齐,在虚拟场景里,我把展台统一安排在1.1米左右,正好和第一人称摄像机高度形成一个舒适的观赏角度,不需要观众频繁低头或抬头。这是很多人容易忽略的细节:虚拟空间里没有实体舒适度,但视角的舒适度依然真实存在。

2.2 盆景模型的层次处理:盆、树、石、苔

盆景模型是整个项目中美术工作量最大的部分。模型不能直接用高模扫描件,展馆里几十盆盆景,全部高模会让移动端直接崩溃。我按“盆—树—石—苔”四个层次分别处理:

  • 盆钵:用圆柱体和倒角结构拼合,重点不是面数,而是材质。紫砂盆用低粗糙度、微表面纹理,釉面盆增加高光反射,让它在射灯下有“润”的感觉;
  • 植物:这一步最痛苦。一开始试过SpeedTree生成树木,但野外树的枝干分叉规律和盆景的“剪扎造型”完全是两回事。后来改成手搭枝干,主干用Spline工具拉出曲折变化,枝片用不透明的多层片状模型叠加,配合半透明材质做叶团;
  • 山石:用低面数石头模型,配合法线贴图模拟皴皱纹理,尽量节省顶点数;
  • 苔点:地面和石面点缀少量半球形小模型,带一点浅绿到深绿的渐变,不能满铺,满铺就失去了盆景的“留白”。

材质方面有一点要特别注意:盆景植物叶片不能用标准PBR材质里那种强高光反射,叶片应该是哑光的。我把Smoothness压到很低,同时在Base Map里叠加了Fresnel效果,让叶团边缘有淡淡的反光,这样在光照下不至于像塑料片。

2.3 光影搭建:烘焙为主、实时为辅

展馆的灯光数量很大——每个展台附近都有射灯定位,中央主展台还有多角度展示灯。如果全部用实时灯光,帧率会掉得非常难看。我的方案是“烘焙为主、实时为辅”。

静态的展馆结构、展台、隔断、墙体全部参与Baked GI(烘焙全局光照),灯光模式设为Baked,把这些物体的光影信息直接烘焙到光照贴图里。动态角色(观众的第一人称控制器)本身不会在场景里留下影子,所以不需要实时主光,靠Light Probe提供环境光照信息就够了。

每个展区我还放了反射探针。金属几架、釉面盆钵这类有镜面反射的物体,没有反射探针会表现得像哑光,整个质感就垮了。反射探针的CubeMap分辨率不用太高,128就够,展区是室内场景,反射内容主要是墙面和灯光的模糊倒影,太清晰反而假。

2.4 氛围调优:颜色LUT与后期

Unity默认渲染出来的画面偏灰、偏平,展馆需要有“文化空间”的温暖感。我在URP的Volume里加了Bloom和Color Adjustments,Bloom强度控制在0.3左右,只在灯光高亮区域看到轻微泛光;Color Adjustments里把饱和度略提,色温偏暖。

色彩上有一个原则:背景不要抢展品。展馆墙面用深灰和暖木色,展台用浅木色,盆景的绿色在这样的大背景下会显得非常突出。灯光色温统一用暖白光,主展台可以加一点点青色补光,形成冷暖对比。后期调完之后,整个展馆的氛围从一个“灰盒子”变成了“有氛围的美术馆”,这一步花的时间不算多,但效果提升非常明显。

3. 漫游控制器的C#实现:移动、视角与碰撞的工程细节

3.1 为什么选Character Controller而不是Rigidbody

虚拟展馆的地面基本是平整的,几乎没有需要物理推动的物体,所以主角移动我用了Character Controller而不是Rigidbody。原因很直接:

Character Controller自带碰撞行为,能够防止角色穿墙、走下台阶时自动滑动,而且不受物理引擎的惯性影响,操控手感更“跟手”。Rigidbody方案适合需要受到外力推动、撞击、堆叠的场景,但它有惯性和摩擦力,如果只是漫游,角色往往会“溜冰”。

如果你计划让观众能推开展品、踢动石子,那才需要切换到Rigidbody + Collider组合。但作为文化展馆项目,稳定、顺滑、不飘是第一优先级。

3.2 移动与视角控制的核心代码

主角控制我写了一个PlayerController脚本,核心代码很简洁:

using UnityEngine; public class PlayerController : MonoBehaviour { public float moveSpeed = 3f; public float lookSensitivity = 2f; public float minY = -30f; public float maxY = 30f; private float rotationX = 0f; private CharacterController controller; void Start() { controller = GetComponent<CharacterController>(); } void Update() { float mouseX = Input.GetAxis("Mouse X") * lookSensitivity; float mouseY = Input.GetAxis("Mouse Y") * lookSensitivity; rotationX -= mouseY; rotationX = Mathf.Clamp(rotationX, minY, maxY); transform.rotation *= Quaternion.Euler(0f, mouseX, 0f); Camera.main.transform.localRotation = Quaternion.Euler(rotationX, 0f, 0f); float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); Vector3 move = transform.right * h + transform.forward * v; if (Input.GetKey(KeyCode.LeftShift)) move *= 1.8f; controller.Move(move * moveSpeed * Time.deltaTime); } }

有几点要说清楚:

  • 视角的上下旋转不能用transform.localEulerAngles直接累加,否则超过90度会出现欧拉角万向锁问题,画面会突然反转。我先把mouseY累积到rotationX变量里,用Clamp限制在上下30度内,再赋给摄像机,这样玩家不会因为抬头低头过度而产生眩晕;
  • 水平旋转是旋转主角自身,垂直旋转只转摄像机。这样以后如果要加一个可以看到自己身体的第一人称模型,不会出现头和身体扭成两截的诡异画面;
  • 移动方向用transform.right和transform.forward,而不是Vector3.right和Vector3.forward。后者是固定世界方向,按W永远往世界北方走,而前者是角色当前面向的方向,按W应该往眼睛看的方向走。

3.3 相机跟随与防止穿墙

第一人称漫游本质上相机就是角色的“眼睛”,所以高度和视角要完全跟随。但虚拟展馆的相机有一个痛点:如果直接把相机锁在角色头上,走到墙角时摄像机可能嵌进墙面,视野里全是墙的截面,非常出戏。

我用了SmoothDamp做平滑跟随,同时在相机和目标之间做了一次球形射线检测:

public class CameraFollow : MonoBehaviour { public Transform target; public float distance = 3f; public float height = 1.6f; public float smoothTime = 0.15f; private Vector3 velocity = Vector3.zero; void LateUpdate() { Vector3 desiredPos = target.position - target.forward * distance + Vector3.up * height; // 防止相机穿墙 Ray ray = new Ray(target.position, target.position - desiredPos); if (Physics.SphereCast(ray, 0.2f, out RaycastHit hit, distance, ~(1 << LayerMask.NameToLayer("Player")))) { desiredPos = hit.point + hit.normal * 0.1f; } transform.position = Vector3.SmoothDamp( transform.position, desiredPos, ref velocity, smoothTime); transform.LookAt(target.position + Vector3.up * 1.2f); } }

这段代码的关键在于:

  • 为什么不放在Update而是LateUpdate?因为主角色在Update里移动,相机如果在Update里跟随,可能这一帧里主角先移动、相机后更新,下一帧又反过来,出现一卡一卡的拖影。LateUpdate保证在主角完成移动之后再校正相机位置;
  • SphereCast的半径0.2f模拟了相机“实际体积”,如果只用射线检测,相机贴墙时仍然会离墙太近,会出现“半张脸被墙挡住”的效果;
  • 排除Player层是为了让射线不跟角色自身的Collider碰撞,否则检测结果永远是自己。

3.4 碰撞体配置与Trigger触发区

场景碰撞体分三层:地面和墙体给静态Collider,展品给Collider,若干展区给Trigger。地面用MeshCollider还是BoxCollider?我建议地面用简单BoxCollider组合,不要让MeshCollider参与大规模场景,它性能和稳定性都不如基本几何体。

展品Collider要和展品模型外形尽量贴合,观众离得近时不至于看着还没碰到就挡住了。可以适当用CapsuleCollider或BoxCollider代替复杂MeshCollider,树冠部分碰撞体积比实际略小,视觉上更宽容。

展区的语音导览用Trigger实现:

public class ZoneTrigger : MonoBehaviour { public string zoneId; public string audioClipName; private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) { AudioManager.Instance.PlayZoneAnnounce(zoneId, audioClipName); } } }

一个容易忽略的细节:Trigger碰撞体建议放在独立层(Trigger层),玩家和其他展品之间的物理碰撞不要经过Trigger。否则进入展区时,角色碰撞可能会被Trigger误判为“进入了某个物体”,导致异常行为。我在项目里把碰撞矩阵设为:Player和Exhibit以物理碰撞为主,Player和Zone以触发事件为主,两边互不干扰。

4. 让展品“可对话”:射线交互、信息面板与观赏模式

4.1 从鼠标到3D世界的射线检测

漫游系统里展品交互的核心是射线检测。玩家视角直对展品,点击鼠标,需要从摄像机中心射出一条射线,检测射线是否命中展品Collider:

public class ExhibitRaycaster : MonoBehaviour { public LayerMask exhibitLayer; public float maxDistance = 10f; public ExhibitInfoPanel infoPanel; void Update() { if (Input.GetMouseButtonDown(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, maxDistance, exhibitLayer)) { ExhibitItem item = hit.collider.GetComponent<ExhibitItem>(); if (item != null) { infoPanel.Show(item); } } } } }

为什么用LayerMask而不是直接检测所有物体?因为展馆里可交互的只有展品,如果射线碰到地面和墙体也触发点击,观众随便点一下就会弹出一堆无关面板,体验非常糟糕。把展品单独放在Exhibit层,射线只检测这一层,性能和误触率都得到控制。

为什么用GetComponent而不是GameObject.Find或标签查找?因为展品数据需要挂在模型上,每个展品可能是多模型的组合体,射线命中的可能是树干、盆钵或者几架,但它们的父节点挂载了ExhibitItem脚本。更稳妥的做法是命中后向上查找组件:

ExhibitItem item = hit.collider.GetComponentInParent<ExhibitItem>();

这样无论观众点到盆景的哪个部位,都能正确找到对应的展品数据。

4.2 展品信息用XML配置,而不是硬编码

刚开始开发时,我把展品信息写在代码里,写了几条就发现完全不可维护。策展人员要改展品说明、换音频、调整位置,总不能每次都用Visual Studio改脚本再打包。

后来我把所有展品信息抽到了XML配置里:

<?xml version="1.0" encoding="UTF-8"?> <exhibits> <exhibit id="P001" type="tree"> <name>云林奇韵</name> <species>黑松</species> <style>悬崖式</style> <era>清中期</era> <size>高83cm</size> <desc>主干自盆沿向下悬垂,小枝随势曲折, 状若云林深处探出的一枝孤松, 是岭南盆景“截干蓄枝”技法的典型作品。</desc> <audio>audio/p001.mp3</audio> </exhibit> </exhibits>

加载代码用C#的XmlDocument或XmlSerializer都行。需要注意一个坑:XML文件放在StreamingAssets目录,用File.ReadAllText读取时,在Windows上系统默认编码是GBK,一旦XML里有中文就用File.ReadAllText读出来乱码。统一用UTF-8读取是关键:

using System.Xml; using UnityEngine; public class ExhibitConfigLoader : MonoBehaviour { private XmlDocument config; void Start() { config = new XmlDocument(); string path = Path.Combine(Application.streamingAssetsPath, "exhibit_config.xml"); string content = File.ReadAllText(path, System.Text.Encoding.UTF8); config.LoadXml(content); } }

如果你用Resources.Load或AssetBundle加载XML,也要注意Unity会默认按UTF-8解析,但仍建议把保存格式固定为UTF-8 with BOM,避免不同平台解析差异。

4.3 观赏模式:拖拽旋转与缩放

信息面板只是文字和图片,但盆景是三维艺术品,观众一定想转着看各个角度。我加了观赏模式:点击展品后,Cinema Machine虚拟相机拉近到目标附近,隐藏第一人称角色控制,同时启用一个观赏脚本,允许玩家用鼠标拖拽旋转盆景、滚动滚轮拉近拉远。

核心代码:

public class ExhibitViewer : MonoBehaviour { public Transform exhibitRoot; public float rotateSpeed = 5f; public float zoomSpeed = 0.3f; public float minDistance = 1f; public float maxDistance = 4f; public Cinemachine.CinemachineVirtualCamera vcam; private float currentDistance = 2.5f; void Update() { if (Input.GetMouseButton(0)) { float x = Input.GetAxis("Mouse X") * rotateSpeed; float y = Input.GetAxis("Mouse Y") * rotateSpeed; exhibitRoot.Rotate(Vector3.up, -x, Space.World); exhibitRoot.Rotate(Vector3.right, y, Space.World); } float scroll = Input.GetAxis("Mouse ScrollWheel"); currentDistance = Mathf.Clamp(currentDistance - scroll * zoomSpeed, minDistance, maxDistance); vcam.m_Lens.FieldOfView = Mathf.Lerp(40f, 70f, (currentDistance - minDistance) / (maxDistance - minDistance)); } }

这里的技巧是:旋转用exhibitRoot.Rotate而不是移动相机。旋转物体更方便,因为盆景的盆、树、山石是整体,旋转父节点即可。缩放不要直接修改transform.localScale,会破坏模型细节比例,我用改变虚拟相机的FOV或者相机距离来实现,更接近“走近看”的体验。

进入观赏模式时,记得把PlayerController.enabled置为false,否则观众拖拽旋转盆景的同事角色还在原地乱走,一个手指头同时控制两个东西,非常晕。

4.4 交互反馈体系:高亮、音效与光标细节

交互反馈是决定“有没有人觉得这个系统卡”的大因素。很多Unity小项目点击展品后没反应、或者要等0.5秒才弹出UI,观众就会反复点击,造成体验崩坏。

我搭了三层反馈:

  • 视觉高亮:鼠标悬停在展品上时,展品边缘出现微弱的轮廓光。用ShaderGraph做一个简单的边缘发光Shader,通过脚本控制Enabled开关。不需要每个展品都实时计算,只在鼠标检测到当前悬停对象时才开启;
  • 听觉反馈:点击瞬间播放极短的交点声音效,信息面板展开时再播放一段轻柔的展开音。人耳对声音反馈的响应比视觉快,点击后有声音,用户潜意识会觉得“系统已经接收了我的操作”;
  • 光标反馈:鼠标悬停在可交互展品上时,光标从默认箭头变成“手指”,同时旁边显示“点击查看详情”的浮动提示。这个细节很廉价,但能极大降低初学者的学习成本。

还有一个容易踩的坑:UI按钮本身会挡住背景射线。当你打开信息面板后,面板上的关闭按钮、滚动条应该优先响应UI事件,而不是让射线穿透到面板后面的展品上。Unity的EventSystem默认会在UI元素被点击时消耗事件,但如果Canvas里的Graphic Raycaster没有把Blocking Mask设置正确,就可能出现“点关闭按钮结果又打开了另一个展品面板”的怪问题。我的做法是:在ExhibitRaycaster的Update里,先判断EventSystem.current.IsPointerOverGameObject(),为true时直接return,不执行场景射线检测。

5. 性能、发布与踩坑:项目落地最值得复盘的部分

5.1 让普通电脑也能跑的优化组合

展馆项目做到后期最焦虑的不是功能,而是帧率。我用的优化组合拳:

优化手段作用我的参数
URP渲染管线比内置管线更高效,且支持后效中等画质预设
Baked GI光照烘焙去掉实时灯光计算灯全Baked,只有Light Probe动态
LOD Group远景用低模每盆景三档LOD,距离30m切档
GPU Instancing重复的几架、挂轴批量渲染相同材质网格自动合批
Occlusion Culling遮挡剔除不渲染被遮挡物体烘焙Occlusion Data
纹理图集小贴图合并减少DrawCall2048x2048区域图集
限制后效Bloom/抗锯齿适量只开Bloom、Color Adjustments和FXAA

Occlusion Culling这一项很容易被忽视。展馆空间墙多、隔断多,遮挡非常严重,烘焙完Occlusion Data之后,中央主展台和旁边展区的物体不会再同时渲染。实测在普通办公笔记本上能稳定跑到65帧,关掉遮挡剔除会掉到45帧左右。

烘焙也要讲技巧。展馆几十盏灯,全分辨率烘焙一次可能要几小时。我先把分辨率降到低,烘焙一遍快速看光影方向对不对,确认后再提高分辨率做最终烘焙。这样能省下大量反复等待的时间。

5.2 WebGL与Windows双平台发布

项目最终要挂到官网,也要在展厅触摸屏运行,所以双平台都要发布。Windows平台直接Build一次过,问题集中在WebGL。

WebGL有几个特殊点:

  • WebGL没有文件系统,StreamingAssets里的XML、音频不能直接File.ReadAllText,要用UnityWebRequest异步加载。WebGL上同步读取会直接报错,这个必须在代码里提前适配;
  • 包体大小。WebGL包体如果超过100MB,初始加载会很痛苦。我用AssetBundle把三个展区拆开,入口场景先加载,观众走到某展区附近时再异步加载对应Bundle,首屏加载体积从80MB降到了35MB,体验明显改善;
  • 发布设置里启用Brotli压缩,压缩率比Gzip更高。配合静态服务器配置,WebGL加载速度会好很多;
  • WebGL的字体渲染对中文支持有限,尤其是动态字体。UI里大量中文的情况下,必须用TextMeshPro + 中文字体Asset,或者预生成字体图集,否则最终页面上全是方框。

5.3 踩坑回顾:中文、字体、UI射线、动态角色偏色

这个项目让我踩了四个印象深刻的坑,都值得单独说:

第一个是TextMeshPro字体。默认的TextMeshPro字体资源只包含ASCII字符,打开项目时UIManager显示“云林奇韵”四个字,全部变成方块。解决办法是把系统中文字体(比如思源黑体、阿里巴巴普惠体)导入Unity,右键Create TextMeshPro Font Asset,然后把中文字体加到字体Asset的Fallback列表里。注意平时开发时Windows下可能没问题,因为TMP会自动提交动态字体给系统渲染,但WebGL下必炸,所以一定要在前期就做中文字体Asset。

第二个是XML乱码。我在Windows上写XML保存时默认用了系统编码,加载到WebGL后中文乱码。最终统一用UTF-8 with BOM保存,并且在读取时强制指定Encoding.UTF8,这个坑才算彻底解决。

第三个是UI射线穿透。前面说过,EventSystem的展示图上,每次点击展品后关闭按钮,射线会继续穿到场景里的展品,导致一连串误触。解决办法就是检查IsPointerOverGameObject,以及在UI面板的Image组件上关闭Raycast Target(能关的尽量关,减少UI射线计算)。

第四个是动态角色光照偏色。烘焙光照后,动态角色只依赖Light Probe。如果某一盏Light Probe放在了展台阴影里,玩家走过去脸部就会变成一块深蓝黑斑。我的解决办法是在每个展区的中央和边缘均匀布置Light Probe Group,并且用预览模式检查每个探针位置的亮度和颜色,重点避开地面阴影区和灯具正下方过亮区。

5.4 给后来者的几条实用建议

如果从零再让我做一遍这个项目,我会先花半天把展品数据和场景结构彻底理清,而不是急着摆模型。具体建议是:

  • 先定义好ExhibitItem数据模型,把所有展品的名称、树种、流派、年代、介绍、音频路径全部配好,再开始搭场景。数据先行,模型后行,后续换展品只需要改XML;
  • 美术资源统一规范。盆景模型按“盆、树、石、苔”分层命名,材质命名带前缀(如MAT_UM_盆、MAT_FJ_苔),不然几十盆盆景摆进去,命名混乱到你根本分不清谁是谁;
  • 功能脚本独立拆开。漫游、交互、观赏、音效、数据加载全部独立,避免一个脚本干所有事。后期调Bug时,你会感谢自己当初把代码拆得足够散;
  • 从第一天就在Assets目录外放好项目版本记录。Unity工程体量大,一个场景动辄几十MB,没有版本回退能力会非常痛苦。中间有一次我调整整面屏风布局后光影全乱,回退到前一天版本才救回来。

盆景文化的表达本来就是慢慢来的事,做虚拟展馆也一样。先把空间逻辑想清楚,再往细节里抠,最后出来的作品才会让人愿意在里面多走几步。

返回列表