文章

[ShaderGraph]定制ShaderGraph(七)Dither替代半透明

[ShaderGraph]定制ShaderGraph(七)Dither替代半透明

[ShaderGraph]定制ShaderGraph(七)Dither替代半透明

00 前置知识

在PBR渲染中, 通常的半透明会遇到线性空间半透明混合问题, 以及半透明渲染排序问题.

对于进行PBR渲染的物体, 半透明并不友好.

用Dither来解决半透明是成熟的通用方法之一.

ShaderGraph其实有自己的Dither Node, 但只有Bayer4x4的版本. 我们可以自己写一个Bayer8x8的版本.

1
2
3
4
5
6
7
8
9
10
11
12
13
void Unity_Dither_float4(float4 In, float4 ScreenPosition, out float4 Out)
{
    float2 uv = ScreenPosition.xy * _ScreenParams.xy;
    float DITHER_THRESHOLDS[16] =
    {
        1.0 / 17.0,  9.0 / 17.0,  3.0 / 17.0, 11.0 / 17.0,
        13.0 / 17.0,  5.0 / 17.0, 15.0 / 17.0,  7.0 / 17.0,
        4.0 / 17.0, 12.0 / 17.0,  2.0 / 17.0, 10.0 / 17.0,
        16.0 / 17.0,  8.0 / 17.0, 14.0 / 17.0,  6.0 / 17.0
    };
    uint index = (uint(uv.x) % 4) * 4 + uint(uv.y) % 4;
    Out = In - DITHER_THRESHOLDS[index];
}

纯数学的版本, 应该是

1
2
3
4
5
6
7
8
9
10
static const float DITHER_Bayer4x4[16] = {
     0,  8,  2, 10,
    12,  4, 14,  6,
     3, 11,  1,  9,
    15,  7, 13,  5
};

uint index = (uint(uv.x) % 4) * 4 + uint(uv.y) % 4;

return (DITHER_Bayer4x4[index] + 0.5) / 16.0;

这里, 我们会做一些工程的处理.

首先是, 用位移运算来取代uint index = (uint(uv.x) % 4) * 4 + uint(uv.y) % 4;

结果是uint index = (pixelCoord.x & 3u) | ((pixelCoord.y & 3u) << 2);

注: 之后在计算DITHER_Bayer8x8的时候同样也会用位移运算来替换数学计算.

然后, 用(DITHER_Bayer4x4[index] + 1.0) / 17.0;来取代(DITHER_Bayer4x4[index] + 0.5) / 16.0;, 这个是为了防止某些像素永远处于全1或者全0. 并且将这一部分的运算在构建数据结构的时候就提前算好, 这样在编译时, 这部分数字就已经就绪可用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ---------------------------------------------------------------------------
// Bayer 4×4 Ordered Dither Matrix
// ---------------------------------------------------------------------------
static const float DITHER_Bayer4x4[16] = {
     0,  8,  2, 10,
    12,  4, 14,  6,
     3, 11,  1,  9,
    15,  7, 13,  5
};

// 使用1 和 N^2+1, 避免出现有些点永远被剔除有些点永远被显示的局面
// uint index = (pixelCoord.x & 3u)       // = pixelCoord.x % 4
//          | ((pixelCoord.y & 3u) << 2); // = (pixelCoord.y % 4) * 4
inline float Dither_Threshold_Bayer4x4(uint2 pixelCoord)
{
    uint index = (pixelCoord.x & 3u) | ((pixelCoord.y & 3u) << 2);
    // return (DITHER_Bayer4x4[index] + 0.5) / 16.0;
    return (DITHER_Bayer4x4[index] + 1.0) / 17.0;
}

01 实施

工程化后的版本

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
// 4×4 Bayer (m+1)/17
static const float Dither_Bayer4x4_Precompute[16] = {
    1/17,9/17,3/17,11/17,
    13/17,5/17,15/17,7/17,
    4/17,12/17,2/17,10/17,
    16/17,8/17,14/17,6/17
};

inline float Dither_Threshold_Bayer4x4_Precompute(uint2 pixelCoord)
{
    uint index = (pixelCoord.x & 3u) | ((pixelCoord.y & 3u) << 2);
    return Dither_Bayer4x4_Precompute[index];
}

static const float DITHER_Bayer8x8_Precompute[64] = {
    // (m + 1) / 65.0 thresholds, 8×8 Bayer pattern
    1.0/65.0, 33.0/65.0, 9.0/65.0, 41.0/65.0, 3.0/65.0, 35.0/65.0, 11.0/65.0, 43.0/65.0,
    49.0/65.0, 17.0/65.0, 57.0/65.0, 25.0/65.0, 51.0/65.0, 19.0/65.0, 59.0/65.0, 27.0/65.0,
    13.0/65.0, 45.0/65.0, 5.0/65.0, 37.0/65.0, 15.0/65.0, 47.0/65.0, 7.0/65.0, 39.0/65.0,
    61.0/65.0, 29.0/65.0, 53.0/65.0, 21.0/65.0, 63.0/65.0, 31.0/65.0, 55.0/65.0, 23.0/65.0,
    4.0/65.0, 36.0/65.0, 12.0/65.0, 44.0/65.0, 2.0/65.0, 34.0/65.0, 10.0/65.0, 42.0/65.0,
    52.0/65.0, 20.0/65.0, 60.0/65.0, 28.0/65.0, 50.0/65.0, 18.0/65.0, 58.0/65.0, 26.0/65.0,
    16.0/65.0, 48.0/65.0, 8.0/65.0, 40.0/65.0, 14.0/65.0, 46.0/65.0, 6.0/65.0, 38.0/65.0,
    64.0/65.0, 32.0/65.0, 56.0/65.0, 24.0/65.0, 62.0/65.0, 30.0/65.0, 54.0/65.0, 22.0/65.0
    };

inline float Dither_Threshold_Bayer8x8_Precompute(uint2 pixelCoord)
{
    uint index = (pixelCoord.x & 7u) | ((pixelCoord.y & 7u) << 3);
    return DITHER_Bayer8x8_Precompute[index];
}

void Dither_Threshold_Bayer4x4_Screen_float(float inValue, float4 screenPosition, out float outValue)
{
    float2 uv = screenPosition.xy * _ScreenParams.xy;
    outValue = inValue - Dither_Threshold_Bayer4x4_Precompute((uint2)uv);
}

void Dither_Threshold_Bayer8x8_Screen_float(float inValue, float4 screenPosition, out float outValue)
{
    float2 uv = screenPosition.xy * _ScreenParams.xy;
    outValue = inValue - Dither_Threshold_Bayer8x8_Precompute((uint2)uv);
}

此时, 将inValue设为1, 然后输入ScreenPosition-Default连入, 输出节点连到Alpha Clip Threshold上, 并将材质球的Alpha Clipping打开, 调整Alpha值即可看到效果. 节点构成如下:

image-20251110164718746

02 其他优化

如果需要修改RenderScale还要保持Dither效果一致, 用以下方法.

image-20251110165325118

参考网页

本文由作者按照 CC BY 4.0 进行授权