☰
Unity 3D模型展示实战:轨道相机、3D标注与拆装动画全解析
2026/10/17 21:15:11 网站建设 项目流程

简介:围绕Unity 3D模型交互展示的完整工程资源,面向Unity开发者、三维可视化从业者及数字孪生/工业仿真学习者,重点解决模型导入、场景布置、交互注释与装配过程演示等问题。资源涵盖静态/动态模型展示、光照系统搭建、3D标注UI与Shader特效、目标环绕相机控制、步骤列表引导,以及基于Mecanim和物理引擎的拆装动画实现,从模型优化到交互逻辑完整打通。压缩包共4300个文件,大小约65.71MB,主体为735个C#脚本和1137个meta元数据,并包含280个PNG贴图、43个材质、32个PSD源文件、26个asset资产、4个unity场景、4个prefab预设,以及Shader、FBX模型和DLL库等,可直接作为Unity工程打开或对照学习。已有3754人学习浏览,这套资源既可直接运行完整Unity工程示例,也提供了可复用的C#交互脚本、标注UI与相机控制逻辑,以及分层目录结构和Prefab组织方式,便于开发者拆解学习或快速迁移到自己的展示项目。无论是产品展示、教学演示还是工业拆装培训,都能从中获得从零构建3D交互演示的完整思路。

1. Unity 3D模型展示:从“能转圈”到“能讲参数”

做 Unity 3D模型展示 的人都会先遇到同一道坎:模型转起来了、放大了、缩放了,客户看了一眼却说“感觉还不如一张 PPT”。真正让产品模型“会讲事情”的,是叠加在查看器上面的三层能力——轨道相机用来给用户完整观察的自由度,3D 标注把视线引向具体参数和安装位置,拆装动画把装配关系变成一段可以反复回放的过程。这套展示资源正好覆盖这三块,而且不是“高配查看器”那种半成品,是一个能直接嵌进展厅触摸屏、售前演示页和培训课件的功能组合包。下面我按落地顺序把三条主线分别拆开,参数、实现、坑位都放在对应章节。

2. 模型查看器基础:轨道相机、缩放与包围盒自适应

2.1 轨道相机组件:旋转轴约定与阻尼参数

做模型展示的第一步是把相机手感调对。工业模型和游戏角色不一样,玩家喜欢的是“角色背后跟随”,展厅里看机械件的人要的是 CAD 式操控:左键旋转、右键平移、滚轮缩放。这个约定在大部分模型展示资源里是默认配置,我一般也这样使用。

下面这段是轨道相机的核心逻辑,点一下模型中心旋转,滚轮控制距离:

public class OrbitCamera : MonoBehaviour { public Transform target; // 模型中心点 public float distance = 8f; // 相机到中心的距离 public float minDistance = 2f; public float maxDistance = 30f; public float rotSpeed = 6f; // 旋转速度 public float zoomSpeed = 4f; public float damping = 0.3f; // 阻尼系数 private float yaw = 0f; private float pitch = 30f; void Update() { if (Input.GetMouseButton(0)) { yaw += Input.GetAxis("Mouse X") * rotSpeed; pitch -= Input.GetAxis("Mouse Y") * rotSpeed; pitch = Mathf.Clamp(pitch, -90f, 90f); } distance -= Input.GetAxis("Mouse ScrollWheel") * zoomSpeed; distance = Mathf.Clamp(distance, minDistance, maxDistance); Quaternion q = Quaternion.Euler(pitch, yaw, 0f); Vector3 dir = q * Vector3.back * distance; transform.position = Vector3.Lerp(transform.position, target.position + dir, damping); transform.LookAt(target); } }

这里的逻辑不复杂,但有两个参数值得说明。pitch不能放开到 90 度上下,一旦俯仰角越过垂直位置,相机的 up 向量会翻转,模型立刻变成“倒着看”,这是轨道相机最常见的眩晕来源。damping这个 0.3 是插值系数,直接决定手感是“跟手”还是“有惯性”,展厅演示建议 0.3 到 0.5,大屏交互太灵敏,反而会显得不稳定。

旋转速度也需要注意不同屏幕尺寸的差异。如果这套展示资源要同时跑在 4K 触摸屏和普通笔记本上,rotSpeed建议按屏幕 DPI 做一次缩放,而不是直接用固定值。

2.2 平移、重置与正交/透视切换

旋转之后就是平移。平移的关键不只是把相机挪开,而是要“跟着模型走”。按下中键或右键时,相机移动方向要按当前视角计算,否则会出现模型往屏幕外漂的违和感。代码里通常这样处理:

void HandlePan() { if (Input.GetMouseButton(1) || Input.GetMouseButton(2)) { float dx = Input.GetAxis("Mouse X") * panSpeed * distance / 10f; float dy = Input.GetAxis("Mouse Y") * panSpeed * distance / 10f; target.position -= transform.right * dx + transform.up * dy; } }

注意distance / 10f这个比例。如果模型缩得很小、相机距离拉近,还按固定像素移动,那么手指只动一点点模型就会跑出屏幕。把平移量与当前相机距离关联起来,是工业模型查看器里比较成熟的做法,远距离轨道就平移得快,近景就玩“慢速微调”。

正交和透视的切换也要处理。很多展示场景喜欢把透视切到正交,用来对比产品尺寸比例。Unity 里切正交很简单,但有个容易遗漏的参数:

public void SetOrthographic(bool isOrtho) { Camera cam = GetComponent<Camera>(); cam.orthographic = isOrtho; if (isOrtho) cam.orthographicSize = distance * 0.5f; }

orthographicSize必须根据当前距离重新算一遍。透视模式下你控制的是 camera 到目标和目标的距离,正交模式下这个距离会失效,用的是视锥半高。如果只切isOrtho不重新赋值,画面基本就是黑屏或者看到一片裁切。这样的提示可以理解为“切换投影方式等于重新对焦”。

2.3 包围盒重算中心:大模型与小零件的兼容

用商店模型和原厂图纸模型有个差异:有的模型根节点离几何体十万八千里,有的零件原点在装配体外侧。直接拿导入的根节点当相机 target,转起来会明显“偏心”,这是资源在真实项目中经常遇到的问题。因此展示资源的相机初始化建议写包围盒逻辑:

Bounds CalcModelBounds(GameObject root) { Renderer[] rends = root.GetComponentsInChildren<Renderer>(); if (rends == null || rends.Length == 0) return new Bounds(); Bounds bounds = rends[0].bounds; for (int i = 1; i < rends.Length; i++) { bounds.Encapsulate(rends[i].bounds); } return bounds; }

用包围盒中心覆盖原始 target 位置,再把近远裁剪面按包围盒尺寸缩放。对一万面以下的小部件,相机近裁面设 0.1、远裁面设 100 没问题;对大型装配体,不做自适应就会出现零件在近端被裁剪、远端又因为深度精度闪烁的问题。下面这组参数是我作为一个常见起点:

参数推荐值说明
nearClipPlane包围盒半径 / 200防止超大模型深度冲突
farClipPlane包围盒半径 * 20避免远处零件被裁剪
camera distance包围盒半径 * 2.2初始视角能包住全模型
orthoSize当前距离 * 0.5切换投影时的同步值

3. 3D 标注:锚点、射线拾取与标签跟随

3.1 锚点方案:子物体挂载与动态拾取

3D 标注的根基不是“画一条线”,而是“找到一个稳定的挂点”。挂点选错,标签会跟着零件来回晃。常规做法有两种:一种是在建模软件里预先留空物体当锚点,好处是美术可以手动调整位置,坏处是每个模型都要重新摆一遍;另一种是在运行时通过射线点击模型表面动态创建锚点,这套展示资源里常见的是后者兼有前者。

动态锚点的本质是把点击位置转成模型子物体下的局部坐标:

private GameObject CreateAnchor(RaycastHit hit) { GameObject anchor = new GameObject("Anchor_" + hit.collider.name); anchor.transform.SetParent(hit.collider.transform); anchor.transform.position = hit.point; anchor.transform.rotation = Quaternion.LookRotation(hit.normal, Vector3.up); return anchor; }

SetParent会把锚点锁在零件上,零件动、锚点跟着动。LookRotation则把标签的正面方向对齐到表面法线,这样标签不会斜着插进模型里。选择局部挂载而不是世界坐标里记录一个点,原因很简单:后续做拆装动画时零件会移走,如果锚点还在原地,标注就和零件分离了。

3.2 射线拾取与层过滤:不要让标注打到错误部件

动态标注最烦的是误触。模型上的辅助几何、透明护壳、内部看不到的线管都会被射线打到,标出来就是无效标注。我习惯给需要标注的部件单独设一层,射线只认这一层:

int targetMask = LayerMask.GetMask("MarkAnchor"); Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f, targetMask)) { GameObject anchor = CreateAnchor(hit); SpawnLabel(anchor); }

用LayerMask.GetMask避开字符串比较,性能更好。100f是检测距离,对模型展示来说足够了,如果模型特别大可以调到 1000。层过滤的关键作用是把“能标注的面”和“不能标注的面”分开,配合高亮材质就能做出交互感:鼠标悬停到可标注部件就亮一下,按下去弹出标签。

这一步在使用时容易出问题的点是:模型导入时,默认往往只有 Default 层,需要自己把部件批量放到 MarkAnchor 层。如果嫌手动麻烦,写一个按名称前缀或材质自动分层的工具,几行代码,能省不少时间。

3.3 标签跟随镜头:屏幕空间 Canvas 与层级穿模

3D 标注有两种显示路线:一种是在世界坐标里放 TextMesh,跟随相机做 Billboard;另一种是把标签放在屏幕空间 Canvas,每帧把 3D 锚点坐标转成屏幕坐标写入 UI。大多数项目最终用的是后者,原因是文字清晰、缩放不模糊、UI 风格统一。

屏幕空间跟随的核心是坐标换算:

void UpdateLabelPosition(Transform anchor, RectTransform rect, CanvasScaler scaler) { Vector3 screenPos = Camera.main.WorldToScreenPoint(anchor.position); // 锚点在相机背后时不显示标签 if (screenPos.z < 0f) { rect.gameObject.SetActive(false); return; } rect.gameObject.SetActive(true); // 将屏幕像素坐标换算到 Canvas 坐标 float baseScale = scaler.referenceResolution.x / Screen.width; rect.anchoredPosition = new Vector2( (screenPos.x - Screen.width * 0.5f) * baseScale, (screenPos.y - Screen.height * 0.5f) * baseScale); }

省略掉screenPos.z < 0这个判断,标签就会在相机转过去时跑到屏幕另一边,看起来像在“穿模”。另外 Canvas Scaler 的模式需要设成 Scale With Screen Size,否则在不同分辨率屏上标签位置会整体偏移。

遮挡问题也是这里的高频坑。屏幕空间 Canvas 默认渲染在所有 3D 物体上,所以标签不会穿模,但它也不做遮挡隐藏。如果你想做“被挡住就隐藏标签”的效果,就得用 WorldToScreenPoint 后做一次深度比较,拿锚点的深度和 Canvas 上对应像素的深度比,这属于进阶优化。就展示资源而言,先保证跟随正确比纠结遮挡更重要。

4. 拆装动画:路径序列、拖拽热区与材质处理

4.1 拆装序列脚本:使用 offset 矢量而不是绝对坐标

拆装动画最容易理解的方式是“每个零件沿一个方向移动一段距离”。有人喜欢直接记录每个零件的目标绝对坐标,这在单个模型上没问题,模型一换全部要重摆。好用的做法是记录 offset 偏移量和时长,这样换个模型只需要重新指定零件和方向:

[System.Serializable] public class ExplodeStep { public Transform part; // 被移动的零件 public Vector3 offset; // 相对当前位置的偏移 public float duration = 0.5f; // 播放时长 public AnimationCurve curve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); }

播放序列时用协程逐步执行:

IEnumerator MoveStep(ExplodeStep step) { Vector3 startLocal = step.part.localPosition; Vector3 endLocal = startLocal + step.offset; float t = 0f; while (t < 1f) { t += Time.deltaTime / step.duration; t = Mathf.Clamp01(t); step.part.localPosition = Vector3.Lerp(startLocal, endLocal, step.curve.Evaluate(t)); yield return null; } step.part.localPosition = endLocal; }

需要注意这里用的是localPosition,不是position。因为拆装中的零件大概率是某个装配体的子物体,父节点自带旋转和位移,用世界坐标计算容易偏移甚至飞向远处。offset的方向要跟着模型的自然坐标轴走,比如向右拆就写new Vector3(0.2f, 0, 0),向屏幕外拆就得看模型自身朝向,这点在不同的 CAD 导出模型中差异很大。

曲线参数用 EaseInOut,会让零件起步慢、中间快、结尾再减速,观感比线性滑动自然很多。如果想让拆装过程更有“机械感”,可以在曲线中间加一段平台期,模拟卡扣脱出前的微小停顿。

4.2 拖拽拆某个零件:平面投影与回位判断

动画播放是“自动拆”,培训场景则常常需要“亲手拖”。把某个零件从装配体里拉出来,松手后判断是装回去还是放下。拖拽的实现不能直接用鼠标坐标改物体位置,那样物体只会跟着屏幕横向移动,没有空间层次。常规做法是在一个经过零件的虚拟平面上做射线求交:

Plane dragPlane = new Plane(Camera.main.transform.forward, selected.position); Vector3 GetDragPoint() { Ray dragRay = Camera.main.ScreenPointToRay(Input.mousePosition); if (dragPlane.Raycast(dragRay, out float enter)) return dragRay.GetPoint(enter); return selected.position; }

这个平面以相机朝向为法线,所以零件基本贴着当前视角移动,视觉上不会出现“零件瞬间飘到相机后面”的离奇效果。松手后判断是否回位:

float threshold = 0.02f; // 约 2 厘米,根据模型比例调整 if (Vector3.Distance(selected.position, originalPosition) < threshold) { selected.position = originalPosition; } else { // 保持在当前拖拽位置,等待下一次操作 }

如果做严格培训流程,可以不给这个“停在半空”的选项,离得远就直接飞回原点,相当于一个“后悔药”。两种模式取决于项目需要:展览型展示建议留停在原位,方便讲解人员摆弄;培训考核型建议强制回位,避免场景越玩越乱。

4.3 拆装期间半透明与碰撞体处理

大批量拆装会出现一个视觉问题:外层的零件还没拆完,夹在中间的待拆件被挡得严严实实。解决思路是让已经完成拆出动作的零件变为半透明,给视线留一条“看进去”的通道。

改材质颜色可以直接操作material.color,但会给每个零件生成新的材质实例,做大量拆装步骤时会带来较大内存开销。更推荐用MaterialPropertyBlock,只改实例属性不复制材质:

void SetPartTransparent(Transform part) { Renderer[] rends = part.GetComponentsInChildren<Renderer>(); foreach (Renderer rend in rends) { MaterialPropertyBlock block = new MaterialPropertyBlock(); rend.GetPropertyBlock(block); block.SetFloat("_Opacity", 0.35f); rend.SetPropertyBlock(block); rend.shadowCastingMode = ShadowCastingMode.Off; } }

对应的 Shader 里要有一个_Opacity属性,且渲染队列被切到 Transparent,不然数值写成平滑但没有实际效果。这算一个隐藏得很深的坑,只写脚本不调 Shader 会让人以为“半透明失效了”。

同一个原则也适配其他碰撞问题:半透明的零件要去掉或关闭 Collider,否则射线捡取时它还会拦截,标注和拖拽会点到看不见的零件。拆装进行时,可以用一个状态枚举把自动播放和玩家拖拽隔离开,防止动画播放到一半时用户又拖走正在移动的零件。

5. 排查避坑:坐标漂移、标签闪烁与拆装穿模的高频坑

5.1 现象:旋转着模型逐渐“跑偏”,中心最终离开机身

表现:轨道相机盯住目标中心,一开始正常,转半圈后相机中心往某个方向偏移,最后模型跑到屏幕一侧。

原因:初始化时直接把模型的根节点当作相机 target,而根节点位置与几何中心严重不匹配;或者模型根节点上存在负缩放,导致 Bounds 计算出来的中心被带偏。

解决:相机的 target 不绑定模型根节点,单独挂一个空物体,用GetComponentsInChildren<Renderer>()的 Bounds 中心去更新它。特别注意检查导入设置里 Scale Factor 和 Root Transform 是否正常,优先在导入阶段把模型校准到单位坐标系,不要在场景里给模型根节点挂随意的旋转和缩放。

5.2 现象:标注标签在屏幕边缘抖动、换 DPI 后错位

表现:标签位置不稳定,转视角时像“粘在屏幕某处”而不是粘在零件上;在 2K 屏上调试好后,换 1080P 或笔记本高分屏就整体偏移。

原因:直接用 WorldToScreenPoint 返回的像素坐标塞给 Canvas,忽略了 CanvasScaler 的缩放系数。不同 DPI 下 Canvas 的 reference resolution 和实际像素不一致,像素坐标不能直接等价于 UI 坐标。

解决:把 CanvasScaler 换算系数加进去,参考前面代码里的baseScale换算。如果 Canvas 选用 Screen Space - Camera 模式,需要把制图调整到 Canvas 根节点坐标。实测后建议每个目标分辨率都跑一遍跟随测试,标签的偏差在边缘位置最容易暴露。

5.3 现象:拆装播放到一半,零件“跳”到起点或穿入内部

表现:拆装序列前几个步骤正常,到中途某一步零件突然位置错乱,再往后所有步骤都偏移。

原因:典型是坐标空间混用。起点用position存了世界坐标,目标位置却用localPosition计算;或者零件本身挂在一个会旋转的子装配体下,父节点动了,局部坐标的参考系跟着变。

解决:在拆装开始前把每个步骤的起始localPosition和目标localPosition全部缓存到数组里,协程运行期间不重新采样“当前 transform 位置”。这样即使中途其他逻辑动了父节点,序列内部也不会因为取样错误而跳变。

5.4 现象:点拆装按钮那一下明显卡顿,帧率瞬间掉到个位数

表现:平时转动模型都流畅,只要按下“拆解”按钮,第一个零件就开始卡,后面反而恢复。

原因:拆装开始时集中创建了大量临时对象。比如每次拆步骤都在 Update 里通过GetComponentInChildren搜 Renderer,或者每帧 new 了一个列表装数据,GC 压力把主线程卡住。

解决:运行前把所有需要操作的 Renderer 和 Transform 缓存成数组,避免运行时查找。拆装曲线的采样结果复用单个 Vector3 临时变量,不在循环里反复申请新对象。标签生成和销毁用对象池,关闭一个标签是把 GameObject 放进池而不是直接 Destroy。

5.5 现象:拖拽零件后松手,零件不回到指定位置,甚至撞向相机

表现:拖拽过程中零件还能跟随,松手后就飞出去或卡到相机附近,回位判断像是失效。

原因:回位阈值和坐标空间不一致。originalPosition记录的是起始位置,但零件经过父节点旋转后,起始位置已经被 ‘transform 改掉’;或者阈值用的是世界坐标的 0.02,但模型整体尺寸超过 10 米,这个阈值永远达不到。

解决:直接把originalPosition在拆装序列开始前用localPosition固定存好,回位判断时也统一用局部坐标比较。阈值建议根据包围盒大小动态换算,比如bounds.extents.magnitude * 0.005f,而不是写死一个量。

6. 进阶验证:交付前把三件事做扎实

资源在本地看着顺眼,不代表换个设备就能直接上。我的交付习惯是固定跑一遍“场景检查单”,把模型展示、标注、拆装三块按表格逐项过:

检查项操作方式期望结果
初始视角重置视角后完整看到模型主体,不裁切
极限缩放滚轮拉到最近和最远不穿模、不黑屏
标注跟随旋转相机 360 度标签始终贴住锚点、不闪烁
拆装回放连续播放两次第二次与第一次轨迹一致
拖拽回位拖出零件后松手在阈值内自动吸回,超阈值停在原处

针对拆装动画,我还习惯在 Editor 环境里把播放速度临时调到 0.25 倍,逐帧检查零件是否在运动过程中与其他件发生肉眼可见的穿插。穿模在自动播放时可以通过高速运动掩盖,但截图和录屏回放会暴露得很彻底。如果穿插是在特定时间点出现,优先检查该零件的 offset 方向是否沿装配自然轴向,而不是单纯放大 offset 去“躲”交叉。

最后提一个容易被忽略的硬件适配点:大屏展示通常用 4K 分辨率跑到 30 帧就行,但鼠标交互产生的 DPI 差异会让缩放速度忽快忽慢。资源里如果给相机速度加了 DPI 自适应,拿 1080P 调试后务必在目标分辨率上再验证一次;如果没加,就确保滚轮缩放的zoomSpeed只和 distance 相关,别和屏幕尺寸相关。

我把这套流程当成项目交付的“兜底习惯”之后,翻车率确实低了一大截。有一次模拟项目上线前临时换了批模型,旧的拆装 offset 方向全部失效,就是因为没有提前统一模型坐标轴。从那以后我每次入库新模型都强制走一遍包围盒校准、标注锚点抽查、拆装方向验证,三个步骤跑完才敢交付。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询