
简介面向Unity开发者和三维交互设计学习者该资源专注于3D模型展示场景的完整实现覆盖3D标注、目标环绕相机、步骤列表与拆装动画等核心功能可直接用于产品展示、教学演示或虚实结合项目的基础搭建无论游戏开发还是建筑可视化、工业产品展示均能从中获得可复用的设计思路。压缩包共4300个文件以C#脚本735个为主体配合材质贴图、场景预制体、Shader、FBX模型及PNG/PSD设计源文件同时包含文档、日志和配置等辅助内容整体体积65.71MB目录结构清晰便于检索。目前已有3736人学习下载。通过源码与场景实例可掌握模型导入优化、UI动态标注跟随、Camera绕物控制、Animator状态机驱动拆装流程等关键技巧配套步骤UI与物理模拟机制也提供了完整的交互范式能帮助开发者避开常见坑点快速搭建具备专业质感的3D展示原型对想要在Unity中落地复杂展示交互的团队或个人极具参考价值。1. 项目需求拆解与整体设计思路1.1 核心需求解析这三件事为什么总被放在一起我最近帮朋友评估一个工业设备的数字孪生演示项目对方开口就是模型能转、能标、能拆听起来简单实际上就是把Unity 3D模型展示、3D标注、拆装动画这三个典型需求揉在一起。这类需求在工业产品说明、设备培训、营销展示里太常见了——客户买了设备不想光看PPT要能在网页或者平板上把设备翻来覆去地看关键部件要有文字标注最好还能演示怎么拆怎么装。先说结论这三个功能单独拆开都不难难的是组合在一起时的架构设计。很多人一上来就急着写Camera控制脚本、拖UI、做动画结果做到后面发现标注和拆装互相打架模型动起来之后标注位置全乱了。我个人的处理习惯是拿到项目先不急着动手把整个场景拆成三个独立模块模型展示层负责相机控制与模型加载标注层负责空间点与UI的映射拆装层负责零件分组与位姿变换。三个模块之间通过事件解耦比如拆装动画播放时通知标注层暂时隐藏或跟随。这样后期无论是调整标注样式还是换一套拆装顺序都不会牵一发动全身。1.2 方案选型为什么是Unity而不是其他方案拿这个项目来说候选方案有Three.js、UE5、Unity还有各种轻量化Web3D平台。我最终选了Unity原因很务实团队里C#基础比前端好Unity的资源商店里有大量现成的工业模型处理工具而且项目未来要打包到Windows触屏一体机和Pico VR设备上Unity跨平台打包省了太多事。很多初学者会问Three.js不也能做标注和拆装吗对能。但遇到比较复杂的装配体比如几百个零件的减速机Three.js的层级管理和动画状态控制代码量会膨胀得厉害而且没有像Unity那样成熟的Inspector可视化调参环境。Unity里你可以直接在编辑器里拖拽调整零件位置、预览相机路径这在和客户对需求时效率极高。Unity版本我建议直接用Unity 2022 LTS或更新版本URP渲染管线起步。内置渲染管线虽然老牌稳定但URP的后期处理和抗锯齿效果更好模型展示类项目对画面质感要求不低。另外一定要装ProBuilder或直接导入FBX/glTF格式的模型后面标注定位点时要频繁用到。2. 3D标注模块的核心细节解析2.1 标注的本质三维坐标到屏幕坐标的投影3D标注听上去很玄其实原理就一句话把模型表面某一点的本地坐标换算成屏幕上的像素坐标然后在这个位置显示UI标签。具体来说用Camera.WorldToScreenPoint将模型上的锚点转换到屏幕空间再把UI元素放到对应位置。这里有几个容易踩的坑。第一点是WorldToScreenPoint返回的z值表示该点与相机的前后距离如果z小于0说明点在相机背后此时必须主动隐藏标注UI否则会出现标注穿帮或者UI倒转的诡异效果。第二点是UI坐标的原点在屏幕左下角而Unity的Canvas坐标原点默认在中心需要做一次偏移计算。第三点是Canvas的缩放模式强烈建议用Scale With Screen Size并且把UI层的EventSystem和3D场景的相机分开处理避免点击标注时误触模型。锚点数据存储时我习惯存模型零件的本地坐标LocalPosition而不是世界坐标因为拆装动画会让零件在世界空间移动本地坐标才是稳定的。通常我会做一个小工具在编辑器模式下点击模型表面自动生成标注点序列化成Json文件随场景加载。2.2 标注线的画法LineRenderer与自定义Shader光有UI标签还不够立体实际项目中客户要求从标签引一条线指到模型零件上这样在多零件场景里才看得清楚谁是谁。这条线有两种做法一种是直接用LineRenderer在世界空间画折线从标签下方的世界位置连到锚点另一种是用Shader在UI上画但UI层的世界坐标和3D空间坐标混在一起处理起来麻烦。LineRenderer方案更直观也很容易调。需要注意的坑是LineRenderer的宽度单位是世界单位标签离相机远时线条会显得很细近时又很粗。解决方案是把线宽写成与距离成正比或者干脆用固定屏幕像素宽度——后者需要写个简单的Shader把顶点坐标变换到屏幕空间后再计算宽度一般项目里没这个必要我通常是在Update里根据相机距离动态调整LineRenderer的widthMultiplier就够用了。还有一个细节Label文字的清晰度。如果Canvas勾选了Screen Space - Overlay模式文字不受景深影响永远清晰但如果你希望标注有跟着模型走、被遮挡住就变淡的效果就得改成World Space Canvas或Screen Space - Camera模式配合深度测试做遮挡处理。我的经验是产品展示类项目用Overlay模式就够了遮挡淡化这类效果纯属锦上添花别为它浪费开发时间。2.3 标注聚类与显隐控制零件一多标注会糊成一团。我做过一个减速机拆装项目30多个零件同时有标注屏幕上密密麻麻全是字。实际处理方案是默认只显示关键标注鼠标划过或点击零件时才显示细节标注。这个逻辑放在一个AnnotationManager里统一管理它维护一个标注列表对外提供ShowAnnotation(string id)、ShowAnnotationsByGroup(string groupName)之类的接口。为了做点击零件显示对应标注需要给每个零件挂一个Collider然后用Raycast射线检测点击到哪个零件再通过零件的标记名找到对应的标注组。这个流程听起来多但写起来就是一套标准的GetComponent、Physics.Raycast、Dictionary查找操作难度不大但架构要提前设计好别把所有标注都堆在一个数组里硬遍历。3. 拆装动画的实现思路与实操3.1 拆装动画的两种主流实现录制动画 vs 代码控制拆装动画核心就是把装配体的爆炸视图做出来再还原回去。实现手段大体分两种第一种是Unity Animation或Timeline录制关键帧在编辑器里手工拖动零件位置记录动画曲线第二种是写代码在运行时通过协程或Tween库控制每个零件的Transform。录制动画的优点是你可以在编辑器里精确调整每一个中间姿态实现非常漂亮的非线性运动轨迹适合零件少、动画固定、不需要交互变化的产品展示缺点是一旦零件数量多关键帧管理会让人崩溃客户改一个顺序就要重新录一条动画维护成本很高。代码控制则灵活得多。你只需要定义每个零件在装配位置和在拆解位置两个姿态写一个通用的爆炸逻辑把零件按照某个基准点沿轴向逐层向外平移播放动画时直接用DOTween或协程控制零件位置从装配态过渡到拆解态。我做项目通常会做一个通用的ExplodeController它接收一个零件分组列表每个分组有拆解方向和距离参数这样客户要求第三个零件先动其他零件同时散开之类的要求只需改配置和调用顺序就行。3.2 分组层级与动线设计拆装的顺序怎么定拆装动画最关键的其实不是动画本身而是装配体层级结构的设定。Unity对装配体的处理通常有两种一种是美术把整个设备导成一个模型零件都是子物体另一种是每个零件独立成物件运行时通过脚本组装。我的习惯是让美术按真实装配关系导出子物体命名用Part01_HousingPart02_GearShaft这类规范格式既方便脚本按名字正则匹配又方便客户维护。在此基础上ExplodeController内部维护一个OrderList按拆解顺序排列零件。播放拆解动画时脚本按顺序遍历零件逐个计算拆解目标位置基准轴通常是装配体中心轴的方向向量乘以该零件在序列中的序号再乘一个层间距系数得到最终的拆解位置。这里有个非常实用的技巧拆解方向不必是单一轴向。比如一个电机你可以让端盖沿Z轴向外移动转子沿X轴向左移动呈放射状散开视觉效果比傻乎乎地全部沿同一个轴平移要好得多。实现方法就是给每个零件或分组额外配置一个Vector3 ExplodeAxis脚本按这个轴计算位移。3.3 实战代码一个可复用的通用爆炸控制器我贴一个前段时间项目里精简过的核心逻辑去掉了项目相关的业务代码保留核心思路你可以直接复制到工程里改一改就能跑通。public class ExplodeController : MonoBehaviour { [System.Serializable] public class PartGroup { public string groupName; public ListTransform parts; public Vector3 explodeAxis Vector3.back; public float explodeDistance 0.5f; [Range(0f, 1f)] public float delayRatio 0f; } public ListPartGroup groups; public float animationDuration 1f; public AnimationCurve easeCurve AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); public bool isExploded { get; private set; } private DictionaryTransform, Vector3 originalPositions new DictionaryTransform, Vector3(); void Start() { foreach (var group in groups) foreach (var part in group.parts) if (part ! null !originalPositions.ContainsKey(part)) originalPositions[part] part.localPosition; } public void Explode() { if (isExploded) return; isExploded true; StopAllCoroutines(); StartCoroutine(PlayExplode(true)); } public void Assemble() { if (!isExploded) return; isExploded false; StopAllCoroutines(); StartCoroutine(PlayExplode(false)); } private IEnumerator PlayExplode(bool explode) { float totalDuration animationDuration animationDuration * 0.2f * (groups.Count - 1); float elapsed 0f; while (elapsed totalDuration) { elapsed Time.deltaTime; float normalized Mathf.Clamp01(elapsed / totalDuration); foreach (var group in groups) { float groupTime Mathf.Clamp01((normalized - group.delayRatio * 0.3f) / (1f - group.delayRatio * 0.3f)); float t easeCurve.Evaluate(groupTime); foreach (var part in group.parts) { if (part null || !originalPositions.ContainsKey(part)) continue; Vector3 target explode ? originalPositions[part] group.explodeAxis.normalized * group.explodeDistance : originalPositions[part]; part.localPosition Vector3.Lerp(originalPositions[part], target, t); } } yield return null; } } }这段代码的基本思路是记录每个零件的初始本地位置然后按分组和延迟系数逐组过渡到目标拆解位置。easeCurve控制运动曲线我们一般用EaseInOut让零件启动和停下时更柔和。delayRatio实现顺序拆解数值越大该组动得越晚。如果你希望所有零件同时散开把delayRatio全设为0即可。3.4 从拆开到组装补全动画回放逻辑上面的代码同时支持Explode和Assemble两个方向核心就是用同一个进度曲线反向播放。这里有个细节沿爆炸方向平移是向外的组装时目标位置要恢复为originalPositions而不是继续沿轴向外。我第一次写的时候就踩过这个坑——把组装目标位置也写成了加偏移结果零件越装越散。安装还原的顺序和爆炸顺序通常相反所以在PlayExplode里处理组装逻辑时需要倒序遍历groups。简单做法是给Assemble方法单独传个标识循环时从groups末尾往前遍历配合delayRatio的反向计算这样先拆的后装的装配逻辑就自然实现了。另外要控制拆卸距离别过大。我见过有人把explodeDistance设置成5米零件直接飞出了相机视野客户还以为模型丢了。一般来说层间距设置为模型包围盒最长边的10%-20%比较合理既能看清零件关系又不至于整个场景散成一片。4. 模型展示与交互整合的工程落地4.1 相机控制转、拉、聚焦三板斧模型展示的基础是相机操控。我们通常实现的是轨道相机模式鼠标左键拖拽旋转模型滚轮缩放鼠标中键平移双击某个零件则相机平滑聚焦到该零件。这套逻辑在工业展示项目里基本上是标配网上也有大量现成脚本可以抄。旋转的核心是维护两个角度变量yaw和pitch每次根据鼠标位移更新角度再把相机的世界坐标设置为围绕目标点旋转后的位置。缩放则要限制距离范围避免穿模或者缩放无度。我的项目里通常把缩放范围设为目标模型包围盒半径的1.2到5倍之间这样展示视角既不会太局促也不会缩得太远看不清。双击聚焦的实现也很简单从鼠标位置打一条Raycast命中框选模型包围盒然后用DOTween或协程让相机从当前位置平滑移动到目标点距离向量的位置同时更新旋转角。一套下来视觉效果很体面客户通常不会挑剔。4.2 交互联动标注、拆装、相机不能各干各的进入整合阶段你会发现真正让项目显得专业的是模块之间的联动逻辑。我举几个常见联动的例子。点击零件时同时高亮该零件并显示对应的标注。高亮可以用Highlight Plus插件或者更朴素的做法给零件换一个Unlit材质颜色调亮一点交互结束后换回去。拆装动画播放时UI层要给出对应的步骤说明。这个可以在ExplodeController播放每个分组时抛一个事件UIManager监听到之后更新文字说明。我一般会用C#的Action或UnityEvent来解耦ExplodeController只负责动画播放UI逻辑放在另一个脚本里两者不互相引用。还有一个容易被忽略的细节装配体在拆开状态下部分零件会飞出原位置原来的碰撞体可能仍在原处导致射线检测依旧能点到零件但点到的其实是空位。解决方式是拆解动画结束后把显隐状态和Collider状态同步一次或者干脆所有零件都用同一个Layer如ModelLayer然后根部管理射线检测的目标层。4.3 常见问题速查表与性能调优建议项目做到后期我整理了一份当时调试中遇到最多的问题清单贴在这里很多问题你现在踩坑时可能会用得上。问题现象可能原因解决办法标注文字闪烁Canvas排序冲突或Overlay模式下DrawCall过高将标注UI单独放一个SortingOrder层避免和全屏UI混排标注线和标签错位WorldToScreenPoint计算时忽略了Canvas缩放模式统一用Screen Space - Camera模式并设置匹配的Canvas缩放零件拆解后穿模拆解距离过小或运动时触发碰撞体判定拆解轴方向加长时间或暂停零件Collider直到动画结束双击无法选中零件零件没有Collider或射线层设置不对检查MeshCollider/BoxCollider并在Physics.Raycast指定LayerMask打包后标注字体丢失动态字体未包含在构建中使用TextMeshPro并添加字体资源或强制把字体加入Always Included Shaders零件数量多导致掉帧每个零件独立DrawCall造成性能瓶颈合并静态网格、使用GPU Instancing或开启Batching性能方面更值得说两句。模型展示项目动辄几十个零件要有效控制DrawCall。最简单的办法是不动的装配体零件尽量合并成一个Mesh编辑器里烘焙合并需要单独运动的零件则保持独立。标注线用LineRenderer合批标注UI用图集。实测下来一个100个零件的模型控制在200-300个DrawCall以内在普通Windows触屏一体机上就能流畅跑60帧。移动端打包时还要注意纹理压缩格式。模型展示类的纹理直接ASTC 6x6就行模型资源尽量用glTF/FBX并开启Optimize Mesh Data减少运行时内存占用。我这一路做下来最大的感受是这类项目没有特别高深的算法难度但对工程组织能力要求很高。标注和拆装看似是两个独立功能实际开发中却要统筹考虑相机控制、层级管理、事件解耦、性能优化多个维度。如果你是从零开始做我的建议是先搭一个包含轨道相机和基础标注的框架再把拆装动画挂上去——这样某个环节出了问题你至少能定位是框架问题还是功能逻辑问题而不是在三千行代码里大海捞针。最后再分享一个小技巧给整个场景做一个一键复位功能。客户演示到一半经常会把模型转得乱七八糟拆得七零八落这时候一个快捷键比如按R键让相机回到初始视角、零件回到装配位置、标注恢复默认显示演示体验会好非常多。这个功能在写ExplodeController的时候就留好接口组装完成后调用一下相机复位逻辑就行成本极低但客户感知极强。本文还有配套的精品资源点击获取