unity shader中 ddx ddy是什么

在 Unity Shader 的 HLSL 中,ddxddy 是用于计算屏幕空间偏导数 的函数。它们只能在片段着色器(Fragment Shader) 中使用,用于衡量某个变量在屏幕上沿 X 或 Y 方向的变化速率。

工作原理

GPU 执行片段着色器时,通常将相邻的 2×2 像素块(称为 quad)打包在一起并行处理。

  • ddx(x) :计算当前变量 x 在屏幕水平方向上(X 轴)的偏导,近似为 右方像素的值 - 左方像素的值

  • ddy(y) :计算当前变量 y 在屏幕垂直方向上(Y 轴)的偏导,近似为 下方像素的值 - 上方像素的值

由于 2×2 quad 提供了一阶差分所需的最少像素数,这个计算非常高效且精确。

主要用途

  1. 自动计算纹理 Mip 等级

    纹理采样函数(如 tex2D)内部使用 ddx(uv) / ddy(uv) 来确定应该采样的 mip 级别。手动计算时也可用于自定义各向异性滤波。

  2. 法线贴图的切线空间转换

    通过 ddx(worldPos) / ddy(worldPos) 可以得到平面局部梯度,用于生成无需切线向量的法线贴图算法(如三平面映射)。

  3. 边缘检测与轮廓渲染

    对比不同像素之间的深度值、法线或 UV 的偏导数,可识别出几何边缘或纹理接缝。

  4. 屏幕空间特效

    例如实现屏幕空间的抗锯齿(FXAA)、景深模糊的采样范围计算,或基于屏幕位置的自适应效果。

注意事项

  • 仅片段着色器可用:顶点着色器中无法调用。

  • 分支敏感 :如果在动态分支(if / else)内部使用,且分支的 2×2 quad 中部分像素走不同分支,则导数结果可能未定义或产生异常值。建议提前计算好再进入分支。

  • DX vs OpenGL :在 HLSL 中习惯用 ddx / ddy(对应 DX);在 GLSL 中对应 dFdx / dFdy。Unity 跨平台时推荐使用 HLSL 写法。

  • 无精度修饰要求ddx / ddy 返回的类型与输入一致,可处理 floathalffloat2 等向量类型。

简单示例

cs 复制代码
float2 uv = i.uv;
float2 uv_dx = ddx(uv);
float2 uv_dy = ddy(uv);
// 基于屏幕空间的 UV 变化率,可以自定义 mip 级别
float mip = 0.5 * log2( dot(uv_dx, uv_dx) + dot(uv_dy, uv_dy) );

类比理解

可以把屏幕当作一个连续坐标系,ddx / ddy 就相当于二元函数 f(x,y)xy 的偏导数

只不过这里的差分是在离散的相邻像素之间进行。

在实际项目开发中,ddxddy 主要用于那些需要感知屏幕空间变化率的效果,以下是我在各类渲染场景中遇到的典型用法:

1. 自动估算纹理 Mip 等级(各向异性或自定义)

纹理采样内部原本就依赖 ddx(uv) / ddy(uv) 来决定 mip 等级。但在某些自定义采样中(例如虚拟纹理贴花投影体积纹理),你需要手动控制 mip 级别来避免闪烁或过度模糊。

cs 复制代码
float2 uv = i.uv;
float2 dx = ddx(uv) * _TextureSize;   // 屏幕空间 UV 变化率(纹素单位)
float2 dy = ddy(uv) * _TextureSize;
float mip = 0.5 * log2( max( dot(dx, dx), dot(dy, dy) ) );
float4 color = tex2Dlod(_MainTex, float4(uv, 0, mip));

这对于大地形漫游UI 九宫格拉伸时保持纹理清晰度非常有用。


2. 无需切线空间的法线重建(三平面映射 / 水面波浪)

当你在世界空间或对象空间中直接计算法线(如三平面贴图、过程化地形、动态水体),可以利用 ddx(P) / ddy(P) 得到表面的局部梯度,从而重建法线。

cs 复制代码
float3 P = i.worldPos;
float3 dPdx = ddx(P);
float3 dPdy = ddy(P);
float3 normal = normalize(cross(dPdy, dPdx));  // 注意顺序

实际项目

  • 大型开放世界中,地形使用多张纹理混合,且没有预存切线空间,就用此法在着色器中动态计算法线,节省内存带宽。

  • 动态水面:通过波浪函数修改顶点位置后,片段着色器中用 ddx/ddy 重新计算复杂波形的法线,避免顶点着色器传递大量数据。


3. 轮廓线渲染 / 边缘检测

通过比较相邻像素的深度法线UV 的偏导数,可以判断是否属于几何边缘,用于描边效果X-Ray透视

cs 复制代码
float depth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(...));
float dx = abs(ddx(depth));
float dy = abs(ddy(depth));
float edge = saturate( (dx + dy) * _Sensitivity );

实际应用

  • 《英雄联盟》式卡通描边(屏幕空间后处理版)。

  • 工业检测模拟中高亮显示物体交界处。

  • 水中折射的边缘加深效果。


4. 屏幕空间抗锯齿(FXAA)

FXAA 需要检测画面的亮度梯度 ,以决定哪些地方需要平滑处理。ddx/ddy 可以高效计算相邻像素的亮度变化。

cs 复制代码
float luma = Luminance(color.rgb);
float lumaDx = ddx(luma);
float lumady = ddy(luma);
float contrast = abs(lumaDx) + abs(lumady);
if(contrast > threshold) // 可能产生锯齿,执行模糊

几乎所有现代游戏后期管线中的 FXAA 实现都会用到偏导数。


5. 屏幕空间扭曲(热畸变、水波效果)

想要在屏幕空间产生偏移扭曲 时,可以用 ddx(uv) / ddy(uv) 作为动态强度的参考,使得扭曲强度与屏幕缩放解耦,避免在不同分辨率下效果不一致。

cs 复制代码
float2 distort = sin(uv * 50 + _Time.y) * 0.02;
float2 scaleFactor = float2( length(ddx(uv)), length(ddy(uv)) );
distort *= scaleFactor;   // 保证像素采样偏移量相对于纹素变化率稳定

6. 自适应采样(SSAO / SSR / 体积光)

屏幕空间的环境光遮蔽或反射追踪中,会根据视角深度梯度决定采样步长。

例如 SSAO 采样核可以根据深度变化率动态缩小半径,避免在边缘出现光晕伪影。

cs 复制代码
float centerDepth = LinearEyeDepth(...);
float depthDelta = max( abs(ddx(centerDepth)), abs(ddy(centerDepth)) );
float radius = _BaseRadius / (1.0 + depthDelta * _RadiusScale);

实际项目中的坑点

  • 分支内部慎用 :若在 if/else 分支里调用 ddx / ddy,且该分支对 quad 内四个像素并不统一,结果会是未定义的(某些平台会报错或产生鬼影)。解决方案:把需要的变量提前计算好,再进入分支。

  • 移动端性能:在某些 PowerVR GPU 上,偏导数指令开销略高,但通常可以接受;避免在复杂循环内反复调用。

  • 不适合顶点着色器:顶点阶段没有像素 quad 概念,无法使用。


一句话总结

ddx/ddy 让你在片段着色器中"感知"屏幕空间的局部变化,是实现众多高质量自适应效果(纹理过滤、边缘检测、法线重建、抗锯齿、扭曲)的核心工具。

相关推荐
郝学胜-神的一滴3 小时前
[简化版 GAMES 101] 计算机图形学 08:三角形光栅化上
c++·unity·游戏引擎·godot·图形渲染·opengl·unreal
nnsix4 小时前
Unity ILRuntime 笔记
unity·游戏引擎
nnsix6 小时前
Unity API 兼容的 .NET Standard 2.1 和 .NET Framework 区别
unity·游戏引擎·.net
mxwin6 小时前
Unity Shader 制作半透明物体 使用多Pass提前写入深度的方式 避免穿模
unity·游戏引擎
nnsix7 小时前
Unity HybridCLR 笔记
笔记·unity·游戏引擎
nnsix9 小时前
Unity Addressables 笔记
unity·游戏引擎
RReality9 小时前
【Unity Shader URP】视差贴图 实战教程
ui·平面·unity·游戏引擎·图形渲染·贴图
小清兔1 天前
Addressable的设置打包流程
笔记·游戏·unity·c#
3D霸霸1 天前
Sourcetree 拉取新工程
数据仓库·unity