0%

Partial Derivatives (fwidth)

原文:
Partial Derivatives (fwidth)

ddxddyfwidth这三个偏导函数,我们平时很少用到,而且刚接触的时候很难理解。但是我却很喜欢它们,因为我觉得有很多适合它们直接使用的场景。所以在这里我也想向你们介绍它们。因此下面主要是针对函数进行讲解,所以并不要求你对渲染有多深的了解。但是你还需要掌握一些基本的渲染相关的知识,所以如果你完全没有基础,建议你先从着色器基础看起。

DDX and DDY

“导数“的意义是表示函数在某个点的变化情况。使用导数的概念,我们可以计算屏幕上任意点与临近点之间的变化情况。在上面三个函数中,ddx、和ddy是最简单的两个。它们分别用来计算水平、和垂直两个方向的相邻两个像素点之间的变化情况。这个计算过程并不涉及到什么复杂大函数,也没有说需要什么复杂的GPU结构支持,它就是一个简单的、像素之间的差值计算。但是,有一点你必须清楚,当我们计算某个像素点的导数时,并不是单独地、针对每一个像素都计算一遍。而是将2x2的像素块组成一个独立的处理单元,我们的片段着色器也是以像素块为单元,进行并行处理的。可以这么理解,片段着色器一次性串行的执行像素块中的四个像素的逻辑,所以一个片段着色器可以同时访问四个像素值,因此可以一次性计算出它们的偏导。其中ddx函数是计算水平方向的两个相邻像素的差值,ddy是计算垂直方向的两个相邻像素的差值。这也意味着,同一个像素块中,相邻的两个像素的ddx、或ddy相同。

这里我实现一个简单的着色器来做个测试。在片段着色器中传入了uv坐标,然后我们使用ddx来计算水平方向相邻两个像素u坐标的变化情况,然后再乘以一个缩放因子便于观察,然后将结果以颜色值得形式显示在屏幕上。

1
2
3
4
5
6
7
8
9
//片段着色器
fixed4 frag(v2f i) : SV_TARGET{
//计算uv坐标中的u值在水平相邻像素之间的差异
float derivative = ddx(i.uv.x) * _Factor;
//将偏导转换为灰度值
fixed4 col = float4(derivative.xxx , 1);
col *= _Color;
return col;
}

然后我们在场景中观察其表现,我们发现模型的灰度值受uv坐标和屏幕坐标之间的关系的影响。开始我们拉进、或者放大观察,发现灰度慢慢变暗,这是因为放大后,相邻两个像素对应的UV坐标的u值差减小了。当我们旋转90度后发现其完全变黑,这是因为旋转后u方向和x方向垂直,所以相邻两个像素对应的UV坐标的u值完全相同,差值为零。

仅仅这些就已经可以实现很多功能了,例如我们可以使用深度图粗略的计算出法向纹理,然后tex2D函数会基于该法向值的变化来选择mipmap的层级。但是更多时候我们需要得到所有的变化情况,而不是某一个方向的变化情况,而fwidth的作用正在于此。

fwidth

如果你想将ddxddy两个函数结合起来使用,最直接的方法就是取各自结果的绝对值求和。下面就是我们自定义的fwidth函数。

1
2
3
float fwidth(float value){
return abs(ddx(value)) + abs(ddy(value));
}

如果我们将前面的着色器中的ddx函数用fwidth来替换,你会发现缩放的时候灰度变化和之前一样,但是旋转的时候灰度变化相对更亮,在90度时也不是全黑了。当然我们使用余弦公式来替代前面的绝对值求和,这样颜色变化的估计值会更准确一点,但是很多时候并不是使用越高级的函数,消耗更多的性能,就可以得到更好的效果。

Non-aliased step

fwidth的第一个应用场景,至少对于我来说,就是在不处理锯齿的情况下,计算出梯度值然后使用指定阈值进行区域划分。它会以不同的形式应用到火焰、水、卡通光照等场景中。进行梯度划分最简单的方法就是使用step函数–也叫阶跃函数,然后将梯度值和阈值传入step函数中,得到的结果在应用到线性插值函数lerp上,可以对不同区域进行上色。当然这里step函数会引入锯齿等边缘问题。这里我们可以实现一个逆插值函数来替代step函数,逆插值函数通过计算单个像素点上的变化情况,来实现区域划分,同时保证中间有渐变过渡带,这样就解决了锯齿问题。

首先我们使用fwidth来计算像素梯度,然后在执行逆插值之前,我们先要计算我们的边界区间,也就是我们的过渡带。下面的0.5就是我们的边界阈值,halfChange是我们的半带宽,当然边界阈值和带宽都可以根据应用场景来定义。重点是后面的逆插值函数,会将当前像素值和过度带进行比较,在过渡带区间的值将映射为0-1之间的值,也就是灰色区域,而不在过渡带区间的值将会映射为小于-1或大于1的值。然后我们使用saturate函数对其进行裁剪,就可以得到无锯齿的区域划分图案。

通过上面的描述我们可能有些属性,这不就是smoothstep函数的功能吗。是的,但是作为内置函数的smoothstep,它在过渡带区域还使用了一些平滑操作,相应的计算量会大一点。但是本身边界区域就很窄,平滑效果并不会很明显,所以我们这里并不需要多余的平滑。使用我们的逆插值函数就足够了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//片段函数
fixed4 frag(v2f i) : SV_TARGET{
//选取用于求梯度的像素值
float gradient = i.uv.x;
//计算梯度
float halfChange = fwidth(gradient) / 2;
//计算过渡带上下边界
float lowerEdge = 0.5 - halfChange;
float upperEdge = 0.5 + halfChange;
//使用你插值函数,将像素值划分为过渡带上侧、中间、下侧三个区域
float stepped = (gradient - lowerEdge) / (upperEdge - lowerEdge);
stepped = saturate(stepped);
//将划分结果转换为颜色显示
fixed4 col = float4(stepped.xxx, 1);
return col;
}

下面是我实现的三种边界效果,左边第一个是使用step实现的,边界具有明显锯齿效应。第二个是使用smoothstep实现的,进行一定的边界平滑。最后一个是通过上面的逆插值方法实现的。后面两个的抗锯齿效果几乎一般无二,所以我建议你在使用smoothstep函数前可以考虑一下是否可以使用逆插值方法,可以节省一部分性能开销。

A better step?

上面我们介绍了一种更好的边界平滑的技术,但是步骤多,写起来比较复杂。虽然这个方法也有一定的固定开销,不可能继续优化,而且99%的性能瓶颈问题都不是函数固定开销引起的。就像前面提到的tex2d,在执行的时候也会调用这些函数,但是这些函数在其中的消耗占比并不高。但是,我们可以将这个方法进行封装,这样我们在使用的时候就可以方便的调用了。

step函数有两个参数,第一个是边界值,第二个是用来比较的值,当后者小于前者时,返回0,否则,返回1。同样,我们可以将上面的方法封装成和step类似的函数,只不过除了0和1,还会返回中间过渡区域的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//我们自定义的加强版的边界划分函数
float aaStep(float compValue, float gradient){
float halfChange = fwidth(gradient) / 2;
//计算过渡带上下边界
float lowerEdge = compValue - halfChange;
float upperEdge = compValue + halfChange;
//使用你插值函数,将像素值划分为过渡带上侧、中间、下侧三个区域
float stepped = (gradient - lowerEdge) / (upperEdge - lowerEdge);
stepped = saturate(stepped);
return stepped;
}

//片段着色器
fixed4 frag(v2f i) : SV_TARGET{
float stepped = aaStep(0.5, i.uv.x);
//显示边界
fixed4 col = float4(stepped.xxx, 1);
return col;
}

An example

通过程序实现火焰效果,是阶跃函数一个比较好的应用方向,这里我大致参考Febucci火焰着色器来实现我们的火焰效果。

这里我们随着时间不断对UV坐标进行偏移,然后用偏移后的UV对噪声图进行采样,采样结果当做是当前位置火焰的强度。另外我对将uv坐标的v值取反然后平方,这样所求得的梯度值就会沿着y方向成递减的趋势,从而使得火焰的形状下密上疏。这里我们使用的噪声图是泊林噪声,是在之前教程中实现的。然后我们将噪声图中的采样值当做是阶跃阈值,这样就得到一个火焰的基本轮廓。为了模拟更逼真的火焰分层效果,这里我们通过偏移,产生多个火焰轮廓,然后使用线性插值函数来对这些分层区域上色。

然后我们将aaStep函数中的梯度值除以2的操作去掉,这样我们就扩大了其过渡区域的宽度。你可以试着修改这个值,然后观察一下产生的变化,选一个比较好的效果。

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
//强化版的阶跃函数
float aaStep(float compValue, float gradient){
float change = fwidth(gradient);
//计算过渡边界
float lowerEdge = compValue - change;
float upperEdge = compValue + change;
//使用逆插值函数
float stepped = (gradient - lowerEdge) / (upperEdge - lowerEdge);
stepped = saturate(stepped);
//最终结果近似于 `smoothstep(lowerEdge, upperEdge, gradient)`
return stepped;
}

//片段着色函数
fixed4 frag(v2f i) : SV_TARGET{
//使用平方值,让火焰变得更加旺盛
float fireGradient = 1 - i.uv.y;
fireGradient = fireGradient * fireGradient;
//滑动uv,产生动画效果
float2 fireUV = TRANSFORM_TEX(i.uv, _MainTex);
fireUV.y -= _Time.y * _ScrollSpeed;
//噪声采样
float fireNoise = tex2D(_MainTex, fireUV).x;

//划分火焰区域
float outline = aaStep(fireNoise, fireGradient);
float edge1 = aaStep(fireNoise, fireGradient - _Edge1);
float edge2 = aaStep(fireNoise, fireGradient - _Edge2);

//定义火焰外层颜色
fixed4 col = _Color1 * outline;
//其他层的颜色
col = lerp(col, _Color2, edge1);
col = lerp(col, _Color3, edge2);

//输出结果
return col;
}

下面我还对比了普通阶跃函数和我们这里加强版的阶跃函数,差别看起来不大,但是如果你的游戏审美要求是像素级别的,那么这个差别还是很明显的。所以我觉得你可以从现在开始,使用这里的方法,让你的游戏画面开起来更加柔和、更加平滑,即便是在低分辨率的情况下。

Sources

  • https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/046_Partial_Derivatives/testing.shader
    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
    Shader "Tutorial/046_Partial_Derivatives/testing"{
    //材质面板
    Properties{
    _Factor("Factor", Range(0, 100)) = 1
    }

    SubShader{
    //不透明物体
    Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

    Cull Off

    Pass{
    CGPROGRAM

    //引入内置函数和变量
    #include "UnityCG.cginc"

    //声明顶点、片段着色器
    #pragma vertex vert
    #pragma fragment frag

    float _Factor;

    //模型网格数据
    struct appdata{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    };

    //中间插值数据
    struct v2f{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD0;
    };

    //顶点着色器
    v2f vert(appdata v){
    v2f o;
    //裁剪坐标
    o.position = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv;
    return o;
    }

    //片段着色器
    fixed4 frag(v2f i) : SV_TARGET{
    //计算梯度
    float derivative = fwidth(i.uv.x) * _Factor;
    //可视化梯度
    fixed4 col = float4(derivative.xxx , 1);
    return col;
    }

    ENDCG
    }
    }
    }
  • https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/046_Partial_Derivatives/aa_step.shader
    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
    67
    68
    69
    70
    Shader "Tutorial/046_Partial_Derivatives/aaStep"{
    //材质面板
    Properties{

    }

    SubShader{
    //不透明物体
    Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

    Cull Off

    Pass{
    CGPROGRAM

    //引入内置函数和变量
    #include "UnityCG.cginc"

    //声明顶点、片段着色器
    #pragma vertex vert
    #pragma fragment frag

    //模型网格数据
    struct appdata{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    };

    //中间插值数据
    struct v2f{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD0;
    };

    //顶点着色器
    v2f vert(appdata v){
    v2f o;
    //裁剪坐标
    o.position = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv;
    return o;
    }

    //强化版阶跃函数
    float aaStep(float compValue, float gradient){
    float halfChange = fwidth(gradient) / 2;
    //计算边界
    float lowerEdge = compValue - halfChange;
    float upperEdge = compValue + halfChange;
    //使用逆插值函数
    float stepped = (gradient - lowerEdge) / (upperEdge - lowerEdge);
    stepped = saturate(stepped);
    //计算结果近似于 `smoothstep(lowerEdge, upperEdge, gradient)`
    return stepped;
    }

    //片段着色器
    fixed4 frag(v2f i) : SV_TARGET{
    float stepped = aaStep(0.5, i.uv.x);
    //梯度可视化
    fixed4 col = float4(stepped.xxx, 1);
    return col;
    }



    ENDCG
    }
    }
    }
  • https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/046_Partial_Derivatives/Fire.shader
    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
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    Shader "Tutorial/046_Partial_Derivatives/fire"{
    //材质面板
    Properties{
    _MainTex ("Fire Noise", 2D) = "white" {}
    _ScrollSpeed("Animation Speed", Range(0, 2)) = 1

    _Color1 ("Color 1", Color) = (0, 0, 0, 1)
    _Color2 ("Color 2", Color) = (0, 0, 0, 1)
    _Color3 ("Color 3", Color) = (0, 0, 0, 1)

    _Edge1 ("Edge 1-2", Range(0, 1)) = 0.25
    _Edge2 ("Edge 2-3", Range(0, 1)) = 0.5
    }

    SubShader{
    //不透明物体
    Tags{ "RenderType"="transparent" "Queue"="transparent"}

    Cull Off
    Blend SrcAlpha OneMinusSrcAlpha
    ZWrite Off

    Pass{
    CGPROGRAM

    //引入内置函数和变量
    #include "UnityCG.cginc"

    //声明顶点、片段着色器
    #pragma vertex vert
    #pragma fragment frag

    //火焰颜色
    fixed4 _Color1;
    fixed4 _Color2;
    fixed4 _Color3;

    float _Edge1;
    float _Edge2;

    float _ScrollSpeed;

    sampler2D _MainTex;
    float4 _MainTex_ST;

    //模型网格数据
    struct appdata{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    };

    //中间插值数据
    struct v2f{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD0;
    };

    //顶点着色器
    v2f vert(appdata v){
    v2f o;
    //裁剪坐标
    o.position = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv;
    return o;
    }

    //强化版阶跃函数
    float aaStep(float compValue, float gradient){
    float change = fwidth(gradient);
    //计算边界
    float lowerEdge = compValue - change;
    float upperEdge = compValue + change;
    //使用逆插值函数
    float stepped = (gradient - lowerEdge) / (upperEdge - lowerEdge);
    stepped = saturate(stepped);
    //结果近似于 `smoothstep(lowerEdge, upperEdge, gradient)`
    return stepped;
    }

    //片段着色器
    fixed4 frag(v2f i) : SV_TARGET{
    //平方使得火焰更旺盛
    float fireGradient = 1 - i.uv.y;
    fireGradient = fireGradient * fireGradient;
    //滑动uv值,产生动画效果
    float2 fireUV = TRANSFORM_TEX(i.uv, _MainTex);
    fireUV.y -= _Time.y * _ScrollSpeed;
    //噪声采样
    float fireNoise = tex2D(_MainTex, fireUV).x;

    //计算火焰轮廓
    float outline = aaStep(fireNoise, fireGradient);
    float edge1 = aaStep(fireNoise, fireGradient - _Edge1);
    float edge2 = aaStep(fireNoise, fireGradient - _Edge2);

    //外层火焰颜色
    fixed4 col = _Color1 * outline;
    //其他层火焰颜色
    col = lerp(col, _Color2, edge1);
    col = lerp(col, _Color3, edge2);

    //输出结果
    return col;
    }

    ENDCG
    }
    }
    }

相关文章

希望你能喜欢这个教程哦!如果你想支持我,可以关注我的推特,或者通过ko-fi、或patreon给两小钱。总之,各位大爷,走过路过不要错过,有钱的捧个钱场,没钱的捧个人场:-)!!!