From a5b4e15ae7de93e0c550da95de6465b6116bf8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin-Karl=20Lefran=C3=A7ois?= Date: Sat, 22 Aug 2026 11:30:06 +0200 Subject: [PATCH] Add support for KHR_materials_retroreflection Implements the Minimal Retroreflective Microfacet (MRM) model: substitutes the view direction with its reflection about the normal before evaluating the metallic and dielectric BRDFs, then linearly blends the result with the regular BRDF by retroreflectionFactor. Applied to both the IBL and punctual light paths. --- source/GltfState/gltf_state.js | 8 ++++ source/Renderer/renderer.js | 8 ++++ source/Renderer/shaders/ibl.glsl | 17 ++++++++ source/Renderer/shaders/material_info.glsl | 20 +++++++++ source/Renderer/shaders/pbr.frag | 41 ++++++++++++++++++ source/Renderer/shaders/textures.glsl | 22 ++++++++++ source/gltf/gltf.js | 1 + source/gltf/material.js | 48 ++++++++++++++++++++++ 8 files changed, 165 insertions(+) diff --git a/source/GltfState/gltf_state.js b/source/GltfState/gltf_state.js index 100df608..7620a31f 100644 --- a/source/GltfState/gltf_state.js +++ b/source/GltfState/gltf_state.js @@ -95,6 +95,8 @@ class GltfState { KHR_materials_dispersion: true, /** KHR_materials_emissive_strength enables emissive factors larger than 1.0 */ KHR_materials_emissive_strength: true, + /** KHR_materials_retroreflection adds a retroreflective response to the metallic and dielectric BRDFs */ + KHR_materials_retroreflection: true, /** KHR_interactivity enables execution of a behavior graph */ KHR_interactivity: true, /** KHR_node_hoverability enables hovering over nodes */ @@ -257,6 +259,12 @@ GltfState.DebugOutput = { IRIDESCENCE_THICKNESS: "Iridescence Thickness" }, + /** KHR_materials_retroreflection */ + retroreflection: { + /** output the retroreflection strength*/ + RETROREFLECTION_FACTOR: "Retroreflection Strength" + }, + /** KHR_materials_anisotropy */ anisotropy: { /** output the anisotropic strength*/ diff --git a/source/Renderer/renderer.js b/source/Renderer/renderer.js index a725f521..31563d6c 100644 --- a/source/Renderer/renderer.js +++ b/source/Renderer/renderer.js @@ -1882,6 +1882,9 @@ class gltfRenderer { this.shader.updateUniform("u_IridescenceThicknessUVSet", material.extensions?.KHR_materials_iridescence?.iridescenceThicknessTexture?.texCoord); this.shader.updateUniform("u_IridescenceThicknessMinimum", material.extensions?.KHR_materials_iridescence?.iridescenceThicknessMinimum); + this.shader.updateUniform("u_RetroreflectionFactor", material.extensions?.KHR_materials_retroreflection?.retroreflectionFactor); + this.shader.updateUniform("u_RetroreflectionUVSet", material.extensions?.KHR_materials_retroreflection?.retroreflectionTexture?.texCoord); + this.shader.updateUniform("u_SheenRoughnessFactor", material.extensions?.KHR_materials_sheen?.sheenRoughnessFactor); this.shader.updateUniform("u_SheenColorFactor", jsToGl(material.extensions?.KHR_materials_sheen?.sheenColorFactor)); this.shader.updateUniform("u_SheenRoughnessUVSet", material.extensions?.KHR_materials_sheen?.sheenRoughnessTexture?.texCoord); @@ -2222,6 +2225,11 @@ class gltfRenderer { shaderDefine: "DEBUG_IRIDESCENCE_THICKNESS" }, + { + debugOutput: GltfState.DebugOutput.retroreflection.RETROREFLECTION_FACTOR, + shaderDefine: "DEBUG_RETROREFLECTION_FACTOR" + }, + { debugOutput: GltfState.DebugOutput.anisotropy.ANISOTROPIC_STRENGTH, shaderDefine: "DEBUG_ANISOTROPIC_STRENGTH" diff --git a/source/Renderer/shaders/ibl.glsl b/source/Renderer/shaders/ibl.glsl index df945109..22385317 100644 --- a/source/Renderer/shaders/ibl.glsl +++ b/source/Renderer/shaders/ibl.glsl @@ -139,6 +139,23 @@ vec3 getIBLRadianceAnisotropy(vec3 n, vec3 v, float roughness, float anisotropy, #endif +#ifdef MATERIAL_RETROREFLECTION +// MRM (Portsmouth et al., JCGT 15(1), 2026): substitute the view direction with +// V_retro = reflect(-v, n) before evaluating the specular lobe. Since the regular +// mirror lookup already reflects the view about n (`reflect(-v, n)`), and that +// reflection is idempotent, sampling the environment at V_retro reflects again and +// simplifies to sampling directly along v - i.e. looking back toward the viewer. +vec3 getIBLRadianceGGXRetroreflection(vec3 n, vec3 v, float roughness) +{ + float lod = roughness * float(u_MipCount - 1); + vec3 reflection = normalize(v); + vec4 specularSample = getSpecularSample(reflection, lod); + + return specularSample.rgb; +} +#endif + + vec3 getIBLRadianceCharlie(vec3 n, vec3 v, float sheenRoughness, vec3 sheenColor) { float NdotV = clampedDot(n, v); diff --git a/source/Renderer/shaders/material_info.glsl b/source/Renderer/shaders/material_info.glsl index 4c601593..7bb8674a 100644 --- a/source/Renderer/shaders/material_info.glsl +++ b/source/Renderer/shaders/material_info.glsl @@ -29,6 +29,9 @@ uniform float u_IridescenceIor; uniform float u_IridescenceThicknessMinimum; uniform float u_IridescenceThicknessMaximum; +// Retroreflection +uniform float u_RetroreflectionFactor; + // Diffuse Transmission uniform float u_DiffuseTransmissionFactor; uniform vec3 u_DiffuseTransmissionColorFactor; @@ -102,6 +105,9 @@ struct MaterialInfo float iridescenceIor; float iridescenceThickness; + // KHR_materials_retroreflection + float retroreflectionFactor; + float diffuseTransmissionFactor; vec3 diffuseTransmissionColorFactor; @@ -329,6 +335,20 @@ MaterialInfo getIridescenceInfo(MaterialInfo info) #endif +#ifdef MATERIAL_RETROREFLECTION +MaterialInfo getRetroreflectionInfo(MaterialInfo info) +{ + info.retroreflectionFactor = u_RetroreflectionFactor; + + #ifdef HAS_RETROREFLECTION_MAP + info.retroreflectionFactor *= texture(u_RetroreflectionSampler, getRetroreflectionUV()).r; + #endif + + return info; +} +#endif + + #ifdef MATERIAL_DIFFUSE_TRANSMISSION MaterialInfo getDiffuseTransmissionInfo(MaterialInfo info) { diff --git a/source/Renderer/shaders/pbr.frag b/source/Renderer/shaders/pbr.frag index 690b9b2c..e4da861c 100644 --- a/source/Renderer/shaders/pbr.frag +++ b/source/Renderer/shaders/pbr.frag @@ -115,6 +115,10 @@ void main() materialInfo = getIridescenceInfo(materialInfo); #endif +#ifdef MATERIAL_RETROREFLECTION + materialInfo = getRetroreflectionInfo(materialInfo); +#endif + #ifdef MATERIAL_DIFFUSE_TRANSMISSION materialInfo = getDiffuseTransmissionInfo(materialInfo); #endif @@ -224,6 +228,17 @@ void main() f_dielectric_brdf_ibl = mix(f_dielectric_brdf_ibl, rgb_mix(f_diffuse, f_specular_dielectric, iridescenceFresnel_dielectric), materialInfo.iridescenceFactor); #endif +#ifdef MATERIAL_RETROREFLECTION + // Retroreflective variant of the metallic and dielectric BRDFs (MRM model): only the + // specular lobe's view direction is substituted with V_retro; the Fresnel mix weight is + // unchanged because NdotV is invariant under the V -> V_retro substitution. + vec3 f_specular_retro = getIBLRadianceGGXRetroreflection(n, v, materialInfo.perceptualRoughness); + vec3 f_metal_brdf_retro = f_metal_fresnel_ibl * f_specular_retro; + vec3 f_dielectric_brdf_retro = mix(f_diffuse, f_specular_retro, f_dielectric_fresnel_ibl); + f_metal_brdf_ibl = mix(f_metal_brdf_ibl, f_metal_brdf_retro, materialInfo.retroreflectionFactor); + f_dielectric_brdf_ibl = mix(f_dielectric_brdf_ibl, f_dielectric_brdf_retro, materialInfo.retroreflectionFactor); +#endif + #ifdef MATERIAL_CLEARCOAT clearcoat_brdf = getIBLRadianceGGX(materialInfo.clearcoatNormal, v, materialInfo.clearcoatRoughness); @@ -350,6 +365,25 @@ void main() l_dielectric_brdf = mix(l_dielectric_brdf, rgb_mix(l_diffuse, l_specular_dielectric, iridescenceFresnel_dielectric), materialInfo.iridescenceFactor); #endif +#ifdef MATERIAL_RETROREFLECTION + // V_retro = reflect(-v, n); NdotV_retro == NdotV, so only the half vector (and thus + // NdotH/VdotH) needs recomputing for the retroreflective specular lobe. + vec3 v_retro = normalize(reflect(-v, n)); + vec3 h_retro = normalize(l + v_retro); + float NdotH_retro = clampedDot(n, h_retro); + float VdotH_retro = clampedDot(v_retro, h_retro); + + vec3 dielectric_fresnel_retro = F_Schlick(materialInfo.f0_dielectric * materialInfo.specularWeight, materialInfo.f90_dielectric, abs(VdotH_retro)); + vec3 metal_fresnel_retro = F_Schlick(baseColor.rgb, vec3(1.0), abs(VdotH_retro)); + + vec3 l_specular_retro = intensity * NdotL * BRDF_specularGGX(materialInfo.alphaRoughness, NdotL, NdotV, NdotH_retro); + vec3 l_metal_brdf_retro = metal_fresnel_retro * l_specular_retro; + vec3 l_dielectric_brdf_retro = mix(l_diffuse, l_specular_retro, dielectric_fresnel_retro); + + l_metal_brdf = mix(l_metal_brdf, l_metal_brdf_retro, materialInfo.retroreflectionFactor); + l_dielectric_brdf = mix(l_dielectric_brdf, l_dielectric_brdf_retro, materialInfo.retroreflectionFactor); +#endif + #ifdef MATERIAL_CLEARCOAT l_clearcoat_brdf = intensity * getPunctualRadianceClearCoat(materialInfo.clearcoatNormal, v, l, h, VdotH, materialInfo.clearcoatF0, materialInfo.clearcoatF90, materialInfo.clearcoatRoughness); @@ -525,6 +559,13 @@ vec3 specularTexture = vec3(1.0); #if DEBUG == DEBUG_IRIDESCENCE_THICKNESS g_finalColor.rgb = vec3(materialInfo.iridescenceThickness / 1200.0); #endif +#endif + + // Retroreflection: +#ifdef MATERIAL_RETROREFLECTION +#if DEBUG == DEBUG_RETROREFLECTION_FACTOR + g_finalColor.rgb = vec3(materialInfo.retroreflectionFactor); +#endif #endif // Anisotropy: diff --git a/source/Renderer/shaders/textures.glsl b/source/Renderer/shaders/textures.glsl index 9bd40eca..1b9b70d7 100644 --- a/source/Renderer/shaders/textures.glsl +++ b/source/Renderer/shaders/textures.glsl @@ -353,6 +353,28 @@ vec2 getIridescenceThicknessUV() #endif +// Retroreflection + + +#ifdef MATERIAL_RETROREFLECTION + +uniform sampler2D u_RetroreflectionSampler; +uniform int u_RetroreflectionUVSet; +uniform mat3 u_RetroreflectionUVTransform; + + +vec2 getRetroreflectionUV() +{ + vec3 uv = vec3(u_RetroreflectionUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_RETROREFLECTION_UV_TRANSFORM + uv = u_RetroreflectionUVTransform * uv; +#endif + return uv.xy; +} + +#endif + + // Diffuse Transmission #ifdef MATERIAL_DIFFUSE_TRANSMISSION diff --git a/source/gltf/gltf.js b/source/gltf/gltf.js index 9290d482..56323185 100644 --- a/source/gltf/gltf.js +++ b/source/gltf/gltf.js @@ -39,6 +39,7 @@ const allowedExtensions = [ "KHR_materials_ior", "KHR_materials_iridescence", "KHR_materials_pbrSpecularGlossiness", + "KHR_materials_retroreflection", "KHR_materials_sheen", "KHR_materials_specular", "KHR_materials_transmission", diff --git a/source/gltf/material.js b/source/gltf/material.js index 0b693f5e..ba58f09a 100644 --- a/source/gltf/material.js +++ b/source/gltf/material.js @@ -33,6 +33,7 @@ class gltfMaterial extends GltfObject { this.hasIridescence = false; this.hasAnisotropy = false; this.hasDispersion = false; + this.hasRetroreflection = false; // non gltf properties this.type = "unlit"; @@ -106,6 +107,12 @@ class gltfMaterial extends GltfObject { if (this.hasDispersion && renderingParameters.enabledExtensions.KHR_materials_dispersion) { defines.push("MATERIAL_DISPERSION 1"); } + if ( + this.hasRetroreflection && + renderingParameters.enabledExtensions.KHR_materials_retroreflection + ) { + defines.push("MATERIAL_RETROREFLECTION 1"); + } return defines; } @@ -482,6 +489,22 @@ class gltfMaterial extends GltfObject { if (this.extensions.KHR_materials_dispersion !== undefined) { this.hasDispersion = true; } + + // KHR Extension: Retroreflection + // See https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_retroreflection + if (this.extensions.KHR_materials_retroreflection !== undefined) { + this.hasRetroreflection = true; + + const retroreflectionTexture = + this.extensions.KHR_materials_retroreflection.retroreflectionTexture; + + if (retroreflectionTexture !== undefined) { + retroreflectionTexture.samplerName = "u_RetroreflectionSampler"; + this.parseTextureInfoExtensions(retroreflectionTexture, "Retroreflection"); + this.textures.push(retroreflectionTexture); + this.defines.push("HAS_RETROREFLECTION_MAP 1"); + } + } } initGlForMembers(this, gltf, webGlContext); @@ -686,6 +709,13 @@ class gltfMaterial extends GltfObject { this.extensions.KHR_materials_ior = new KHR_materials_ior(); this.extensions.KHR_materials_ior.fromJson(jsonExtensions.KHR_materials_ior); } + + if (jsonExtensions.KHR_materials_retroreflection !== undefined) { + this.extensions.KHR_materials_retroreflection = new KHR_materials_retroreflection(); + this.extensions.KHR_materials_retroreflection.fromJson( + jsonExtensions.KHR_materials_retroreflection + ); + } } } @@ -825,6 +855,24 @@ class KHR_materials_iridescence extends GltfObject { } } +class KHR_materials_retroreflection extends GltfObject { + static animatedProperties = ["retroreflectionFactor"]; + constructor() { + super(); + this.retroreflectionFactor = 0; + this.retroreflectionTexture = undefined; + } + + fromJson(jsonRetroreflection) { + super.fromJson(jsonRetroreflection); + if (jsonRetroreflection.retroreflectionTexture !== undefined) { + const retroreflectionTexture = new gltfTextureInfo(); + retroreflectionTexture.fromJson(jsonRetroreflection.retroreflectionTexture); + this.retroreflectionTexture = retroreflectionTexture; + } + } +} + class KHR_materials_sheen extends GltfObject { static animatedProperties = ["sheenRoughnessFactor", "sheenColorFactor"]; constructor() {