前言

GGX作为描述高光形状的函数,非常重要,但同时它也比较消耗性能,适合PC端使用,虽然Walter也提出一种适合移动端的GGX,但并没有解决fp16精度下导致的问题

本文将介绍Walter GGX存在的问题,并分享一种可以解决Walter GGX问题的GGX算法

三种GGX

  • 最经典的GGX实现如下:
    // GGX / Trowbridge-Reitz
    // [Walter et al. 2007, "Microfacet models for refraction through rough surfaces"]
    float D_GGX(float a2, float NoH)
    {
      float d = (NoH * a2 - NoH) * NoH + 1.f; // 2 mad
      return a2 / (PI * d * d + 1e-4);        // 4 mul, 1 rcp
    }
    
    • 特点

    • 利用指令优化。通过多项式展开规避了标准公式中的 (1−NoH2) 这种需要多次减法的运算

    • 缺点

    • α2依赖高精度。当 a2 非常小,分母的 d 会趋向于 0,若是fp16,会有可能得到NaN

      当roughness非常小如0.01,a2 = 0.0001,且normal与half vec刚好对齐——NoH = 1,d = 0.0001,分母 = $3.14 * 10^{-8}$

      问题就在于分母,由于fp16的最小值为6.1×10−5,分母已经远远超过fp16的精度范围

  • Walter Mobile GGX

    float D_GGX_Mobile(float Roughness, float NoH)
    {
      // Walter et al. 2007, "Microfacet Models for Refraction through Rough Surfaces"
      float OneMinusNoHSqr = 1.0 - NoH * NoH;
      float a = Roughness * Roughness;
      float n = NoH * a;
      float p = a / (OneMinusNoHSqr + n * n);
      float d = (1.0 / PI) * p * p;
      // clamp to avoid overlfow in a bright env
      return min(d, 2048.0);
    }
    
    • 特点:针对FP16做了保护低精度溢出的设计

    • 缺点:牺牲了高光细节

    因为做了min(d, 2048.0)

  • Filament GGX

    #define MEDIUMP_FLT_MAX    65504.0
    #define saturateMediump(x) min(x, MEDIUMP_FLT_MAX)
    
    float D_GGX(float roughness, float NoH, const vec3 n, const vec3 h) {
      vec3 NxH = cross(n, h);
      float a = NoH * roughness;
      float k = roughness / (dot(NxH, NxH) + a * a);
      float d = k * k * (1.0 / PI);
      return saturateMediump(d);
    }
    
    • 为什么cross可以代替1- NoH^2

    使用到了拉格朗日等式:|a×b|^2 = |a|^2|b|^2 - (a·b)^2

    又因为a、b都是单位向量,那么|a×b|^2 = 1 - (a·b)^2

    • 特点:支持FP16,且保留高光细节

    • 缺点:计算消耗比Walter Mobile GGX略高一些

总结

方案 优点 缺点
Walter GGX 速度快、高光细节高 fp16下高光不稳定
Walter Mobile GGX 比GGX性能更好 fp16下损失高光细节
Filament GGX fp16下保留高光细节且稳定 性能比Walter Mobile GGX略高

Reference

Physically Based Rendering in Filament


他们曾如此骄傲的活过,贯彻始终