Unity3D Shader 坐标空间详解

Unity3D Shader 坐标空间详解

前言

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

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

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

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

什么是"空间"

所谓"空间",本质上就是:

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

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

c 复制代码
(0, 1, 0)

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

所以:

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

Unity Shader 中最常见的 4 个空间

1 对象空间(Object Space)

也叫:

  • 本地空间
  • 模型空间

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

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

c 复制代码
(0, 0.5, 0)

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

c 复制代码
float4 vertex : POSITION;

也就是说:

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

2 世界空间(World Space)

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

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

比如:

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

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

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

c 复制代码
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

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

它会把:

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

都计算进去。

3 观察空间(View Space)

观察空间可以理解为:

以摄像机为原点的空间。

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

它的意义是:

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

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

4 裁剪空间(Clip Space)

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

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

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

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

c 复制代码
o.pos = UnityObjectToClipPos(v.vertex);

这行代码等价于:

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

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

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

最常见的变换流程如下:

c 复制代码
对象空间
   ↓
世界空间
   ↓
观察空间
   ↓
裁剪空间
   ↓
屏幕空间

你可以把它理解成:

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

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

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

为什么一定要区分空间

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

c 复制代码
float4 pos = v.vertex;
float3 normal = UnityObjectToWorldNormal(v.normal);
pos.xyz += normal * 0.1;

这段代码的问题是:

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

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

这种写法在某些情况下可能"看起来还能跑",但本质上是错误的,

一旦模型旋转、缩放,就容易出问题。

正确做法应该是:

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

Unity 中常见的空间转换写法

对象空间 → 裁剪空间

c 复制代码
o.pos = UnityObjectToClipPos(v.vertex);

最常用。

对象空间 → 世界空间

c 复制代码
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

常用于:

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

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

c 复制代码
float3 worldNormal = UnityObjectToWorldNormal(v.normal);

常用于:

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

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

下面写一个简单 Shader:

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

这样你可以直观看到:

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

c 复制代码
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
        }
    }
}

代码解读

c 复制代码
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

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

然后在片段着色器里:

c 复制代码
float h = saturate(i.worldPos.y * 0.2);

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

也就是说:

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

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

那法线也有空间吗

有的。

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

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

最常见的是:

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

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

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

它不是以世界为参考,也不是以模型原点为参考,

而是以模型表面某一点的局部方向为参考:

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

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

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

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

常见错误总结

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

例如:

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

这是最常见的问题。

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

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

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

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

一个简单经验:

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

什么时候用哪种空间

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

总结

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

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

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

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

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

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

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

相关推荐
RReality1 天前
【Unity Shader URP】Matcap 材质捕捉实战教程
java·ui·unity·游戏引擎·图形渲染·材质
魔士于安1 天前
unity urp材质球大全
游戏·unity·游戏引擎·材质·贴图·模型
南無忘码至尊1 天前
Unity学习90天 - 第 6 天 -学习物理 Material + 重力与阻力并实现弹跳球和冰面滑动效果
学习·unity·游戏引擎
mxwin1 天前
Unity 单通道立体渲染(Single Pass Instanced)对 Shader 顶点布局的特殊要求
unity·游戏引擎·shader
魔士于安1 天前
unity 低多边形 无人小村 木质建筑 晾衣架 盆子手推车,桌子椅子,罐子,水井
游戏·unity·游戏引擎·贴图·模型
RReality1 天前
【Unity Shader URP】简易卡通着色(Simple Toon)实战教程
ui·unity·游戏引擎·图形渲染·材质
魔士于安2 天前
unity 骷髅人 连招 武器 刀光 扭曲空气
游戏·unity·游戏引擎·贴图·模型
瑞瑞小安2 天前
Unity功能篇:文本框随文字内容动态调整
ui·unity
南無忘码至尊2 天前
Unity学习90天-第7天-学习委托与事件(简化版)
学习·unity·游戏引擎
君莫愁。2 天前
【Unity】解决UGUI的Button无法点击/点击无反应的排查方案
unity·c#·游戏引擎·解决方案·ugui·按钮·button