Skip to content

Add specular_retroreflectivity parameter#255

Merged
jstone-lucasfilm merged 9 commits intoAcademySoftwareFoundation:dev_1.2from
portsmouth:retroreflection
Feb 23, 2026
Merged

Add specular_retroreflectivity parameter#255
jstone-lucasfilm merged 9 commits intoAcademySoftwareFoundation:dev_1.2from
portsmouth:retroreflection

Conversation

@portsmouth
Copy link
Contributor

@portsmouth portsmouth commented May 12, 2025

Adding a short, rather minimal description of the retroreflectivity functionality to the spec. It currently references the unpublished white paper from Matthias, but it will be updated to something published eventually (at the least, a white paper on the arxiv, say).

The MaterialX implementation requires a modification to their spec.

Some images are needed also.

image image

@portsmouth portsmouth marked this pull request as ready for review December 6, 2025 19:52
@portsmouth
Copy link
Contributor Author

In edbedcf the spec text is updated to apply retroreflectivity to both the metal and dielectric base.

I thus moved the main discussion of retroreflectivity to the microfacet section, since it functions as a generic modification to both the metal and dielectric microfacet models. (Except in the dielectric case, we explicitly require that the BTDF is unmodified). I also broke that section into sub-sections to make the structure more clear.

image

I updated the tables in the sections for the metallic base, dielectric base, and the parameter summary:

image image image

@virtualzavie
Copy link
Contributor

Thank you for the proposal. This feature has been requested multiple times by users, so this is a welcome addition to the specification.

There are several points I would consider:

  • From an artist point of view, I think it is important that the effect can apply to both dielectrics and metals. The example I have in mind are bicycle wheel reflectors, which appear to be made entirely of molded plastic. Requiring a metallic base to get retroreflection would be counter intuitive in such a case. For this reason I welcome the proposed change to extend the feature to the entire base layer.
  • From a real-time implementation point of view, I am concerned that the proposed specification implies evaluating two lobes. If that's possible, I think it would be preferable to alter the specular BSDF instead of mixing two of them.
  • Finally, the parametrisation appears to be clear and straightforward.

@msuzuki-nvidia
Copy link
Contributor

There are two implementation options:

Option 1: Add a retroreflective boolean flag to the microfacet BSDFs and blend it with the normal BSDF in the node graph.
Option 2: Add a retroreflectivity float parameter to the microfacet BSDFs and blend it with the normal BSDF in-place.

I believe option 2 would be more efficient because each BSDF function computes additional data beyond the BSDF itself. What are your thoughts?

@msuzuki-nvidia
Copy link
Contributor

@jstone-lucasfilm Here is a sample implementation of the Option 2 for the conductor BSDF:

void mx_conductor_bsdf(ClosureData closureData, float weight, vec3 ior_n, vec3 ior_k, vec2 roughness, float retroreflectivity, float thinfilm_thickness, float thinfilm_ior, vec3 N, vec3 X, int distribution, inout BSDF bsdf)
{
    bsdf.throughput = vec3(0.0);

    if (weight < M_FLOAT_EPS)
    {
        return;
    }

    vec3 V = closureData.V;
    vec3 L = closureData.L;

    N = mx_forward_facing_normal(N, V);
    float NdotV = clamp(dot(N, V), M_FLOAT_EPS, 1.0);

    FresnelData fd = mx_init_fresnel_conductor(ior_n, ior_k, thinfilm_thickness, thinfilm_ior);

    vec2 safeAlpha = clamp(roughness, M_FLOAT_EPS, 1.0);
    float avgAlpha = mx_average_alpha(safeAlpha);

    if (closureData.closureType == CLOSURE_TYPE_REFLECTION)
    {
        X = normalize(X - dot(X, N) * N);
        vec3 Y = cross(N, X);

        float NdotL = clamp(dot(N, L), M_FLOAT_EPS, 1.0);

        float rr = clamp(retroreflectivity, 0.0, 1.0);

        // Normal reflection
        vec3 reflResponse = vec3(0.0);
        if (rr < 1.0)
        {
            vec3 H = normalize(L + V);
            float VdotH = clamp(dot(V, H), M_FLOAT_EPS, 1.0);
            vec3 Ht = vec3(dot(H, X), dot(H, Y), dot(H, N));

            vec3 F = mx_compute_fresnel(VdotH, fd);
            float D = mx_ggx_NDF(Ht, safeAlpha);
            float G = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha);
            vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);

            // Note: NdotL is cancelled out
            reflResponse = D * F * G * comp * closureData.occlusion * weight / (4.0 * NdotV);
        }

        // Retroreflection
        vec3 retroResponse = vec3(0.0);
        if (rr > 0.0)
        {
            vec3 R = normalize(reflect(-V, N));
            vec3 H = normalize(L + R);
            float RdotH = clamp(dot(R, H), M_FLOAT_EPS, 1.0);
            float NdotR = clamp(dot(N, R), M_FLOAT_EPS, 1.0);
            vec3 Ht = vec3(dot(H, X), dot(H, Y), dot(H, N));

            vec3 F = mx_compute_fresnel(RdotH, fd);
            float D = mx_ggx_NDF(Ht, safeAlpha);
            float G = mx_ggx_smith_G2(NdotL, NdotR, avgAlpha);
            vec3 comp = mx_ggx_energy_compensation(NdotR, avgAlpha, F);

            // Note: NdotL is cancelled out
            retroResponse = D * F * G * comp * closureData.occlusion * weight / (4.0 * NdotR);
        }

        bsdf.response = mix(reflResponse, retroResponse, rr);
    }
    else if (closureData.closureType == CLOSURE_TYPE_INDIRECT)
    {
        vec3 F = mx_compute_fresnel(NdotV, fd);
        vec3 comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F);
        vec3 Li = mx_environment_radiance(N, V, X, safeAlpha, distribution, fd);
        bsdf.response = Li * comp * weight;
    }
}

@msuzuki-nvidia
Copy link
Contributor

We go for Option 1, as discussed.

Copy link
Member

@jstone-lucasfilm jstone-lucasfilm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me, @portsmouth, and I'll highlight the MaterialX update in progress, found here:

AcademySoftwareFoundation/MaterialX#2783

@jstone-lucasfilm jstone-lucasfilm changed the title Add specular_retroreflectivity parameter. Add specular_retroreflectivity parameter Feb 23, 2026
@jstone-lucasfilm jstone-lucasfilm merged commit ae29610 into AcademySoftwareFoundation:dev_1.2 Feb 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants