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() {