前言
在Unity Shader中,可以通过ddx和ddy函数间接求出经过顶点变换后的法线,但这是一种特定场景下的替代方案,并非标准做法。以下是详细解释:
对惹,这里有一个游戏开发交流小组 ,希望大家可以点击进来一起交流一下开发经验呀!
核心原理
ddx和ddy的作用
这两个函数在片段着色器中计算屏幕空间中的偏导数(梯度):
-
ddx(var):当前像素与右侧像素的差值。 -
ddy(var):当前像素与下方像素的差值。
它们基于屏幕像素网格,适用于任何插值后的变量(如位置、UV等)。 -
法线重建的原理
若在片段着色器中已有视图空间(View Space)或世界空间(World Space)的位置,可通过以下步骤重建法线:
// 假设 i.posWS 是世界空间位置
float3 dpdx = ddx(i.posWS); // 屏幕X方向的位置梯度
float3 dpdy = ddy(i.posWS); // 屏幕Y方向的位置梯度
float3 faceNormal = normalize(cross(dpdy, dpdx)); // 叉积得到几何法线
关键细节
- 得到的是面法线(几何法线)
-
此法线基于三角形面的实际朝向,未经过顶点法线的平滑插值。
-
效果较“硬”,适合需要棱角感的表面(如岩石、低模风格)。
-
坐标系需一致
-
位置变量必须位于同一空间(如统一为世界空间或视图空间)。
-
叉积顺序
cross(dpdy, dpdx)需匹配渲染管线的坐标系(右手/左手)。 -
性能考量
-
ddx/ddy是硬件加速操作,效率较高。 -
但需在片段着色器中额外计算,若原始法线已通过插值获得,直接使用插值法线更高效。
与标准方法的对比
| 方法 | 优点 | 缺点 |
|---|---|---|
| ddx/ddy重建法线 | 无需传递法线属性,节省顶点数据 | 仅得面法线,无平滑插值效果 |
| 插值顶点法线 | 支持平滑着色 | 需传递法线并插值,占用带宽 |
示例代码(Unity ShaderLab)
Shader "Custom/DDXNormal" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float4 pos : SV_POSITION;
float3 posWS : TEXCOORD0; // 世界空间位置
};
v2f vert (appdata_base v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.posWS = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target {
// 用ddx/ddy重建法线
float3 dpdx = ddx(i.posWS);
float3 dpdy = ddy(i.posWS);
float3 normal = normalize(cross(dpdy, dpdx));
// 可视化法线(转换到[0,1]范围)
return fixed4(normal * 0.5 + 0.5, 1);
}
ENDCG
}
}
}
适用场景
- 无预算法线时
-
如深度纹理后处理、屏幕空间效果(SSR, SSAO)。
-
需要几何真实法线时
-
如CAD可视化、硬表面渲染。
-
节省顶点数据
-
避免向片段着色器传递法线属性。
注意事项
- 背面问题:重建的法线方向可能与需求相反,可用
face = sign(dot(faceNormal, viewDir))校正。 - 非线性变换:若模型有非均匀缩放,需在视图/世界空间计算,避免物体空间畸变。
- 边缘不连续:在UV或位置不连续处(如硬边),
ddx/ddy可能产生 artifacts。
结论
可以通过 ddx/ddy 求出变换后的法线,但结果本质是屏幕空间重建的几何面法线。若需要平滑插值的顶点法线,仍应通过顶点着色器传递并插值。根据需求权衡选择方案。
更多教学视