Unity3D Shader 坐标空间详解

Unity3D Shader 坐标空间详解。

Unity3D Shader 坐标空间详解

前言

学 Shader 的过程中,你一定会反复看到这些词:

  • 对象空间(Object Space)
  • 世界空间(World Space)
  • 观察空间(View Space)
  • 裁剪空间(Clip Space)

很多初学者一开始最容易卡住的地方就是:

这些“空间”到底是什么?为什么同一个顶点要换来换去?

什么是“空间”

所谓“空间”,本质上就是:

同一个点,在不同参考坐标系下的表示方式。

比如一个模型顶点,在模型自身看来,它的位置可能是:

1
(0, 1, 0)

但如果把模型摆到场景里,这个点在世界中的位置就变成了另外一个值。

所以:

  • 点没变
  • 只是参考系变了

Unity Shader 中最常见的 4 个空间

1 对象空间(Object Space)

也叫:

  • 本地空间
  • 模型空间

它是 以模型自身为原点 的坐标系。

例如一个 Cube 的中心在模型原点,那么它顶部某个点可能是:

1
(0, 0.5, 0)

在顶点着色器里,最开始拿到的 v.vertex,通常就是对象空间坐标。

1
float4 vertex : POSITION;

也就是说:

Mesh 里的顶点数据,默认就是对象空间。

2 世界空间(World Space)

世界空间是 整个场景统一使用的坐标系

不管场景里有多少模型,只要都转到世界空间,就能放在同一个参考系里比较。

比如:

  • 模型 A 在 (0,0,0)
  • 模型 B 在 (10,0,0)
  • 摄像机在 (0,5,-10)

这些位置,都是世界空间下的描述。

对象空间转世界空间,常见写法:

1
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

这里的 unity_ObjectToWorld 是 Unity 提供的模型矩阵。

它会把:

  • 模型的位移
  • 旋转
  • 缩放

都计算进去。

3 观察空间(View Space)

观察空间可以理解为:

以摄像机为原点的空间。

把世界中的所有点,都换算成“站在摄像机视角下看”的坐标。

它的意义是:

  • 更方便做某些和摄像机相关的计算
  • 用于后续投影流程

在日常入门 Shader 中,手动使用观察空间的机会没有世界空间那么多,但它是渲染流程里必经的一步。

4 裁剪空间(Clip Space)

裁剪空间是顶点着色器最终必须输出的空间。

因为 GPU 后续要根据这个空间的数据去做:

  • 视锥裁剪
  • 透视除法
  • 光栅化

在 Unity 中,最常见的写法是:

1
o.pos = UnityObjectToClipPos(v.vertex);

这行代码等价于:

对象空间 → 世界空间 → 观察空间 → 裁剪空间

也就是说,它帮你把整个变换链都做了。

顶点是如何一步步走到屏幕上的

最常见的变换流程如下:

1
2
3
4
5
6
7
8
9
对象空间

世界空间

观察空间

裁剪空间

屏幕空间

你可以把它理解成:

  1. 顶点先在模型自己的局部坐标里定义
  2. 再放到场景中
  3. 再转换到摄像机视角
  4. 最后投影到屏幕上

所以 Shader 中很多“位置错误”的问题,本质上都是:

拿了 A 空间的数据,却和 B 空间的数据混着算。

为什么一定要区分空间

看一个最经典的错误示例:

1
2
3
float4 pos = v.vertex;
float3 normal = UnityObjectToWorldNormal(v.normal);
pos.xyz += normal * 0.1;

这段代码的问题是:

  • pos对象空间
  • normal世界空间

把两个不同空间的数据直接相加了。

这种写法在某些情况下可能“看起来还能跑”,但本质上是错误的,
一旦模型旋转、缩放,就容易出问题。

正确做法应该是:

  • 要么位置和法线都在对象空间算
  • 要么位置和法线都先转到世界空间再算

Unity 中常见的空间转换写法

对象空间 → 裁剪空间

1
o.pos = UnityObjectToClipPos(v.vertex);

最常用。

对象空间 → 世界空间

1
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

常用于:

  • 世界空间光照
  • 世界空间特效
  • 和摄像机位置做比较

对象空间法线 → 世界空间法线

1
float3 worldNormal = UnityObjectToWorldNormal(v.normal);

常用于:

  • 光照计算
  • Fresnel
  • 描边方向

一个最简单的坐标空间可视化示例

下面写一个简单 Shader:

  • 顶点正常显示
  • 片段颜色根据 世界空间高度 变化

这样你可以直观看到:

同一个 Shader,在场景不同高度的位置,会显示不同颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Shader "Custom/WorldPosColor"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
};

struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
};

v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target
{
float h = saturate(i.worldPos.y * 0.2);
return fixed4(h, h, 1, 1);
}
ENDCG
}
}
}

代码解读

1
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

这一步把顶点从对象空间转成世界空间。

然后在片段着色器里:

1
float h = saturate(i.worldPos.y * 0.2);

根据世界坐标的 y 值控制颜色。

也就是说:

  • 放得越高,颜色越亮
  • 放得越低,颜色越暗

这就是世界空间参与计算的一个简单例子。

那法线也有空间吗

有的。

法线本质上也是方向向量,所以它同样可能处于:

  • 对象空间
  • 世界空间
  • 切线空间

最常见的是:

  • Mesh 自带法线:通常是 对象空间法线
  • 光照计算常用:世界空间法线
  • 法线贴图常用:切线空间法线

进阶:什么是切线空间(Tangent Space)

如果你后面学习法线贴图,一定会遇到切线空间。

它不是以世界为参考,也不是以模型原点为参考,
而是以模型表面某一点的局部方向为参考:

  • Tangent(切线)
  • Bitangent(副切线)
  • Normal(法线)

这三个方向构成一个局部坐标系。

法线贴图里存的“蓝紫色法线”,大多数就是切线空间法线。

这部分先有个概念就够了,后面讲法线贴图时再深入。

常见错误总结

1 把不同空间的数据直接混算

例如:

  • 对象空间顶点 + 世界空间法线
  • 世界空间位置 和 观察空间方向做点乘

这是最常见的问题。

2 误以为 UnityObjectToClipPos 得到的是屏幕坐标

它得到的是 裁剪空间坐标,不是最终屏幕像素坐标。

后面 GPU 还要继续做投影和光栅化。

3 不知道什么时候该用对象空间,什么时候该用世界空间

一个简单经验:

  • 模型内部形变:常用对象空间
  • 和场景、灯光、摄像机相关的计算:常用世界空间

什么时候用哪种空间

需求 常用空间
顶点位移、模型内部动画 对象空间
与场景位置相关的效果 世界空间
与摄像机相关的计算 观察空间 / 世界空间
顶点最终输出 裁剪空间
法线贴图 切线空间

总结

可以先记住一句最核心的话:

空间不是“多出来的数据”,而是“同一份数据所处的参考系”。

在 Unity Shader 中,最重要的几个空间是:

  • 对象空间:模型自己的局部坐标
  • 世界空间:场景统一坐标
  • 观察空间:摄像机视角坐标
  • 裁剪空间:顶点着色器最终输出

只要后面写 Shader 时始终注意:

参与同一次计算的数据,必须尽量处于同一个空间。

那么很多 Shader 问题都会一下子清晰很多。


Unity3D Shader 坐标空间详解
http://weikunou.github.io/2026/03/29/unity-shader-coordinate-space/
作者
Awake
发布于
2026年3月29日
许可协议