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

Implement OpenPBR's sheen BRDF #1819

Merged

Conversation

fpsunflower
Copy link
Contributor

Description

This PR adds an implementation of the Zeltner-Burley sheen closure which is the new default in OpenPBR. I've exposed it via a new (optional) mode keyword arg in the existing closure, though it would be easy to switch the interface if needed.

The implementation is based on a fit done by Stephen Hill here: https://blog.selfshadow.com/sheen/ltc_sheen.html

Tests

I've modified the existing sheen render unit tests to use the new closure on half of the sphere so we can keep testing old and new modes.

Checklist:

  • I have read the contribution guidelines.
  • I have updated the documentation, if applicable.
  • I have ensured that the change is tested somewhere in the testsuite (adding new test cases if necessary).
  • My code follows the prevailing code style of this project. If I haven't
    already run clang-format v17 before submitting, I definitely will look at
    the CI test that runs clang-format and fix anything that it highlights as
    being nonconforming.

@fpsunflower fpsunflower changed the title New sheen closure Implement OpenPBR's sheen BRDF Jun 1, 2024
const float len2 = dot(wiOriginal, wiOriginal);
const float det = aInv * aInv;
const float jacobian = det / (len2 * len2);
pdf = jacobian * std::max(wiOriginal.z, 0.0f) * float(M_1_PI);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason not to call eval(wo, wi) here?

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 just seemed redundant to transform/untransform the vector and recompute the ltc factors several times. Since its just a few lines of code, it seemed easier to just do the small amount of work again.

Copy link
Contributor

@aconty aconty left a comment

Choose a reason for hiding this comment

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

Looks very good!

Copy link

@selfshadow selfshadow left a comment

Choose a reason for hiding this comment

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

I think it would be better to reference the MaterialX implementation for now*, since the WebGL sandbox was just a test and may go away or move.

  • I'm planning to publish something about the fitting methodology later once I've settled on a final implementation, at which point this can be the canonical reference.

float pdf;
Sampling::sample_cosine_hemisphere(Vec3(0, 0, 1), rx, ry, wi, pdf);

wi = Vec3(wi.x - wi.z * bInv, wi.y, wi.z * aInv).normalize();

Choose a reason for hiding this comment

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

Nice idea of scaling up by aInv here. I'll see if I can borrow that for the MaterialX implementation.

// TODO: is there a cheaper way to get the jacobian here?
const Vec3 wiOriginal(aInv * wi.x + bInv * wi.z, aInv * wi.y, wi.z);
const float len2 = dot(wiOriginal, wiOriginal);
const float det = aInv * aInv;

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice - I'll go through this and try to consolidate the logic. Didn't realize yours was added to the MaterialX side already.

Choose a reason for hiding this comment

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

I think if you retain the aInv multiplication of wi but otherwise follow what I'm doing then you'll end up with square(len2 / aInv) for the jacobian, which should still be a little bit cheaper than what you have here.

NB: I may end up changing to fitting to work with the non-inverse matrix coefficients (TBD, but it's possible that they could be more linear), but that shouldn't change the implementation all that much.

const float NdotV = clamp(N.dot(V), 0.0f, 1.0f);
const Vec3 ltc = fetch_ltc(NdotV);

const Vec3 T1 = (V - N * NdotV).normalize();

Choose a reason for hiding this comment

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

There's a danger of generating NaNs here if V = N (I've certainly hit this in other LTC-related code in production). I didn't bother to handle that in the WebGL sandbox, but I'm doing so in the MaterialX version:
https://github.com/AcademySoftwareFoundation/MaterialX/blob/main/libraries/pbrlib/genglsl/lib/mx_microfacet_sheen.glsl#L117-L133

Things get a little simpler if vectors are already in local space:
https://github.com/shill-lucasfilm/OpenPBR-viewer/blob/main/glsl/fuzz_brdf.glsl#L30-L37

@fpsunflower
Copy link
Contributor Author

I've made the suggested changes. I also fixed the TangentFrame class so it could be used here and replace the ad-hoc one (which wasn't robust as you pointed out).

This did invalidate all the reference images with small noise differences, but I think its worth the cleanup.

@fpsunflower
Copy link
Contributor Author

@jstone-lucasfilm Do you have any preference for the keyword argument used to pick the new sheen variant?

As I've implemented it so far, using the new sheen would look like this:

Ci = sheen_bsdf (N, SheenColor, Roughness "mode", 1);

@jstone-lucasfilm
Copy link
Member

@fpsunflower This is a great question, and my initial thinking is that the MaterialX node would have a mode input with two enumerated options: conty_kulla and zeltner_burley. This would allow for additional techniques to be added in the future, and the two enumerated options would simply become 0 and 1 in hardware shading languages such as GLSL and MSL.

Since we have a great set of subject-matter experts on the thread, I'd be very interested in what your thoughts are, and I'm happy to defer to the group.

@@ -16,7 +16,7 @@ sheen
)
{
if (N.x < 0)
Ci = sheen_bsdf (N, Cs, roughness * 2, "mode", 1);
Ci = sheen_bsdf (N, Cs, sqrt(roughness), "mode", 1);

Choose a reason for hiding this comment

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

Are you sure this is correct? I thought the external parameterisation was roughness (alpha in the Zeltner implementation) and that only the underlying SGGX model uses sigma. The table itself (and my analytic refit) should be parameterised by alpha, not sigma.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When looking at the wedges of both sheen models, the sqrt seemed to bring them closer together. This part is just for the testsuite where its handy to see both models at once on the same sphere.

The materialx closure just exposes the "raw" parameter in any case (just like GGX).

Choose a reason for hiding this comment

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

Okay, makes sense.

const float y = roughness;
const float A = ((2.58126f * x + 0.813703f * y) * y) / (1.0f + 0.310327f * x * x + 2.60994f * x * y);
const float B = sqrtf(1.0f - x) * (y - 1.0f) * y * y * y / (0.0000254053f + 1.71228f * x - 1.71506f * x * y + 1.34174f * y * y);
const float invs = (0.0379424f + y * (1.32227f + y)) / (y * (0.0206607f + 1.58491f * y));

Choose a reason for hiding this comment

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

Good idea! I had originally tried to fit 1/s directly but it made the optimisation unstable. I don't know why I didn't follow through on doing it as a post step but I'll bring this over to a future MaterialX GLSL version, assuming I don't replace the Gaussian fitting (I might try a logistic).

u = v.cross(w);
// fallsback to an arbitrary basis if the tangent is 0 or colinear with n
static TangentFrame from_normal_and_tangent(const Vec3& n, const Vec3& t) {
Vec3 y = n.cross(t);

Choose a reason for hiding this comment

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

Out of curiosity, was there a reason you went with computing y here rather than x (via t - n*dot(n, t))? Numerical stability or performance?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No real reason actually. Now that I look at both in the light of day, they should be equivalent, and the dot product way might be a few less operations (especially considering the dot product is needed for the LTC lookup anyway).

Choose a reason for hiding this comment

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

Okay, fair enough. I just wanted to check that I wasn't missing anything.

@jstone-lucasfilm
Copy link
Member

@fpsunflower @shill-lucasfilm I've had a chance to write a pull request for the mode input of sheen_bsdf in MaterialX, and I wanted to post a link here to compare notes between these two implementations:

AcademySoftwareFoundation/MaterialX#1880

Following additional discussion with Stephen, our current thinking is that the two modes should be named conty_kulla and zeltner. Since the Zeltner paper has three authors (Zeltner, Burley, and Chiang), it seemed incorrect to mention just two of them in the sheen option, so we're instead leaning towards zeltner as a parallel construction to "Zeltner, et al".

Let me know how this sounds to you, and I'm very open to making adjustments based on feedback from this group.

@fpsunflower
Copy link
Contributor Author

Sounds good to me.

@fpsunflower
Copy link
Contributor Author

Finally got the testsuite passing. Just needed a handful of extra reference images and had to clamp the roughness to a small value to avoid generating NaNs for the R term.

I will squash and merge since I don't believe there are any outstanding comments.

The two remaining failures also happen in main and are udim related -- I am guessing that is coming from the latest OIIO.

@fpsunflower fpsunflower merged commit 65024b0 into AcademySoftwareFoundation:main Jun 21, 2024
21 of 23 checks passed
@fpsunflower fpsunflower deleted the new-sheen-closure branch June 21, 2024 16:18
@@ -1529,7 +1529,7 @@ struct ZeltnerBurleySheen final : public BSDF, MxSheenParams {
// for the implementation in MaterialX:
// https://github.com/AcademySoftwareFoundation/MaterialX/blob/main/libraries/pbrlib/genglsl/lib/mx_microfacet_sheen.glsl
const float x = NdotV;
const float y = roughness;
const float y = std::max(roughness, 1e-3f);

Choose a reason for hiding this comment

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

Ahh yes, I should've caught this, since I hit the same problem in the MaterialX GLSL implementation. I decided to clamp to the minimum used by Zeltner et. al for their table:
https://github.com/AcademySoftwareFoundation/MaterialX/blob/main/libraries/pbrlib/genglsl/mx_sheen_bsdf.glsl#L30

Note: I may come up with a new fit against a higher-resolution table in the future, at which point the minimum roughness may be lower.

Choose a reason for hiding this comment

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

Oops, I left this comment as pending earlier. Oh well!

chellmuth pushed a commit to chellmuth/OpenShadingLanguage that referenced this pull request Sep 6, 2024
* fix: reparameter string needed to be ustringhash (AcademySoftwareFoundation#1841)
* fix: make backfacing shadeop indicate backfacing shader-global is needed (AcademySoftwareFoundation#1827)
* Fix the subsurface_bssrdf parameters (AcademySoftwareFoundation#1823)
* Implement OpenPBR's sheen BRDF in testrender (AcademySoftwareFoundation#1819)
* cleanup: OIIO deprecations (AcademySoftwareFoundation#1834)

See merge request spi/dev/3rd-party/osl-feedstock!61
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants