Code that approximates the GGX lookup table?

Does anyone have a GLSL function that will approximate the contents of this image?

I feel like having a texture lookup that uses texcoords resulting from another texture lookup is something to be avoided when possible.

I think it might come from here?

The green channel (“bias”) is pretty challenging to approximate analytically, but many attempts have been made. I personally feel that Knarkowicz’s polynomial approach is quite good in terms of the ALU vs. accuracy tradeoff. Karis’s mobile optimized versions are good if you need something dirt-cheap. Both build on prior work by Lazarov, and both have been ported to GLSL in this Shadertoy, though you might want to play with the roughness-gloss remapping in the Knarkowicz port. Finally, if you want to burn even more ALU for marginal improvements, I created a degree-3 polynomial approximation of my own. I don’t recommend the latter for production work.

Wow, thank you for the detailed answer. My thinking here is that:

  1. My users do not care if lighting is physically accurate, they just want it to look good. The two features I can point out that are actually noticeable in “PBR” lighting is the diffuse term is a lot sharper than a simple dot product, and the long specular reflections.
  2. The GGX table isn’t necessarily accurate anyways, since each material should have its own BRDF.

I will definitely look more into these articles. Thanks again!

Agreed on all points. Subjective image quality is always paramount. Many resources on PBR place an emphasis on physical plausibility, versatility, and artistic control because they make authoring of assets easier and more consistent, but of course every model has its limits.

Regarding the specular lobe in particular, the UE4 split-sum approximation has long been known to temper stretched specular reflections (because of its assumption of isotropy of the BRDF with respect to viewing angle), but we make these tradeoffs because they give plausible results cheaply enough to be used in real-time rendering.

I tried the first two methods linked to above. Performance on an AMD 280 (released in 2014) at 1920x1080 was indistinguishable from the texture-based approach. So I think I probably should not worry about it.

Are you the Warren who used to hang out in the Blitz3D forums?

//---------------------------------------------------------------
// Specular Image-based Lighting
//---------------------------------------------------------------

#define ALGOBRDF

#ifdef ALGOBRDF

// https://knarkowicz.wordpress.com/2014/12/27/analytical-dfg-term-for-ibl/
vec3 EnvDFGPolynomial(in vec3 specularColor, float roughness, float ndotv)
{
    float x = 1.0 - roughness;
    float y = ndotv;
    
    float b1 = -0.1688;
    float b2 = 1.895;
    float b3 = 0.9903;
    float b4 = -4.853;
    float b5 = 8.404;
    float b6 = -5.069;
    float bias = clamp( min( b1 * x + b2 * x * x, b3 + b4 * y + b5 * y * y + b6 * y * y * y ), 0.0, 1.0);
    
    float d0 = 0.6045;
    float d1 = 1.699;
    float d2 = -0.5228;
    float d3 = -3.603;
    float d4 = 1.404;
    float d5 = 0.1939;
    float d6 = 2.661;
    float delta = clamp( d0 + d1 * x + d2 * y + d3 * x * x + d4 * x * y + d5 * y * y + d6 * x * x * x, 0.0, 1.0);
    float scale = delta - bias;
    
    bias *= clamp( 50.0 * specularColor.y, 0.0, 1.0);
    return specularColor * scale + bias;
}
 
//https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile
vec3 EnvBRDFApprox( vec3 SpecularColor, float Roughness, float NoV )
{
	const vec4 c0 = { -1, -0.0275, -0.572, 0.022 };
	const vec4 c1 = { 1, 0.0425, 1.04, -0.04 };
	vec4 r = Roughness * c0 + c1;
	float a004 = min( r.x * r.x, exp2( -9.28 * NoV ) ) * r.x + r.y;
	vec2 AB = vec2( -1.04, 1.04 ) * a004 + r.zw;
	return SpecularColor * AB.x + AB.y;
}

#endif

vec3 getIBLRadianceGGX(in sampler2D u_GGXLUT, in vec3 specularSample, vec3 n, vec3 v, float roughness, vec3 F0, float specularWeight)
{
    float NdotV = clampedDot(n, v);
#ifdef ALGOBRDF
    return EnvBRDFApprox(specularSample, roughness, NdotV) * specularWeight;
    //return EnvDFGPolynomial(specularSample, roughness, NdotV) * specularWeight;
#else
    vec3 reflection = normalize(reflect(-v, n));
    vec2 brdfSamplePoint = clamp(vec2(NdotV, roughness), vec2(0.0, 0.0), vec2(1.0, 1.0));
    vec2 f_ab = texture(u_GGXLUT, brdfSamplePoint).rg;
    //vec4 specularSample = getSpecularSample(u_GGXEnvSampler, reflection, lod);

    vec3 specularLight = specularSample.rgb;

    // see https://bruop.github.io/ibl/#single_scattering_results at Single Scattering Results
    // Roughness dependent fresnel, from Fdez-Aguera
    vec3 Fr = max(vec3(1.0 - roughness), F0) - F0;
    vec3 k_S = F0 + Fr * pow(1.0 - NdotV, 5.0);
    vec3 FssEss = k_S * f_ab.x + f_ab.y;

    return specularWeight * specularLight * FssEss;
#endif
}

That must’ve been a different Warren; I’ve only visited the Blitz3D forums during Google searches.

Regarding your results, you might be benefiting from latency hiding when using a LUT in spite of it being a dependent read. Even if you have high occupancy, maybe the relatively slower ALU of those older GCN units makes it a wash. Was worth investigating, I think.

1 Like