improved the dispersion computation (#9544)

- Split the function so that the 3-components loop is more apparent.
- Factored out the parts of the computation that don't depend on the
index of refraction, so they're only computed once.
- Moved the lod-from-roughness computation out of the loop as well. 
  It does depend on the IOR, but so little that it's not worth it.

The most important result of this change is that the code is now in
good shape if we wanted to change the implementation of dispersion for
these cases (for e.g.) :
- do the computation in the XYZ space
- use more than 3 wavelengths samples
- or use a stochastic approach, trading banding for noise

RDIFF_BRANCH=ma/better-dispersion
This commit is contained in:
Mathias Agopian
2026-01-05 20:27:34 -08:00
committed by GitHub
parent bcb9d258c2
commit 5661365248

View File

@@ -119,8 +119,7 @@ float perceptualRoughnessToLod(float perceptualRoughness) {
return frameUniforms.iblRoughnessOneLevel * perceptualRoughness * (2.0 - perceptualRoughness);
}
vec3 prefilteredRadiance(const vec3 r, float perceptualRoughness) {
float lod = perceptualRoughnessToLod(perceptualRoughness);
vec3 prefilteredRadiance(const vec3 r, float lod) {
return decodeDataForIBL(textureLod(sampler0_iblSpecular, r, lod));
}
@@ -391,7 +390,8 @@ void evaluateSheenIBL(const PixelParams pixel, float diffuseAO,
vec3 reflectance = pixel.sheenDFG * pixel.sheenColor;
reflectance *= specularAO(shading_NoV, diffuseAO, pixel.sheenRoughness, cache);
Fr += reflectance * prefilteredRadiance(shading_reflected, pixel.sheenPerceptualRoughness);
Fr += reflectance * prefilteredRadiance(shading_reflected,
perceptualRoughnessToLod(pixel.sheenPerceptualRoughness));
#endif
#endif
}
@@ -422,7 +422,8 @@ void evaluateClearCoatIBL(const PixelParams pixel, float diffuseAO,
// TODO: Should we apply specularAO to the attenuation as well?
float specularAO = specularAO(clearCoatNoV, diffuseAO, pixel.clearCoatRoughness, cache);
Fr += prefilteredRadiance(clearCoatR, pixel.clearCoatPerceptualRoughness) * (specularAO * Fc);
Fr += prefilteredRadiance(clearCoatR,
perceptualRoughnessToLod(pixel.clearCoatPerceptualRoughness)) * (specularAO * Fc);
}
#endif
}
@@ -448,7 +449,7 @@ struct Refraction {
};
void refractionSolidSphere(float etaIR, float etaRI, float thickness,
const vec3 n, vec3 r, out Refraction ray) {
const vec3 n, vec3 r, out Refraction ray) {
r = refract(r, n, etaIR);
float NoR = dot(n, r);
float d = thickness * -NoR;
@@ -459,7 +460,7 @@ void refractionSolidSphere(float etaIR, float etaRI, float thickness,
}
void refractionSolidBox(float etaIR, float thickness,
const vec3 n, vec3 r, out Refraction ray) {
const vec3 n, vec3 r, out Refraction ray) {
vec3 rr = refract(r, n, etaIR);
float NoR = dot(n, rr);
float d = thickness / max(-NoR, 0.001);
@@ -474,7 +475,7 @@ void refractionSolidBox(float etaIR, float thickness,
}
void refractionThinSphere(float etaIR, float uThickness,
const vec3 n, vec3 r, out Refraction ray) {
const vec3 n, vec3 r, out Refraction ray) {
float d = 0.0;
#if defined(MATERIAL_HAS_MICRO_THICKNESS)
// note: we need the refracted ray to calculate the distance traveled
@@ -490,90 +491,94 @@ void refractionThinSphere(float etaIR, float uThickness,
ray.d = d;
}
vec3 evaluateRefraction(
const PixelParams pixel,
const vec3 n0, vec3 E) {
vec3 evaluateRefraction(const PixelParams pixel, const vec3 n0, const float lod,
const float etaIR, const float etaRI) {
#if REFRACTION_TYPE == REFRACTION_TYPE_THIN
// For thin surfaces, the light will bounce off at the second interface in the direction of
// the reflection, effectively adding to the specular, but this process will repeat itself.
// Each time the ray exits the surface on the front side after the first bounce,
// it's multiplied by E^2, and we get: E + E(1-E)^2 + E^3(1-E)^2 + ...
// This infinite series converges and is easy to simplify.
// Note: we calculate these bounces only on a single component,
// since it's a fairly subtle effect.
E *= 1.0 + pixel.transmission * (1.0 - E.g) / (1.0 + E.g);
Refraction ray;
#if REFRACTION_TYPE == REFRACTION_TYPE_SOLID
refractionSolidSphere(etaIR, etaRI, pixel.thickness, n0, -shading_view, ray);
#elif REFRACTION_TYPE == REFRACTION_TYPE_THIN
refractionThinSphere(etaIR, pixel.uThickness, n0, -shading_view, ray);
#else
# error invalid REFRACTION_TYPE
#endif
/* sample the cubemap or screen-space */
#if REFRACTION_MODE == REFRACTION_MODE_CUBEMAP
// when reading from the cubemap, we are not pre-exposed so we apply iblLuminance
// which is not the case when we'll read from the screen-space buffer
vec3 t = prefilteredRadiance(ray.direction, lod) * frameUniforms.iblLuminance;
#else
// compute the point where the ray exits the medium, if needed
highp vec4 p = mulMat4x4Float3(getClipFromWorldMatrix(), ray.position);
vec2 uv = uvToRenderTargetUV(p.xy * (0.5 / p.w) + 0.5);
vec3 t = textureLod(sampler0_ssr, vec3(uv, 0.0), lod).rgb;
#endif
// apply absorption
#if defined(MATERIAL_HAS_ABSORPTION)
// compute transmission T
vec3 T = saturate(exp(-pixel.absorption * ray.d));
t *= T;
#endif
return t;
}
vec3 evaluateRefraction(const PixelParams pixel, const vec3 n0, vec3 E) {
vec3 Ft;
// Note: We use the average IOR for the roughness lod calculation.
// Roughness remapping so that an IOR of 1.0 means no microfacet refraction and an IOR
// of 1.5 has full microfacet refraction
#if defined(MATERIAL_HAS_DISPERSION) && (REFRACTION_TYPE == REFRACTION_TYPE_SOLID)
float perceptualRoughness = mix(pixel.perceptualRoughnessUnclamped, 0.0, saturate(pixel.etaIR[1] * 3.0 - 2.0));
#else
float perceptualRoughness = mix(pixel.perceptualRoughnessUnclamped, 0.0, saturate(pixel.etaIR * 3.0 - 2.0));
#endif
#if REFRACTION_MODE == REFRACTION_MODE_CUBEMAP
float lod = perceptualRoughnessToLod(perceptualRoughness);
#else
// distance to camera plane
const float invLog2sqrt5 = 0.8614;
float lod = max(0.0, (2.0 * log2(perceptualRoughness) + frameUniforms.refractionLodOffset) * invLog2sqrt5);
#endif
#if defined(MATERIAL_HAS_DISPERSION) && (REFRACTION_TYPE == REFRACTION_TYPE_SOLID)
for (int i = 0; i < 3; i++) {
float etaIR = pixel.etaIR[i];
float etaRI = pixel.etaRI[i];
#else
float etaIR = pixel.etaIR;
float etaRI = pixel.etaRI;
#endif
Refraction ray;
#if REFRACTION_TYPE == REFRACTION_TYPE_SOLID
refractionSolidSphere(etaIR, etaRI, pixel.thickness, n0, -shading_view, ray);
#elif REFRACTION_TYPE == REFRACTION_TYPE_THIN
refractionThinSphere(etaIR, pixel.uThickness, n0, -shading_view, ray);
#else
#error invalid REFRACTION_TYPE
#endif
// compute transmission T
#if defined(MATERIAL_HAS_ABSORPTION)
vec3 T = min(vec3(1.0), exp(-pixel.absorption * ray.d));
#endif
// Roughness remapping so that an IOR of 1.0 means no microfacet refraction and an IOR
// of 1.5 has full microfacet refraction
float perceptualRoughness = mix(pixel.perceptualRoughnessUnclamped, 0.0,
saturate(etaIR * 3.0 - 2.0));
/* sample the cubemap or screen-space */
#if REFRACTION_MODE == REFRACTION_MODE_CUBEMAP
// when reading from the cubemap, we are not pre-exposed so we apply iblLuminance
// which is not the case when we'll read from the screen-space buffer
vec3 t = prefilteredRadiance(ray.direction, perceptualRoughness) * frameUniforms.iblLuminance;
#else
// compute the point where the ray exits the medium, if needed
vec4 p = vec4(getClipFromWorldMatrix() * vec4(ray.position, 1.0));
p.xy = uvToRenderTargetUV(p.xy * (0.5 / p.w) + 0.5);
// distance to camera plane
const float invLog2sqrt5 = 0.8614;
float lod = max(0.0, (2.0 * log2(perceptualRoughness) + frameUniforms.refractionLodOffset) * invLog2sqrt5);
vec3 t = textureLod(sampler0_ssr, vec3(p.xy, 0.0), lod).rgb;
#endif
// base color changes the amount of light passing through the boundary
t *= pixel.diffuseColor;
// fresnel from the first interface
t *= 1.0 - E;
// apply absorption
#if defined(MATERIAL_HAS_ABSORPTION)
t *= T;
#endif
#if defined(MATERIAL_HAS_DISPERSION) && (REFRACTION_TYPE == REFRACTION_TYPE_SOLID)
vec3 t = evaluateRefraction(pixel, n0, lod, pixel.etaIR[i], pixel.etaRI[i]);
Ft[i] = t[i];
}
#else
Ft = t;
Ft = evaluateRefraction(pixel, n0, lod, pixel.etaIR, pixel.etaRI);
#endif
#if REFRACTION_TYPE == REFRACTION_TYPE_THIN
// For thin surfaces, the light will bounce off at the second interface in the direction of
// the reflection, effectively adding to the specular, but this process will repeat itself.
// Each time the ray exits the surface on the front side after the first bounce,
// it's multiplied by E^2, and we get: E + E(1-E)^2 + E^3(1-E)^2 + ...
// This infinite series converges and is easy to simplify.
// Note: we calculate these bounces only on a single component,
// since it's a fairly subtle effect.
E *= 1.0 + pixel.transmission * (1.0 - E.g) / (1.0 + E.g);
#endif
// fresnel from the first interface
Ft *= 1.0 - E;
// base color changes the amount of light passing through the boundary
Ft *= pixel.diffuseColor;
return Ft;
}
#endif
void evaluateIBL(const MaterialInputs material, const PixelParams pixel, inout vec3 color) {
// specular layer
vec3 Fr = vec3(0.0);
@@ -615,7 +620,7 @@ void evaluateIBL(const MaterialInputs material, const PixelParams pixel, inout v
vec3 E = specularDFG(pixel);
if (ssrFr.a < 1.0) { // prevent reading the IBL if possible
vec3 r = getReflectedVector(pixel, shading_normal);
Fr = E * prefilteredRadiance(r, pixel.perceptualRoughness);
Fr = E * prefilteredRadiance(r, perceptualRoughnessToLod(pixel.perceptualRoughness));
}
#elif IBL_INTEGRATION == IBL_INTEGRATION_IMPORTANCE_SAMPLING
vec3 E = vec3(0.0); // TODO: fix for importance sampling