news 2026/10/10 6:47:32

Unity 3D模型展示实战:3D标注与拆装动画全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity 3D模型展示实战:3D标注与拆装动画全流程

简介:这份资源面向Unity开发者、教学课件制作者与产品展示设计人员,聚焦3D模型展示中的交互呈现问题,涵盖模型导入优化、3D标注、环绕相机、步骤列表与拆装动画等核心知识点,适合具备一定Unity基础、希望提升展示类应用互动性的中阶学习者。压缩包共4300个文件,约65.71MB,以cs脚本、meta配置、png贴图、mat材质、prefab预制体、unity场景、shader着色器及fbx模型为主,另含md说明、json数据与dll插件,覆盖从资源导入到动画控制的完整工程结构。目前已有3750人学习下载。读者可从中获得可运行的Unity工程示例,理解如何用Animator状态机与关键帧实现平滑拆装过渡,借助脚本控制Camera围绕目标旋转实现360度查看,并通过UI标签与粒子效果完成随模型移动的注解,同时掌握Rigidbody物理模拟增强真实感的思路,为教育演示、产品拆解与虚拟仿真项目提供可直接参考的实现方案。

1. 从一次“模型转不动”的评审说起

做过 Unity 3D 模型展示的人大概都遇到过这种场面:模型导进来挺好看,鼠标一拖就穿模,标注文字永远背对镜头,拆装动画播到一半零件飞了。更尴尬的是评审现场有人问“这个螺栓能不能单独拆下来看看”,你只能假装没听见。Unity 3D 模型展示、3D 标注、拆装动画这三件事,单拎出来都不算难,难的是它们要跑在同一套场景里,还要让非技术用户点两下就会用。这套方案适合做工业设备演示、教学课件、产品结构说明的开发者,也适合手里已经有 FBX 模型、想快速搭一个可交互展示 Demo 的人。下面按“模型怎么进来、标注怎么挂上去、拆装怎么动起来”的顺序,把我在实际项目里踩过的路讲清楚。

2. 模型导入与场景搭建:从 FBX 到可交互展示

2.1 模型准备阶段的三个硬性检查

在 Unity 里做展示,模型本身的质量决定了后面一半的坑。我一般拿到 FBX 之后先做三件事:检查单位比例、检查 pivot 位置、检查材质球数量。单位比例不对,模型要么像蚂蚁要么像哥斯拉;pivot 不在几何中心,旋转和拆装动画的轴心就会偏;材质球太多,移动端直接爆 draw call。

常见做法是在建模软件里就把单位设成米,导出时勾选“嵌入媒体”,Unity 导入后把 Scale Factor 设成 1。如果模型来自不同来源,比例不一致,可以在导入设置里单独调,但更推荐在建模阶段统一。

// ModelImportChecker.cs // 挂在空物体上,导入后自动检查模型包围盒和材质数量 using UnityEngine; public class ModelImportChecker : MonoBehaviour { void Start() { // 获取所有子物体的 Renderer Renderer[] renderers = GetComponentsInChildren<Renderer>(); if (renderers.Length == 0) { Debug.LogWarning("模型下没有找到任何 Renderer,检查 FBX 是否导入成功"); return; } // 计算整体包围盒 Bounds bounds = renderers[0].bounds; foreach (var r in renderers) { bounds.Encapsulate(r.bounds); } Debug.Log($"模型包围盒尺寸: {bounds.size}, 中心点: {bounds.center}"); // 统计材质数量 var materials = new System.Collections.Generic.HashSet<Material>(); foreach (var r in renderers) { foreach (var m in r.sharedMaterials) { if (m != null) materials.Add(m); } } Debug.Log($"独立材质数量: {materials.Count},超过 20 个建议合并"); } }

这段代码的逻辑很简单:遍历所有 Renderer,算出一个能包住整个模型的包围盒,同时统计独立材质数量。包围盒尺寸用来判断比例是否合理,材质数量用来判断是否需要做图集合并。参数方面,如果包围盒某个轴特别小(比如小于 0.01),说明模型可能被压扁了;材质超过 20 个,移动端就要考虑用 TexturePacking 合并贴图。

2.2 相机控制:轨道旋转与聚焦的取舍

展示类场景的相机控制,我见过三种做法:自己写鼠标事件、用 Cinemachine、用现成的轨道控制器。自己写最灵活但容易出 bug,Cinemachine 适合做镜头动画但不适合自由观察,轨道控制器最稳但需要调参数。

我一般会写一个轻量的轨道控制器,核心逻辑是:鼠标左键旋转、右键平移、滚轮缩放,同时限制俯仰角避免翻转。下面是一个最小实现。

// OrbitCamera.cs // 轻量轨道相机,支持旋转、平移、缩放 using UnityEngine; public class OrbitCamera : MonoBehaviour { public Transform target; // 围绕的目标点 public float rotateSpeed = 5f; // 旋转速度 public float zoomSpeed = 2f; // 缩放速度 public float panSpeed = 0.5f; // 平移速度 public float minDistance = 0.5f; // 最近距离 public float maxDistance = 20f; // 最远距离 public float minPitch = -80f; // 最小俯仰角 public float maxPitch = 80f; // 最大俯仰角 private float distance = 5f; private float yaw = 0f; private float pitch = 20f; void Start() { if (target == null) { // 没有目标就创建一个空物体放在原点 GameObject go = new GameObject("CameraTarget"); target = go.transform; } // 初始化距离为当前距离 distance = Vector3.Distance(transform.position, target.position); } void LateUpdate() { // 旋转:按住左键 if (Input.GetMouseButton(0)) { yaw += Input.GetAxis("Mouse X") * rotateSpeed; pitch -= Input.GetAxis("Mouse Y") * rotateSpeed; pitch = Mathf.Clamp(pitch, minPitch, maxPitch); } // 平移:按住右键 if (Input.GetMouseButton(1)) { float x = -Input.GetAxis("Mouse X") * panSpeed * distance; float y = -Input.GetAxis("Mouse Y") * panSpeed * distance; target.Translate(new Vector3(x, y, 0), Space.Self); } // 缩放:滚轮 float scroll = Input.GetAxis("Mouse ScrollWheel"); distance -= scroll * zoomSpeed * distance; distance = Mathf.Clamp(distance, minDistance, maxDistance); // 计算相机位置 Quaternion rotation = Quaternion.Euler(pitch, yaw, 0); Vector3 position = target.position - rotation * Vector3.forward * distance; transform.rotation = rotation; transform.position = position; } }

这段代码的关键参数是minPitch和maxPitch,限制在 -80 到 80 度之间,避免相机翻到模型底下导致方向感丢失。panSpeed乘以distance是为了让平移速度随距离变化,远看时移动快,近看时移动慢,手感更自然。LateUpdate里更新相机,保证在模型动画之后执行,避免抖动。

2.3 场景光照与后处理的低成本方案

展示场景不需要太复杂的光照,我一般用一盏方向光加环境光,再配一个后处理 Volume 做轻微的抗锯齿和色彩调整。方向光的角度设成 45 度俯角,强度 1.2 左右,环境光用 Gradient 模式,天空色偏冷、地面色偏暖。后处理里 Bloom 强度不要超过 0.5,否则模型边缘会糊。

如果模型是金属材质,记得在导入设置里勾选“Read/Write Enabled”,否则运行时改材质属性会报错。这个坑我踩过不止一次,模型看起来没问题,一改颜色就变黑,查半天才发现是导入设置的问题。

3. 3D 标注系统:让文字和模型一起转

3.1 标注的两种挂载方式:世界空间与屏幕空间

3D 标注的核心问题是:文字要跟着模型转,但不能被模型挡住,也不能永远背对镜头。常见做法有两种:世界空间 Canvas 和屏幕空间 Canvas 加世界坐标转换。

世界空间 Canvas 的优点是实现简单,把 Canvas 的 Render Mode 设成 World Space,然后作为模型的子物体就行。缺点是文字会随模型旋转,转到背面就看不见了。屏幕空间 Canvas 的优点是文字永远面向镜头,但需要每帧计算模型上某个点的屏幕坐标,性能开销稍大。

我一般用屏幕空间方案,因为展示场景里用户最关心的是“这个零件叫什么”,而不是“这个文字在三维空间里的朝向”。下面是一个标注管理器的实现。

// AnnotationManager.cs // 管理所有标注点,每帧更新屏幕位置 using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class AnnotationManager : MonoBehaviour { [System.Serializable] public class AnnotationData { public string label; // 标注文字 public Transform anchor; // 模型上的锚点 public Vector3 offset; // 屏幕偏移 } public Canvas uiCanvas; // 屏幕空间 Canvas public GameObject labelPrefab; // 标注预制体(Text + Image) public List<AnnotationData> annotations = new List<AnnotationData>(); private List<RectTransform> labelRects = new List<RectTransform>(); private Camera mainCam; void Start() { mainCam = Camera.main; foreach (var data in annotations) { GameObject go = Instantiate(labelPrefab, uiCanvas.transform); go.GetComponentInChildren<Text>().text = data.label; labelRects.Add(go.GetComponent<RectTransform>()); } } void LateUpdate() { for (int i = 0; i < annotations.Count; i++) { var data = annotations[i]; if (data.anchor == null) continue; // 世界坐标转屏幕坐标 Vector3 screenPos = mainCam.WorldToScreenPoint(data.anchor.position); // 判断是否在相机前方 bool isBehind = screenPos.z < 0; labelRects[i].gameObject.SetActive(!isBehind); if (!isBehind) { // 加上偏移,避免文字盖住锚点 screenPos += data.offset; labelRects[i].position = screenPos; } } } }

这段代码的核心是WorldToScreenPoint,把模型上的锚点转成屏幕坐标,然后设置 UI 元素的位置。screenPos.z < 0用来判断锚点是否在相机背面,如果在背面就隐藏标注,避免出现“文字飘在模型外面”的诡异现象。offset参数用来微调文字位置,一般设成 (0, 30, 0) 让文字浮在锚点上方。

3.2 标注的遮挡处理:射线检测与透明度渐变

标注文字被模型挡住是常见问题。如果不管,用户会看到文字穿过模型显示出来,很出戏。我一般用射线检测来判断锚点是否被遮挡,如果被遮挡就把标注透明度降低。

// 在 AnnotationManager 的 LateUpdate 里加入遮挡检测 // 需要给模型层设置 LayerMask public LayerMask modelLayer; public float occludedAlpha = 0.3f; // 在设置位置之前加入 Vector3 dir = data.anchor.position - mainCam.transform.position; float dist = dir.magnitude; RaycastHit hit; bool occluded = Physics.Raycast(mainCam.transform.position, dir.normalized, out hit, dist, modelLayer); // 设置透明度 CanvasGroup group = labelRects[i].GetComponent<CanvasGroup>(); if (group == null) group = labelRects[i].gameObject.AddComponent<CanvasGroup>(); group.alpha = occluded ? occludedAlpha : 1f;

这段代码从相机向锚点发射一条射线,如果打到模型层上的物体,说明锚点被遮挡。modelLayer需要手动设置,只包含模型所在的层,避免射线打到 UI 或其他物体。occludedAlpha设成 0.3 左右,既能让用户知道标注还在,又不会太抢眼。

3.3 标注数据的配置与动态增删

实际项目里,标注数据一般从 JSON 或 ScriptableObject 读取。我倾向于用 ScriptableObject,因为可以在 Inspector 里直接编辑,不用手写 JSON。下面是一个简单的配置类。

// AnnotationConfig.cs // 标注配置,可在 Inspector 里编辑 using UnityEngine; [CreateAssetMenu(fileName = "AnnotationConfig", menuName = "展示/标注配置")] public class AnnotationConfig : ScriptableObject { [System.Serializable] public class Item { public string label; public string anchorName; // 模型上锚点的名字 public Vector3 offset; } public Item[] items; }

用的时候在AnnotationManager里根据anchorName找到对应的 Transform,然后填充annotations列表。这样非技术同事也能在 Inspector 里改标注文字,不用动代码。动态增删的话,调用Instantiate和Destroy就行,记得同步维护labelRects列表。

4. 拆装动画:从爆炸图到分步装配

4.1 拆装动画的两种实现路径:Animator 与代码驱动

拆装动画有两种做法:用 Unity 的 Animator 做关键帧动画,或者用代码控制零件位移。Animator 适合固定的拆装顺序,做一次就能反复播;代码驱动适合动态调整拆装距离和顺序,灵活性更高。

我一般用代码驱动,因为展示场景里经常需要根据用户操作调整拆装进度。核心思路是:记录每个零件的初始位置和拆装目标位置,用插值控制进度。

// DisassemblyController.cs // 拆装控制器,支持整体拆装和单零件拆装 using System.Collections.Generic; using UnityEngine; public class DisassemblyController : MonoBehaviour { [System.Serializable] public class Part { public Transform transform; // 零件 public Vector3 explodeDir; // 拆装方向(本地坐标) public float explodeDistance; // 拆装距离 [HideInInspector] public Vector3 originPos; [HideInInspector] public Vector3 targetPos; } public List<Part> parts = new List<Part>(); public float duration = 1f; // 动画时长 public AnimationCurve easeCurve; // 缓动曲线 private float progress = 0f; // 0=装配,1=拆开 private float targetProgress = 0f; private float velocity = 0f; void Start() { foreach (var part in parts) { part.originPos = part.transform.localPosition; // 目标位置 = 初始位置 + 方向 * 距离 part.targetPos = part.originPos + part.explodeDir.normalized * part.explodeDistance; } } void Update() { // 平滑过渡 progress = Mathf.SmoothDamp(progress, targetProgress, ref velocity, duration * 0.3f); foreach (var part in parts) { float t = easeCurve.Evaluate(progress); part.transform.localPosition = Vector3.Lerp(part.originPos, part.targetPos, t); } } // 整体拆开 public void ExplodeAll() { targetProgress = 1f; } // 整体装配 public void AssembleAll() { targetProgress = 0f; } // 单个零件拆开 public void ExplodePart(int index) { if (index < 0 || index >= parts.Count) return; // 这里可以扩展成单独控制某个零件的进度 // 简单做法是直接设置该零件的位置 parts[index].transform.localPosition = parts[index].targetPos; } }

这段代码的核心是SmoothDamp和Vector3.Lerp。SmoothDamp让进度平滑变化,避免生硬跳变;Lerp根据进度插值位置。easeCurve用来控制缓动效果,一般用 EaseInOut 曲线,开始和结束慢、中间快。explodeDir是本地坐标下的方向,这样模型旋转后拆装方向依然正确。

4.2 拆装顺序与依赖关系:用队列控制先后

有些设备的拆装是有顺序的,比如先拆外壳再拆内部零件。我一般用一个简单的队列来控制。

// 在 DisassemblyController 里加入顺序控制 public List<int> explodeOrder = new List<int>(); // 拆装顺序索引 private int currentStep = 0; public void NextStep() { if (currentStep >= explodeOrder.Count) return; int index = explodeOrder[currentStep]; // 把当前零件加入拆装列表 // 这里简化处理,直接设置目标进度 currentStep++; } public void ResetOrder() { currentStep = 0; targetProgress = 0f; }

实际项目里,我会把每个零件的拆装进度单独存一个值,然后用一个协程按顺序推进。这样用户点“下一步”的时候,只拆当前步骤的零件,前面的保持拆开状态。

4.3 拆装动画的性能优化:合批与 LOD

零件多了之后,拆装动画会掉帧。我一般做两件事:合并材质减少 draw call,给远处的零件加 LOD。合并材质可以用 Unity 的 TexturePacking,把多个小贴图打成一个图集,然后所有零件共用一个材质。LOD 的话,给每个零件加一个 LOD Group,远处显示低模,近处显示高模。

还有一个容易忽略的点:拆装动画更新的是localPosition,如果零件层级很深,每次更新都会触发 Transform 的脏标记,影响性能。我一般会把需要拆装的零件放在同一层级下,减少层级嵌套。

5. 避坑与排查:那些让我加班到凌晨的问题

5.1 模型导入后比例不对,相机拉近就穿模

现象:模型导入后看起来正常,但相机拉近到一定程度就穿到模型内部,或者模型突然变得巨大。

原因:FBX 导出时的单位比例和 Unity 的默认单位不一致。Unity 默认 1 单位 = 1 米,但有些建模软件默认 1 单位 = 1 厘米。

解决:在导入设置里把 Scale Factor 调成 0.01 或 100,或者在建模软件里统一单位。我一般会在导入后跑一次ModelImportChecker,看包围盒尺寸是否合理。如果包围盒某个轴小于 0.01 或大于 1000,基本就是比例问题。

5.2 标注文字被模型挡住,射线检测失效

现象:标注文字明明在模型前面,但射线检测判断为遮挡,透明度一直很低。

原因:射线检测的 LayerMask 设置不对,或者锚点位置在模型内部。还有一种可能是模型的 Collider 没有正确生成。

解决:检查 LayerMask 是否只包含模型层,检查锚点的世界坐标是否在模型表面。如果模型没有 Collider,可以手动加一个 MeshCollider,或者用Physics.Raycast的重载版本,指定QueryTriggerInteraction.Ignore。

5.3 拆装动画播完后零件位置偏移

现象:拆装动画播完后,零件没有回到原位,或者位置有微小偏移。

原因:SmoothDamp的velocity没有重置,或者progress没有精确到达 0 或 1。

解决:在ExplodeAll和AssembleAll里把velocity置零,并且在Update里判断Mathf.Abs(progress - targetProgress) < 0.001f时直接设置progress = targetProgress。另外,originPos要在Start里记录,不要在Awake里记录,因为Awake时模型可能还没初始化。

5.4 移动端拆装动画卡顿,帧率掉到 20 以下

现象:在编辑器里跑得很流畅,打包到移动端就卡。

原因:材质数量太多,draw call 过高;或者拆装动画每帧更新太多 Transform。

解决:用 TexturePacking 合并材质,把 draw call 降到 50 以下;拆装动画改成每两帧更新一次,或者用 Job System 并行计算位置。还有一个取巧的办法:拆装动画播完后把脚本禁用,需要时再启用。

5.5 标注文字在不同分辨率下位置偏移

现象:在 1080p 屏幕上标注位置正常,在 2K 或 4K 屏幕上标注偏了。

原因:屏幕空间 Canvas 的CanvasScaler没有设置成“Scale With Screen Size”,或者WorldToScreenPoint返回的坐标没有考虑 DPI。

解决:把CanvasScaler的Ui Scale Mode设成Scale With Screen Size,参考分辨率设成 1920x1080,匹配模式选Match Width Or Height,一般设 0.5。这样 UI 会按比例缩放,标注位置就不会偏。

6. 进阶技巧:用状态机管理展示流程

前面讲的都是单个功能的实现,实际项目里往往需要把模型展示、标注、拆装串成一条流程。我一般用一个简单的状态机来管理,状态包括:自由观察、标注模式、拆装模式、装配模式。每个状态有自己的进入和退出逻辑,比如进入拆装模式时禁用相机旋转,退出时恢复。

// ShowStateMachine.cs // 展示流程状态机 using UnityEngine; public class ShowStateMachine : MonoBehaviour { public enum State { FreeView, Annotation, Disassemble, Assemble } public State currentState = State.FreeView; public OrbitCamera orbitCamera; public AnnotationManager annotationManager; public DisassemblyController disassemblyController; void Start() { EnterState(State.FreeView); } public void EnterState(State newState) { // 退出当前状态 ExitState(currentState); currentState = newState; // 进入新状态 switch (newState) { case State.FreeView: orbitCamera.enabled = true; annotationManager.gameObject.SetActive(false); disassemblyController.AssembleAll(); break; case State.Annotation: orbitCamera.enabled = true; annotationManager.gameObject.SetActive(true); break; case State.Disassemble: orbitCamera.enabled = false; disassemblyController.ExplodeAll(); break; case State.Assemble: orbitCamera.enabled = false; disassemblyController.AssembleAll(); break; } } void ExitState(State oldState) { // 这里可以加一些清理逻辑 // 比如停止动画、重置参数 } }

这个状态机的核心是EnterState和ExitState,每个状态切换时先清理旧状态,再初始化新状态。orbitCamera.enabled用来控制相机是否可操作,拆装模式下禁用相机旋转,避免用户误操作。annotationManager.gameObject.SetActive用来控制标注的显示隐藏。

验证这套流程是否正常,我一般会做三个测试:第一,从自由观察切到标注模式,标注是否正常显示;第二,从标注模式切到拆装模式,标注是否隐藏、拆装是否播放;第三,从拆装模式切回自由观察,零件是否回到原位、相机是否恢复控制。如果这三个测试都通过,基本就没大问题了。

还有一个技巧:用ScriptableObject把状态配置存起来,比如每个状态对应的相机距离、标注透明度、拆装速度。这样调整参数不用改代码,直接在 Inspector 里改就行。我一般会建一个ShowConfig资产,里面存所有可调参数,运行时读取。

最后说一个我自己的习惯:每次做完一个展示 Demo,我都会在真机上跑一遍,用最低端的设备测。编辑器里再流畅,真机上也可能翻车。尤其是拆装动画,零件一多,移动端很容易掉帧。如果真机测试过不了,宁可减少零件数量,也不要硬扛。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 6:47:24

C盘扩容实战:从D盘借空间给系统盘,无损分区工具操作指南

1. 为什么C盘总是先满&#xff0c;以及扩容前必须想清楚的几件事但凡用过几年电脑的人&#xff0c;大概率都经历过C盘飘红的绝望。系统盘就像家里的玄关&#xff0c;进门出门都得从这儿过&#xff0c;各种软件默认往这儿装、临时文件往这儿堆、系统更新往这儿塞&#xff0c;时间…

作者头像 李华
网站建设 2026/10/10 6:47:16

macOS 进程排查:功能组中只有 Running 状态,App 是如何被拉起的?

大概一年前&#xff0c;我在一台 Mac 上遇到一个挺诡异的现象&#xff1a;某个 App 的窗口都没出现在 Dock 里&#xff0c;进程却一直活着。我用ps看了一眼它的 functional group&#xff0c;发现这个功能组里只有一个进程&#xff0c;状态一直是 R。正常情况下&#xff0c;一个…

作者头像 李华
网站建设 2026/10/10 6:47:08

Apache Pulsar PIP-91 深度解读:将 Lookup 超时从 Operation 超时中分离

消息队列流处理后端微服务消息路由 【免费下载链接】pulsar Apache Pulsar - distributed pub-sub messaging system 项目地址&#xff1a; https://gitcode.com/gh_mirrors/pu/pulsar 点击查看 免费下载 导读 PIP-91&#xff08;PIP-91: Separate lookup timeout from opera…

作者头像 李华
网站建设 2026/10/10 6:46:55

10款降AI率工具横向测评:毕业论文如何避开AIGC检测

先说结论&#xff1a;这届本科生不再是查重一个坎了&#xff0c;查重后面还蹲着一个AIGC检测。2026届的毕业论文季&#xff0c;我身边几乎每个人都在问同一句话——“怎么降AI率”。我花了大概两个月时间&#xff0c;把市面上常被拿来当“降AI率”用的10款工具&#xff0c;全部…

作者头像 李华
网站建设 2026/10/10 6:46:47

硬链接与软链接:文件系统链接机制的原理与实战指南

1. 先搞清楚这两个“链接”到底在链接什么在写这一篇之前&#xff0c;我刚处理完一台出问题的服务器。现象很简单&#xff1a;日志目录里的文件明明还在&#xff0c;占用的空间却在疯狂增长。查了一圈发现&#xff0c;某个程序每次启动都会往同一个路径写日志&#xff0c;而运维…

作者头像 李华
网站建设 2026/10/10 6:46:16

Playwright实战:电商动态渲染采集与反爬对抗全攻略

我们天天都在说"爬虫要懂反爬"&#xff0c;可真当你面对一个纯静态页面能轻松搞定、一碰动态渲染的页面就抓瞎的时候&#xff0c;才会明白工具选型有多重要。这几年做电商数据采集和评论区挖掘&#xff0c;我大部分时间都耗在 Playwright 和 Selenium 这两套自动化工…

作者头像 李华