ARTICLE DETAIL

资讯详情

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

Unity实现3D模型展示、3D标注与拆装动画的完整指南

Unity实现3D模型展示、3D标注与拆装动画的完整指南 简介这是一份面向Unity初中级开发者与3D可视化爱好者的完整项目资源围绕“3D模型展示”场景系统覆盖3D标注、环绕相机、步骤列表与拆装动画等核心交互功能适用于产品展示、教学演示及AR/VR应用开发。压缩包共4300个文件包含735个C#控制脚本、280张PNG贴图、138个Markdown说明文档并有材质、FBX模型、Prefab预制体、Shader、UnityPackage等辅助资源其中C#脚本负责交互逻辑贴图与材质提供渲染表现文档可指导快速上手整体体积65.71MB目录结构清晰方便按功能模块检索。已有3745人学习使用。通过该资源开发者可以掌握Unity中3D模型导入与优化技巧了解如何搭建可交互的标注系统、实现360度环绕观察视角并借助Animator状态机与物理引擎设计拆装动画与步骤引导最终构建出具有高度互动性和教育价值的3D应用有效提升产品展示或教学项目的体验。1. 把模型展示、3D标注、拆装动画放进同一个Unity工程值不值一个做机械臂毕业设计的朋友上周问我客户想看零件拆装过程模型已经用SolidWorks画好能不能直接在Unity里做3D模型展示顺带点一下零件就出标注还能按顺序播放拆装动画我说能而且这三件事放同一个Unity工程里做比拆成三个软件分别演示划算得多。Unity 3D模型展示解决“看得见”3D标注解决“看得懂”拆装动画解决“看得透”一套工程既能当销售演示又能当售后培训素材还能导出WebGL扔到网页上给客户自己点。适合谁适合做工业设备展示、产品说明书电子化、机械教学课件的人以及被客户临时加需求的技术支持。本文按我实际做过的方案讲模型怎么导、标注怎么点、拆装怎么写最后给你一份能照着改的脚本。2. 从模型导入到轨道相机先把3D展示的地基打牢2.1 模型格式与导入管线为什么首选FBX而不是直接拖原始格式做Unity 3D模型展示第一个坑往往不是代码而是模型进不了Unity。客户交付的模型常见格式是STP、IGES、STEP这是CAD软件的通用交换格式Unity不认。另一类常见格式是OBJUnity虽然直接支持但OBJ不带骨骼、不带层级、材质也经常丢。我的习惯是所有原始模型先让建模同事在3ds Max、Blender或SolidWorks自带的转换器里统一转成FBX勾选“嵌入媒体”选项让贴图跟着模型走。FBX之所以是首选是因为它同时保留模型层级、材质引用、动画轨道和轴信息。对于拆装动画来说模型层级尤其关键——如果零件之间的父子关系在FBX里是完整的后面的拆装脚本可以直接按层级遍历子物体少写一半代码。glTF格式在Web端是王者配合model-viewer做纯网页3D展示很轻量但我做Unity项目还是优先FBX因为Unity对FBX的原生管线最成熟导入设置里的选项最全。导入时有一个必调参数缩放单位。SolidWorks和3ds Max的默认单位不同导出FBX后进Unity经常出现模型巨大或微小到看不见。选中导入的模型在Inspector面板的Scale Factor里核对我一般会把“Use File Scale”关掉手动把它调成1然后看场景里模型的实际尺寸。还有一个常见问题是轴方向。CAD软件习惯Y轴向上Unity也是Y轴向上一般不用改但某些机械设计软件导出的模型是Z轴向上转过来后模型会躺倒这时在FBX导入设置的Scene一栏里旋转90度修正即可。2.2 相机与光照让展示效果不翻车的三个最少配置模型立住之后展示效果取决于相机和光照。我见过不少工程模型挺好但场景里只有一个平行光金属零件看上去是黑的或者转起来时明暗变化剧烈。这里给出我固定的三个最少配置。第一颜色空间用Linear。在Player Settings里把Color Space从Gamma切到Linear金属质感的模型在Linear下反射分布更真实这是性价比最高的一次设置。第二光照方案用“一主一辅一环境”一个平行光做主光方向略向下倾斜一个强度很低的点光源或半球光做补光防止阴影面死黑环境光用Skybox默认即可但要把Environment Lighting的Intensity调到0.5到0.8之间避免整体过曝。第三相机抗锯齿开FXAA。做模型展示时用户会高频旋转视角MSAA在这种场景下对性能开销偏高FXAA实现简单且不会让边缘闪烁。相机控制方面用户最熟悉的手势是“拖拽旋转、滚轮缩放”这也是热搜里“Unity摄像机跟随”最常见的诉求。这里给一个最小可用的轨道相机脚本挂在主相机上把模型的中心空物体拖到Target字段里即可。using UnityEngine; public class OrbitCamera : MonoBehaviour { public Transform target; // 旋转围绕的目标点通常是模型的中心空物体 public float rotateSpeed 5f; // 拖拽旋转速度 public float zoomSpeed 2f; // 滚轮缩放速度 public float minDistance 0.5f; public float maxDistance 20f; private float currentX 0f; private float currentY 20f; // 初始俯视角 private float currentDistance 5f; void Update() { // 鼠标左键拖拽旋转视角 if (Input.GetMouseButton(0)) { currentX Input.GetAxis(Mouse X) * rotateSpeed; currentY - Input.GetAxis(Mouse Y) * rotateSpeed; currentY Mathf.Clamp(currentY, -90f, 90f); // 限制俯仰角避免视角穿到地下 } // 滚轮控制距离 float scroll Input.GetAxis(Mouse ScrollWheel); currentDistance - scroll * zoomSpeed; currentDistance Mathf.Clamp(currentDistance, minDistance, maxDistance); } void LateUpdate() { if (target null) return; // 用四元数计算旋转后的方向再按距离偏移得到相机位置 Quaternion rotation Quaternion.Euler(currentY, currentX, 0f); Vector3 direction rotation * Vector3.back; transform.position target.position direction * currentDistance; transform.LookAt(target.position); } }这段脚本的逻辑是用两个角度变量currentX和currentY累计鼠标输入然后用四元数乘Vector3.back得到相机在目标后方的方向向量乘上距离后就是相机位置最后LookAt保证始终看向模型中心。参数说明rotateSpeed建议设在3到6之间太快了用户容易头晕太慢了显得木currentY初始值设20到30度让模型呈现轻微俯视的“展示视角”比正平视更有立体感。需要强调一句这段脚本只处理了旋转和缩放没有处理平移工业模型经常需要平移查看细节你可以在Update里加一行在鼠标中键拖拽时让target的位置反向移动即可。提示如果你的模型非常大比如几十米长的设备Camera的Far Clip Plane要调大否则转着转着模型后半部分被裁剪掉了看起来像模型被“切了一刀”。Far值建议设为模型最大尺寸的10倍以上。3. 用射线拾取做3D标注一套能落到工程里的标注交互3.1 从屏幕点击到模型表面射线拾取与坐标换算3D标注需求拆开看就是“点击模型上的某个位置在那里挂一个标签标签指向这个位置并说明这是什么零件”。这和“3D点云标注”不同点云标注是把点云数据里的离散点归类和打框而这里是网格模型上的表面点拾取。两者的共同点是都要做空间坐标的捕捉区别在于点云没有连续表面射线打到的点要靠最近邻搜索网格模型直接用Physics.Raycast就能命中表面三角面。射线拾取的核心代码就是把屏幕坐标转成一条从相机出发的射线然后检测它撞到哪个Collider。这里有一个前置条件模型上的每个零件都必须有Collider。很多从CAD导出的FBX模型在Unity里是空有MeshRenderer没有Collider射线打上去直接穿透。我一般建议在模型的根节点下加一个“自动添加Collider”的编辑器脚本统一给所有带MeshRenderer的子物体补一个MeshCollider。注意MeshCollider性能开销较大零件数量超过几百个时建议改用BoxCollider或Convex类型。using UnityEngine; public class AnnotationClicker : MonoBehaviour { public Camera mainCamera; public GameObject annotationPrefab; // 标注预制体建议是一个世界空间Canvas 文本 指线 void Update() { if (Input.GetMouseButtonDown(0)) { // 从屏幕点击位置生成射线 Ray ray mainCamera.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // 射线命中模型表面 if (Physics.Raycast(ray, out hit, Mathf.Infinity)) { // 把命中点从世界坐标转到模型本地坐标保存时才有意义 Vector3 localPoint hit.collider.transform.InverseTransformPoint(hit.point); CreateAnnotation(hit.collider.gameObject, hit.point, localPoint); } } } void CreateAnnotation(GameObject target, Vector3 worldPoint, Vector3 localPoint) { // 在命中点生成一个标记点并记录它挂在哪个零件下 GameObject mark Instantiate(annotationPrefab, worldPoint, Quaternion.identity, target.transform); AnnotationData data mark.GetComponentAnnotationData(); data.partName target.name; data.localPosition localPoint; mark.name Annotation_ target.name; } }这段脚本做了两件重要的事一是用ScreenPointToRay把鼠标位置变成世界空间的射线二是把命中点的世界坐标转成目标物体的本地坐标并保存。为什么必须保存本地坐标因为模型如果有拆装动画零件会移动、旋转、甚至飞出画面如果标注点只记录世界坐标零件一动标注就跟丢了。挂在零件Transform下零件移动时标注自动跟随这是做3D标注的基本素养。参数说明Physics.Raycast的第四个参数没写LayerMask意味着所有带Collider的物体都会被射线检测到包括地面、UI面板、相机周围的辅助碰撞体。实际项目里建议给模型专门设一个Layer比如叫“Model”然后在Raycast时传LayerMask避免点击时误触其他对象。另外annotatedPrefab要提前做好我下面会讲预制体的结构。3.2 标注预制体设计世界空间Canvas、指线、文本三者如何组合标注物的设计决定了最终视觉效果。我测试过三种方案第一种是用TextMesh优点是轻量、随场景缩放缺点是不能被UI系统统一管理分层控制不方便第二种是屏幕空间Canvas加射线检测标注永远面向屏幕但做不到指向真实模型表面第三种是我最后采用的方案也是工业展示里最常用的做法世界空间Canvas 指线 标签底板。具体预制体结构是根节点挂一个世界空间CanvasCanvas下放一个RawImage作为标签底板再放一个TextMeshPro作为说明文字根节点上还挂一个LineRenderer用来画从标签到模型表面的引线。这里的核心技巧是让标签面向相机。世界空间Canvas默认垂直于Z轴如果相机旋转标签就会侧着甚至背面朝外。做法是在标注物脚本的Update里每帧执行transform.LookAt(相机)但要注意让标签的正面朝相机而不是背面朝相机所以LookAt后还要绕本地Y轴旋转180度。指线的画法是把LineRenderer的PositionCount设为2第一个点是标签底部锚点的位置第二个点是标注点位置。这样标签悬浮在标明点上方一点点的位置引线从标签底部连到零件表面视觉上像一个“针脚”指向性很强。为了美观可以让引线在靠近标注点的那端稍微缩短一点点防止LineRenderer被模型表面遮挡导致线头插进模型里。注意标注文字一旦多起来性能瓶颈会出现在TextMeshPro的批量重建上。一个标注物对应一个Canvas每帧都会有rebuild开销。如果界面同时显示超过20个标注建议考虑把Canvas数量减少用一个大Canvas挂多个子节点。4. 拆装动画的两条实现路线动画烘焙与代码驱动怎么选4.1 方案AAnimator 子物体位移适合固定动作序列拆装动画从技术实现上分两个阵营。第一个阵营是“在Unity里直接制作动画”——选中一个零件点Animation窗口的录制键把零件从装配位置移动到拆除位置记录关键帧就得到一段位移动画。这种方法适合拆装路径完全固定、不需要用户干预的场景比如自动播放的演示视频。但用Animator做拆装动画有一个隐藏问题零件移动的路径是不自然的。如果只在位置A和位置B打两个关键帧零件走直线当A到B的直线穿过其他零件时就会穿模。解决方法是添加中间关键帧让零件先向外拔出、再向上移动、最后放到目标位置路径要人工设计成“绕行”。我一般会在拆卸路径上打三到四个关键帧初始位置、位移方向的起始段、转折点、终点。另外拆装顺序在状态机里设定非常不直观一个20个零件的拆装序列光状态机的箭头就让人头大。所以Animator方案适合什么适合展示相对简单的产品比如一个鼠标、一个水泵零件数量在20个以内拆装动作是演示型的不需要用户参与。如果客户只要求“播放拆装动画我坐在旁边看”用Animator录好后释放双手效率最高。注意在Recorder模式下录动画时建议把动画采样率设为30帧/秒运动曲线会平滑一些零件的缩放不要录进关键帧除非你想拍“零件变小再消失”的花活。4.2 方案B代码驱动Transform用协程按轴逐件移出第二个阵营是“写代码控制Transform”。它的核心思想是每个零件事先在Inspector里配好一个拆装路径拆装时让Transform按路径移动。这种方式的好处是灵活可以让用户点击任意零件单独拆也可以按序播放还能中途暂停、回退甚至把零件高亮一下再拆。我为机械臂案例写过的拆装方案是这样的在某个零件上挂一个DisassemblyPart组件组件里有几个关键字段OutDirection拆除方向、MoveDistance移动距离、Delay本零件开始移动的延时。组件启动后用协程把零件从当前位置沿指定方向平移到目标位置再旋转一个角度模拟“拔出来再放一边”的效果。using UnityEngine; using System.Collections; public class DisassemblyPart : MonoBehaviour { public Vector3 outDirection Vector3.up; // 拆出方向按需在Inspector里改 public float moveDistance 0.5f; // 拆出距离 public float duration 1.2f; // 移动动画时长 public float startDelay 0f; // 该零件开始拆装的延迟 public bool dismantled false; public IEnumerator Disassemble() { // 如果已经拆过避免重复执行 if (dismantled) yield break; dismantled true; Vector3 startPos transform.localPosition; Vector3 endPos startPos transform.rotation * outDirection * moveDistance; float elapsed 0f; // 延迟指定时间后再开始移动 while (elapsed startDelay) { elapsed Time.deltaTime; yield return null; } // 用插值完成移动时长由duration控制 elapsed 0f; while (elapsed duration) { float t Mathf.SmoothStep(0f, 1f, elapsed / duration); transform.localPosition Vector3.Lerp(startPos, endPos, t); elapsed Time.deltaTime; yield return null; } transform.localPosition endPos; } }关键点有三个。第一移动的是localPosition而不是position。因为零件通常是其他父物体的子物体用本地坐标移动能保证方向始终相对于父物体不管父物体在场景里怎么旋转拆装方向都不会乱。第二outDirection要乘以transform.rotation否则当零件本身有旋转角度时Inspector里指定的up实际并不是世界坐标的up。把方向预先应用零件自身的旋转是避免拆飞方向跑偏的血泪经验。第三用SmoothStep而不是直接用Lerp的原始t值这样零件的启动和停止都有轻微缓动看起来像是有人在用手拆而不是被机关弹出去。调用时机如果想让所有零件按顺序依次拆出不能同时启动所有协程那样会一片全飞。做法是把所有零件统一交给一个拆装控制器控制器遍历List并按StartDelay排序逐个协程等待。5. 避坑与排查标注、拆装、渲染里最常见的5个翻车点5.1 场景标注文字被模型遮挡或显示在最前面现象点击模型零件生成标注后标注文字有时被模型挡住有时又隔着模型显示在最前面看起来像浮在屏幕上。原因世界空间Canvas的渲染顺序和模型叠加时ZTest判定出了问题。世界空间Canvas本质上是一个放在场景里的Quad默认的Canvas渲染mode会同模型做深度测试但Canvas上的UI元素自身没有正确逐字做深度测试会出现文字被模型盖住或者穿透模型的情况。解决与其纠结Canvas的深度测试不如直接把Canvas上的Canvas组件RenderMode设为World Space后在CanvasGroup组件里勾选“Ignore Parent Groups”之外的优化项更稳妥的做法是把标注预制体的Shader换成带ZTest的UI Shader或者在生成标注时把Canvas的缩放放到极小再拉大来强制刷新深度。我通常的做法是在标注组件的Start里调用一次transform.SetAsLastSibling()并在Canvas的sortingOrder上给标注层单独设一个值比如100让标注层永远显示在最上面。这个方案能解决90%的标注遮挡问题。5.2 场景拆装动画播放时零件互相穿模现象按顺序拆零件结果零件A经过零件B所在位置时直接穿过B视觉上极其尴尬。原因拆装路径只考虑了起点和终点没有考虑路径中段会不会与其他零件相交。另一个原因是拆装顺序不对——如果A被B卡住正常情况下应该先拆B再拆A但动画序列写成了先拆A。解决两层解法。第一检查拆装顺序每个零件拆出前判断它是否被别的零件“压住”被压住的零件必须先拆。这个检查可以在编辑器里用一个辅助脚本批量检测零件包围盒的重叠关系。第二调整路径把outDirection从直线拆出改为“先沿一个初始方向移动一小段距离再转向移动”。在DisassemblyPart里增加一个两段移动的参数先垂直拔出一段再水平移出。比如螺丝先沿Z轴拧出0.1米再向上移出0.5米全程不与其他零件相交。穿模不是玄学本质上是几何路径设计问题。5.3 场景导入的模型全黑或全灰金属表面没有反光现象模型放进场景后一片漆黑或者灰蒙蒙的像石膏怎么调灯光都没用。原因CAD导出的FBX模型自带材质大多是Standard Shader但法线方向错误导致光照计算全完——模型三角面的法线指向内部光照从外面打过来全部被背光面吸收。另一个原因是双面材质问题模型表面法线朝向混乱时有些面正常、有些面是黑的。解决在导入设置里勾选“Mark Meshes as Convex”之外检查法线——选中模型在导入模型的Model页签下勾选“Swap Tangents”或“Generate Lightmap UVs”往往无法修复法线问题。正确做法是在3ds Max或Blender里做一次“统一法线方向”把模型所有面的法线翻到外面。如果实在没有建模软件可以在Unity里给材质换成双面Shader。自定义Shader的Cull Mode设为Off就可以让渲染器忽略背面剔除模型就不再黑面了。但要注意双面Shader会让渲染量翻倍只建议临时救急用。5.4 场景点击UI按钮时背后的模型也被拾取标注乱飞现象界面上点“自动拆装”按钮结果模型上那个位置也生成了一个标注。原因射线拾取没有做UI过滤。事件系统接收到鼠标点击后射线依然穿透UI面板打到了后面的模型上。解决在射线拾取前先判断是否点到了UI。通用做法是用EventSystem判断using UnityEngine; using UnityEngine.EventSystems; public class AnnotationClicker : MonoBehaviour { // 被UI遮挡时禁止拾取模型 private bool IsPointerOverUI() { return EventSystem.current ! null EventSystem.current.IsPointerOverGameObject(); } void Update() { if (Input.GetMouseButtonDown(0) !IsPointerOverUI()) { // 执行拾取逻辑 } } }这个判断虽然简单但要注意一个边界如果UI上的Image组件关闭了Raycast TargetIsPointerOverGameObject就识别不到。所以UI组件上有可点击区域就必须保持Raycast Target勾选否则会把点击事件漏给模型。还有NGUI或旧版Unity UI项目里要用EventSystem.current.IsPointerOverGameObject(Input.touchCount 0 ? 0 : -1)来处理触摸事件。5.5 场景打包到安卓后标注文字发虚、看不清现象编辑器中标注文字锐利清晰打包到手机后模糊成一片。原因TextMeshPro默认字体在低DPI下没有做好动态字体处理或者根Canvas的缩放模式不对。解决给标注Canvas的RectTransform设置正确尺寸并在Canvas Scaler里把Dynamic Pixels Per Unit设为10以上具体值可以根据测试调整。TextMeshPro字体资源要开启“Auto Size”或者给一个下限值防止它在设备上被缩放过小。另外Texture的Generate Mip Maps要在字体纹理上开启否则文字远离相机时会出现闪烁或发虚。这个问题在三星和华为设备上出现频率明显偏高跟GPU的纹理过滤方式有关提前在模拟器里用多个分辨率测试比事后返工省心。6. 把演示做成产品标注数据序列化与自动拆装回放6.1 用ScriptableObject保存标注与拆装顺序换模型不必改代码放任标注脚本裸奔的工程过两天你会发现“点击生成标注”的玩法在客户现场翻车了因为临时生成的标注位置全部存在场景里换一台电脑场景被覆盖就全没了。正确做法是把标注数据和拆装顺序序列化成资源文件让策划或建模同事能直接在Inspector里配置。做法是定义一个ScriptableObject里面保存一个标注列表每个标注项包含零件名、本地坐标、说明文字、是否默认显示。保存成Asset文件后不管谁来改配置都不用动代码。6.2 自动拆装回放用协程定时播放标注联动显示结合前几节的DisassemblyPart做一个拆装控制器控制整个流程按顺序执行。控制器遍历所有零件按StartDelay排序逐个调用协程并等待完成。自动拆装演示时每拆出一个零件对应标注就在界面上浮出——把展示、标注、拆装三件事串成一个闭环。这里用协程作为调度器天然支持中途暂停、逐帧查看比Animator方案更可控。using UnityEngine; using System.Collections; using System.Collections.Generic; public class DisassemblyController : MonoBehaviour { public ListDisassemblyPart parts; // 在Inspector里按拆装顺序拖入 public float stepPause 0.5f; // 每拆完一个零件后的停留时间 public bool isPlaying { get; private set; } public IEnumerator PlaySequence() { if (isPlaying) yield break; isPlaying true; foreach (DisassemblyPart part in parts) { yield return StartCoroutine(part.Disassemble()); yield return new WaitForSeconds(stepPause); } isPlaying false; } }这段控制器的逻辑是按List顺序逐个拆装。每个零件通过自己的Disassemble方法完成移动后再进入下一个。stepPause控制节奏0.5秒比较适中客户演示时建议调到1秒留出讲解时间。如果你需要“全部同时动作”的效果比如多个零件同时拆出就把foreach里的yield return StartCoroutine改成直接StartCoroutine但要注意拆装过程中零件互相穿模的问题会更难控制。我的建议是给客户的演示版本统一用顺序播放工程内部调试可以用并行模式快速检查路径。6.3 交付前检查清单对照着跑一遍走过几轮项目后我形成了自己的验收清单建议提测前自己先过一遍检查项标准有问题时看哪一节模型导入缩放与朝向模型朝正前方尺寸与真实设备比例一致2.1轨道相机操作手感拖拽旋转无漂移缩放不过近不过远2.2标注位置准确性点击模型表面后标注物精确落在点击点附近3.1拆装路径不穿模慢速播放时零件之间无视觉穿插4.2、5.1UI点击与模型拾取互不干扰按钮点击不会触发模型标注5.3移动端字体清晰度安卓低分辨率机不糊不闪5.5标注数据序列化重启工程后标注位置与文字不丢失6.1另外一个强烈建议交付时在Editor里把工程内所有场景备份一份同时把“自动生成标注”的入口做成一个可开关的Debug按钮免得客户误点把展示界面弄乱。演示版本默认关闭自动拾取功能只保留自动拆装回放和预设标注现场效果更稳。做成产品不是功能越多越好而是该藏的藏好、该显的显出来。这是我的习惯也是踩了多次坑之后最想让你带走的经验。希望帮到你。本文还有配套的精品资源点击获取
返回列表