如何在不同的坐标空间之间进行坐标转换?
在 Unity 中,不同坐标空间的转换核心是 明确“源空间”和“目标空间”,再通过 Unity 封装的 API 实现映射(底层是矩阵运算,无需手动处理)。以下是 按“转换场景”分类的完整方案,含 API 用法、示例和关键注意点,覆盖所有常用坐标空间交互:
一、先理清:核心坐标空间关系
所有转换都围绕 4 大核心空间展开,关系如下:
局部空间 ↔ 世界空间 ↔ 相机空间 ↔ 屏幕空间
↓
UI 空间(RectTransform)
- 「局部↔世界」:父子物体层级相关,用
Transform类 API; - 「世界↔相机」:3D 物体相对相机位置,用
Camera类 API; - 「世界↔屏幕」:3D 与鼠标/UI 交互,用
Camera类 API; - 「屏幕↔UI」:UI 定位/鼠标判断,用
RectTransformUtility工具类; - 向量转换(无原点):用
Transform专属向量 API(忽略位置,仅处理旋转/缩放)。
二、分场景实现:坐标空间转换(含代码+用法)
场景 1:局部空间 ↔ 世界空间(父子物体/自定义点)
核心是将“相对于某个物体 pivot 的局部坐标”与“全局唯一的世界坐标”互转,需用 Transform 类 API。
| 转换方向 | 核心 API | 作用 | 注意事项 |
|---|---|---|---|
| 局部 → 世界 | Transform.TransformPoint(Vector3 localPos) |
把“相对于当前物体的局部点”转世界坐标 | 会应用当前物体的位置、旋转、缩放 |
| 世界 → 局部 | Transform.InverseTransformPoint(Vector3 worldPos) |
把“世界点”转“相对于当前物体的局部点” | 同上,需注意物体 pivot 位置 |
示例 1:子物体自定义局部点转世界坐标
父物体 Parent 位置 (2,0,0)、旋转 90°,子物体相对于父物体的“自定义局部点” (1,0,0),转世界坐标:
Transform parent = GameObject.Find("Parent").transform;
Vector3 localCustomPos = new Vector3(1, 0, 0); // 父物体局部空间的点
Vector3 worldPos = parent.TransformPoint(localCustomPos);
// 结果:父物体位置 + 旋转后的局部点(因父旋转 90°,最终世界坐标可能是 (2,0,1))
Debug.Log("自定义局部点的世界坐标:" + worldPos);
示例 2:世界坐标转父物体局部坐标
已知世界坐标 (5,0,3),转成相对于 Parent 的局部坐标:
Transform parent = GameObject.Find("Parent").transform;
Vector3 worldPos = new Vector3(5, 0, 3);
Vector3 localPos = parent.InverseTransformPoint(worldPos);
Debug.Log("相对于父物体的局部坐标:" + localPos);
简化用法(无需 API):
- 直接获取物体自身的局部坐标:
transform.localPosition(相对于父物体); - 直接获取物体的世界坐标:
transform.position; - 仅当需要转换“非自身 pivot 的自定义点”时,才用
TransformPoint/InverseTransformPoint。
场景 2:世界空间 ↔ 相机空间(3D 物体相对相机)
相机空间以相机为原点,Z 轴指向世界 -Z 方向(Unity 相机默认“看向自身 -Z 轴”),用于判断物体相对相机的位置(如前方/后方)。
| 转换方向 | 核心 API | 作用 | 关键特性 |
|---|---|---|---|
| 世界 → 相机 | Camera.WorldToCameraPoint(Vector3 worldPos) |
世界点转“相对于相机的坐标” | Z 值 = 物体到相机的距离(正值=前方) |
| 相机 → 世界 | Camera.CameraToWorldPoint(Vector3 cameraPos) |
相机局部点转世界坐标 | 必须指定 Z 值(正值=相机前方,否则无效) |
示例 1:判断物体是否在相机前方
Camera mainCam = Camera.main;
Transform target = GameObject.Find("Target").transform;
Vector3 cameraSpacePos = mainCam.WorldToCameraPoint(target.position);
if (cameraSpacePos.z > 0)
{
Debug.Log("物体在相机前方(可见)");
}
else
{
Debug.Log("物体在相机后方(不可见)");
}
示例 2:在相机前方 10 米处生成物体
Camera mainCam = Camera.main;
// 相机空间坐标:原点(相机位置)+ Z=10(前方 10 米)
Vector3 cameraLocalPos = new Vector3(0, 1, 10); // Y 轴偏移 1 米(避免贴地)
Vector3 worldPos = mainCam.CameraToWorldPoint(cameraLocalPos);
Instantiate(Resources.Load("Cube"), worldPos, Quaternion.identity);
场景 3:世界空间 ↔ 屏幕空间(3D ↔ 鼠标/UI 交互)
屏幕空间是像素坐标系(左下角 (0,0),右上角 (Screen.width, Screen.height)),核心用于“3D 物体转屏幕位置”(如血条跟随)或“鼠标位置转世界位置”(如点击生成)。
| 转换方向 | 核心 API | 作用 | 关键注意 |
|---|---|---|---|
| 世界 → 屏幕 | Camera.WorldToScreenPoint(Vector3 worldPos) |
3D 世界点转 2D 屏幕像素坐标 | Z 值 = 物体到相机的距离;视锥外的点会超出屏幕范围 |
| 屏幕 → 世界 | Camera.ScreenToWorldPoint(Vector3 screenPos) |
屏幕像素点转 3D 世界坐标 | 必须指定 screenPos.z(物体到相机的距离),否则 Z=0 无效 |
示例 1:3D 物体上方显示 UI 血条(世界→屏幕)
public RectTransform bloodBar; // UI 血条(Canvas 设为 Screen Space - Overlay)
public Transform target; // 3D 目标物体
private Camera mainCam;
void Start() => mainCam = Camera.main;
void Update()
{
// 1. 世界坐标转屏幕坐标(物体上方 2 米,避免遮挡)
Vector3 screenPos = mainCam.WorldToScreenPoint(target.position + new Vector3(0, 2, 0));
// 2. 屏幕坐标转 UI 局部坐标(适配 Canvas 空间)
RectTransformUtility.ScreenPointToLocalPointInRectangle(
bloodBar.parent as RectTransform, // 父节点(Canvas)
screenPos,
null, // Overlay 模式设为 null
out Vector2 uiLocalPos
);
// 3. 设置血条位置
bloodBar.localPosition = new Vector3(uiLocalPos.x, uiLocalPos.y, 0);
}
示例 2:鼠标点击地面生成物体(屏幕→世界)
Camera mainCam = Camera.main;
void Update()
{
if (Input.GetMouseButtonDown(0))
{
// 1. 构建屏幕坐标:鼠标位置 + Z=10(相机前方 10 米,确保在视锥内)
Vector3 screenPos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 10);
// 2. 屏幕坐标转世界坐标
Vector3 worldPos = mainCam.ScreenToWorldPoint(screenPos);
// 3. 修正 Y 轴为地面高度(假设地面 Y=0)
worldPos.y = 0;
// 4. 生成物体
Instantiate(Resources.Load("Cube"), worldPos, Quaternion.identity);
}
}
更精准方案:用射线检测替代手动 Z 值设置(避免地面高度不确定):
if (Input.GetMouseButtonDown(0) && Physics.Raycast(mainCam.ScreenPointToRay(Input.mousePosition), out RaycastHit hit)) { Instantiate(Resources.Load("Cube"), hit.point, Quaternion.identity); // hit.point 是地面碰撞点(世界坐标) }
场景 4:屏幕空间 ↔ UI 空间(RectTransform 专属)
UI 组件(Image、Button 等)的坐标是「RectTransform 局部空间」,与屏幕空间的转换需用 RectTransformUtility(因 Canvas 模式不同,参考系不同)。
| 转换方向 | 核心 API | 作用 | 关键参数 |
|---|---|---|---|
| 屏幕 → UI | RectTransformUtility.ScreenPointToLocalPointInRectangle(父 RectTransform, 屏幕坐标, 相机, out 局部坐标) |
屏幕像素点转 UI 局部坐标 | 相机:Overlay 模式设为 null;Camera 模式传 Canvas 相机 |
| UI → 屏幕 | RectTransformUtility.LocalPointToScreenPointInRectangle(父 RectTransform, UI 局部坐标, 相机, out 屏幕坐标) |
UI 局部点转屏幕像素坐标 | 同上 |
示例:判断鼠标是否在 UI 按钮内(屏幕→UI)
public RectTransform buttonRect; // 按钮的 RectTransform
private Camera uiCam; // Canvas 对应的相机(Camera 模式用)
void Start()
{
Canvas canvas = buttonRect.GetComponentInParent<Canvas>();
uiCam = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
}
void Update()
{
// 屏幕坐标(鼠标)转 UI 局部坐标
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
buttonRect, // 目标 UI 的 RectTransform
Input.mousePosition,
uiCam,
out Vector2 uiLocalPos
))
{
// 判断局部坐标是否在按钮 Rect 范围内
if (buttonRect.rect.Contains(uiLocalPos))
{
Debug.Log("鼠标在按钮内");
}
}
}
场景 5:向量转换(非位置点,仅方向/大小)
上述转换均针对「位置点」(有原点参考),若需转换「向量」(如速度、前进方向、法线),需用专门的 API(忽略位置,仅处理旋转和缩放)。
| 转换方向 | 向量 API(方向/大小) | 点 API(位置) | 区别 |
|---|---|---|---|
| 局部 → 世界 | Transform.TransformDirection(Vector3 localDir) |
Transform.TransformPoint |
向量 API 忽略物体位置,仅应用旋转 |
| 局部 → 世界 | Transform.TransformVector(Vector3 localVec) |
- | 应用旋转+缩放(适合有大小的向量,如速度) |
| 世界 → 局部 | Transform.InverseTransformDirection(Vector3 worldDir) |
Transform.InverseTransformPoint |
忽略位置,仅反向旋转 |
| 世界 → 局部 | Transform.InverseTransformVector(Vector3 worldVec) |
- | 反向旋转+反向缩放 |
示例:子物体局部前进方向转世界方向
Transform child = GameObject.Find("Child").transform;
Vector3 localForward = Vector3.forward; // 局部 Z 轴(前进方向)
// 转换为世界方向(忽略缩放,仅旋转)
Vector3 worldForward = child.TransformDirection(localForward);
Debug.Log("子物体世界前进方向:" + worldForward);
// 若需保留缩放(如速度向量),用 TransformVector
Vector3 localSpeed = new Vector3(0, 0, 5); // 局部速度 5m/s
Vector3 worldSpeed = child.TransformVector(localSpeed);
三、关键注意事项(避坑核心)
- Z 值不能省略:
ScreenToWorldPoint和CameraToWorldPoint必须指定 Z 值(物体到相机的距离),否则 Z=0 会转换到相机内部,结果无效。
- Canvas 模式影响 UI 转换:
- Overlay 模式:UI 直接覆盖屏幕,
RectTransformUtility的camera参数设为null; - Camera 模式:UI 通过相机渲染,必须传入 Canvas 的
worldCamera,否则坐标偏移。
- Overlay 模式:UI 直接覆盖屏幕,
- Pivot 中心点的影响:
物体的局部坐标参考系是自身pivot(而非模型中心),若模型中心与 pivot 不一致(如 pivot 在底部),转换时需注意点的位置。 - 视锥范围判断:
WorldToScreenPoint转换视锥外的物体时,屏幕坐标可能超出屏幕范围(x<0、x>Screen.width 等),需先判断有效性:Vector3 screenPos = mainCam.WorldToScreenPoint(target.position); if (screenPos.x >= 0 && screenPos.x <= Screen.width && screenPos.y >= 0 && screenPos.y <= Screen.height && screenPos.z > 0) { // 物体在屏幕内 } - 缩放继承问题:
子物体的局部坐标会继承父物体的缩放,TransformPoint会自动应用缩放;若需忽略缩放,可用TransformDirection(向量)或临时重置父物体缩放。
四、总结:转换流程速查
- 明确「源空间」和「目标空间」(如“世界→屏幕”“屏幕→UI”);
- 根据空间类型选择 API :
- 局部↔世界:
Transform.TransformPoint/InverseTransformPoint; - 世界↔相机:
Camera.WorldToCameraPoint/CameraToWorldPoint; - 世界↔屏幕:
Camera.WorldToScreenPoint/ScreenToWorldPoint; - 屏幕↔UI:
RectTransformUtility; - 向量转换:
Transform.TransformDirection/InverseTransformDirection;
- 局部↔世界:
- 检查关键参数(如 Z 值、相机、Canvas 模式),避免无效转换。
按这个逻辑,所有坐标空间转换场景都能覆盖,且无需关注底层矩阵运算,直接用 Unity 封装的 API 即可高效实现。