文章

法线压缩技术的发展

Heads up!

目前主流的方案大多使用 $ semi- $ $octahedral$ $normal$ $encoding$ 技术

  • https://rootjhon.github.io/posts/%E6%B3%95%E7%BA%BF%E5%8E%8B%E7%BC%A9/

介绍

>> demo

下面是各种常见的编码方法及其比较。

  • 错误图像为:1-pow(dot(n1,n2),1024)abs(n1-n2)*30,其中 n1是实际法线,n2是法线编码到纹理中、读回并解码。 MSE 和 PSNR 是在差异 ( abs(n1-n2) ) 图像上计算的。

BaseLine:存储 X&Y&Z

均方误差:0.000008;峰值信噪比:51.081 分贝。

| | | img | | ———————————————————— | —- | —- |

Encoding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
half4 encode (half3 n, float3 view)
{
    return half4(n.xyz*0.5+0.5,0);
}

/*
ps_3_0
def c0, 0.5, 0, 0, 0
dcl_texcoord_pp v0.xyz
mad_pp oC0, v0.xyzx, c0.xxxy, c0.xxxy
*/

/*
1 ALU
Radeon HD 2400: 1 GPR, 1.00 clk
Radeon HD 3870: 1 GPR, 1.00 clk
Radeon HD 5870: 1 GPR, 0.50 clk
GeForce 6200: 1 GPR, 1.00 clk
GeForce 7800GT: 1 GPR, 1.00 clk
GeForce 8800GTX: 6 GPR, 8.00 clk
*/

Decoding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
half3 decode (half4 enc, float3 view)
{
    return enc.xyz*2-1;
}

/*
ps_3_0
def c0, 2, -1, 0, 0
dcl_texcoord2 v0.xy
dcl_2d s0
texld_pp r0, v0, s0
mad_pp oC0.xyz, r0, c0.x, c0.y
mov_pp oC0.w, c0.z
*/

/*
2 ALU, 1 TEX
Radeon HD 2400: 1 GPR, 1.00 clk
Radeon HD 3870: 1 GPR, 1.00 clk
Radeon HD 5870: 1 GPR, 0.50 clk
GeForce 6200: 1 GPR, 1.00 clk
GeForce 7800GT: 1 GPR, 1.00 clk
GeForce 8800GTX: 6 GPR, 10.00 clk
*/

Method #1: 存储 X&Y,重建 Z

被《杀戮地带 2》等游戏所使用(PDF 链接)。

均方误差:0.013514;峰值信噪比:18.692 分贝。

img img img

优点缺点
编解码非常简单Normal can point away from the camera.
参阅“Resistance 2 预照明”论文(PDF 链接

Encoding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
half4 encode (half3 n, float3 view)
{
    return half4(n.xy*0.5+0.5,0,0);
}

/*
ps_3_0
def c0, 0.5, 0, 0, 0
dcl_texcoord_pp v0.xy
mad_pp oC0, v0.xyxx, c0.xxyy, c0.xxyy
*/

/*
1 ALU
Radeon HD 2400: 1 GPR, 1.00 clk
Radeon HD 3870: 1 GPR, 1.00 clk
Radeon HD 5870: 1 GPR, 0.50 clk
GeForce 6200: 1 GPR, 1.00 clk
GeForce 7800GT: 1 GPR, 1.00 clk
GeForce 8800GTX: 5 GPR, 7.00 clk
*/

Decoding

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
half3 decode (half2 enc, float3 view)
{
    half3 n;
    n.xy = enc*2-1;
    n.z = sqrt(1-dot(n.xy, n.xy));
    return n;
}

/*
ps_3_0
def c0, 2, -1, 1, 0
dcl_texcoord2 v0.xy
dcl_2d s0
texld_pp r0, v0, s0
mad_pp r0.xy, r0, c0.x, c0.y
dp2add_pp r0.z, r0, -r0, c0.z
mov_pp oC0.xy, r0
rsq_pp r0.x, r0.z
rcp_pp oC0.z, r0.x
mov_pp oC0.w, c0.w
*/

/*
7 ALU, 1 TEX
Radeon HD 2400: 1 GPR, 1.00 clk
Radeon HD 3870: 1 GPR, 1.00 clk
Radeon HD 5870: 1 GPR, 0.50 clk
GeForce 6200: 1 GPR, 4.00 clk
GeForce 7800GT: 1 GPR, 3.00 clk
GeForce 8800GTX: 5 GPR, 15.00 clk
*/

Method #2: 球坐标 - Spherical Coordinates

可以使用球坐标来编码法线。因为我们知道它的单位长度,所以我们可以只存储两个角度。

均方误差:0.000062;峰值信噪比:42.042 分贝。

img img img

优点缺点
通常情况下适用于法线 (not necessarily view space)使用三角函数指令(ALU 相当繁重)。不过,可以用纹理查找来替换其中的一些。

Encoding

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
#define kPI 3.1415926536f
half4 encode (half3 n, float3 view)
{
    return half4(
      (half2(atan2(n.y,n.x)/kPI, n.z)+1.0)*0.5,
      0,0);
}

/*
ps_3_0
def c0, 0.999866009, 0, 1, 3.14159274
def c1, 0.0208350997, -0.0851330012,
    0.180141002, -0.330299497
def c2, -2, 1.57079637, 0.318309873, 0.5
dcl_texcoord_pp v0.xyz
add_pp r0.xy, -v0_abs, v0_abs.yxzw
cmp_pp r0.xz, r0.x, v0_abs.xyyw, v0_abs.yyxw
cmp_pp r0.y, r0.y, c0.y, c0.z
rcp_pp r0.z, r0.z
mul_pp r0.x, r0.x, r0.z
mul_pp r0.z, r0.x, r0.x
mad_pp r0.w, r0.z, c1.x, c1.y
mad_pp r0.w, r0.z, r0.w, c1.z
mad_pp r0.w, r0.z, r0.w, c1.w
mad_pp r0.z, r0.z, r0.w, c0.x
mul_pp r0.x, r0.x, r0.z
mad_pp r0.z, r0.x, c2.x, c2.y
mad_pp r0.x, r0.z, r0.y, r0.x
cmp_pp r0.y, v0.x, -c0.y, -c0.w
add_pp r0.x, r0.x, r0.y
add_pp r0.y, r0.x, r0.x
add_pp r0.z, -v0.x, v0.y
cmp_pp r0.zw, r0.z, v0.xyxy, v0.xyyx
cmp_pp r0.zw, r0, c0.xyyz, c0.xyzy
mul_pp r0.z, r0.w, r0.z
mad_pp r0.x, r0.z, -r0.y, r0.x
mul_pp r0.x, r0.x, c2.z
mov_pp r0.y, v0.z
add_pp r0.xy, r0, c0.z
mul_pp oC0.xy, r0, c2.w
mov_pp oC0.zw, c0.y
*/

/*
26 ALU
Radeon HD 2400: 1 GPR, 17.00 clk
Radeon HD 3870: 1 GPR, 4.25 clk
Radeon HD 5870: 2 GPR, 0.95 clk
GeForce 6200: 2 GPR, 12.00 clk
GeForce 7800GT: 2 GPR, 9.00 clk
GeForce 8800GTX: 9 GPR, 43.00 clk
*/

Decoding

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
half3 decode (half2 enc, float3 view)
{
    half2 ang = enc*2-1;
    half2 scth;
    sincos(ang.x * kPI, scth.x, scth.y);
    half2 scphi = half2(sqrt(1.0 - ang.y*ang.y), ang.y);
    return half3(scth.y*scphi.x, scth.x*scphi.x, scphi.y);
}

/*
ps_3_0
def c0, 2, -1, 0.5, 1
def c1, 6.28318548, -3.14159274, 1, 0
dcl_texcoord2 v0.xy
dcl_2d s0
texld_pp r0, v0, s0
mad_pp r0.xy, r0, c0.x, c0.y
mad r0.x, r0.x, c0.z, c0.z
frc r0.x, r0.x
mad r0.x, r0.x, c1.x, c1.y
sincos_pp r1.xy, r0.x
mad_pp r0.x, r0.y, -r0.y, c0.w
mul_pp oC0.zw, r0.y, c1
rsq_pp r0.x, r0.x
rcp_pp r0.x, r0.x
mul_pp oC0.xy, r1, r0.x
*/

/*
17 ALU, 1 TEX
Radeon HD 2400: 1 GPR, 17.00 clk
Radeon HD 3870: 1 GPR, 4.25 clk
Radeon HD 5870: 2 GPR, 0.95 clk
GeForce 6200: 2 GPR, 7.00 clk
GeForce 7800GT: 1 GPR, 5.00 clk
GeForce 8800GTX: 6 GPR, 23.00 clk
*/

Method #3: 球体贴图变换 - Spheremap Transform

球形环境映射(间接)将反射矢量映射到 [0..1] 范围内的纹理坐标。反射矢量可以指向远离相机的方向,就像我们的视图空间法线一样。

有关球体贴图数学,请参阅Siggraph 99 注释

正常情况下我们要编码的是R,结果值为(s,t)。

如果我们假设传入的法线是标准化的,那么从其他地方派生的方法最终会完全相同

  • 用于 Cry Engine 3,由 Martin Mittring 在“A bit more Deferred”演示文稿中介绍(PPT 链接,幻灯片 13)。对于 Unity,我必须取消视图空间法线的 Z 分量才能产生良好的结果,我猜 Unity 和 Cry Engine 的坐标系是不同的。代码是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    half2 encode (half3 n, float3 view)
    {
        half2 enc = normalize(n.xy) * (sqrt(-n.z*0.5+0.5));
        enc = enc*0.5+0.5;
        return enc;
    }
      
    half3 decode (half4 enc, float3 view)
    {
        half4 nn = enc*half4(2,2,0,0) + half4(-1,-1,1,-1);
        half l = dot(nn.xyz,-nn.xyw);
        nn.z = l;
        nn.xy *= sqrt(l);
        return nn.xyz * 2 + half3(0,0,-1);
    }
    
  • 兰伯特方位等积投影(维基百科链接)。由 Sean Barrett 在本文的评论中建议。代码是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    half2 encode (half3 n, float3 view)
    {
        half f = sqrt(8*n.z+8);
        return n.xy / f + 0.5;
    }
      
    half3 decode (half4 enc, float3 view)
    {
        half2 fenc = enc*4-2;
        half f = dot(fenc,fenc);
        half g = sqrt(1-f/4);
        half3 n;
        n.xy = fenc*g;
        n.z = 1-f/2;
        return n;
    }
    

均方误差:0.000016;峰值信噪比:48.071 分贝。

img img img

优点缺点
质量相当不错!
编解码效率高。
Cry Engine 3 也是采用该方式
???

Encoding

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
half4 encode (half3 n, float3 view)
{
    half p = sqrt(n.z*8+8);
    return half4(n.xy/p + 0.5,0,0);
}

/*
ps_3_0
def c0, 8, 0.5, 0, 0
dcl_texcoord_pp v0.xyz
mad_pp r0.x, v0.z, c0.x, c0.x
rsq_pp r0.x, r0.x
mad_pp oC0.xy, v0, r0.x, c0.y
mov_pp oC0.zw, c0.z
*/

/*
4 ALU
Radeon HD 2400: 2 GPR, 3.00 clk
Radeon HD 3870: 2 GPR, 1.00 clk
Radeon HD 5870: 2 GPR, 0.50 clk
GeForce 6200: 1 GPR, 4.00 clk
GeForce 7800GT: 1 GPR, 2.00 clk
GeForce 8800GTX: 5 GPR, 12.00 clk
*/

Decoding

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
half3 decode (half2 enc, float3 view)
{
    half2 fenc = enc*4-2;
    half f = dot(fenc,fenc);
    half g = sqrt(1-f/4);
    half3 n;
    n.xy = fenc*g;
    n.z = 1-f/2;
    return n;
}

/*
ps_3_0
def c0, 4, -2, 0, 1
def c1, 0.25, 0.5, 1, 0
dcl_texcoord2 v0.xy
dcl_2d s0
texld_pp r0, v0, s0
mad_pp r0.xy, r0, c0.x, c0.y
dp2add_pp r0.z, r0, r0, c0.z
mad_pp r0.zw, r0.z, -c1.xyxy, c1.z
rsq_pp r0.z, r0.z
mul_pp oC0.zw, r0.w, c0.xywz
rcp_pp r0.z, r0.z
mul_pp oC0.xy, r0, r0.z
*/

/*
8 ALU, 1 TEX
Radeon HD 2400: 2 GPR, 3.00 clk
Radeon HD 3870: 2 GPR, 1.00 clk
Radeon HD 5870: 2 GPR, 0.50 clk
GeForce 6200: 1 GPR, 6.00 clk
GeForce 7800GT: 1 GPR, 3.00 clk
GeForce 8800GTX: 6 GPR, 15.00 clk
*/

Method #4 立体投影 - Stereographic Projection

使用立体投影(维基百科链接),加上重新缩放,以便“实际可见”的法线范围映射到单位圆(常规立体投影将球体映射到无限大小的圆)。

在我的测试中,比例因子 $1.7777$ 产生了最佳结果;实际上,这取决于所使用的 FOV 以及您对远离相机的法线的关心程度。

均方误差:0.000038;峰值信噪比:44.147 分贝。

img img img

优点缺点
质量相当不错!
编解码效率高。
???

Encoding

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
half4 encode (half3 n, float3 view)
{
    half scale = 1.7777;
    half2 enc = n.xy / (n.z+1);
    enc /= scale;
    enc = enc*0.5+0.5;
    return half4(enc,0,0);
}

/*
ps_3_0
def c0, 1, 0.281262308, 0.5, 0
dcl_texcoord_pp v0.xyz
add_pp r0.x, c0.x, v0.z
rcp r0.x, r0.x
mul_pp r0.xy, r0.x, v0
mad_pp oC0.xy, r0, c0.y, c0.z
mov_pp oC0.zw, c0.w
*/

/*
5 ALU
Radeon HD 2400: 2 GPR, 4.00 clk
Radeon HD 3870: 2 GPR, 1.00 clk
Radeon HD 5870: 2 GPR, 0.50 clk
GeForce 6200: 1 GPR, 2.00 clk
GeForce 7800GT: 1 GPR, 2.00 clk
GeForce 8800GTX: 5 GPR, 12.00 clk
*/

Decoding

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
half3 decode (half4 enc, float3 view)
{
    half scale = 1.7777;
    half3 nn = enc.xyz*half3(2*scale,2*scale,0) + half3(-scale,-scale,1);
    half g = 2.0 / dot(nn.xyz,nn.xyz);
    half3 n;
    n.xy = g*nn.xy;
    n.z = g-1;
    return n;
}

/*
ps_3_0
def c0, 3.55539989, 0, -1.77769995, 1
def c1, 2, -1, 0, 0
dcl_texcoord2 v0.xy
dcl_2d s0
texld_pp r0, v0, s0
mad_pp r0.xyz, r0, c0.xxyw, c0.zzww
dp3_pp r0.z, r0, r0
rcp r0.z, r0.z
add_pp r0.w, r0.z, r0.z
mad_pp oC0.z, r0.z, c1.x, c1.y
mul_pp oC0.xy, r0, r0.w
mov_pp oC0.w, c0.y
*/

/*
7 ALU, 1 TEX
Radeon HD 2400: 2 GPR, 4.00 clk
Radeon HD 3870: 2 GPR, 1.00 clk
Radeon HD 5870: 2 GPR, 0.50 clk
GeForce 6200: 1 GPR, 4.00 clk
GeForce 7800GT: 1 GPR, 4.00 clk
GeForce 8800GTX: 6 GPR, 12.00 clk
*/

Method #5: 像素视图空间 - Per-pixel View Space

如果我们计算每个像素的视图空间,那么法线的 Z 分量永远不会为负。然后只需存储 X&Y,并计算 Z。

均方误差:0.000134;峰值信噪比:38.730 分贝。

img img img

Encoding

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
float3x3 make_view_mat (float3 view)
{
    view = normalize(view);
    float3 x,y,z;
    z = -view;
    x = normalize (float3(z.z, 0, -z.x));
    y = cross (z,x);
    return float3x3 (x,y,z);
}

half4 encode (half3 n, float3 view)
{
    return half4(mul (make_view_mat(view), n).xy*0.5+0.5,0,0);
}

/*
ps_3_0
def c0, 1, -1, 0, 0.5
dcl_texcoord_pp v0.xyz
dcl_texcoord1 v1.xyz
mov r0.x, c0.z
nrm r1.xyz, v1
mov r1.w, -r1.z
mul r0.yz, r1.xxzw, c0.xxyw
dp2add r0.w, r1.wxzw, r0.zyzw, c0.z
rsq r0.w, r0.w
mul r0.xyz, r0, r0.w
mul r2.xyz, -r1.zxyw, r0
mad r1.xyz, -r1.yzxw, r0.yzxw, -r2
dp2add r0.x, r0.zyzw, v0.xzzw, c0.z
dp3 r0.y, r1, v0
mad_pp oC0.xy, r0, c0.w, c0.w
mov_pp oC0.zw, c0.z
*/

/*
17 ALU
Radeon HD 2400: 3 GPR, 11.00 clk
Radeon HD 3870: 3 GPR, 2.75 clk
Radeon HD 5870: 2 GPR, 0.80 clk
GeForce 6200: 4 GPR, 12.00 clk
GeForce 7800GT: 4 GPR, 8.00 clk
GeForce 8800GTX: 8 GPR, 24.00 clk
*/

Decoding

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
half3 decode (half4 enc, float3 view)
{
    half3 n;
    n.xy = enc*2-1;
    n.z = sqrt(1+dot(n.xy,-n.xy));
    n = mul(n, make_view_mat(view));
    return n;
}

/*
ps_3_0
def c0, 2, -1, 1, 0
dcl_texcoord1 v0.xyz
dcl_texcoord2 v1.xy
dcl_2d s0
mov r0.y, c0.w
nrm r1.xyz, v0
mov r1.w, -r1.z
mul r0.xz, r1.zyxw, c0.yyzw
dp2add r0.w, r1.wxzw, r0.xzzw, c0.w
rsq r0.w, r0.w
mul r0.xyz, r0, r0.w
mul r2.xyz, -r1.zxyw, r0.yzxw
mad r2.xyz, -r1.yzxw, r0.zxyw, -r2
texld_pp r3, v1, s0
mad_pp r3.xy, r3, c0.x, c0.y
mul r2.xyz, r2, r3.y
mad r0.xyz, r3.x, r0, r2
dp2add_pp r0.w, r3, -r3, c0.z
rsq_pp r0.w, r0.w
rcp_pp r0.w, r0.w
mad_pp oC0.xyz, r0.w, -r1, r0
mov_pp oC0.w, c0.w
*/

/*
21 ALU, 1 TEX
Radeon HD 2400: 3 GPR, 11.00 clk
Radeon HD 3870: 3 GPR, 2.75 clk
Radeon HD 5870: 2 GPR, 0.80 clk
GeForce 6200: 3 GPR, 12.00 clk
GeForce 7800GT: 3 GPR, 9.00 clk
GeForce 8800GTX: 12 GPR, 29.00 clk
*/

性能对比

 #1: X & Y#2: Spherical#3: Spheremap#4: Stereo#5: PPView
Encoding, GPU cycles     
Radeon HD24001.0017.003.004.0011.00
Radeon HD58700.500.950.500.500.80
GeForce 62001.0012.004.002.0012.00
GeForce 88007.0043.00*12.00*12.0024.00
Decoding, GPU cycles     
Radeon HD24001.0017.003.004.0011.00
Radeon HD58700.500.950.501.000.80
GeForce 62004.007.006.004.0012.00
GeForce 8800*15.00*23.0015.0012.0029.00
Encoding, D3D ALU+TEX instruction slots     
SM3.01264517
Decoding, D3D ALU+TEX instruction slots     
SM3.08189822

质量对比

PSNR based, higher numbers are better.

MethodPSNR, dB
#1: X & Y18.629
#2: Spherical42.042
#3: Spheremap48.071
#4: Stereographic44.147
#5: Per pixel view38.730

《王牌竞速》- GDC

>>Achieving High-Quality 90fps Realism in a Mobile Game, Technically and Visually

我们在车辆上使用SH进行镜面反射,并使用IBL纹理进行环境反射。我们还使用光照贴图来存储场景的全局光照,我们烘焙了自动曝光数据。我们还烘焙了潜在的可见性数据,这是本次演讲中没有提到的,但我们确实有这么做。很明显,所有这些技术的关键词都是烘焙

  • SH lighting on vehicles
  • IBL textures
  • light map in scenes
  • eye adaption data
  • PVS data

PBR渲染共有三张贴图,我们在思考如何处理它们。

PBR参数是相关的,这种相关性是冗余数据。如果可以减少这些数据,我们就可以节省空间。

我们决定使用PCA来减少这种相关性。这里我将通过简单的二维例子演示PCA是如何工作的。X、Y平面上有很多红点,我们需要用二维值X和Y来表示它们。如果我们能找到一条穿过所有红点的直线,那么我们只需要一个维值T表示所有的红点,因为它们都在同一条线上。PCA的工作是找到穿过所有点的最佳直线。回到我们的问题。我们可以将PBR纹理表示为9维向量,而PCA的工作就是找到一个四维平面来拟合9维点。但与参考相比,PCA的效果更亮一些,所以我们增加了权重。我们给输入添加权重并使用机器学习来确定权重。最后,通过使用一种压缩纹理,我们获得了与PBR参考相同的结果。

17129051487434ccb6c85037a3fca74c1caf7ee0fd42.png

17129054047401712905404426.png

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