文章

定制ShaderGraph(四)---Geometric Specular Anti-aliasing的URP实现

定制ShaderGraph(四)---Geometric Specular Anti-aliasing的URP实现

定制ShaderGraph(四)—Geometric Specular Anti-aliasing的URP实现

00 前置知识

Unity在HDRP中是自带这个算法及优化的.

可以在文件Library/PackageCache/com.unity.render-pipelines.high-definition@12.1.15/Runtime/Material/Lit/LitData.hlsl中找到如下代码(Unity6中, 这部分直接被合并到了CommonMaterial.hlsl):

1
2
3
4
5
6
7
8
#if defined(_ENABLE_GEOMETRIC_SPECULAR_AA) && !defined(SHADER_STAGE_RAY_TRACING)
    // Specular AA
    #ifdef PROJECTED_SPACE_NDF_FILTERING
    surfaceData.perceptualSmoothness = ProjectedSpaceGeometricNormalFiltering(surfaceData.perceptualSmoothness, input.tangentToWorld[2], _SpecularAAScreenSpaceVariance, _SpecularAAThreshold);
    #else
    surfaceData.perceptualSmoothness = GeometricNormalFiltering(surfaceData.perceptualSmoothness, input.tangentToWorld[2], _SpecularAAScreenSpaceVariance, _SpecularAAThreshold);
    #endif
#endif

点击函数名可以跳转到文件Library/PackageCache/com.unity.render-pipelines.core@12.1.15/ShaderLibrary/CommonMaterial.hlsl

注意, 该文件并不专属于HDRP, 而是属于com.unity.render-pipelines.core, 即在URP中, 可以直接调用.

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
// Reference: Error Reduction and Simplification for Shading Anti-Aliasing
// Specular antialiasing for geometry-induced normal (and NDF) variations: Tokuyoshi / Kaplanyan et al.'s method.
// This is the deferred approximation, which works reasonably well so we keep it for forward too for now.
// screenSpaceVariance should be at most 0.5^2 = 0.25, as that corresponds to considering
// a gaussian pixel reconstruction kernel with a standard deviation of 0.5 of a pixel, thus 2 sigma covering the whole pixel.
float GeometricNormalVariance(float3 geometricNormalWS, float screenSpaceVariance)
{
    float3 deltaU = ddx(geometricNormalWS);
    float3 deltaV = ddy(geometricNormalWS);

    return screenSpaceVariance * (dot(deltaU, deltaU) + dot(deltaV, deltaV));
}

// Return modified perceptualSmoothness
float GeometricNormalFiltering(float perceptualSmoothness, float3 geometricNormalWS, float screenSpaceVariance, float threshold)
{
    float variance = GeometricNormalVariance(geometricNormalWS, screenSpaceVariance);
    return NormalFiltering(perceptualSmoothness, variance, threshold);
}

float ProjectedSpaceGeometricNormalFiltering(float perceptualSmoothness, float3 geometricNormalWS, float screenSpaceVariance, float threshold)
{
    float variance = GeometricNormalVariance(geometricNormalWS, screenSpaceVariance);
    return ProjectedSpaceNormalFiltering(perceptualSmoothness, variance, threshold);
}

可以看到两个函数调用的都是GeometricNormalVariance这个函数(这个函数的算法到Unity6都没有更新过)

论文来源是テクノロジー推進部 ADVANCED TECHNOLOGY DIVISION | SQUARE ENIX

中的Error Reduction and Simplification for Shading Anti-Aliasing, 2017年的版本.

实际上, 在2019年, 同一组作者(Yusuke Tokuyoshi and Anton S. Kaplanyan)发布了Improved Geometric Specular Antialiasing, 更新的版本.

然后启用宏”PROJECTED_SPACE_NDF_FILTERING”后, 是利用的同一作者在2021年发布的Stable Geometric Specular Antialiasing with Projected-Space NDF Filtering.

这很古怪, 除非2021年论文中用的是2017年的variance算法, 否则这个算法就有问题.

本次计划先尝试还原HDRP的版本, 然后再将算法更新至2019年的版本.

另: 在这部分代码下面, 还能找到基于教团: 1886的NormalMapfilter的相关函数.

01 实施

首先, 我们需要需要在SurfaceData中添加一些参数, 用来承载数据传输.

Library/PackageCache/com.unity.render-pipelines.high-definition@12.1.15/Runtime/Material/Lit/Lit.cs.hlsl中可以找到HDRP的SurfaceData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct SurfaceData
{
    uint materialFeatures;
    real3 baseColor;
    real specularOcclusion;
    float3 normalWS;
    real perceptualSmoothness;
    real ambientOcclusion;
    real metallic;
    real coatMask;
    real3 specularColor;
    uint diffusionProfileHash;
    real subsurfaceMask;
    real thickness;
    float3 tangentWS;
    real anisotropy;
    real iridescenceThickness;
    real iridescenceMask;
    real3 geomNormalWS;
    real ior;
    real3 transmittanceColor;
    real atDistance;
    real transmittanceMask;
};

对比URP的Library/PackageCache/com.unity.render-pipelines.universal@12.1.10/ShaderLibrary/SurfaceData.hlsl

1
2
3
4
5
6
7
8
9
10
11
12
13
struct SurfaceData
{
    half3 albedo;
    half3 specular;
    half  metallic;
    half  smoothness;
    half3 normalTS;
    half3 emission;
    half  occlusion;
    half  alpha;
    half  clearCoatMask;
    half  clearCoatSmoothness;
};

不需要进行传递, 直接用SubGraph就可以完成

两个Filter算法的函数如下, 在文件Library/PackageCache/com.unity.render-pipelines.core@12.1.15/ShaderLibrary/CommonMaterial.hlsl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Return modified perceptualSmoothness based on provided variance (get from GeometricNormalVariance + TextureNormalVariance)
float NormalFiltering(float perceptualSmoothness, float variance, float threshold)
{
    float roughness = PerceptualSmoothnessToRoughness(perceptualSmoothness);
    // Ref: Geometry into Shading - http://graphics.pixar.com/library/BumpRoughness/paper.pdf - equation (3)
    float squaredRoughness = saturate(roughness * roughness + min(2.0 * variance, threshold * threshold)); // threshold can be really low, square the value for easier control

    return RoughnessToPerceptualSmoothness(sqrt(squaredRoughness));
}

float ProjectedSpaceNormalFiltering(float perceptualSmoothness, float variance, float threshold)
{
    float roughness = PerceptualSmoothnessToRoughness(perceptualSmoothness);
    // Ref: Stable Geometric Specular Antialiasing with Projected-Space NDF Filtering - https://yusuketokuyoshi.com/papers/2021/Tokuyoshi2021SAA.pdf
    float squaredRoughness = roughness * roughness;
    float projRoughness2 = squaredRoughness / (1.0 - squaredRoughness);
    float filteredProjRoughness2 = saturate(projRoughness2 + min(2.0 * variance, threshold * threshold));
    squaredRoughness = filteredProjRoughness2 / (filteredProjRoughness2 + 1.0f);

    return RoughnessToPerceptualSmoothness(sqrt(squaredRoughness));
}
  • 将重复运算部分提取出来, 然后整理为ShaderGraph和通常都可兼容的hlsl文件, 内容如下:
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
110
111
112
113
114
#ifndef JAUVED_GEOMETRIC_SPECULARAA_INCLUDED
#define JAUVED_GEOMETRIC_SPECULARAA_INCLUDED

#ifndef SHADERGRAPH_PREVIEW
// 这里放非UnityShaderGraph自定的hlsl
#include  "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#endif

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"

// // Reference: Error Reduction and Simplification for Shading Anti-Aliasing
// // Specular antialiasing for geometry-induced normal (and NDF) variations: Tokuyoshi / Kaplanyan et al.'s method.
// // This is the deferred approximation, which works reasonably well so we keep it for forward too for now.
// // screenSpaceVariance should be at most 0.5^2 = 0.25, as that corresponds to considering
// // a gaussian pixel reconstruction kernel with a standard deviation of 0.5 of a pixel, thus 2 sigma covering the whole pixel.
// float GeometricNormalVariance(float3 geometricNormalWS, float screenSpaceVariance)
// {
//     float3 deltaU = ddx(geometricNormalWS);
//     float3 deltaV = ddy(geometricNormalWS);
//
//     return screenSpaceVariance * (dot(deltaU, deltaU) + dot(deltaV, deltaV));
// }

// Return modified perceptualSmoothness based on provided variance (get from GeometricNormalVariance + TextureNormalVariance)
float NormalFilteringRoughness(float roughness, float variance, float threshold)
{
    // float roughness = PerceptualSmoothnessToRoughness(perceptualSmoothness);
    // Ref: Geometry into Shading - http://graphics.pixar.com/library/BumpRoughness/paper.pdf - equation (3)
    float squaredRoughness = saturate(roughness * roughness + min(2.0 * variance, threshold * threshold));
    // threshold can be really low, square the value for easier control

    // return RoughnessToPerceptualSmoothness(sqrt(squaredRoughness));
    return sqrt(squaredRoughness);
}

float ProjectedSpaceNormalFilteringRoughness(float roughness, float variance, float threshold)
{
    // float roughness = PerceptualSmoothnessToRoughness(perceptualSmoothness);
    // Ref: Stable Geometric Specular Antialiasing with Projected-Space NDF Filtering - https://yusuketokuyoshi.com/papers/2021/Tokuyoshi2021SAA.pdf
    float squaredRoughness = roughness * roughness;
    float projRoughness2 = squaredRoughness / max(1.0 - squaredRoughness, FLT_MIN);
    float filteredProjRoughness2 = saturate(projRoughness2 + min(2.0 * variance, threshold * threshold));
    squaredRoughness = filteredProjRoughness2 / (filteredProjRoughness2 + 1.0f);

    // return RoughnessToPerceptualSmoothness(sqrt(squaredRoughness));
    return sqrt(squaredRoughness);
}

// Return modified perceptualSmoothness
float GeometricNormalFiltering(float roughness, float geometricNormalVariance, float threshold)
{
    // float variance = GeometricNormalVariance(geometricNormalWS, screenSpaceVariance);
    return NormalFilteringRoughness(roughness, geometricNormalVariance, threshold);
}

float ProjectedSpaceGeometricNormalFiltering(float roughness, float geometricNormalVariance, float threshold)
{
    // float variance = GeometricNormalVariance(geometricNormalWS, screenSpaceVariance);
    return ProjectedSpaceNormalFilteringRoughness(roughness, geometricNormalVariance, threshold);
}

// 调用伪代码, 取自Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Lit/LitData.hlsl
// #if defined(_ENABLE_GEOMETRIC_SPECULAR_AA) && !defined(SHADER_STAGE_RAY_TRACING)
//     // Specular AA
//     #ifdef PROJECTED_SPACE_NDF_FILTERING
//     surfaceData.perceptualSmoothness = ProjectedSpaceGeometricNormalFiltering(surfaceData.perceptualSmoothness, input.tangentToWorld[2], _SpecularAAScreenSpaceVariance, _SpecularAAThreshold);
//     #else
//     surfaceData.perceptualSmoothness = GeometricNormalFiltering(surfaceData.perceptualSmoothness, input.tangentToWorld[2], _SpecularAAScreenSpaceVariance, _SpecularAAThreshold);
//     #endif
// #endif

void GeometricSpecularAAPerceptualSmoothness_float(float perceptualSmoothness, float3 geometricNormalWS,
                                                   float screenSpaceVariance, float threshold,
                                                   bool geometricSpecularAAOn, bool projectedSpaceNDFFiltering,
                                                   out float smoothness)
{
    // 静态分支, 因为完全可以确定是走哪个分支
    UNITY_BRANCH
    if (geometricSpecularAAOn)
    {
        float geometricSpecularAARoughness;
        float geometricNormalVariance = GeometricNormalVariance(geometricNormalWS, screenSpaceVariance);
        float roughness = PerceptualSmoothnessToRoughness(perceptualSmoothness);
        // 静态分支, 因为完全可以确定是走哪个分支
        UNITY_BRANCH
        if (projectedSpaceNDFFiltering)
        {
            geometricSpecularAARoughness = ProjectedSpaceGeometricNormalFiltering(
                roughness, geometricNormalVariance, threshold);
        }
        else
        {
            geometricSpecularAARoughness = GeometricNormalFiltering(roughness, geometricNormalVariance, threshold);
        }
        smoothness = RoughnessToPerceptualSmoothness(geometricSpecularAARoughness);
    }
    else
    {
        smoothness = perceptualSmoothness;
    }
}

void GeometricSpecularAAPerceptualSmoothness_half(half perceptualSmoothness, half3 geometricNormalWS,
                                                   half screenSpaceVariance, half threshold,
                                                   bool geometricSpecularAAOn, bool projectedSpaceNDFFiltering,
                                                   out half smoothness)
{
    GeometricSpecularAAPerceptualSmoothness_float(perceptualSmoothness,geometricNormalWS,
        screenSpaceVariance,threshold,geometricSpecularAAOn,projectedSpaceNDFFiltering,smoothness);
}


#endif

其中涉及到UNITY_FLATTEN和UNITY_BRANCH两个宏.

UNITY_FLATTEN

当你的两条分支的计算都非常简单的时候, 就不要走真正的分支, 而是两边都计算, 然后通过条件来取结果. 适用于一次渲染产生不同结果的情况.

UNITY_BRANCH

当你的两条分支中, 其中一条/两条比较昂贵(比如运输量特别大, 或者涉及到贴图采样), 但你又非常确定在一次渲染中只会走其中一条分支的时候, 采用这个宏, 代价是大概12条运算指令.

当一次渲染产生不同的结果, 同时其中一条/多条比较昂贵的时候, 请避免这种情况的发生.

详情参见流控制 - Win32 apps | Microsoft Learn

  • 以自定义节点的方式接入ShaderGraph, 并加入随视线距离变化而还原正常的Smoothness的功能, 这样, 在极近距离则可以完全的体现硬表面建模的细节. 代价是当GeometricSpecularAA生效时环境贴图会达不到最高的精度, 但HDRP同样会有该缺陷.

    image-20250912184146256

参考网页
本文由作者按照 CC BY 4.0 进行授权