- 将方向向量由模型空间变换到世界空间
float3 UnityObjectToWorldDir(float3 dir)
// 相当于
float3 normalize(mul((float3x3)unity_ObjectToWorld, dir));
变换方向只需要乘 float3x3 矩阵,这是因为方向向量的 w 坐标为 0 [x,y,z,0] 而顶点的 w 为 1 [x,y,z,1],变换方向如果与 float4x4 矩阵相乘,矩阵中第四列平移量将不会对变换起影响。
- 将顶点由模型空间变换到世界空间
// 与变换方向相比,变换顶点就需要 float4x4 矩阵
float3 mul(unity_ObjectToWorld, v.vertex).xyz;
- 将顶点由模型空间变换到观察空间
// 只返回顶点的 xyz 坐标,且 z 是负值(视野空间为右手系)
float3 UnityObjectToViewPos(float3/float4 pos)
- 将法向量由模型变换到世界空间
float3 UnityObjectToWorldNormal(v.normal)
// 内部实现细节
#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)}
// 解析上行注释:右乘逆转置矩阵 => 左乘逆矩阵 => 分别点乘逆矩阵的列(点乘列相当于点乘转置后的行)
// 根据上面的等式,则可使用左乘变换方向矩阵的逆矩阵来得到结果(I_M 等同 unity_WorldToObject)
return normalize(mul(norm, (float3x3)unity_WorldToObject));
#endif
如果模型经过非统一缩放如 Scale(1,2,1),如果使用变换顶点相同的矩阵变换法线,则变换后的法线无法保持原垂直性。根据矩阵的运算,左乘的意义是相当于右乘该矩阵的转置矩阵。
- 将法向量由模型变换到观察空间
// 根据上面法线变换到世界空间可知,非统一缩放时,需要右乘逆转置矩阵
// Unity 直接提供了这个逆转置矩阵 UNITY_MATRIX_IT_MV
float3 mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
- 世界空间下的视线方向(顶点朝向摄像机的方向)
float3 UnityWorldSpaceViewDir(float3 worldPos)
// 相当于(注意结果未 normalize)
return _WorldSpaceCameraPos.xyz - worldPos;
// 一般使用方式:
float3 normalize(UnityWorldSpaceViewDir(worldPos))
- 模型空间到切线空间的变换矩阵
// 内置宏,输出 rotation 矩阵,变量名固定为 rotation
TANGENT_SPACE_ROTATION
// 实现细节
// v.tangent.w 决定副切线的方向
float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal ) // 这是行向量矩阵
切线空间到模型空间的矩阵是由切线、副切线、法线的顺序按列排列即可得到,再根据正交矩阵的性质,仅存在旋转和平移的矩阵的逆矩阵等于它的转置矩阵。因此把切线、副切线、法线按行排列则可得到这个逆矩阵。
- 切线空间到世界空间的变换矩阵
// 获取顶点法线、切线、副切线在世界空间下的表示
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
// 这三项相当于子空间坐标轴在父空间下的表示,直接按列构造矩阵则得到切线空间到世界空间的变换矩阵
o.TtoW0 = float3(worldTangent.x, worldBinormal.x, worldNormal.x); // 矩阵第一行
o.TtoW1 = float3(worldTangent.y, worldBinormal.y, worldNormal.y); // 矩阵第二行
o.TtoW2 = float3(worldTangent.z, worldBinormal.z, worldNormal.z); // 矩阵第三行
使用多个变量构建矩阵,可自由的按列向量或行向量排列。使用 float3x3 变量是按行向量排列的矩阵。
// 举例:使用该矩阵将切线空间法线贴图储存的向量变换到世界空间下
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.BumpUv));
fixed3 normal = normalize(half3(dot(i.TtoW0, bump), dot(i.TtoW1, bump), dot(i.TtoW2, bump)));
按照矩阵乘法的定义,正好是由 bump 点乘 列矩阵 的每一行来获得相应的分量
- 获取顶点对应的屏幕空间采样坐标
// 目的是,得到 [0,1] 之间可采样屏幕纹理的坐标
// 实际上,函数本身只将 xy 变换到 [0,w] (zw不变),需要在片元函数中进行透视除法得到最终的 [0,1]
// 其中 z 是观察空间的 z 经过缩放平移(投影矩阵变换)后的非线性值
// 而 w 是观察空间的深度值(取正),即 view space's -z
// 注意:接收的参数是 clip space position
float4 ComputeScreenPos (float4 clipPos)
// for sampling a GrabPass texure (对采样坐标进行了跨平台处理)
float4 ComputeGrabScreenPos (float4 clipPos)
为什么不在函数中直接进行透视除法?因为从顶点函数到片元函数经过的插值过程将导致结果不正确。
- 采样法线贴图并获取正确的法线信息
fixed3 UnpackNormal(fixed4 packednormal) // packednormal = tex2D(_BumpMap, i.BumpUv)
// 实现细节:
#if defined(UNITY_NO_DXT5nm) // 如果纹理未压缩,则直接还原到 [-1,1] 之间
return packednormal.xyz * 2 - 1;
#else // 否则(经过压缩),通过 xy 分量计算出 z 分量
return UnpackNormalmapRGorAG(packednormal);
// UnpackNormalmapRGorAG 函数的实现细节
packednormal.x *= packednormal.w; // This do the trick
fixed3 normal;
normal.xy = packednormal.xy * 2 - 1;
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
return normal;
当把法线纹理的 Texture Type 标识为 normal map 时可以让 Unity 根据不同的平台压缩法线纹理,压缩之后纹理只保存两个通道,第三个通道可用另两个推导出来(法线是单位向量,且切线空间下 z 分量始终为正)。
// 一种控制凹凸程度的技巧:缩放 xy 分量再重新计算 z
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.BumpUv));
bump.xy *= _Scale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
- 获取顶点深度
// 获取观察空间深度值(取正值,本身是负值)
// 方式一:使用宏,并将结果输出到 o (o.screenPos.z) 中
o.screenPos = ComputeScreenPos(o.vertex);
COMPUTE_EYEDEPTH(o.screenPos.z);
// 方式二:其实就是 COMPUTE_EYEDEPTH(o) 的内部实现
float -UnityObjectToViewPos( v.vertex ).z
// 方式三:通过投影矩阵知道,齐次坐标下的 w 值,就是观察空间 z 值取正
o.screenPos.w
// 获得[0,1]之间的深度值
// _ProjectionParams.w is 1/FarPlane
float -UnityObjectToViewPos(v.vertex).z * _ProjectionParams.w
// 等同于
float -mul(UNITY_MATRIX_MV, v.vertex).z * _ProjectionParams.w
- 获取并使用深度图
// 获取深度图
// 延迟渲染中,已生产深度、法线缓存,在 shader 中直接使用变量即可获得
// 前向渲染中,需要设置摄像机手动获取(底层使用着色器替换,并使用 ShadowCaster Pass 得到深度)
Camera.depthTextureMode = DepthTextureMode.Depth
Camera.depthTextureMode = DepthTextureMode.DepthNormals // 深度 + 法线
// 在 shader 的变量
sampler2D _CameraDepthTexture
sampler2D _CameraDepthNormalsTexture
// 采样 _CameraDepthTexture
// 方式1,一般用于屏幕后期效果
// uv 来自 Graphics.Blit 产生的 full-screen quad
fixed4 d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv)
// 通过 MVP 转换后,得到的深度纹理中的值是非线性的高精度值(主要是投影矩阵变换后Z值是非线性的)
// LinearEyeDepth 转换回观察空间的线性深度值(+Z)
// Linear01Depth 转换到 [0,1] 的线性深度值
float linearDepth = Linear01Depth(d)
// 方式2,用于单个物体需要深度信息时
float4 d = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.screenPos);
float linearDepth = Linear01Depth(d);
// 采样 _CameraDepthNormalsTexture
fixed4 d = tex2D(_CameraDepthNormalsTexture, i.uv);
float depth; // 接受解码后的深度,是 [0,1] 的线性值
float3 normal; // 接受观察空间法线,范围 [-1,1]
// 解码深度与观察空间法线
DecodeDepthNormal(d, depth, normal);
- Shader 中计算某空间顶点的距离
// 实际就是求向量的模(长度/大小)
float sqrt(dot(float3 pos, float3 pos))
上面的函数相当于: pos.x2+pos.y2+pos.z2−−−−−−−−−−−−−−−−−−−−√ p o s . x 2 + p o s . y 2 + p o s . z 2 ,即点乘同一个顶点(向量)再开方。
// 一些用例:
// 采样光源纹理的衰减值,使用了距离(模)的平方,避免了开方带来的开销。
// dot(lightCoord, lightCoord) 和脚本中的 lightCoord.SqrMagnitude 是一个意思
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
// UnpackNormal 中计算 z 分量
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
- CG 常用几何函数
// 两个vector之间的距离
float distance(x, y)
// vector的模
float length(x)
// 通过入射光线与表面法线来获取反射矢量
vector reflect(i, n) // 注意 i 是指向顶点的方向
// 通常用法:
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 得到世界空间下的顶点
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos); // 顶点指向摄像机的视线方向
o.worldRefl = reflect(-o.worldViewDir, o.worldNormal); // 反射矢量
// 通过入射光线与表面法线来获取折射矢量(i,n必须是归一化后的矢量)
vector refract(i, n, float(x)) // i 是指向顶点,x 是入射介质与折射介质折射率的比值
// 通常用法:
o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);
常用内置函数 官方文档
Shader Property attributes and drawers
// Attributes recognized by Unity:
[HideInInspector]
[NoScaleOffset]
[Normal]
[HDR]
[Gamma]
[PerRendererData]
// Drawers:
// Will set "_INVERT_ON" shader keyword when set
[Toggle] _Invert ("Invert?", Float) = 0
// Will set "ENABLE_FANCY" shader keyword when set.
[Toggle(ENABLE_FANCY)] _Fancy ("Fancy?", Float) = 0
// Also will set "JUSTOUTLINE_ON" shader keyword when set.
[MaterialToggle] JustOutline ("JustOutline", Float) = 1
Attributes 详情查看 Property attributes and drawers,更多 Drawers 请查看 MaterialPropertyDrawer