Skip to content

Conversation

@lander-vr
Copy link
Contributor

Closes godotengine/godot-proposals#11670
Supersedes #104585

Adds contact hardening and fresnel based roughness to reflection probes reflections when box projection is enabled.

Screenshots on master and PR are with #106241

Master PR Cycles
image image image
image image image
image image image
image image image
image image image
image image image

@lander-vr lander-vr requested a review from a team as a code owner May 11, 2025 21:29
@lander-vr lander-vr force-pushed the Improve-reflection-probe-box-capture-roughness branch 2 times, most recently from 38c7aca to 2d34dc6 Compare May 11, 2025 21:44
@Calinou Calinou added this to the 4.x milestone May 11, 2025
@clayjohn
Copy link
Member

This looks pretty good, but I am a little worried about how complex it is. What was the reason for going with something custom instead of using the approach from Frostbite described in https://seblagarde.wordpress.com/wp-content/uploads/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf (page 72)?

float computeDistanceBaseRoughness (float distInteresectionToShadedPoint ,float distInteresectionToProbeCenter , float linearRoughness ) {
    // To avoid artifacts we clamp to the original linearRoughness
    // which introduces an acceptable bias and allows conservation
    // of mirror reflection behavior for a smooth surface .
    float newLinearRoughness = clamp ( distInteresectionToShadedPoint / distInteresectionToProbeCenter * linearRoughness , 0, linearRoughness );
    return lerp ( newLinearRoughness , linearRoughness , linearRoughness );
}

@lander-vr
Copy link
Contributor Author

lander-vr commented May 12, 2025

What was the reason for going with something custom instead of using the approach from Frostbite

I just built it upon Calinous initial implementation. I found the issues with it were just due to fresnel not being taken into account for roughness, I suspect that frostbite implementation by itself would have the same issue since just that formula by itself would also select a mipmap regardless of viewing angle.

*Note that the roughness on the floor doesn't match, focus on the ceiling in these comparisons. :)
Without taking fresnel into account:

25_05_10_16_16__godot.windows.editor.x86_64_52AeOAWQJ3.mp4

Custom implementation:

25_05_12_10_59__godot.windows.editor.x86_64_0Yg9maUqeR.mp4

Cycles (Ignore the floor here, as I forgot to adjust the roughness to match):

25_05_10_16_19__blender_5uHMwvve61.mp4

image

@Calinou
Copy link
Member

Calinou commented May 12, 2025

Is it planned to port this code to Mobile and Compatibility too (like my original PR)?

Copy link
Member

@Calinou Calinou left a comment

Choose a reason for hiding this comment

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

Tested locally, it works as expected. Code looks good to me.

Before

Includes #106241.

fix_stretching.mp4

After

Includes #106241.

contact_hardening_and_fix_stretching.mp4

@akien-mga akien-mga requested a review from clayjohn May 13, 2025 16:01
@lander-vr
Copy link
Contributor Author

Is it planned to port this code to Mobile and Compatibility too (like my original PR)?

Mobile works already as it runs the same reflection_process() function as Forward+.
I do want to port it to compatibility, but I'm seeing some pretty severe issues in terms of roughness behavior in compatibility that make it difficult to validate whether or not this pr is working as intended.

@lander-vr lander-vr force-pushed the Improve-reflection-probe-box-capture-roughness branch from 2d34dc6 to 7d1a4b2 Compare May 14, 2025 15:41
@lander-vr
Copy link
Contributor Author

lander-vr commented May 14, 2025

Seems like my compatibility issues were project related.

Working as expected in compatibility renderer:
image

Master:

25_05_14_22_43__godot.windows.editor.x86_64_GRvnl8QBvS.mp4

PR:

25_05_14_17_37__godot.windows.editor.x86_64_wmbANe8HQK.mp4


float distance_to_hit_point = 0.0;
float mip = sqrt(roughness) * MAX_ROUGHNESS_LOD;
float mip_min = pow(roughness, 2.0) * MAX_ROUGHNESS_LOD; // Ensures fully rough materials don't have reflection contact hardening.
Copy link
Member

Choose a reason for hiding this comment

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

Squaring roughness doesn't make sense here. Previously we sqrted the roughness value because that is how we mapped roughness to the array layers when baking the reflection probe. Therefore mip_min doesn't really make any sense, it is an arbitrary value that is small than mip but converges with mip at 0 and 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it is an arbitrary value that is small than mip but converges with mip at 0 and 1

This is done to prevent fully rough surfaces exhibiting reflections. If we just lerp from 0.0 to mip, there will always be mirror-like reflections where distance_to_hit_point is small, regardless the roughness. This fades out the hardening effect as the roughness is increased, which produces results significantly closer to Cycles and SSR.

In the frostbite implementation this is handled by doing return lerp ( newLinearRoughness , linearRoughness , linearRoughness ), they state the same reasoning:

This works decently for low roughness values but becomes inaccurate for high roughness. In order to avoid this issue and since this effect is more visible for low roughness values, we linearly interpolate the “distance based roughness” to the original roughness, based on roughness values.

fresnel = pow(fresnel, 2.0);

float reflection_roughness = distance_to_hit_point * (1.0 - fresnel); // Adjust contact hardening strength by viewing angle.
reflection_roughness /= MAX_ROUGHNESS_LOD;
Copy link
Member

Choose a reason for hiding this comment

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

This looks like an arbitrary amount. MAX_ROUGHNESS_LOD is set by rendering/reflections/sky_reflections/roughness_layers which has no relationship to the fresnel value or the distance to hit point. It doesn't really make sense here

Copy link
Contributor Author

@lander-vr lander-vr Jun 5, 2025

Choose a reason for hiding this comment

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

It remaps the distance_to_hit_point range based on the amount of mips. This makes it so that the perceived roughness on the surfaces remains more or less the same between different MAX_ROUGHNESS_LOD values (to the extent that that is possible).

Comparisons between various roughness_layers settings (Note the FPS difference to the commented out version):
image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've adjusted this so reflection_roughness is established by doing that remap by dividing by MAX_ROUGHNESS_LOD, instead of doing that fresnel multiplication first. Makes a little more intuitive sense this way.

@lander-vr lander-vr force-pushed the Improve-reflection-probe-box-capture-roughness branch 2 times, most recently from 2637f38 to e9a71e6 Compare June 5, 2025 19:31
@passivestar
Copy link
Contributor

Tested, this is a big improvement

Before After
1 2

This is cycles pathtracer with roughly the same roughness values for overall comparison, not a 1 to 1:

image

This is how it behaves with different roughness values in this PR:

roughness.mp4

@lander-vr lander-vr force-pushed the Improve-reflection-probe-box-capture-roughness branch 2 times, most recently from 9807460 to 3ee5c2d Compare June 12, 2025 18:09
ref_normal = (local_matrix * vec4(ref_normal, 0.0)).xyz;

float mip = sqrt(roughness) * MAX_ROUGHNESS_LOD;
float mip_min = pow(roughness, 2.0) * MAX_ROUGHNESS_LOD; // Ensures fully rough materials don't have reflection contact hardening.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
float mip_min = pow(roughness, 2.0) * MAX_ROUGHNESS_LOD; // Ensures fully rough materials don't have reflection contact hardening.
float mip_min = (roughness * roughness) * MAX_ROUGHNESS_LOD; // Ensures fully rough materials don't have reflection contact hardening.

I would feel better if you avoided the pow function in this case. Sure, some compilers might recognize that this has an integer exponent of two, but I would rather not rely on that.

vec3 local_ref_vec = (reflections.data[ref_index].local_matrix * vec4(ref_vec, 0.0)).xyz;

float mip = sqrt(roughness) * MAX_ROUGHNESS_LOD;
float mip_min = pow(roughness, 2.0) * MAX_ROUGHNESS_LOD; // Ensures fully rough materials don't have reflection contact hardening.
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above, I don't trust mobile compilers to realize that the pow can be computed with a single multiplication.

@Repiteo Repiteo modified the milestones: 4.x, 4.6 Jun 19, 2025
@lander-vr
Copy link
Contributor Author

lander-vr commented Jun 25, 2025

I've done a bit more thinking on this implementation vs. Frostbites, and while I don't fully understand why the frostbite implemenation does distInteresectionToShadedPoint / distInteresectionToProbeCenter, as in my understanding this calculation can result in a reduction of roughness as the intersection gets further away from the reflected surface, shown in this drawing:

image

The main thing I believe this calculation aims to achieve is make the mip selection relative to the size of the probe: Reflection probes resolutions are static, so the cubemap of a probe of 5x5 meters has the same resolution as a probe of 50x50 meters, so the texel sizes (and in turn the perceived roughness) can vary wildly, the mip selection should take this into account.

Edit:
I understand now, and it is quite obvious now, values higher than 1.0 don't contribute due to the clamp. 😅
I'll attempt to implement this, though I think our implementation will still be different than what's in the Frostbite approach, since I suspect we will still need that Fresnel approximation to avoid things looking overly rough.

ref_normal = posonbox - box_offset.xyz;

float fresnel = 1.0 - max(dot(normal, -normalize(vertex)), 0.0);
fresnel = pow(fresnel, 2.0);
Copy link
Contributor

Choose a reason for hiding this comment

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

here as well

…s reflections when using box projection

Co-authored-by: Calinou <[email protected]>
@lander-vr lander-vr force-pushed the Improve-reflection-probe-box-capture-roughness branch from 3ee5c2d to 54e5112 Compare August 12, 2025 22:17
@Repiteo Repiteo requested a review from clayjohn September 20, 2025 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve ReflectionProbe sampling on semi-rough materials to use realistic falloff according to object <-> reflection distance

6 participants