坐标变换 坐标变换是图形学最基础的东西,这个搞不明白一切都是瞎搞,不自己老老实实走一遍看再多的代码,做再多的效果都是纸上谈兵。
从 vertex 到 frag 在顶点函数里面拿到的坐标就是局部坐标,如下,通过 “UnityObjectToWorld” 这个方法可以拿到世界坐标。
从模型到世界 逻辑意义,将所有模型的坐标都放在一个统一的坐标系中。
1 2 3 4 5 6 7 8 9 10 v2f vert (appdata v) { v2f o; ... float4 worldPosition = UnityObjectToWorld(v.vertex); //上面的方法等效下面的方法 //float4 worldPosition = mul(unity_ObjectToWorld, v.vertex); ... return o; }
从世界到视图 逻辑意义:让相机作为坐标的原点,并且固定相机的朝向,使得相机的 Y 轴朝上,看相 -Z 轴,X轴对准 X轴。
1 float4 viewPosition = mul(unity_MatrixV, worldPosition); // 从世界空间到视图空间
从视图到裁剪 逻辑意义:对坐标进行缩放,然后可以轻易判断出是否处于视锥体内,满足的条件如下:
1 2 3 -w<= x <= w -w<= y <= w -w<= z <= w
透视投影下的坐标此时经过变换的 W 的值为 -z,而正交投影下的坐标此时 W 的值仍然为 1。
Shader 中的变换代码如下。
1 o.vertex = mul(UNITY_MATRIX_P,viewPosition); // 从视图空间到裁剪空间
传给片元的坐标需要的是裁剪空间下的坐标,所以需要上面的转换,也就是我们常说的 MVP,不过 Unity 自带一个方法,将上面的方法全部做了
1 2 3 4 5 6 7 //一个函数就可以转为裁剪空间 o.vertex = UnityObjectToClipPos(v.vertex); //下面的三个方法等效于上面的方法,具体的矩阵可以在 “UnityShaderVariables.cginc” 文件里面找到。 //float4 worldPosition = mul(unity_ObjectToWorld, v.vertex); // 从模型空间到世界空间 //float4 viewPosition = mul(unity_MatrixV, worldPosition); // 从世界空间到视图空间 //o.vertex = mul(UNITY_MATRIX_P,viewPosition); // 从视图空间到裁剪空间
现在如果你追到 Unity 编辑器的安装路径找到 “UnityShaderVariables.cginc” 这个文件,会发现这个方法内部是这样的。
1 2 3 4 5 6 7 8 9 10 // Tranforms position from object to homogenous space inline float4 UnityObjectToClipPos(in float3 pos) { #if defined(STEREO_CUBEMAP_RENDER_ON) return UnityObjectToClipPosODS(pos); #else // More efficient than computing M*VP matrix product return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0))); #endif }
注意这个方法内部矩阵的计算顺序,如果你自己想做点什么不要搞反了。
从裁剪到屏幕
先进行透视除法,得到 NDC 坐标,此时无论是正交相机还是透视相机,坐标都会变换到一个相同的立方体内,且此时的xy坐标的区间是 [-1,1]。
对 NDC 坐标进行映射,得到统一的屏幕坐标。
映射公式如下:
$ \begin{array}{l} \text { screen }{x}=\frac{\text { clip } {x} \cdot \text { pixelWidth }}{2 \cdot \text { clip }_{w}}+\frac{\text { pixelWidth }}{2} \end{array}$
$ \begin{array}{l} \text { screen }{y}=\frac{\text { clip } {y} \cdot \text { pixelHeight }}{2 \cdot \text { clip }_{w}}+\frac{\text { pixelHeight }}{2} \end{array}$
NDC 坐标的 Z 值会被放入深度缓冲中,而 W 分量经过透视除法已经变成了1.
记录一下可能会用到的 Unity 已经提供的方法 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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 //世界坐标到裁剪坐标 inline float4 UnityWorldToClipPos( in float3 pos ) { return mul(UNITY_MATRIX_VP, float4(pos, 1.0)); } // 视图坐标到裁剪坐标 inline float4 UnityViewToClipPos( in float3 pos ) { return mul(UNITY_MATRIX_P, float4(pos, 1.0)); } // 模型坐标到视图坐标 inline float3 UnityObjectToViewPos( in float3 pos ) { return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz; } // 世界坐标到视图坐标 inline float3 UnityWorldToViewPos( in float3 pos ) { return mul(UNITY_MATRIX_V, float4(pos, 1.0)).xyz; } // 方向向量从模型到世界,并且归一化,通常用于计算光照 inline float3 UnityObjectToWorldDir( in float3 dir ) { return normalize(mul((float3x3)unity_ObjectToWorld, dir)); } // 方向向量从世界到模型,并且归一化,通常用于计算光照 inline float3 UnityWorldToObjectDir( in float3 dir ) { return normalize(mul((float3x3)unity_WorldToObject, dir)); } // 将一个法线从模型转换到世界 inline float3 UnityObjectToWorldNormal( in float3 norm ) { #ifdef UNITY_ASSUME_UNIFORM_SCALING return UnityObjectToWorldDir(norm); #else // mul(IT_M, norm) => mul(norm, I_M) => {dot(norm, I_M.col0), dot(norm, I_M.col1), dot(norm, I_M.col2)} return normalize(mul(norm, (float3x3)unity_WorldToObject)); #endif } // 计算当前世界坐标的视线变量 inline float3 UnityWorldSpaceViewDir( in float3 worldPos ) { return _WorldSpaceCameraPos.xyz - worldPos; } // 返回一个范围在[0,1]的线性深度值 inline float Linear01Depth( float z ) { return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y); } // 深度纹理的采样结果转换到视角空间下的深度值 inline float LinearEyeDepth( float z ) { return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w); }
记录一些重要的已经定义的变量 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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 _WorldSpaceCameraPos //主相机的世界坐标 UNITY_PI struct appdata_base { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct appdata_tan { float4 vertex : POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct appdata_full { float4 vertex : POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; float4 texcoord1 : TEXCOORD1; float4 texcoord2 : TEXCOORD2; float4 texcoord3 : TEXCOORD3; fixed4 color : COLOR; UNITY_VERTEX_INPUT_INSTANCE_ID }; // Time (t = time since current level load) values from Unity float4 _Time; // (t/20, t, t*2, t*3) float4 _SinTime; // sin(t/8), sin(t/4), sin(t/2), sin(t) float4 _CosTime; // cos(t/8), cos(t/4), cos(t/2), cos(t) float4 unity_DeltaTime; // dt, 1/dt, smoothdt, 1/smoothdt // x = 1 or -1 (-1 if projection is flipped) // y = near plane // z = far plane // w = 1/far plane float4 _ProjectionParams; // x = width // y = height // z = 1 + 1.0/width // w = 1 + 1.0/height float4 _ScreenParams; // Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt) // x = 1-far/near // y = far/near // z = x/far // w = y/far // or in case of a reversed depth buffer (UNITY_REVERSED_Z is 1) // x = -1+far/near // y = 1 // z = x/far // w = 1/far float4 _ZBufferParams; // x = orthographic camera's width // y = orthographic camera's height // z = unused // w = 1.0 if camera is ortho, 0.0 if perspective float4 unity_OrthoParams;