Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scene Shaders - Vertex Shading Fixes #100503

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

StaydMcMuffin
Copy link
Contributor

@StaydMcMuffin StaydMcMuffin commented Dec 17, 2024

Part three of: #100348
Requires the preceding: #100441

Addresses six five errors found in the scene shader files that relate to per-vertex shading.

  • In Forward+ and Compatibility, the geometry normals used for per-vertex shading were not normalized prior to their use, causing per-vertex shading to be either too bright or too dark depending on the overall scale of the model matrix. Specular shading in particular was strongly affected by this.

  • For per-vertex shading specifically, the Lambert Wrap diffuse model mistakenly used the clamped cNdotL rather than the signed NdotL, meaning it could not, in fact, wrap.

  • Per-vertex shading completely ignored both the contributing lights' specular_amount and size parameters.

  • Per-vertex specularity lacked a Geometry term entirely, resulting in overly bright rough highlights and a generally different "look" compared to per-pixel shading. It now uses the same approximate G term used for clearcoat specular.
    This may be considered as much of an improvement as it is a fix, but it's a fairly innocent one at that.

    Dropped for performance.

  • The roughness_to_shininess mapping from GGX roughness to Blinn-Phong shininess used for per-vertex shading resulted in very different specular responses between per-pixel and per-vertex shading, generally making materials look far more glossy despite having the same roughness. I've replaced it with a new formulation entirely, which loosely follows the curve of the common 2 / roughness * roughness - 2 remapping, but with a much lower maximum value more suitable for per-vertex shading.

  • Per-vertex shading incorrectly used the unmodified Blinn-Phong normalization term (shininess + 2.0) / (8.0 * PI) as opposed to the modified one (shininess + 8.0) / (8.0 * PI) (the modified term is the one you want when multiplying by NdotL).

Here is a swatch of roughness values, from 0.0 on the left to 1.0 on the right in intervals of 0.2. The top row uses per-pixel shading while the bottom uses per-vertex.

Before After
image image

Here is a material tester using per-vertex shading with the Lambert Wrap diffuse mode and a roughness of 0.4. The mesh also has a uniform scale of 0.8, which causes the specularity to disappear completely in the "Before" image.

Before After Per-pixel
image image image

Fixes two errors related to the normal, tangent,
and bitangent vectors, namely normals not always
being inverted on backfaces, and normalization
being reversed from what MikkTSpace expects.
@StaydMcMuffin StaydMcMuffin requested a review from a team as a code owner December 17, 2024 09:37
@Chaosus Chaosus added this to the 4.x milestone Dec 17, 2024
// Uses slightly more FMA instructions (2x rate) to avoid special instructions (0.25x rate).
// Range is reduced to [0.64,4977] from [068,2,221,528] which makes mediump feasible for the rest of the shader.
// Converts GGX roughness to Blinn-Phong shininess.
// Roughly approximates `2.0 / roughness * roughness - 2.0` with a much lower high end.
Copy link
Member

Choose a reason for hiding this comment

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

Where does 2.0 / roughness * roughness - 2.0 come from? I don't think its a very good approximation for converting roughness to specular power.

Here is a desmos graph showing the different approaches (and our approximations) https://www.desmos.com/calculator/nzby8ouch4

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 believe here is where I first encountered it, and of the methods I've tried I consider it the most perceptually accurate for matching Blinn-Phong to GGX.
I'm also not sure why exp2(15.0 * (1.0 - roughness) + 1.0) * 0.25 was chosen originally, as shown in my comparison screenshots it's not a very close fit to what GGX produces. There are certainly other mappings we could use and I'm open to trying them, but the current method really leaves a lot to be desired.

Copy link
Member

@clayjohn clayjohn Dec 19, 2024

Choose a reason for hiding this comment

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

I thought it was from the Frostbite PBR slides, but looking quickly at them now I don't see it. AFAIK, this, or something like this has been the standard for a long time.

I guess if our goal is a visual approximation then it doesn't matter how close the curve is as long as the end result looks good.

I like the range of the new function, it does a good job of keeping the intermediate values in a sufficient range where you won't risk losing precision when using mediump. Its a shame the new approximation is slightly more expensive than the old, but if it looks better, then oh well.

Comment on lines +398 to +401
float size_A = 0.0;
if (omni_lights[idx].size > 0.0) {
float t = omni_lights[idx].size / max(0.001, light_length);
size_A = max(0.0, 1.0 - 1.0 / sqrt(1.0 + t * t));
}
Copy link
Member

Choose a reason for hiding this comment

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

This was removed intentionally. The purpose of vertex lighting is twofold:

  1. To be very fast when you need fast but simple lighting (i.e for particles)
  2. To allow users to make old school looking games.

Neither of those cases require having area lights. so we dropped the code. The nature of specular reflections in vertex shaded materials also makes the effect barely visible except at extreme values, so its really not worth the cost IMO.

I wouldn't bring it back unless we have enough users saying that they absolutely need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's fair. I figured the cost wasn't much of a concern since it's only calculated when the user wants it to be.
That being said, if the only concern is performance, I can easily optimize this so it only uses one quarter-rate instruction instead of three (or even none, as size_A = max(0.0, 1.0 - 1.0 / sqrt(1.0 + t * t)) simply clamps the value below 1.0 with a very subtle rolloff, so can easily be dropped in favor of size_A = clamp(t, 0.0, 1.0), which would bring the area light code down to a multiply, a clamp, and three adds (two if we also drop the geometry term I added)).

Copy link
Member

Choose a reason for hiding this comment

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

I would wait and see if users ask for it. There is no point in throwing away performance for something that may never get used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Speaking as a user, this is something that I want. Since area lights affect diffuse shading too, and our area lights aren't energy conserving (larger area lights simply emit more light than smaller ones), ignoring area lights causes a brightness discrepancy between per-vertex and per-pixel shaded materials. If I'm using per-vertex shading for particles, I want them to be lit consistently with the rest of their environment, regardless of my lighting setup.

Perhaps it's outside the scope of this discussion, but I don't think we should always wait for users to notice deficiencies before addressing them, especially when the performance cost for doing so can be reduced to as little as three instructions (two when not using specularity).

Copy link
Member

Choose a reason for hiding this comment

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

I strongly agree with @StaydMcMuffin. Personally, I would expect and want this behavior too.

Approximating omni lights with a non-zero radius as point lights merely to save a few operations (per vertex!) is a bad idea. These approximations create noticeable visual inconsistencies for a negligible performance gain.
Particularly in both scenarios mentioned above: particle systems and retro-style games (as well as other use cases).
In general, per-vertex specular lightning looks better with larger light sources.

However, if desired, I could do some benchmarking to verify that this computation wouldn't be a problem performance-wise.

float shininess = roughness_to_shininess(roughness);
float blinn = pow(cNdotH, shininess);
blinn *= (shininess + 2.0) * (1.0 / (8.0 * M_PI)) * cNdotL;
blinn *= (shininess + 8.0) * (1.0 / (8.0 * M_PI)) * (0.25 / max(0.0001, cLdotH * cLdotH)) * cNdotL * specular_amount;
Copy link
Member

Choose a reason for hiding this comment

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

Where does this extra term come from? I don't think it is necessary for a simple blinn NDF.

IIRC the current normalization factor comes from http://www.thetenthplanet.de/archives/255 and it is energy conserving. I wouldn't change it without a good reason

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the Geometry term mentioned in the fourth point, and is the same method we use for clearcoat specular. It was added to more closely match per-pixel shading with minimal extra cost, but we can drop this if that's not a priority.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I see. I would drop it unless it makes a huge difference. Seeing as we already don't use the Fresnel term, I don't think the extra cost is worth it just to be slightly more correct. Fresnel makes way more of a difference, but you don't really miss it with vertex shading.

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 relatively subtle so yeah, I'll go ahead and drop this.

@clayjohn
Copy link
Member

Per-vertex shading incorrectly used the Blinn-Phong RDF normalization term (shininess + 2.0) / (8.0 * PI) as opposed to the NDF one (shininess + 8.0) / (8.0 * PI) (the NDF term is the one you want when multiplying by NdotL).

You have this backwards

@StaydMcMuffin
Copy link
Contributor Author

Per-vertex shading incorrectly used the Blinn-Phong RDF normalization term (shininess + 2.0) / (8.0 * PI) as opposed to the NDF one (shininess + 8.0) / (8.0 * PI) (the NDF term is the one you want when multiplying by NdotL).

You have this backwards

Ah, right you are, I misremembered the table from here. I meant to refer to Blinn-Phong (modified), not Blinn-Phong (NDF), as the former is the case when multiplying specular by NdotL.
Still, we do multiply by NdotL, so (shininess + 8.0) / (8.0 * PI) is the correct term (or (shininess + 6.0) / (8.0 * PI), if you'd prefer to use the lower bound like before).

@clayjohn
Copy link
Member

Per-vertex shading incorrectly used the Blinn-Phong RDF normalization term (shininess + 2.0) / (8.0 * PI) as opposed to the NDF one (shininess + 8.0) / (8.0 * PI) (the NDF term is the one you want when multiplying by NdotL).

You have this backwards

Ah, right you are, I misremembered the table from here. I meant to refer to Blinn-Phong (modified), not Blinn-Phong (NDF), as the former is the case when multiplying specular by NdotL. Still, we do multiply by NdotL, so (shininess + 8.0) / (8.0 * PI) is the correct term (or (shininess + 6.0) / (8.0 * PI), if you'd prefer to use the lower bound like before).

Its very strange that we do multiply by NdotL now that you mention it. Here is the PR where we switched over to the NDF normalization term #51411. At the same time we switched to using the NDF instead of the RDF. I recall thinking that the NDF was more in line with what we wanted (maybe because it was closer to classic Blinn-Phong?). Then somehow when adding vertex shading the NdotL term snuck back in alongside the NDF normalization factor #83360. You can see in 3.x we don't use NdotL either (

float blinn = pow(cNdotH, shininess);
blinn *= (shininess + 2.0) * (1.0 / (8.0 * M_PI));
specular_brdf_NL = blinn;
)

I can't remember why I preferred the NDF over the RDF previously. Its probably worth checking what the visual difference is if you remove the NdotL altogether vs. using the proper normalization for the RDF as you have here.

@StaydMcMuffin
Copy link
Contributor Author

Per-vertex shading incorrectly used the Blinn-Phong RDF normalization term (shininess + 2.0) / (8.0 * PI) as opposed to the NDF one (shininess + 8.0) / (8.0 * PI) (the NDF term is the one you want when multiplying by NdotL).

You have this backwards

Ah, right you are, I misremembered the table from here. I meant to refer to Blinn-Phong (modified), not Blinn-Phong (NDF), as the former is the case when multiplying specular by NdotL. Still, we do multiply by NdotL, so (shininess + 8.0) / (8.0 * PI) is the correct term (or (shininess + 6.0) / (8.0 * PI), if you'd prefer to use the lower bound like before).

Its very strange that we do multiply by NdotL now that you mention it. Here is the PR where we switched over to the NDF normalization term #51411. At the same time we switched to using the NDF instead of the RDF. I recall thinking that the NDF was more in line with what we wanted (maybe because it was closer to classic Blinn-Phong?). Then somehow when adding vertex shading the NdotL term snuck back in alongside the NDF normalization factor #83360. You can see in 3.x we don't use NdotL either (

float blinn = pow(cNdotH, shininess);
blinn *= (shininess + 2.0) * (1.0 / (8.0 * M_PI));
specular_brdf_NL = blinn;

)

I can't remember why I preferred the NDF over the RDF previously. Its probably worth checking what the visual difference is if you remove the NdotL altogether vs. using the proper normalization for the RDF as you have here.

Aha, that explains why the unmodified term was used in the first place.
From experience I can say that multiplying by NdotL is definitely something you want to do with Blinn-Phong, otherwise the specular lobe can appear on the unlit side of an object with an odd shape (Phong can get away with not doing this, but not Blinn-Phong). We very much want to keep this, so the new normalization term would be the correct one. As to whether we should use the upper or lower bound, both work fine, so I leave that decision up to you.

Fixes five errors related to per-vertex shading,
with a focus on maintaining close visual parity
between per-pixel and per-vertex shading modes.
@StaydMcMuffin StaydMcMuffin force-pushed the scene-shaders-vertex-shading-fixes branch from b6864fc to 1810135 Compare December 20, 2024 01:49
@Geometror
Copy link
Member

What's the state of this PR? Are there any things that need a final decision (besides the specular computation for area lights)?

@StaydMcMuffin
Copy link
Contributor Author

What's the state of this PR? Are there any things that need a final decision (besides the specular computation for area lights)?

I believe the area light handling is the only hold-up. Since the main concern is performance I could implement some trivial optimizations that I was originally saving for a dedicated optimization-focused PR, if that might help to move things along.

@insomniacUNDERSCORElemon

In 4.4 stable I'm seeing just one of my models rendering extremely bright if the roughness is low (and non-0 metallic) on a per-vertex material.

Is that relevant (and fixed?) here, or is there some other issue/PR or perhaps just a way to fix the model?

@Calinou
Copy link
Member

Calinou commented Mar 7, 2025

Is that relevant (and fixed?) here, or is there some other issue/PR or perhaps just a way to fix the model?

See #103627. There are several bugs with metallic per-vertex shaded materials right now.

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.

6 participants