尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Unity 3D模型展示:标注与拆装动画实践指南

发布时间:2026/9/26 12:18:53

资讯中心
01
ARTICLE

Unity 3D模型展示:标注与拆装动画实践指南

Unity 3D模型展示:标注与拆装动画实践指南
简介这是一套面向Unity开发者与3D交互设计学习者的完整项目资源聚焦于3D模型展示、动态标注、目标环绕相机与拆装动画等典型交互功能的实现方案。压缩包共4300个文件其中包含735个C#逻辑脚本、280张PNG贴图、138个Markdown说明文档以及Unity场景文件、Prefab预制体、Shader着色器、FBX模型、材质与PSD源文件等整体约65.71MB可支撑从艺术资源导入、场景搭建到程序控制的完整开发链路。项目覆盖模型导入与光照优化、AR/VR式3D标注UI标签与自定义Shader效果、基于摄像机的360°环绕观察、步骤列表驱动拆装流程、Mecanim动画状态机与物理模拟等关键知识点适合产品展示、工业教学、虚拟仿真或游戏内交互场景。资源内还包含26个Asset、21个JSON配置、12个程序集定义以及大量bin数据便于理解工程结构并二次复用。目前已有3745人学习下载对于希望系统掌握Unity交互展示技术的开发者是一份兼具完整性与实操参考价值的资源包。1. Unity 3D模型展示这套资源标注与拆装动画到底解决什么问题我见过太多Unity 3D模型展示项目最后死在最后一公里FBX拖进场景、甩一个旋转脚本看起来挺像那么回事结果客户开了半小时会问了三个问题——能不能给零件加标注能不能把外壳拆开看内部能不能点一下某个零件就让别的地方躲开然后就没有然后了。这套资源做的事情就是把这三个需求一次做完不是Demo不是教学片段是能直接拿去给客户演示的成品方案。它包含3D模型展示的完整场景搭建、World Space标注组件以及运行时拆装动画脚本适合要交付中小型装配体展示项目的开发者也适合刚接触Unity、手头只有一个FBX却要做出“看起来专业”演示效果的新手。下面按我的拆解顺序来聊模型怎么导入不翻车、标注怎么做才不飘、拆装动画怎么用代码而不是Animator来实现以及几个我实际跑项目时踩过的坑。2. 模型导入与场景搭建中心点、缩放、法线三件套2.1 先定坐标规范模型的中心点决定你后面省不省心拿到FBX或者STP转出来的模型第一件事不是拖进Unity是先在建模软件里确认坐标和中心点。我一般把装配体的世界原点设在整个产品的几何中心而不是设在底座左下角。原因很简单拆装动画在运行时是以模型中心为基准计算每个零件的位移方向的如果中心点偏到一边动画看起来就像整个产品往一个方向“炸开”而不是零件四散分离。Unity侧还要检查一个容易被忽略的选项导入设置里的Model选项卡把Scale Factor设置为1除非你的模型文件本身是厘米级或毫米级。很多设计软件导出时单位不一致导致Unity里出现一个100倍大的模型相机怎么摆都装不下。我习惯导入后立刻用一个临时的Cube对比尺寸这个Cube是Unity默认单位1米如果你的零件是50厘米应该正好是它一半大。这一步不花一分钟但能规避后面所有的相机和光线问题。2.2 材质与光照为什么你的模型拖进来是黑的模型导入后发黑、发灰八成是材质球没有正确绑定。FBX自带的标准材质在Unity里通常会被转换成默认的Legacy Shader或Standard Shader如果你的模型用了多个材质球导入后默认会按材质ID拆成多个Mesh Renderer的Material数组。这时候不要直接改FBX里的材质我是在场景里给每个零件单独挂一个Material实例然后手动指定这样改材质不会污染FBX源文件。光照方面展示类场景不要用Directional Light硬怼那会把细节全压平。我通常的做法是主光是方向光强度0.8到1.0旋转角度让阴影打在侧下方补一盏Fill Light用暖色强度0.3再放一盏Back Light从背后勾出轮廓。这组光照方案适合99%的机械类模型展示因为机械产品大多是金属和深色塑料没有这种三点布光细节看起来一团黑。2.3 相机与初始视角用一个自转脚本把模型“转”给客户看模型摆好后我习惯加一个最简单的自转脚本作为产品展示的基础动作注意这个脚本要挂在模型根节点上而不是相机上。相机不动模型自己转这样客户移动鼠标观察时永远不会晕。自转速度我设置在15度每秒到20度每秒之间太慢显得呆太快显得慌。代码就几行核心是每一帧绕Y轴旋转using UnityEngine; public class AutoRotate : MonoBehaviour { public float rotateSpeed 15f; // 每秒旋转角度建议范围 10-30 void Update() { // 绕着自身坐标系的 Y 轴旋转而不是世界坐标避免模型位置偏移 transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime); } }这段脚本做的事情很简单在Update里以固定速度绕Y轴旋转模型。这里有两个要点第一是Vector3.up表示绕物体自身局部坐标的Y轴转如果模型是斜着放置的这个旋转也是斜的第二是Time.deltaTime这个因子必须乘上不然帧率高的电脑转得快、帧率低的转得慢同一套代码在不同机器上效果会差很多。参数rotateSpeed可以在Inspector面板里实时调我调试时习惯把这个值拉到50确认旋转方向对了再改回15。2.4 层级结构规划把零件分组而不是平铺一个装配体可能有几十个零件如果全部平铺在场景根节点下后面写拆装脚本和标注脚本时会非常痛苦。我一般把模型整理成这样的结构根节点挂AutoRotate和后续的ExplosionController→ 外壳组、内部组、运动部件组。分组的原则不是按材质而是按拆装动画的“层级关系”外壳组件会被先拆掉内部核心部件后拆管线类零件最后拆。这样做的好处是运行时拆装脚本只需要遍历这几个子组就能生成合理的动画顺序不需要手动给每个零件配AnimationClip。这一层组织工作大概花二十分钟但省下的调试时间是这个的十倍。3. 3D标注组件World Space Canvas与坐标换算的四个细节3.1 为什么用World Space Canvas而不是Screen Space给3D模型做标注最直接的做法是在每个零件旁边放一个3D Text但这是最丑的做法字体边缘锯齿、不能换背景、不能做冒泡框。更常见的做法是用Canvas而且必须用Render Mode为World Space的Canvas。Screen Space的Canvas是盖在屏幕上的它不会跟随模型旋转客户从侧面看的时候标注会“浮”在屏幕前方位置完全对不上。World Space Canvas则可以把标注当成场景中的一个物体扎在零件旁边跟随模型一起转。我实际项目里的结构是这样场景里放一个专门的CanvasRender Mode设成World SpaceEvent Camera指向主相机。然后给每个要标注的零件生成一个子物体上面挂一个Image背景、一个TextMeshPro文字、一根连接线。这样整个结构是动态生成的不是手摆的。手摆一次可以但客户后期要改标注内容或调整位置时你就得一个一个改很被动。3.2 标注自动跟随与朝向矫正文字要一直正对相机这是标注体验的核心。做法是每帧让标注物体的transform.LookAt相机位置但这里有一个坑默认的LookAt会让文字面向相机背面——也就是从相机看过去文字是镜像的。因为3D UI的正向是Z轴负方向所以需要反过来转一下。我用的脚本是这样的using UnityEngine; public class LabelFacingCamera : MonoBehaviour { private Transform cam; void Start() { // 缓存主相机引用避免每帧 Camera.main 做一次查找那样会产生 GC 开销 cam Camera.main.transform; } void Update() { // 先看向相机再把自身旋转 180 度把 UI 的正面扭回到相机方向 transform.LookAt(cam.position); transform.Rotate(0f, 180f, 0f); } }这段脚本挂在每个标注对象的根节点上。LookAt让物体的Z轴指向相机但对于World Space UI来说它的可读面在Z轴负方向所以紧接着的Rotate把物体转了180度。如果你发现标注文本在某个角度下变成左右镜像就把这行Rotate去掉再试一次。这个组件跟执行的顺序无关Update里先LookAt再Rotate是稳定的。3.3 显示与隐藏按距离和角度做控制标注数量多的时候场景会变得很乱。我见过很多展示项目把所有标注一股脑显示出来客户根本分不清哪个标签对应哪个零件。我的策略有两个维度第一个维度是距离相机离标注点太远时隐藏太近了才显示第二个维度是角度标注点偏离相机视角中心超过一定角度就隐藏。这个逻辑可以用脚本实现核心是把标注点的世界坐标换算到相机的屏幕坐标再做判断using UnityEngine; public class LabelVisibility : MonoBehaviour { public float showDistance 8f; // 相机离标注点小于此距离才显示 public float maxViewAngle 60f; // 标注点与相机正前方夹角小于此值才显示 private CanvasGroup group; void Awake() { group GetComponentCanvasGroup(); // 用 CanvasGroup 控制整组透明度 } void Update() { Vector3 pos transform.position; float dist Vector3.Distance(Camera.main.transform.position, pos); Vector3 dir (pos - Camera.main.transform.position).normalized; float angle Vector3.Angle(Camera.main.transform.forward, dir); bool visible dist showDistance angle maxViewAngle; group.alpha visible ? 1f : 0f; group.interactable visible; group.blocksRaycasts visible; // 隐藏时不要挡射线 } }这里需要注意blocksRaycasts这个属性很多标注叠加在零件上零件本身要做点击拾取如果隐藏的标注还挡着射线点击就会穿透到标注上零件拾取就不灵了。用CanvasGroup而不是直接SetActive(false)是因为频繁的SetActive会造成实例化抖动而且透明度变化可以做平滑过渡后面想加淡入淡出也方便。3.4 标注内容格式编号、名称、描述三段标注的文字内容我固定用三段式编号、名称、批注。编号用加粗大字号放在左上角名称用正常字号放在中间批注用灰色斜体小字放在下面。这个格式不是随便定的客户反馈说最有用的是编号因为在拆装动画里编号可以和拆装顺序对应起来。比如标注写着“01-上盖”拆装动画里第一个拆下来的零件就是它客户能建立起预期。你不用做得像产品说明书那样复杂但格式统一很重要建议把标注的文字配置单独放到一个ScriptableObject里方便后期维护文本内容。4. 拆装动画不用Animator用运行时Transform控制4.1 为什么不用Animator拆装动画最直觉的做法是Animator Animation Clip在Unity里拖关键帧记录每个零件每一帧的位置。但我实际做下来发现这套流程在模型展示项目里很别扭第一零件多的时候Clip里的关键帧数量爆炸不好改第二装配体的零件坐标是设计软件里定死的如果客户在展示中途要求“把某个零件额外拆得更远”你得回去重新拖关键帧第三Animator的状态机在播放中途被打断时容易被卡在一个诡异姿势需要处理Exit Time和过渡。所以我选择写一个运行时拆装控制器每个零件在动画中的位置通过代码实时计算。这么做的好处是可以随时调整拆装距离、顺序和速度甚至可以让用户用进度条拖拽动画进度——这件事用Animator做极其痛苦用代码计算做起来就很自然。核心思路是为每个零件保存初始位置然后按拆装顺序和深度用插值把零件从初始位置移动到“拆开后位置”。4.2 拆装方向的计算逻辑包围盒中心到零件中心的方向拆装动画的数学逻辑不复杂但有一个工程细节要处理好零件往哪个方向飞出去。如果每个零件都朝世界坐标的Y轴方向飞拆到第三个零件时整个画面会被挡住。正确做法是以装配体中心为原点从原点指向零件包围盒中心的方向就是拆装方向。包裹大小的计算用Renderer.bounds这个属性返回的是世界空间下的AABB直接拿来做方向计算是准的。下面的脚本比较完整它能驱动一个组里的所有零件向外拆开也能反过来做装配。脚本挂在模型根节点上using System.Collections.Generic; using UnityEngine; public class ExplosionController : MonoBehaviour { [System.Serializable] public class Part { public Transform part; // 零件 public float explodeDistance 1.2f; // 拆开距离单位为米 public bool isStatic; // 该零件不参与拆装 } public ListPart parts new ListPart(); public AnimationCurve easeCurve AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); public float duration 1f; // 单次拆装时间秒 private DictionaryTransform, Vector3 startPos new DictionaryTransform, Vector3(); private DictionaryTransform, Vector3 endPos new DictionaryTransform, Vector3(); void Start() { foreach (Part p in parts) { if (p.isStatic) continue; startPos[p.part] p.part.localPosition; // 记录初始局部坐标跟父物体移动无关 Vector3 toCenter p.part.position - transform.position; toCenter.y * 0.3f; // Y 方向衰减一些拆开效果更集中 endPos[p.part] p.part.localPosition toCenter.normalized * p.explodeDistance; } } public void Explode() { StopAllCoroutines(); StartCoroutine(MoveParts(false)); } public void Assemble() { StopAllCoroutines(); StartCoroutine(MoveParts(true)); } private System.Collections.IEnumerator MoveParts(bool isAssembling) { float t 0f; while (t duration) { t Time.deltaTime; float time easeCurve.Evaluate(t / duration); // 用曲线控制速度 foreach (var kv in startPos) { Transform p kv.Key; // 装配时反向插值拆装时正向插值 p.localPosition Vector3.LerpUnclamped( isAssembling ? endPos[p] : kv.Value, isAssembling ? kv.Value : endPos[p], time); } yield return null; } } }这段代码有几个关键点。第一LerpUnclamped而不是Lerp是因为EaseInOut曲线在边缘处的值可能略小于0或略大于1Clamp会削掉这个微妙的缓动效果。第二起始位置记录的是localPosition不是position这个细节很重要如果模型根节点有别的脚本移动了位置世界坐标会被二次叠加导致拆开后的位置错乱。第三toCenter.y * 0.3f这个操作是我调出来的——完全水平展开会显得太像爆炸碎片完全沿中心到零件方向又会导致上下层零件穿模压减Y轴分量后拆开效果更像是“从侧面展开”对观感提升很明显。这四个值是关联项目经验调出来的你们拿到手先跑一遍再按自己的模型调整。4.3 进度条驱动拆装让客户自己拖有了上面的基础做进度条驱动就非常直接了。无非是把MoveParts里的t改成由Slider提供。我在项目里做了一个SliderMin为0Max为100它的值除以100就是拆装进度。进度为0时全部装配态进度为1时全部拆开。代码改动很小——把IEnumerator MoveParts里的t改成直接读取Slider的value再乘上easeCurve.Evaluate就实现了拖拽进度条控制拆装动画的效果。这个功能客户看了基本都会喜欢因为它让演示从“放视频”变成了“可控操作”。5. 避坑指南从模型翻转、标注抖动、动画错位到拾取遮挡5.1 模型导入后表面是黑的或铁青色现象FBX拖进来光照也打了但模型大面积发黑角落泛出不自然的铁青色。原因90%的情况是法线翻转到了内侧或者Unity导入设置里没有正确计算法线。这一类问题多见于从Catia、SolidWorks转出的模型这些软件导出的STL或OBJ常常带法线方向不一致的问题。解决我在Blender中重新计算法线全选模型后执行Mesh - Normals - Recalculate Outside重新导出FBX如果不想回建模软件在Unity导入设置的Model选项卡里把Normals Tangents设为Calculate让Unity自动重算法线。我一般更推荐前者因为Unity自动重算法线会让硬边软化机械模型的倒角看起来会发糊。5.2 拆装动画里零件飞出去之后转不回来现象点击“装配”按钮零件没有按原来的路径飞回来而是跑到场景里某个莫名其妙的位置或者转了一瞬间就变成散落状态。原因这个坑十有八九是localPosition和position混用了。如果你在Start里存了part.position在Update里赋给part.localPosition而父节点自身也有位移那么每个子节点的目标位置都会叠加父节点的偏移量。解决统一用局部坐标。起始位置存localPosition目标位置也基于localPosition计算动画过程中始终赋localPosition。还有一种少见的情况是模型根节点挂载了前面的AutoRotate脚本旋转会因为坐标空间变换干扰拆装计算我在拆装动画播放时会先把AutoRotate脚本的enabled设成false动画结束后再打开。5.3 标注文字在旋转相机时轻微抖动现象相机转动时标注文字跟着移动但偶尔会像被拉扯一样顿一下不是流畅的跟随。原因抖动原因是文字Update的时机和相机移动时机不一致或者说LookAt更新发生在渲染之前而相机在这帧的变换没有提交导致视觉上相差一两帧。另一个常见因素是World Space Canvas的缩放值不是整数UI元素尺寸小于4像素时Unity的TextMeshPro会触发亚像素渲染产生移动时的不稳定。解决把Canvas的DynamicPixels Per Unit从默认1提到4或8这个参数在TextMeshPro的Extra Settings里。另外把LabelFacingCamera脚本从Update挪到LateUpdate这样会等相机和所有动画逻辑都更新完再对齐跟随就不抖了。这个方法也适用于任何需要“紧跟”相机的物体。5.4 射线拾取总是点不到被外壳遮住的内部零件现象做点击零件高亮时明明点击的是屏幕上的内部区域射线命中的却是前面的外壳内部零件永远选不中。原因这不是Bug是3D拾取的基本规则——射线只能命中第一个遮挡物。展示场景中外壳通常会挡住内部核心如果外壳本身也是可点选的就永远轮不到内部零件。解决我的方案有两个层次。如果产品结构是固定的我给外壳MeshCollider加了一个自定义的InteriorIgnore标签射线检测时跳过这层Collider。如果外壳内部有透明观察窗更合理的做法是把内部零件放在一个单独的Layer里用Physics.Raycast时传入LayerMask先对内部Layer做检测如果没命中再检测外部Layer。代码段如下public class PartPicker : MonoBehaviour { public LayerMask innerMask; // 在 Inspector 里指定内部零件所在的 Layer public LayerMask outerMask; void Update() { if (Input.GetMouseButtonDown(0)) { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); // 先检测内部层再检测外部层实现“穿透外壳选内部” if (Physics.Raycast(ray, out RaycastHit hit, 100f, innerMask)) { Debug.Log(命中内部零件: hit.collider.name); } else if (Physics.Raycast(ray, out hit, 100f, outerMask)) { Debug.Log(命中外部零件: hit.collider.name); } } } }LayerMask是位掩码在Inspector里勾选对应的Layer即可。这个“先内部后外部”的顺序逻辑只适用于模型中内外关系固定的场景。如果你的模型支持拆装拆开后内外关系会变那时候检测顺序应该反过来这个逻辑建议和拆装状态联动。5.5 遮挡剔除没做大模型展示卡成幻灯片现象模型零件多到几千面相机转了半圈FrameRate直接跌到20以下。原因Unity默认把场景里所有可见物体都提交渲染即使被完全遮挡的物体也一样在计算。机械装配体内部零件密集遮挡比例极高不做剔除性能浪费巨大。解决给相机加Occlusion Culling。步骤是选中所有参与展示的模型和地面在Inspector里把它们标记为Occluder Static和Occludee Static然后打开Window Rendering Occlusion Culling窗口点Bake。烘焙过程中Unity会预计算遮挡关系运行时自动跳过被遮挡的物体。这类模型的烘焙时间通常几秒到几十秒根据零件复杂度而定。如果烘焙后出现洞穿效果把Occlusion Culling窗口里的Smallest Hole调小Smallest Occluder调大再重新烘焙。6. 进阶玩法动画回放、单帧截图与自动复位到这一步基础的展示、标注、拆装已经能工作了。我最后再分享三个在交付项目里经常被客户要求加的小功能它们让原型变成一个“有完成度”的演示系统。6.1 用协程做拆装动画回放客户看演示时经常说“哎你刚才那个动作再放一遍”。手动重新点按钮可以但演示过程中最好能按一个键就自动完成“拆开 → 保持两秒 → 装回”的完整循环。这个用协程控制流程就很简单using System.Collections; using UnityEngine; public class DemoReplay : MonoBehaviour { public ExplosionController explosion; public KeyCode replayKey KeyCode.Space; void Update() { if (Input.GetKeyDown(replayKey)) { StopAllCoroutines(); // 防止多次按快捷键导致协程叠加重入 StartCoroutine(ReplaySequence()); } } private IEnumerator ReplaySequence() { explosion.Assemble(); // 先回到初始装配态 yield return new WaitForSeconds(1.2f); // 停一下让客户看清整体 explosion.Explode(); // 拆开 yield return new WaitForSeconds(2f); // 保持拆开状态 explosion.Assemble(); // 装回 } }这个协程写清楚了一个演示节奏装配态驻留1.2秒拆开态驻留2秒。这两个停留时间不要设成一样人眼对“静态”和“运动后静止”的感知速度不同拆开态多停留一会客户才能看清内部结构。6.2 单帧截图用于制作说明书和多视角存档机械类产品演示结束后客户经常会伸手要“刚才那个角度的截图我放PPT里”。与其让他们用系统截图工具截出带UI的图我更喜欢在项目里内置一个截图工具。Unity的ScreenCapture.CaptureScreenshot只能截全屏而且会截进UI。我这里是做一个快捷键调低UI透明度后截图using System.IO; using UnityEngine; public class ScreenshotHelper : MonoBehaviour { public Canvas uiCanvas; // 标注和操作面板的 Canvas public KeyCode screenshotKey KeyCode.F12; void Update() { if (Input.GetKeyDown(screenshotKey)) { StartCoroutine(ScreenshotWithUIToggle()); } } private System.Collections.IEnumerator ScreenshotWithUIToggle() { uiCanvas.enabled false; // 先隐藏 UI yield return new WaitForEndOfFrame(); // 等GPU渲染完这一帧 string path Path.Combine(Application.dataPath, ../Screenshots); Directory.CreateDirectory(path); string filename shot_ System.DateTime.Now.ToString(yyyyMMdd_HHmmss) .png; ScreenCapture.CaptureScreenshot(Path.Combine(path, filename), 2); // 第2个参数是分辨率倍率 uiCanvas.enabled true; // 截完立刻恢复 UI } }这里有两个细节截图前必须WaitForEndOfFrame否则截出来的画面缺失一帧保存路径我用的是工程根目录下新建的Screenshots文件夹不要输出在Assets里方便打包后依然可用。6.3 标注重置与高亮联动演示过程中经常遇到这样的情况拆装动画结束后原来跟着零件走的标注还在原来的世界坐标位置和零件实体分离了。这个问题我一开始没发现直到客户指出来。从那以后我每次做拆装动画都会在结尾强制调用一次LabelManager.ResetAll()把所有标注点的位置重新绑定到对应零件当前的新坐标并重算一次朝向。这个联动逻辑最好放在拆装动画的Assemble和Explode两个方法的末尾而不是单独做一个Update检测——因为协程播放中Update里检测坐标是否漂移的成本太高且容易在动画播放中途产生一次错误复位。这套资源的边界也很清楚它是为中小型装配体的展示场景设计的没有去做BIM级别的海量构件管理也没做多用户协同但单机演示、标注、留档这三个最刚需的场景覆盖得相当完整。如果你正好卡在模型展示交付的最后一步希望这一路的拆解能让你少走几圈弯路。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。