1. 项目概述为什么是矩阵在Unity开发中实现物体跟随是一个高频需求。无论是让摄像机平滑跟随主角让武器吸附在角色手上还是让UI图标追踪世界空间中的某个目标我们都在处理“跟随”这件事。新手开发者最直觉的做法可能是每帧直接修改跟随物体的Transform.position比如follower.position target.position offset。这种方法简单直接在大多数情况下也能工作。但当你需要处理更复杂的跟随关系时比如跟随一个同时进行旋转、缩放的父物体或者需要将跟随行为与本地坐标系、世界坐标系转换结合起来时直接操作位置就会显得力不从心代码会变得冗长且容易出错。这时矩阵Matrix就登场了。它不是一个高深莫测的数学怪物而是Unity中描述物体位置、旋转、缩放的最底层、最统一的数据结构。Transform组件那些方便的position、rotation和localScale属性其底层计算都依赖于一个4x4的变换矩阵。所以当我们谈论“用矩阵实现物体跟随”时本质上是在学习如何绕过Transform的部分高层接口直接操作其底层数据。这能带来几个核心优势一次性计算一个矩阵乘法Matrix4x4.MultiplyPoint就能同时完成位置、旋转、缩放的复合变换效率更高代码更简洁。处理复杂层级关系可以轻松计算物体在任意坐标系下的变换例如“将物体A放置到物体B的局部坐标系下的某个特定位置和朝向”。理解引擎原理这是深入理解3D图形学和Unity引擎运作机制的重要一步。当你理解了矩阵你对Transform、Camera.worldToCameraMatrix、渲染管线中的MVP矩阵等概念都会有豁然开朗的感觉。这个项目就是带你从“会用Transform”升级到“懂其原理并能用矩阵更优雅地解决复杂跟随问题”。我们将从零开始拆解矩阵在跟随中的应用并提供可直接“抄作业”的代码方案。2. 核心原理矩阵如何描述物体变换在深入代码之前我们必须先建立对变换矩阵的直观理解。在Unity中一个Matrix4x4矩阵通常用来表示一个从局部坐标系到世界坐标系的变换。2.1 4x4变换矩阵的结构一个标准的4x4变换矩阵可以看作由四个部分构成| m00 m01 m02 m03 | // 前三列构成3x3旋转/缩放矩阵 (M) | m10 m11 m12 m13 | // 它决定了新坐标系的X, Y, Z轴方向 | m20 m21 m22 m23 | // 以及物体沿这些轴的缩放 | m30 m31 m32 m33 | // 最后一行通常是 (0, 0, 0, 1)用于齐次坐标更具体地我们可以这样解读前三行三列m00-m22这是一个3x3的矩阵它包含了旋转和缩放信息。这个矩阵的三个列向量分别对应变换后新坐标系的X轴、Y轴、Z轴在世界坐标系下的方向向量单位向量乘以缩放系数。第四列的前三个元素m03, m13, m23这是一个3D向量它表示位移Translation即物体的原点在世界坐标系下的位置。第四行m30, m31, m32, m33在用于仿射变换平移、旋转、缩放的矩阵中这一行固定为(0, 0, 0, 1)。它的存在是为了让矩阵乘法能够统一处理平移变换这是3x3矩阵做不到的这就是齐次坐标的妙用。注意Unity的Matrix4x4类采用列优先存储。但在C#代码中我们通过m[row, column]或mRC属性访问时索引是直观的。例如位移向量是new Vector3(m03, m13, m23)。2.2 从Transform到Matrix4x4Unity的Transform组件为我们封装了矩阵操作。我们可以轻松地在两者间转换获取世界变换矩阵Matrix4x4 worldMatrix target.transform.localToWorldMatrix;这个矩阵描述了“如何将target局部空间中的一个点变换到世界空间”。获取局部变换矩阵Matrix4x4 localMatrix target.transform.worldToLocalMatrix;这个矩阵是上面那个的逆矩阵描述了“如何将世界空间中的一个点变换到target的局部空间”。为什么理解这个很重要因为“跟随”的本质就是为跟随者计算一个新的localToWorldMatrix。这个新矩阵应该基于目标物体的变换矩阵并可能附加一些我们自定义的偏移比如摄像机在目标后方5米高2米的位置。2.3 矩阵乘法的几何意义这是核心中的核心。矩阵乘法对应着变换的叠加。 假设我们有一个点P_local它在物体A的局部坐标系中。首先我们用A的局部到世界的矩阵M_A乘以P_local得到它在世界坐标的位置P_world。P_world M_A * P_local然后如果我们想把这个点放到物体B的局部坐标系下去看就用B的世界到局部的矩阵M_B_inv即B.worldToLocalMatrix去乘P_world。P_in_B_local M_B_inv * P_world M_B_inv * (M_A * P_local)根据矩阵乘法的结合律这等价于(M_B_inv * M_A) * P_local。这个M_B_inv * M_A就是一个从A局部空间到B局部空间的变换矩阵。在跟随场景中的应用我们希望跟随者Follower以某种相对关系“长在”目标Target上。那么我们可以先定义跟随者在目标局部空间中的期望位置和朝向一个变换矩阵offsetMatrixLocal。然后将目标的局部到世界矩阵target.localToWorldMatrix与这个局部偏移矩阵相乘。得到的结果矩阵就是跟随者在世界空间中应该具有的最终变换矩阵。用公式表示就是followerWorldMatrix target.localToWorldMatrix * offsetMatrixLocal这个公式是理解所有矩阵跟随方案的钥匙。接下来的所有实操都是对这个公式的具体实现和变种。3. 基础实操实现一个简单的矩阵跟随让我们从一个最简单的例子开始让一个立方体Follower始终固定在另一个移动旋转的球体Target的正上方2个单位处并且跟随者的底面始终平行于目标的上表面即跟随者的Y轴与世界Y轴对齐但位置随目标移动。3.1 场景与脚本设置在Unity场景中创建两个物体一个Sphere命名为“Target”一个Cube命名为“Follower”。为Target添加一个简单的移动旋转脚本方便我们观察跟随效果。例如using UnityEngine; public class TargetMover : MonoBehaviour { public float moveSpeed 5f; public float rotateSpeed 90f; // 度/秒 void Update() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 move new Vector3(h, 0, v) * moveSpeed * Time.deltaTime; transform.Translate(move, Space.World); if (Input.GetKey(KeyCode.Q)) transform.Rotate(Vector3.up, -rotateSpeed * Time.deltaTime); if (Input.GetKey(KeyCode.E)) transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime); } }为Follower创建一个新的C#脚本命名为MatrixFollowerBasic。3.2 核心跟随逻辑实现在MatrixFollowerBasic脚本中我们将实现基于矩阵的跟随。using UnityEngine; public class MatrixFollowerBasic : MonoBehaviour { [Header(跟随目标)] public Transform target; // 拖拽Target物体到此 [Header(局部空间偏移)] public Vector3 localPositionOffset new Vector3(0, 2, 0); // 在目标局部空间Y轴正方向2个单位 public Vector3 localRotationOffset Vector3.zero; // 欧拉角偏移 void Update() { if (target null) return; // 1. 构建一个表示“期望局部位置和旋转”的变换矩阵 // 这个矩阵描述的是在目标物体的局部坐标系下跟随者应该在哪里是什么朝向。 Matrix4x4 offsetMatrix Matrix4x4.TRS( localPositionOffset, Quaternion.Euler(localRotationOffset), Vector3.one // 缩放保持为1不继承目标的缩放 ); // 2. 获取目标物体从局部到世界的变换矩阵 Matrix4x4 targetWorldMatrix target.localToWorldMatrix; // 3. 核心公式跟随者的世界矩阵 目标的世界矩阵 * 局部偏移矩阵 // 这意味着先将偏移应用到目标的局部空间再随着目标一起变换到世界空间。 Matrix4x4 followerWorldMatrix targetWorldMatrix * offsetMatrix; // 4. 将计算出的世界矩阵分解并应用到跟随者的Transform上 // 注意这里我们只提取位置和旋转缩放保持独立Vector3.one // 你也可以选择提取缩放实现完全的父子级仿射变换。 transform.position followerWorldMatrix.GetPosition(); transform.rotation followerWorldMatrix.rotation; // transform.localScale Vector3.one; // 明确设置缩放不继承 } }3.3 代码逐行解析与注意事项Matrix4x4.TRS(Vector3 pos, Quaternion q, Vector3 s)这是一个极其重要的静态方法。它根据给定的位置平移、旋转四元数和缩放缩放向量构建一个对应的变换矩阵。T是Translation平移R是Rotation旋转S是Scale缩放。我们用它来构建offsetMatrix。localPositionOffset这个偏移量是在目标Target的局部坐标系下定义的。(0, 2, 0)意味着“在Target自身坐标系中正上方2米处”。无论Target如何旋转这个“上方”始终是Target的Y轴正方向。乘法顺序targetWorldMatrix * offsetMatrix这是关键在Unity中矩阵乘法顺序是从右向左应用变换。所以先应用offsetMatrix将点从“跟随者的局部空间”变换到“目标的局部空间”。再应用targetWorldMatrix将上一步的结果从“目标的局部空间”变换到“世界空间”。 这个顺序符合我们的思维“在目标本地某个地方放个东西然后随着目标一起动”。GetPosition()和.rotationMatrix4x4提供了这两个便捷的属性来从矩阵中提取出位置向量和旋转四元数。这比手动从矩阵元素中解析要安全可靠得多。关于缩放在上面的例子中我们刻意忽略了缩放设置Vector3.one。这是因为如果offsetMatrix的缩放是(1,1,1)并且我们也不从结果矩阵提取缩放那么跟随者将保持自身的原始缩放。如果你希望跟随者完全像子物体一样继承目标的缩放你可以构建offsetMatrix时缩放设为Vector3.one然后从followerWorldMatrix中提取缩放transform.localScale followerWorldMatrix.lossyScale;。但要注意lossyScale是全局缩放可能不是我们想要的。更精确的做法是计算相对缩放target.lossyScale乘以一个自定义的缩放系数。这取决于具体需求。运行效果将脚本挂到Follower上把Target拖拽赋值。运行后无论Target如何移动、旋转Cube都会稳稳地固定在Sphere正上方2米处并且Cube的底面始终与Sphere的“上表面”即Target局部坐标的XZ平面平行。4. 进阶应用实现坐标系无关的复杂跟随基础跟随解决了固定局部偏移的问题。但在实际项目中我们常常需要更灵活的跟随逻辑。例如摄像机跟随摄像机应该在目标的后方一定距离并且看向目标。这个“后方”是相对于目标的朝向但摄像机自身的“向上”方向通常需要锁定为世界Y轴避免画面倾斜。武器挂点武器需要精确地吸附在角色手部的骨骼节点上并且方向要与手掌完全一致。UI世界标签一个UI元素需要始终悬浮在3D世界中的某个物体上方并且要保证其始终面向摄像机Billboard。这些需求都可以通过组合不同的矩阵操作来实现。下面我们以实现一个第三人称摄像机跟随为例。4.1 需求分析与矩阵拆解我们希望摄像机始终位于玩家角色的后方例如(0, 2, -5)在玩家局部空间。镜头始终看向玩家角色的位置或一个瞄准点。摄像机的“向上”方向始终是世界Y轴避免当角色在斜坡上时镜头跟着歪斜。这比简单的固定偏移复杂因为“看向”这个行为涉及到旋转的计算而“锁定向上轴”意味着我们不能直接使用目标矩阵的旋转部分。4.2 实现方案分离位置与旋转计算我们创建一个新的脚本MatrixCameraFollower。using UnityEngine; public class MatrixCameraFollower : MonoBehaviour { public Transform target; // 玩家角色 public Vector3 localOffsetFromTarget new Vector3(0, 2, -5); // 摄像机在目标局部空间中的偏移 public Transform lookAtTarget; // 可以是一个空物体作为摄像机看向的点。如果为空则看向target.position public float followSmoothTime 0.1f; // 位置平滑时间 public float rotationSmoothTime 0.05f; // 旋转平滑时间 private Vector3 _currentVelocity; private float _currentAngularVelocity; // 用于平滑阻尼 void LateUpdate() { if (target null) return; // --- 1. 计算期望的世界空间位置 --- // 使用基础跟随中的矩阵乘法计算期望位置 Matrix4x4 offsetMatrix Matrix4x4.TRS( localOffsetFromTarget, Quaternion.identity, // 位置偏移不需要旋转部分 Vector3.one ); Matrix4x4 targetWorldMatrix target.localToWorldMatrix; Matrix4x4 desiredPosMatrix targetWorldMatrix * offsetMatrix; Vector3 desiredWorldPosition desiredPosMatrix.GetPosition(); // 对位置进行平滑阻尼可选但很重要 Vector3 smoothedPosition Vector3.SmoothDamp(transform.position, desiredWorldPosition, ref _currentVelocity, followSmoothTime); // --- 2. 计算期望的旋转看向目标并锁定向上轴 --- Vector3 lookAtPoint (lookAtTarget ! null) ? lookAtTarget.position : target.position; // 计算从摄像机位置指向目标点的方向向量 Vector3 directionToTarget lookAtPoint - smoothedPosition; // 使用 Quaternion.LookRotation 创建旋转。第二个参数指定“向上”方向这里固定为世界Y轴。 Quaternion desiredRotation Quaternion.LookRotation(directionToTarget, Vector3.up); // 对旋转进行平滑使用四元数的球面插值Slerp或角度平滑 float smoothRot Mathf.SmoothDampAngle(Quaternion.Angle(transform.rotation, desiredRotation), 0, ref _currentAngularVelocity, rotationSmoothTime); Quaternion smoothedRotation Quaternion.Slerp(transform.rotation, desiredRotation, smoothRot); // --- 3. 应用最终的变换 --- // 注意这里我们没有使用一个统一的矩阵而是分别设置了位置和旋转。 // 因为我们的旋转计算是独立于目标旋转的锁定了World Up。 transform.position smoothedPosition; transform.rotation smoothedRotation; } }4.3 方案解析与避坑指南这个方案巧妙地将问题分解了位置计算依然使用target.localToWorldMatrix * offsetMatrix这个核心公式。这确保了摄像机的位置偏移后方、上方是严格基于目标的局部坐标系的。当目标转向时“后方”这个方向会自然改变。旋转计算完全独立于位置计算。我们使用Quaternion.LookRotation来让摄像机看向目标点。关键参数是Vector3.up它强制摄像机的向上向量与世界Y轴对齐解决了画面倾斜问题。平滑处理直接应用计算出的位置和旋转会非常生硬。我们使用Vector3.SmoothDamp和Quaternion.Slerp结合Mathf.SmoothDampAngle来引入平滑的过渡这是专业摄像机控制的基础。常见问题与排查摄像机抖动或旋转异常检查更新顺序摄像机跟随逻辑务必放在LateUpdate中执行。LateUpdate在所有Update函数之后执行确保我们使用的是目标物体在当前帧最终的位置和旋转。检查平滑参数followSmoothTime和rotationSmoothTime过小会导致响应过快而抖动过大则会有严重延迟。通常从0.1f和0.05f开始调整。检查LookAt目标确保lookAtPoint的计算是稳定的。如果lookAtTarget本身每帧在抖动摄像机也会抖。摄像机穿墙或卡进地面矩阵跟随本身不处理碰撞。这是一个渲染/逻辑问题需要额外的物理检测模块。通常的解决方案是从目标点向摄像机期望位置发射射线检测如果中间有障碍物则将摄像机位置拉近到碰撞点前方。这超出了纯矩阵变换的范畴需要结合Physics.Raycast来实现。缩放导致的奇怪现象如果你的目标物体有缩放非1localToWorldMatrix会包含缩放信息。在我们计算位置时localOffsetFromTarget会被这个缩放影响。例如目标X轴缩放为2那么localOffsetFromTarget.x的实际效果也会被放大2倍。解决方案如果不希望偏移受缩放影响在构建offsetMatrix时可以尝试使用目标缩放的反数来修正偏移量或者更简单的方法直接使用target.TransformPoint(localOffsetFromTarget)来计算世界坐标位置这个函数内部会处理缩放。但用矩阵理解TransformPoint本质上就是执行了localToWorldMatrix.MultiplyPoint。5. 性能优化与底层API深入当我们理解了原理后可以关注一些性能和实践细节。5.1 矩阵操作的性能考量缓存矩阵transform.localToWorldMatrix是一个属性每次访问时如果物体的变换自上次访问后发生了改变Unity会重新计算这个矩阵。虽然计算很快但在极高频率的循环中如每帧对上百个物体进行矩阵运算频繁访问可能成为瓶颈。优化如果一帧内需要多次使用同一个目标的变换矩阵应该先将其存储在一个局部变量中。void Update() { Matrix4x4 targetMatrix target.localToWorldMatrix; // 缓存 for(int i 0; i followers.Length; i) { // 对每个follower使用同一个targetMatrix进行计算 followers[i].worldMatrix targetMatrix * offsetMatrices[i]; } }矩阵乘法 vs 直接赋值对于简单的、无缩放的子物体跟随直接使用Transform的SetParent并设置localPosition和localRotation性能通常优于手动矩阵计算因为Unity内部会做优化。矩阵方法的优势在于灵活性和对底层原理的控制适用于非父子关系的复杂变换、批量处理或Shader等图形管线编程。5.2 使用 Matrix4x4 的 MultiplyPoint 方法在我们之前的例子中我们构建了整个偏移矩阵然后进行全矩阵乘法最后提取位置。如果只需要计算位置偏移而不需要旋转有一个更高效的方法Matrix4x4.MultiplyPoint。// 更高效的位置计算仅平移无旋转缩放影响 Vector3 desiredWorldPosition target.localToWorldMatrix.MultiplyPoint(localOffsetFromTarget); // 等价于target.TransformPoint(localOffsetFromTarget);MultiplyPoint方法内部会正确处理齐次坐标的第四分量w1进行完整的仿射变换。如果偏移量是方向向量如法线则应使用MultiplyVector它会忽略平移分量只应用旋转和缩放。5.3 处理非均匀缩放与矩阵分解陷阱从变换矩阵中完美地、唯一地分解出平移、旋转和缩放尤其是非均匀缩放是一个数学上复杂的问题有时甚至无解。Unity的Matrix4x4的.lossyScale属性提供的是一个“全局缩放近似值”在存在旋转和非均匀缩放的层级关系中它可能不是精确的局部缩放。实操心得在游戏逻辑中尽量避免直接操作和依赖从复杂层级矩阵中分解出的缩放值。如果必须处理缩放最好的做法是在设计上规避尽量让需要复杂变换的物体处于缩放为 (1,1,1) 的根节点下通过子物体来实现视觉上的缩放。显式传递如果需要继承缩放定义一个明确的缩放系数变量而不是从矩阵中提取。理解lossyScale的局限它适用于渲染和碰撞体缩放等需要最终世界比例的场合但不适合用于推导局部的、可逆的缩放变换。6. 实战扩展在Shader与图形管线中的应用矩阵跟随的概念不仅限于C#脚本逻辑层它在Shader编写和图形渲染管线中无处不在理解这一点能让你真正打通任督二脉。6.1 Shader中的矩阵传递在Unity Shader中模型从本地坐标到屏幕坐标的变换就是通过一系列矩阵乘法完成的UNITY_MATRIX_M (Model Matrix)等同于transform.localToWorldMatrix将顶点从模型局部空间变换到世界空间。UNITY_MATRIX_V (View Matrix)摄像机矩阵将顶点从世界空间变换到摄像机观察空间。UNITY_MATRIX_P (Projection Matrix)投影矩阵将顶点从观察空间变换到齐次裁剪空间。在顶点着色器中你通常会看到v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); // 内部就是 M * V * P // ... return o; }UnityObjectToClipPos这个内置函数本质上就是帮你完成了mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)))这个矩阵乘法链。6.2 在Shader中实现自定义物体空间跟随假设你想让一片树叶的飘动不仅基于自身顶点还受到一个“控制点”世界位置的影响。你可以在Shader中这样计算Shader Custom/WorldOffsetShader { Properties { ... } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; ... }; struct v2f { float4 pos : SV_POSITION; ... }; float4 _WorldFollowPoint; // 通过脚本传递的世界坐标点 float _FollowStrength; v2f vert (appdata v) { v2f o; // 1. 将模型顶点变换到世界空间 float4 worldPos mul(unity_ObjectToWorld, v.vertex); // 2. 计算世界空间下的偏移方向从物体原点指向目标点 float3 worldOffset _WorldFollowPoint.xyz - mul(unity_ObjectToWorld, float4(0,0,0,1)).xyz; // 3. 应用偏移可受强度参数和顶点原始位置影响 worldPos.xyz worldOffset * _FollowStrength * v.vertex.y; // 例如根据顶点Y坐标影响强度 // 4. 将偏移后的世界坐标变换回裁剪空间 o.pos mul(UNITY_MATRIX_VP, worldPos); return o; } ENDCG } } }在这个Shader中我们手动进行了类似“跟随”的计算获取每个顶点的世界坐标根据一个世界空间的目标点计算偏移然后再进行视图和投影变换。这完全跳过了C#脚本每帧修改顶点数据的开销性能更高。6.3 脚本与Shader的矩阵协同通常我们需要在C#脚本中计算好矩阵然后传递给Shader。例如实现一个总是面向摄像器的广告牌Billboard效果可以在脚本中计算旋转矩阵// C# Script void Update() { // 计算一个让Quad面向摄像机的旋转绕世界Y轴 Vector3 dirToCamera Camera.main.transform.position - transform.position; dirToCamera.y 0; // 锁定Y轴只在水平面旋转 Quaternion lookRot Quaternion.LookRotation(dirToCamera); // 将旋转矩阵传递给Shader Matrix4x4 rotationMatrix Matrix4x4.TRS(Vector3.zero, lookRot, Vector3.one); material.SetMatrix(_RotationMatrix, rotationMatrix); }然后在Shader的顶点着色器中将本地顶点坐标乘以这个旋转矩阵再乘以MVP矩阵就能实现CPU控制的广告牌效果。这种方式比在Shader中用UNITY_MATRIX_V计算更灵活因为你可以加入C#逻辑的平滑、条件判断等。7. 总结与核心心法通过这一系列的拆解我们从最简单的固定偏移跟随到复杂的摄像机控制再到Shader层面的应用完整地遍历了“使用矩阵实现物体跟随”这一主题。其核心心法可以归纳为以下几点理解本质“跟随”就是为跟随者计算一个正确的世界变换矩阵。这个矩阵通常是目标物体的世界变换矩阵与你定义的局部偏移矩阵的乘积。分清空间永远要清楚你定义的偏移位置、方向是在哪个坐标系下局部、世界。localToWorldMatrix是将局部坐标转为世界坐标的桥梁。善用工具Matrix4x4.TRS()用于构建矩阵.GetPosition()和.rotation用于分解矩阵MultiplyPoint用于高效计算点变换。分解问题像摄像机跟随那样将复杂需求位置跟随旋转看向锁定向上轴分解成独立的子问题用矩阵算位置用LookRotation算旋转分别解决后再组合思路会更清晰。知其所以然明白Transform的底层是矩阵明白Shader渲染的顶点变换也是矩阵连乘。这能让你在遇到任何空间变换问题时都能找到最根本的解决路径而不是盲目地尝试各种Transform的API。矩阵是3D编程的基石。掌握它你就获得了一把打开更高级图形编程和引擎原理大门的钥匙。起初可能会觉得抽象但多动手写几个例子在场景中直观地观察变换结果这种思维方式很快就会内化成你的本能。下次再遇到棘手的跟随、吸附、坐标转换问题时不妨先在心里构建一下那个关键的矩阵乘法公式FinalMatrix ParentWorldMatrix * LocalOffsetMatrix很多问题都会迎刃而解。