Skip to content

Update specular BRDF for image-based lighting #12083

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Updated geometric self-shadowing function to improve direct lighting on models using physically-based rendering. [#12063](https://github.com/CesiumGS/cesium/pull/12063)
- Fixed environment map LOD selection in image-based lighting. [#12070](https://github.com/CesiumGS/cesium/pull/12070)
- Corrected calculation of diffuse component in image-based lighting. [#12082](https://github.com/CesiumGS/cesium/pull/12082)
- Updated specular BRDF for image-based lighting. [#12083](https://github.com/CesiumGS/cesium/pull/12083)

### 1.119 - 2024-07-01

Expand Down
43 changes: 30 additions & 13 deletions packages/engine/Source/Shaders/BrdfLutGeneratorFS.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ vec2 hammersley2D(int i, int N)
return vec2(float(i) / float(N), vdcRadicalInverse(i));
}

vec3 importanceSampleGGX(vec2 xi, float roughness, vec3 N)
vec3 importanceSampleGGX(vec2 xi, float alphaRoughness, vec3 N)
{
float a = roughness * roughness;
float alphaRoughnessSquared = alphaRoughness * alphaRoughness;
float phi = 2.0 * M_PI * xi.x;
float cosTheta = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y));
float cosTheta = sqrt((1.0 - xi.y) / (1.0 + (alphaRoughnessSquared - 1.0) * xi.y));
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);
vec3 upVector = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
Expand All @@ -40,15 +40,31 @@ vec3 importanceSampleGGX(vec2 xi, float roughness, vec3 N)
return tangentX * H.x + tangentY * H.y + N * H.z;
}

float G1_Smith(float NdotV, float k)
/**
* Estimate the geometric self-shadowing of the microfacets in a surface,
* using the Smith Joint GGX visibility function.
* Note: Vis = G / (4 * NdotL * NdotV)
* see Eric Heitz. 2014. Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs. Journal of Computer Graphics Techniques, 3
* see Real-Time Rendering. Page 331 to 336.
* see https://google.github.io/filament/Filament.md.html#materialsystem/specularbrdf/geometricshadowing(specularg)
*
* @param {float} alphaRoughness The roughness of the material, expressed as the square of perceptual roughness.
* @param {float} NdotL The cosine of the angle between the surface normal and the direction to the light source.
* @param {float} NdotV The cosine of the angle between the surface normal and the direction to the camera.
*/
float smithVisibilityGGX(float alphaRoughness, float NdotL, float NdotV)
{
return NdotV / (NdotV * (1.0 - k) + k);
}
float alphaRoughnessSq = alphaRoughness * alphaRoughness;

float G_Smith(float roughness, float NdotV, float NdotL)
{
float k = roughness * roughness / 2.0;
return G1_Smith(NdotV, k) * G1_Smith(NdotL, k);
float GGXV = NdotL * sqrt(NdotV * NdotV * (1.0 - alphaRoughnessSq) + alphaRoughnessSq);
float GGXL = NdotV * sqrt(NdotL * NdotL * (1.0 - alphaRoughnessSq) + alphaRoughnessSq);

float GGX = GGXV + GGXL; // 2.0 if NdotL = NdotV = 1.0
if (GGX > 0.0)
{
return 0.5 / GGX; // 1/4 if NdotL = NdotV = 1.0
}
return 0.0;
}

vec2 integrateBrdf(float roughness, float NdotV)
Expand All @@ -57,18 +73,19 @@ vec2 integrateBrdf(float roughness, float NdotV)
float A = 0.0;
float B = 0.0;
const int NumSamples = 1024;
float alphaRoughness = roughness * roughness;
for (int i = 0; i < NumSamples; i++)
{
vec2 xi = hammersley2D(i, NumSamples);
vec3 H = importanceSampleGGX(xi, roughness, vec3(0.0, 0.0, 1.0));
vec3 H = importanceSampleGGX(xi, alphaRoughness, vec3(0.0, 0.0, 1.0));
vec3 L = 2.0 * dot(V, H) * H - V;
float NdotL = clamp(L.z, 0.0, 1.0);
float NdotH = clamp(H.z, 0.0, 1.0);
float VdotH = clamp(dot(V, H), 0.0, 1.0);
if (NdotL > 0.0)
{
float G = G_Smith(roughness, NdotV, NdotL);
float G_Vis = G * VdotH / (NdotH * NdotV);
float G = smithVisibilityGGX(alphaRoughness, NdotL, NdotV);
float G_Vis = 4.0 * G * VdotH * NdotL / NdotH;
float Fc = pow(1.0 - VdotH, 5.0);
A += (1.0 - Fc) * G_Vis;
B += Fc * G_Vis;
Expand Down
43 changes: 22 additions & 21 deletions packages/engine/Source/Shaders/Builtin/Functions/pbrLighting.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,29 @@ vec3 fresnelSchlick2(vec3 f0, vec3 f90, float VdotH)

#ifdef USE_ANISOTROPY
/**
* @param {float} roughness Material roughness (along the anisotropy bitangent)
* @param {float} bitangentRoughness Material roughness (along the anisotropy bitangent)
* @param {float} tangentialRoughness Anisotropic roughness (along the anisotropy tangent)
* @param {vec3} lightDirection The direction from the fragment to the light source, transformed to tangent-bitangent-normal coordinates
* @param {vec3} viewDirection The direction from the fragment to the camera, transformed to tangent-bitangent-normal coordinates
*/
float smithVisibilityGGX_anisotropic(float roughness, float tangentialRoughness, vec3 lightDirection, vec3 viewDirection)
float smithVisibilityGGX_anisotropic(float bitangentRoughness, float tangentialRoughness, vec3 lightDirection, vec3 viewDirection)
{
vec3 roughnessScale = vec3(tangentialRoughness, roughness, 1.0);
vec3 roughnessScale = vec3(tangentialRoughness, bitangentRoughness, 1.0);
float GGXV = lightDirection.z * length(roughnessScale * viewDirection);
float GGXL = viewDirection.z * length(roughnessScale * lightDirection);
float v = 0.5 / (GGXV + GGXL);
return clamp(v, 0.0, 1.0);
}

/**
* @param {float} roughness Material roughness (along the anisotropy bitangent)
* @param {float} bitangentRoughness Material roughness (along the anisotropy bitangent)
* @param {float} tangentialRoughness Anisotropic roughness (along the anisotropy tangent)
* @param {vec3} halfwayDirection The unit vector halfway between light and view directions, transformed to tangent-bitangent-normal coordinates
*/
float GGX_anisotropic(float roughness, float tangentialRoughness, vec3 halfwayDirection)
float GGX_anisotropic(float bitangentRoughness, float tangentialRoughness, vec3 halfwayDirection)
{
float roughnessSquared = roughness * tangentialRoughness;
vec3 f = halfwayDirection * vec3(roughness, tangentialRoughness, roughnessSquared);
float roughnessSquared = bitangentRoughness * tangentialRoughness;
vec3 f = halfwayDirection * vec3(bitangentRoughness, tangentialRoughness, roughnessSquared);
float w2 = roughnessSquared / dot(f, f);
return roughnessSquared * w2 * w2 / czm_pi;
}
Expand Down Expand Up @@ -73,15 +73,15 @@ float smithVisibilityGGX(float alphaRoughness, float NdotL, float NdotV)
* the halfway vector, which is aligned halfway between the directions from
* the fragment to the camera and from the fragment to the light source.
*
* @param {float} roughness The roughness of the material.
* @param {float} alphaRoughness The roughness of the material, expressed as the square of perceptual roughness.
* @param {float} NdotH The cosine of the angle between the surface normal and the halfway vector.
* @return {float} The fraction of microfacets aligned to the halfway vector.
*/
float GGX(float roughness, float NdotH)
float GGX(float alphaRoughness, float NdotH)
{
float roughnessSquared = roughness * roughness;
float f = (NdotH * roughnessSquared - NdotH) * NdotH + 1.0;
return roughnessSquared / (czm_pi * f * f);
float alphaRoughnessSquared = alphaRoughness * alphaRoughness;
float f = (NdotH * alphaRoughnessSquared - NdotH) * NdotH + 1.0;
return alphaRoughnessSquared / (czm_pi * f * f);
}

/**
Expand All @@ -91,16 +91,16 @@ float GGX(float roughness, float NdotH)
* @param {vec3} lightDirection The unit vector pointing from the fragment to the light source.
* @param {vec3} viewDirection The unit vector pointing from the fragment to the camera.
* @param {vec3} halfwayDirection The unit vector pointing from the fragment to halfway between the light source and the camera.
* @param {float} roughness The roughness of the material.
* @param {float} alphaRoughness The roughness of the material, expressed as the square of perceptual roughness.
* @return {float} The strength of the specular reflection.
*/
float computeDirectSpecularStrength(vec3 normal, vec3 lightDirection, vec3 viewDirection, vec3 halfwayDirection, float roughness)
float computeDirectSpecularStrength(vec3 normal, vec3 lightDirection, vec3 viewDirection, vec3 halfwayDirection, float alphaRoughness)
{
float NdotL = dot(normal, lightDirection);
float NdotV = abs(dot(normal, viewDirection));
float G = smithVisibilityGGX(roughness, NdotL, NdotV);
float G = smithVisibilityGGX(alphaRoughness, NdotL, NdotV);
float NdotH = clamp(dot(normal, halfwayDirection), 0.0, 1.0);
float D = GGX(roughness, NdotH);
float D = GGX(alphaRoughness, NdotH);
return G * D;
}

Expand Down Expand Up @@ -138,19 +138,20 @@ vec3 czm_pbrLighting(vec3 viewDirectionEC, vec3 normalEC, vec3 lightDirectionEC,
F *= material.specularWeight;
#endif

float alpha = material.roughness;
float alphaRoughness = material.roughness * material.roughness;
#ifdef USE_ANISOTROPY
mat3 tbn = mat3(material.anisotropicT, material.anisotropicB, normalEC);
vec3 lightDirection = lightDirectionEC * tbn;
vec3 viewDirection = viewDirectionEC * tbn;
vec3 halfwayDirection = halfwayDirectionEC * tbn;
float anisotropyStrength = material.anisotropyStrength;
float tangentialRoughness = mix(alpha, 1.0, anisotropyStrength * anisotropyStrength);
float G = smithVisibilityGGX_anisotropic(alpha, tangentialRoughness, lightDirection, viewDirection);
float D = GGX_anisotropic(alpha, tangentialRoughness, halfwayDirection);
float tangentialRoughness = mix(alphaRoughness, 1.0, anisotropyStrength * anisotropyStrength);
float bitangentRoughness = clamp(alphaRoughness, 0.001, 1.0);
float G = smithVisibilityGGX_anisotropic(bitangentRoughness, tangentialRoughness, lightDirection, viewDirection);
float D = GGX_anisotropic(bitangentRoughness, tangentialRoughness, halfwayDirection);
vec3 specularContribution = F * G * D;
#else
float specularStrength = computeDirectSpecularStrength(normalEC, lightDirectionEC, viewDirectionEC, halfwayDirectionEC, alpha);
float specularStrength = computeDirectSpecularStrength(normalEC, lightDirectionEC, viewDirectionEC, halfwayDirectionEC, alphaRoughness);
vec3 specularContribution = F * specularStrength;
#endif

Expand Down
27 changes: 14 additions & 13 deletions packages/engine/Source/Shaders/Model/ImageBasedLightingStageFS.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ vec3 getProceduralSkyMetrics(vec3 positionWC, vec3 reflectionWC)
}

/**
* Compute the diffuse irradiance for a procedural sky lighting model
* Compute the diffuse irradiance for a procedural sky lighting model.
*
* @param {vec3} skyMetrics The dot products of the horizon and reflection directions with the nadir, and an atmosphere boundary distance.
* @return {vec3} The computed diffuse irradiance
* @return {vec3} The computed diffuse irradiance.
*/
vec3 getProceduralDiffuseIrradiance(vec3 skyMetrics)
{
Expand All @@ -30,10 +30,12 @@ vec3 getProceduralDiffuseIrradiance(vec3 skyMetrics)
}

/**
* Compute the specular irradiance for a procedural sky lighting model
* Compute the specular irradiance for a procedural sky lighting model.
*
* @param {vec3} reflectionWC The reflection vector in world coordinates.
* @param {vec3} skyMetrics The dot products of the horizon and reflection directions with the nadir, and an atmosphere boundary distance.
* @return {vec3} The computed specular irradiance
* @param {float} roughness The roughness of the material.
* @return {vec3} The computed specular irradiance.
*/
vec3 getProceduralSpecularIrradiance(vec3 reflectionWC, vec3 skyMetrics, float roughness)
{
Expand All @@ -42,7 +44,7 @@ vec3 getProceduralSpecularIrradiance(vec3 reflectionWC, vec3 skyMetrics, float r
reflectionWC = -normalize(czm_temeToPseudoFixed * reflectionWC);
reflectionWC.x = -reflectionWC.x;

float inverseRoughness = 1.04 - roughness;
float inverseRoughness = 1.0 - roughness;
inverseRoughness *= inverseRoughness;
vec3 sceneSkyBox = czm_textureCube(czm_environmentMap, reflectionWC).rgb * inverseRoughness;

Expand Down Expand Up @@ -173,11 +175,12 @@ vec3 sampleSpecularEnvironment(vec3 cubeDir, float roughness)
return czm_sampleOctahedralProjection(czm_specularEnvironmentMaps, czm_specularEnvironmentMapSize, cubeDir, lod, maxLod);
#endif
}
vec3 computeSpecularIBL(vec3 cubeDir, float NdotV, float VdotH, vec3 f0, float roughness)
vec3 computeSpecularIBL(vec3 cubeDir, float NdotV, vec3 f0, float roughness)
{
float reflectance = czm_maximumComponent(f0);
vec3 f90 = vec3(clamp(reflectance * 25.0, 0.0, 1.0));
vec3 F = fresnelSchlick2(f0, f90, VdotH);
// see https://bruop.github.io/ibl/ at Single Scattering Results
// Roughness dependent fresnel, from Fdez-Aguera
vec3 f90 = max(vec3(1.0 - roughness), f0);
vec3 F = fresnelSchlick2(f0, f90, NdotV);

vec2 brdfLut = texture(czm_brdfLut, vec2(NdotV, roughness)).rg;
vec3 specularSample = sampleSpecularEnvironment(cubeDir, roughness);
Expand Down Expand Up @@ -224,11 +227,9 @@ vec3 textureIBL(

#ifdef SPECULAR_IBL
vec3 reflectMC = normalize(model_iblReferenceFrameMatrix * reflectEC);
float NdotV = abs(dot(normalEC, viewDirectionEC)) + 0.001;
vec3 halfwayDirectionEC = normalize(viewDirectionEC + lightDirectionEC);
float VdotH = clamp(dot(viewDirectionEC, halfwayDirectionEC), 0.0, 1.0);
float NdotV = clamp(dot(normalEC, viewDirectionEC), 0.0, 1.0);
vec3 f0 = material.specular;
vec3 specularContribution = computeSpecularIBL(reflectMC, NdotV, VdotH, f0, material.roughness);
vec3 specularContribution = computeSpecularIBL(reflectMC, NdotV, f0, material.roughness);
#else
vec3 specularContribution = vec3(0.0);
#endif
Expand Down
5 changes: 3 additions & 2 deletions packages/engine/Source/Shaders/Model/LightingStageFS.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ vec3 addClearcoatReflection(vec3 baseLayerColor, vec3 position, vec3 lightDirect

// compute specular reflection from direct lighting
float roughness = material.clearcoatRoughness;
float directStrength = computeDirectSpecularStrength(normal, lightDirection, viewDirection, halfwayDirection, roughness);
float alphaRoughness = roughness * roughness;
float directStrength = computeDirectSpecularStrength(normal, lightDirection, viewDirection, halfwayDirection, alphaRoughness);
vec3 directReflection = F * directStrength * NdotL;
vec3 color = lightColorHdr * directReflection;

#ifdef SPECULAR_IBL
// Find the direction in which to sample the environment map
vec3 reflectMC = normalize(model_iblReferenceFrameMatrix * reflect(-viewDirection, normal));
vec3 iblColor = computeSpecularIBL(reflectMC, NdotV, NdotV, f0, roughness);
vec3 iblColor = computeSpecularIBL(reflectMC, NdotV, f0, roughness);
color += iblColor * material.occlusion;
#elif defined(USE_IBL_LIGHTING)
vec3 positionWC = vec3(czm_inverseView * vec4(position, 1.0));
Expand Down
17 changes: 7 additions & 10 deletions packages/engine/Source/Shaders/Model/MaterialStageFS.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,7 @@ void setSpecularGlossiness(inout czm_modelMaterial material)
material.specular = specular;

// glossiness is the opposite of roughness, but easier for artists to use.
float roughness = 1.0 - glossiness;
material.roughness = roughness * roughness;
material.roughness = 1.0 - glossiness;
}
#elif defined(LIGHTING_PBR)
float setMetallicRoughness(inout czm_modelMaterial material)
Expand All @@ -272,7 +271,7 @@ float setMetallicRoughness(inout czm_modelMaterial material)

vec3 metallicRoughness = texture(u_metallicRoughnessTexture, metallicRoughnessTexCoords).rgb;
float metalness = clamp(metallicRoughness.b, 0.0, 1.0);
float roughness = clamp(metallicRoughness.g, 0.04, 1.0);
float roughness = clamp(metallicRoughness.g, 0.0, 1.0);
#ifdef HAS_METALLIC_FACTOR
metalness = clamp(metalness * u_metallicFactor, 0.0, 1.0);
#endif
Expand All @@ -288,7 +287,7 @@ float setMetallicRoughness(inout czm_modelMaterial material)
#endif

#ifdef HAS_ROUGHNESS_FACTOR
float roughness = clamp(u_roughnessFactor, 0.04, 1.0);
float roughness = clamp(u_roughnessFactor, 0.0, 1.0);
#else
float roughness = 1.0;
#endif
Expand All @@ -303,9 +302,8 @@ float setMetallicRoughness(inout czm_modelMaterial material)
// diffuse only applies to dielectrics.
material.diffuse = mix(material.baseColor.rgb, vec3(0.0), metalness);

// roughness is authored as perceptual roughness
// square it to get material roughness
material.roughness = roughness * roughness;
// This is perceptual roughness. The square of this value is used for direct lighting
material.roughness = roughness;

return metalness;
}
Expand Down Expand Up @@ -418,9 +416,8 @@ void setClearcoat(inout czm_modelMaterial material, in ProcessedAttributes attri
#endif

material.clearcoatFactor = clearcoatFactor;
// roughness is authored as perceptual roughness
// square it to get material roughness
material.clearcoatRoughness = clearcoatRoughness * clearcoatRoughness;
// This is perceptual roughness. The square of this value is used for direct lighting
material.clearcoatRoughness = clearcoatRoughness;
#ifdef HAS_CLEARCOAT_NORMAL_TEXTURE
material.clearcoatNormal = getClearcoatNormalFromTexture(attributes, attributes.normalEC);
#else
Expand Down