Unity3D中医药展馆交互展示设计与性能优化实战
2026/9/15 2:46:44 网站建设 项目流程

简介:面向毕业设计、课程设计及中期竞赛等场景,这份资源是以Unity3D开发的中医药展馆交互展示项目,采用VR游览视角,让用户沉浸式了解中医药文化,同时覆盖项目策划、程序开发与集成的完整流程。资源共639个文件,压缩包约130.85MB,既有C#脚本、预制体、材质、贴图、FBX模型、动画与控制器等常规工程内容,也带有XR Interaction Toolkit输入配置、Shader、光照数据与Render Texture等交互与渲染文件,另附一份PDF格式的说明性文档,工程目录分层明确,便于按模块查阅与修改。已有285人学习浏览,项目代码经测试可稳定运行,复现门槛低。项目答辩评审平均分达96分,除了可直接用于课程与毕设答辩展示,也可基于其中的交互框架与展馆场景继续扩展新功能,对想系统了解Unity XR开发流程的初学者同样有参考价值,所附文档亦可作为设计报告写作参考。

1. 为什么是 Unity3D:中医药展馆交互展示的选型逻辑

一个中医药展馆如果只靠展板和静态模型,参观者停留时间通常不会超过两分钟。把展品变成可拆解、可旋转、可触发解释的交互对象,是目前最常见的展示升级路径。做这类项目时,我一般优先选 Unity3D,而不是 Web 页面或原生开发。原因是交互展示的实时性、跨平台发布和三维资源管线的成熟度,正好被 Unity3D 的组件化架构覆盖。你既可以发布到触屏一体机,也可以导出到 Android 盒子或 PC 大屏,一套交互逻辑基本不用重写。这篇内容会顺着“模型进场景—交互做设计—性能调优—验收交付”这条线,把中医药展馆交互展示设计与实现过程中最容易被忽略的细节拆开讲。

2. 从展馆模型到 Unity3D 场景:模型管线、坐标与材质透视

2.1 处理来自 SolidWorks/3ds Max 的模型:OBJ 与 FBX 的选择

中医药展馆里常见的展品是中药材标本、人体经络铜人、针灸铜人和古代制药器具。这些模型往往来自 SolidWorks、3ds Max 或 Maya,导入 Unity3D 前必须先统一格式。我一般优先用 FBX,因为它能保留动画、材质绑定和 UV 信息;OBJ 只适合静态网格,而且不带骨骼和动画。若源文件是.sldprt.sldasm,先用 SolidWorks 导出为 STEP 或 IGES,再在 3ds Max 里转成 FBX。注意单位换算:SolidWorks 默认毫米,Unity3D 默认米,导入时若发现展品巨大无比,先检查 FBX 导入器的 Scale Factor 是否设为 1,再把模型在 3ds Max 里统一成厘米或米再导出。

导入 Unity3D 时,还需要处理坐标系差异。SolidWorks 的 Z 轴朝上,Unity3D 的 Y 轴朝上。解决办法是在 3ds Max 导出 FBX 前,把模型旋转到 Z 轴朝前或 Y 轴朝上。如果已经倒进来了,可以在 Unity3D 中创建一个空物体作为根节点,把模型作为子物体,手动旋转X = 90,然后调整位置。更稳妥的做法是编写一个导入后处理脚本,放在Assets/Editor目录下,自动修正旋转和缩放。下面这个脚本可以直接放进项目:

using UnityEditor; using UnityEngine; public class ModelImportFixer : AssetPostprocessor { private void OnPreprocessModel() { ModelImporter importer = assetImporter as ModelImporter; if (importer == null) return; // 将模型单位换算为米,适合从 SolidWorks 导出的毫米单位 importer.globalScale = 0.01f; importer.useFileScale = true; // 修正坐标轴:如果模型原本是 Z 轴朝上 importer.importRotation = new Quaternion(0.7071f, 0, 0, -0.7071f); importer.animationType = ModelImporterAnimationType.None; } }

这个脚本会在模型导入前自动修改导入参数,globalScale按 0.01 缩放是因为毫米转米,importRotation里的四元数把默认的 Z 轴朝上旋转到 Y 轴朝上。如果盒子里的中药柜模型本身已经摆正,就不需要这步旋转,否则会转歪。

2.2 场景搭建:光照、相机与碰撞体配置

展馆场景不是只有展品,还要有地面、墙面、灯光和参观动线。在 Unity3D 中,我常用Floor平面模拟展台地面,用Cube等比拉伸做墙面,这样烘焙光照时更快。关于灯光,优先用Directional Light做主光源,再补几个Point Light打在展品周围。注意不要用多个实时光影,否则在低配一体机上会卡。展馆内环境光设置成淡黄色比较符合中医药氛围,可在Window > Rendering > Lighting里把Environment Lighting的 Ambient Color 调成#FFF1D0这类暖色。

相机方面,不建议直接拖动 Main Camera。我会给每个展位单独建一个CameraCinemachineVirtual Camera,通过切换Priority来实现镜头跳转。交互展示通常会用到两种镜头:俯视全馆的全局镜头和进入展位后的特写镜头。用Cinemachine能平滑过渡,避免生硬的切屏。

碰撞体配置是容易遗漏的一步。交互点击依赖射线,而射线只能打到有碰撞体的对象。导入的模型如果只带 Mesh,需要手动添加Box ColliderMesh Collider。对复杂的药材根茎模型,Mesh Collider 更准确,但开销大;我一般会把整套模型拆成几个Box Collider组合体,既能保证点击识别,又能减少物理引擎负担。注意展柜玻璃这类透明对象,碰撞体最好单独挂,不要和展品绑定,否则点击玻璃时可能挡住后面的展品。

2.3 用 C# 脚本处理材质透视与视频流贴图

展馆里经常需要做“外层玻璃变透明、露出内部药材”的动画。这种效果在 Unity3D 里有两种做法:一是切换渲染模式,二是动态修改材质颜色 alpha。前者适合整体切换,后者适合渐变过渡。默认的StandardShader 在Rendering ModeTransparent时才能显示半透明效果。用代码控制时,需要先拿到材质的_Color属性。

using UnityEngine; public class MaterialFade : MonoBehaviour { public float fadeSpeed = 1.5f; private Material mat; private float targetAlpha; void Start() { Renderer renderer = GetComponent<Renderer>(); mat = renderer.material; // 将渲染模式改为 Transparent,否则 alpha 不生效 mat.SetFloat("_Mode", 3); mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetInt("_ZWrite", 0); mat.DisableKeyword("_ALPHATEST_ON"); mat.EnableKeyword("_ALPHABLEND_ON"); mat.renderQueue = 3000; targetAlpha = mat.color.a; } public void SetTransparent(float alpha) { targetAlpha = alpha; } void Update() { Color c = mat.color; c.a = Mathf.Lerp(c.a, targetAlpha, Time.deltaTime * fadeSpeed); mat.color = c; } }

代码里的SetFloat("_Mode", 3)对应 Standard Shader 的Transparent模式,必须同时设置_SrcBlend_DstBlend才能正确混合。_ZWrite置 0 表示不写入深度缓冲,这样透过玻璃能看到内部物体。否则即便 alpha 降低了,后面的物体也可能被深度剔除掉。

如果你想在展馆墙上放动态的药材炮制过程视频,可以不用视频播放器组件,直接把视频流作为纹理贴到RawImage或平面材质上。下面是最小实现:

using UnityEngine; using UnityEngine.Video; public class VideoTextureAssign : MonoBehaviour { public VideoClip clip; public Renderer targetRenderer; void Start() { VideoPlayer player = gameObject.AddComponent<VideoPlayer>(); player.clip = clip; player.isLooping = true; player.playOnAwake = true; Texture tex = player.texture; if (tex == null) { player.Prepare(); player.prepareCompleted += _ => { targetRenderer.material.mainTexture = player.texture; player.Play(); }; } else { targetRenderer.material.mainTexture = tex; player.Play(); } } }

这里先准备好视频,再在prepareCompleted回调里把纹理赋给材质,避免首帧黑屏。实际项目中,视频文件应放在StreamingAssets或通过Addressables加载,不要直接拖入Resources目录,不然会打进包体并增加启动加载时间。对于展馆里的多路视频,我还建议用VideoPlayerrenderMode = VideoRenderMode.RenderTexture,把多路视频渲染到不同的RenderTexture,再赋给多个平面,这样可以在 UI 上自由叠加。

3. 交互展示设计的核心:射线检测、UI 系统与动画控制

3.1 用 Physics.Raycast 实现展品点击识别

触屏一体机和鼠标的基本交互都是“点击物体—弹出信息”。实现时最常用的是从相机发射一条射线,检测射线是否击中了目标展品。注意不要在主线程用Update每帧检测鼠标按钮,而是用Input.GetMouseButtonDown(0)Physics.Raycast。要做到只响应点击,不响应拖动,我会记录按下时的屏幕坐标,进行比较。

using UnityEngine; using UnityEngine.EventSystems; public class ExhibitClick : MonoBehaviour { public Camera rayCamera; public float clickThreshold = 20f; private Vector3 downPosition; private GameObject currentTarget; void Update() { if (Input.GetMouseButtonDown(0)) { downPosition = Input.mousePosition; } if (Input.GetMouseButtonUp(0)) { // 判断是否是点击而非拖动 if (Vector3.Distance(downPosition, Input.mousePosition) > clickThreshold) return; // 先判断鼠标是否在 UI 上,在 UI 上时不触发物体点击 if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject()) return; Ray ray = rayCamera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { ExhibitData data = hit.collider.GetComponent<ExhibitData>(); if (data != null) { OnExhibitClicked(data); } } } } private void OnExhibitClicked(ExhibitData data) { // 发送事件,由 UI 管理器统一响应 EventBus.Trigger("ExhibitClicked", data.exhibitId); } }

这段代码里我额外加了一个clickThreshold,避免游客在触屏上滑动查看 3D 模型时误触发点击。EventSystem.current.IsPointerOverGameObject()用来排除 UI 遮挡。实际展馆项目里,展品模型往往放在透明玻璃展柜内,玻璃有碰撞体但不应响应点击,所以玻璃的 collider 所在物体不能挂ExhibitData组件,或在脚本里检测hit.collider.gameObject.layer,只处理Exhibit层的物体。

ExhibitData组件用来标识展品身份,它只存储一个 ID 和少量配置。不建议在点击脚本里直接查数据库,那样会耦合。我通常让点击脚本发送一个事件,UI 层监听事件后从配置表或 JSON 里读取对应展品的文字、图片和语音。这也是交互展示项目里最常见的解耦方式。

3.2 UI 面板的布局与数据绑定

信息弹窗是中医药展馆交互展示的核心界面。它需要展示药材名称、性味归经、功效主治、图片和语音讲解按钮。Unity3D 自带的 UGUI 足够完成这些,关键是布局和数据绑定。我建议用ScrollRect包裹Content,因为不同药材的文字长度差异很大。每个展品的介绍页面用一个Panel模板,通过代码动态填充字段。

using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; public class HerbInfoPanel : MonoBehaviour { public Text nameText; public Text propertyText; public Text effectText; public RawImage imageArea; public Button voiceButton; public void Show(HerbData data) { nameText.text = data.name; propertyText.text = "性味归经:" + data.property; effectText.text = "功效:" + data.effect; if (data.image != null) { imageArea.texture = data.image; imageArea.gameObject.SetActive(true); } else { imageArea.gameObject.SetActive(false); } // 清除旧的监听,避免重复点击 voiceButton.onClick.RemoveAllListeners(); voiceButton.onClick.AddListener(() => AudioManager.Instance.PlayVoice(data.voiceClip)); gameObject.SetActive(true); } }

这里最需要注意的是RemoveAllListeners。如果使用对象池复用面板,监听器会不断叠加,导致点击一次播放多段语音。另一个细节是图片的加载:不要直接用Resources.Load在 UI 线程加载大图,尤其当展品超过 50 种时,应使用AddressablesAssetBundle异步加载,加载完成后再赋值给RawImage.textureRawImageImage更适合直接贴 Texture2D,如果你用Image,会多一步把纹理转 Sprite 的操作,性能开销更高。

面板的显示/隐藏动画也值得做。我这里用CanvasGroup的 alpha 和DOTween做淡入淡出,比直接SetActive更平滑。注意CanvasGroup.blocksRaycasts在隐藏时要设为 false,否则透明面板仍会拦截射线。

3.3 用 Animator 做展品拆解与药材生长动画

中医药展馆最爱用的交互是“拆解针灸铜人”或“一颗药材从种子到成株的生长过程”。这类动画有两种实现:美术预烘焙的骨骼动画和代码控制的程序动画。拆解类我建议用 Animator,把模型的各个部件做成子物体,在动画关键帧里改变每个部件的 localPosition 和 rotation,形成散开的视觉效果。部件散开后,还可以用AnimationEvent在特定帧触发语音讲解。

代码控制的生长动画更适合程序化生成药材茎叶。比如当归、人参这类根茎药材,可以用一系列圆柱体和球体拼接,按时间改变 scale 和颜色。下面是一个用Mathf.SmoothStep控制整体缩放和颜色渐变的示例:

using UnityEngine; public class HerbGrowAnimation : MonoBehaviour { public Transform root; public Transform stem; public Transform leaves; public float growDuration = 3f; private float progress = 0f; private bool isGrowing = false; public void StartGrow() { isGrowing = true; progress = 0f; } void Update() { if (!isGrowing) return; progress += Time.deltaTime / growDuration; progress = Mathf.Clamp01(progress); float t = Mathf.SmoothStep(0f, 1f, progress); root.localScale = Vector3.Lerp(Vector3.zero, Vector3.one, t * 0.8f); stem.localScale = new Vector3(1f, t, 1f); leaves.localScale = Vector3.Lerp(Vector3.zero, Vector3.one, Mathf.Clamp01((t - 0.5f) * 2f)); if (progress >= 1f) { isGrowing = false; } } }

这个脚本把生长过程拆成三个阶段:根部先出现、然后茎伸长、最后叶子展开。SmoothStep让变化不是线性而带有缓入缓出效果。这种程序动画的优势在于不依赖美术资源,换一套模型也能复用。注意把动画逻辑和交互触发分离:点击按钮调用StartGrow(),而不是直接在Update里判断鼠标。

动画过程中如果有透视需求——比如要透过外层叶片看到内部根茎,可以配合第 2.3 节的材质透视脚本,在动画后半段把叶片材质的 alpha 降到 0.3。这样既展示生长,又保留了“透视看内部”的互动趣味。

4. 性能优化与排错:LOD、遮挡剔除与异步加载

4.1 展馆模型减面与 LOD Group 设置

中医药展馆的模型普遍精细,尤其是经络铜人,单个面数可能超过 30 万。直接放进 Unity3D 运行起来会很吃力。首先是模型减面,我通常在 3ds Max 里用 ProOptimizer 或 Decimate 修改器,把近看模型保留 8 万面,中景 3 万面,远景 1 万面。在 Unity3D 里则使用LOD Group组件,把不同精度的模型放到同一物体下,相机距离远时自动切低模。

手工建 LOD 组太麻烦。我常写一个编辑器脚本自动扫描场景中所有大 Mesh,按面数分组并生成 LOD Group。下面是一个简洁的自动生成版本:

#if UNITY_EDITOR using UnityEngine; using UnityEditor; public class AutoLODCreator : EditorWindow { [MenuItem("Tools/Auto LOD")] static void CreateLODs() { GameObject[] allObjects = FindObjectsOfType<GameObject>(); foreach (GameObject go in allObjects) { MeshFilter filter = go.GetComponent<MeshFilter>(); if (filter == null || filter.sharedMesh == null) continue; if (filter.sharedMesh.vertexCount < 30000) continue; LODGroup group = go.GetComponent<LODGroup>(); if (group == null) group = go.AddComponent<LODGroup>(); // 这里仅为示例,实际项目中应调用 MeshUtility 或第三方减面工具生成 LOD 网格 LOD[] lods = new LOD[2]; lods[0] = new LOD(0.5f, new Renderer[] { go.GetComponent<Renderer>() }); // 模拟低模数据,实际应替换为减面后的 Mesh lods[1] = new LOD(0.2f, new Renderer[] { go.GetComponent<Renderer>() }); group.SetLODs(lods); group.RecalculateBounds(); } EditorUtility.SetDirty(All); } } #endif

这个脚本只是骨架,真正的低模需要美术或减面工具生成后填入。实际项目里,我会用MeshSimplifier开源库在编辑时离线减面,而不是运行时再减。减面要小心 UV 和法线,否则纹理拉伸会很明显。药材表面的纹理细节如果重要,就不要减到低于 2 万面。

4.2 遮挡剔除与 Occlusion Culling 参数

展馆场景包含大量隔墙、展柜和柱子,游客视角只能看到一部分区域。Unity3D 自带的遮挡剔除(Occlusion Culling)能把被遮挡物体的渲染排除掉。操作路径是Window > Rendering > Occlusion Culling,点击Bake前需要给所有遮挡物(如墙面、地板)设置Occluder Static,给展品和装饰物设置Occludee Static

有时烘焙后画面异常变黑或物体闪烁,多半是遮挡几何体设置错误。注意展柜玻璃不能设为Occluder,因为玻璃透明但仍需要渲染。如果半透明物体被误剔除,可以把它设为Occludee Static但取消Allow Occlusion的选择,强制始终渲染。

参数调试方面,我建议先把Smallest Occluder调大到 1 或 2,这能显著减少烘焙时间。Smallest Hole设置成 0.05 到 0.1,避免墙上小孔导致漏剔除。Backface Threshold默认 100 即可,不需要动。烘焙完成后在Occlusion Culling窗口的Visualization模式下查看,蓝色区域表示被剔除的范围,如果展品在某个镜头下异常消失,就是剔除范围过大了。

下面是一个表格,列出常用优化项和推荐参数:

优化项工具/组件推荐参数适用场景
模型面数MeshSimplifier / ProOptimizer高模 8 万面,低模 1 万面经络铜人、药材标本
阴影Directional Light + 阴影距离Shadow Distance 30m展馆主光源
抗锯齿Quality SettingsMSAA 2x + TAA视频一体机
LODLOD Group3 级,切换距离 5/12/25m大场景展品
遮挡剔除Occlusion CullingSmallest Occluder=2, Smallest Hole=0.05有隔墙的展馆
纹理压缩ASTC / ETC2Compress 后每通道 8bitAndroid 触屏

4.3 使用 Addressables 异步加载避免卡顿

展馆里几十种药材介绍图片和语音,如果全部在启动时加载,内存和加载时间都不可接受。更常见的问题是在点击展品时,用Resources.Load同步加载大图,导致画面卡死几百毫秒。我现在的做法是统一用Addressables管理所有非核心资源。启动时只加载 UI 基础资源和公共图集,展品详情资源在点击时异步加载。

using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; public class HerbDetailLoader { public void LoadHerbDetails(string exhibitId, System.Action<HerbData> onLoaded) { Addressables.LoadAssetAsync<HerbData>($"HerbData/{exhibitId}").Completed += handle => { if (handle.Status == AsyncOperationStatus.Succeeded) { HerbData data = handle.Result; // 异步加载图片 Addressables.LoadAssetAsync<Texture2D>(data.imageAddress).Completed += imgHandle => { data.image = imgHandle.Result; onLoaded?.Invoke(data); }; } else { Debug.LogError($"Failed to load {exhibitId}"); } }; } }

这里有两个重要参数:exhibitId对应的 Address 命名必须与构建的 Addressable 组一致,否则加载失败。另一个是图片加载完之后的引用释放。如果游客反复点击不同展品,旧的图片和HerbData对象会残留在内存中。在关闭信息面板时,应该调用Addressables.Release(handle)释放。切记不要只释放Texture2D不释放HerbData,因为HerbData里可能包含对材质或音频的引用。

遇到加载后纹理变紫的问题,是图片未压缩或格式不支持。在Addressables分组设置里,建议把图片的Compression设为Compressed,格式用ASTC (6x6),对移动端和桌面都能兼顾。展馆一体机大多是 Android 系统,ETC2 兼容性也行,但 ASTC 在 PC 上可能不被部分显卡支持,所以如果项目最终要跑在 Windows 触屏一体机上,建议用RGBA Compressed自动选择,或者准备两套格式。

5. 交互体验验证与项目交付前的打磨

5.1 多终端下的触控与鼠标响应验证

同一个 Unity3D 项目最后可能跑在 Windows 触屏一体机、Android 平板或普通 PC 上。鼠标点击和手指触摸的事件差异不大,但要注意触摸的Input.touches和鼠标的Input.GetMouseButton在部分模拟器上会互相干扰。我的建议是统一封装一个输入管理器,只判断Input.touchCount > 0时用触摸坐标,否则用鼠标坐标。验证时在Edit > Project Settings > Player里勾选Run In Background,避免设备息屏导致程序暂停。展馆无人值守时,还要设置屏幕休眠为从不。

5.2 交互数据埋点与展示流程校验

交互展示做完不是终点。我一般会在每个展品的点击事件后记录时间戳和展品 ID,写入本地PlayerPrefs或轻量 SQLite,方便运营人员统计哪个展品最受欢迎。这样既能追溯交互故障,也能调整展馆动线。因为 Unity3D 里 JSON 序列化轻松,我会把埋点数据缓存在内存,每 30 秒统一写入磁盘一次。最后还要做一个自动巡检,用一个编辑器脚本遍历所有展品 ID,确认对应面板、语音和图片都能正常加载。这一步能大幅减少交付现场才发现资源缺失的风险。把巡检工具挂在菜单栏Tools/Check Exhibition,每次打包前跑一遍,就能把“某个展品点击没反应”这类问题提前拦在门外。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询