ARTICLE DETAIL

资讯详情

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

Unity3D中医药展馆交互展示设计与性能优化实战

Unity3D中医药展馆交互展示设计与性能优化实战 简介面向毕业设计、课程设计及中期竞赛等场景这份资源是以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。我会给每个展位单独建一个Camera加Cinemachine的Virtual Camera通过切换Priority来实现镜头跳转。交互展示通常会用到两种镜头俯视全馆的全局镜头和进入展位后的特写镜头。用Cinemachine能平滑过渡避免生硬的切屏。碰撞体配置是容易遗漏的一步。交互点击依赖射线而射线只能打到有碰撞体的对象。导入的模型如果只带 Mesh需要手动添加Box Collider或Mesh Collider。对复杂的药材根茎模型Mesh Collider 更准确但开销大我一般会把整套模型拆成几个Box Collider组合体既能保证点击识别又能减少物理引擎负担。注意展柜玻璃这类透明对象碰撞体最好单独挂不要和展品绑定否则点击玻璃时可能挡住后面的展品。2.3 用 C# 脚本处理材质透视与视频流贴图展馆里经常需要做“外层玻璃变透明、露出内部药材”的动画。这种效果在 Unity3D 里有两种做法一是切换渲染模式二是动态修改材质颜色 alpha。前者适合整体切换后者适合渐变过渡。默认的StandardShader 在Rendering Mode为Transparent时才能显示半透明效果。用代码控制时需要先拿到材质的_Color属性。using UnityEngine; public class MaterialFade : MonoBehaviour { public float fadeSpeed 1.5f; private Material mat; private float targetAlpha; void Start() { Renderer renderer GetComponentRenderer(); 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.AddComponentVideoPlayer(); 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目录不然会打进包体并增加启动加载时间。对于展馆里的多路视频我还建议用VideoPlayer的renderMode 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.GetComponentExhibitData(); 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 种时应使用Addressables或AssetBundle异步加载加载完成后再赋值给RawImage.texture。RawImage比Image更适合直接贴 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 FindObjectsOfTypeGameObject(); foreach (GameObject go in allObjects) { MeshFilter filter go.GetComponentMeshFilter(); if (filter null || filter.sharedMesh null) continue; if (filter.sharedMesh.vertexCount 30000) continue; LODGroup group go.GetComponentLODGroup(); if (group null) group go.AddComponentLODGroup(); // 这里仅为示例实际项目中应调用 MeshUtility 或第三方减面工具生成 LOD 网格 LOD[] lods new LOD[2]; lods[0] new LOD(0.5f, new Renderer[] { go.GetComponentRenderer() }); // 模拟低模数据实际应替换为减面后的 Mesh lods[1] new LOD(0.2f, new Renderer[] { go.GetComponentRenderer() }); 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 Occluder2, Smallest Hole0.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.ActionHerbData onLoaded) { Addressables.LoadAssetAsyncHerbData($HerbData/{exhibitId}).Completed handle { if (handle.Status AsyncOperationStatus.Succeeded) { HerbData data handle.Result; // 异步加载图片 Addressables.LoadAssetAsyncTexture2D(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每次打包前跑一遍就能把“某个展品点击没反应”这类问题提前拦在门外。本文还有配套的精品资源点击获取
返回列表