AcademySoftwareFoundation / OpenPBR

Specification and reference implementation for the OpenPBR Surface shading model
Apache License 2.0
407 stars 18 forks source link

Coat roughening convolution formula #172

Closed portsmouth closed 2 months ago

portsmouth commented 3 months ago

Essentially, an improved version of the Standard Surface roughening formula. We need to suggest some approximation, as implementations need to account for this somehow. As discussed on Slack, thinking about the roughening as a convolution suggests an "as simple as possible" physically-based formula for the effect. I assume this is closer to the ground truth than the ad-hoc Standard-Surface formula, but verifying that would require more work.

See the interactive graphs here for the difference between the Standard Surface (green) and convolution (blue) formulas: https://www.desmos.com/calculator/tvvzlmjqin

image

portsmouth commented 3 months ago

To be more clear, we should cross reference this formula in the dielectric and metal sections, to remind the reader to apply the roughening if the coat is present.

fpsunflower commented 3 months ago

Using (rb^4+rc^4)^(1/4) has to be clamped at 1.0 for implementations that have a limit (as @peterkutz mentioned, for example because of tabulated energy compensation tables). So you get something like this:

formula_a

If that discontinuity bothers you, the following formula is almost the same but is smooth everywhere:

(1-(1-rb^4)(1-rc^4))^(1/4)

formula_b

I think the current wording in the spec is totally fine, no need to change it. Just pointing out an alternative that would be very close.

Depending on where we end up with respect to anisotropy for coat vs base, we may need to generalize the formula to operate on the 2x2 matrix of alpha values (haven't thought about exactly what the math should be, but its probably a similar kind of formula that would come back to @portsmouth's one in the isotropic case).

peterkutz commented 3 months ago

Nice suggestion and visualization @fpsunflower! It's nice that your modified formula produces very similar values across most of the domain while avoiding the discontinuity. It also meets all of the other requirements such as producing an output at least as large as the inputs.

In case it's easier to read or reason about, here's a different way of writing it:

(rb^4 + rc^4 - rb^4 * rc^4)^(1/4)

$r'_B = ( r^4_B + r^4_C - r^4_B r^4_C)^\frac{1}{4}$

portsmouth commented 3 months ago

Thanks Chris, that's a nice formula. I plotted it here (where green is mine, dashed green is yours, blue is Standard Surface, red is max):

https://www.desmos.com/calculator/svkp9j7kwc

image

Your formula is very close to the original one, unless the base roughness is quite large. I think it would be fine to re-word the section to say, the convolution would give my formula, but we suggest your modification (or Peter's form of it) to ensure the curve is close to that but smooth between the [0,1] endpoints.

portsmouth commented 3 months ago

In case it's easier to read or reason about, here's a different way of writing it:

$$ \left( r^4\mathrm{B} + r^4\mathrm{C} - r^4\mathrm{B} \, r^4\mathrm{C} \right)^\frac{1}{4} $$

That has a somewhat nice similarity to the Standard Surface formula, which is the same but just removing the fourth powers:

$$ r\mathrm{B} + r\mathrm{C} - r\mathrm{B} \, r\mathrm{C} $$

portsmouth commented 3 months ago

Depending on where we end up with respect to anisotropy for coat vs base, we may need to generalize the formula to operate on the 2x2 matrix of alpha values (haven't thought about exactly what the math should be, but its probably a similar kind of formula that would come back to @portsmouth's one in the isotropic case).

I posted a possible sketch of the math for that on Slack:

One thought for a more principled approach is to approximate the NDFs of coat and base as 2d Gaussians in slope space, say Gc, Gb (with principle axes and std-devs given by the alphas and T, B for coat/base), which is the Beckmann model, then the roughened base could be taken to have the alphas of the Gaussian NDF given by their convolution Gc * Gb which is found (it turns out) by just adding the covariance matrices of coat and base. That would also take into account the anisotropy of coat or base.

Though I think that it is too much math for the spec. Ideally someone should investigate whether a scheme like that works, and verify with some Monte Carlo simulations. (Then we could just reference that).

Incidentally, the assumption that the NDF of the observed base lobe seen through a rough coat is given by convolving the NDFs of coat and base, is just a guess (loosely based on the Belcour paper where he does similar approximations with the variance of lobes, to account for layering, which possibly we should reference). The reality will obviously be more complex (the full BRDF of coat-on-base involving accounting for all possible scattering paths within the coat, and a term with the two NDFs being integrated together is just one lowest-order part of that).

Technically OpenPBR doesn't say this convolution formula must be used though, it's just a suggestion (the ground truth would be whatever happens in the full light transport).

portsmouth commented 3 months ago

The reality will obviously be more complex (the full BRDF of coat-on-base involving accounting for all possible scattering paths within the coat, and a term with the two NDFs being integrated together is just one lowest-order part of that.

Actually the NDF of the coat should presumably count more highly in the mix, since in the lowest-order path the ray has to pass through the coat boundary twice, and bounces only once on the base surface..

So a more realistic formula would involve doubling the variance contribution of the coat, i.e.

$$ \left( r^4\mathrm{B} + 2r^4\mathrm{C} \right)^\frac{1}{4} $$

Then the base roughness saturates to 1 at:

$$ r^*_\mathrm{C} = \Bigl( \frac{1 - r^4_B}{2} \Bigr)^\frac{1}{4} $$

e.g. a smooth base becomes maximally rough at $r^*_\mathrm{C}$ = 0.840 rather than when the coat is maximal roughness.

Below green is with the coat counting twice, and red the original formula. If you do this, I'm not sure it makes sense to try to smoothen out the discontinuity.

https://www.desmos.com/calculator/qizje7v8bx

image

Possibly you could still regard Chris's formula (dashed red) as a reasonable, smooth bounded approximation of that as well. This is all loose enough that perhaps it doesn't matter exactly what formula we recommend, if it has nice visual behavior reasonably similar to the expected ground truth (and as a bonus has some kind of theoretical rationalization).

It would probably help to see some renders with varying base and coat roughness, and the effect of the formula with the clamp in comparison to the smooth formula. (I'm expecting barely perceptible difference, but will see).

portsmouth commented 2 months ago

Re-wording in https://github.com/AcademySoftwareFoundation/OpenPBR/pull/172/commits/6c66e3b0251011e7e7fc253c1bebad3ad34f0e41

image

portsmouth commented 2 months ago

The MaterialX implementation of this is needed. (Just need to decide which formula..).

portsmouth commented 2 months ago

I tried rendering a model with a shiny metal base, covered by a coat with a textured roughness (driven by noise). The renders below show the result with both the formulas suggested above. This suggests at least that the difference between them is pretty negligible (so I’d propose to just use the more physically motivated “accurate” formula with the clamp):

Approx formula Accurate formula
openpbr_approx_coat_roughening openpbr_accurate_coat_roughening
portsmouth commented 2 months ago

I added a simple MaterialX implementation of the more realistic coat roughening formula as in Equation 55 from the spec shown above, replacing the old Standard Surface one.

The clamped version seems fine to use, and more principled than the approximate version of Equation 56 (which I removed from the spec text as well).

portsmouth commented 2 months ago

Current PR adds roughening as a short sub-section of the coat discussion:

image

image

portsmouth commented 2 months ago

Ready to merge.

portsmouth commented 2 months ago

@virtualzavie suggested offline that this could benefit from a more detailed discussion of the derivation of the suggested formula, which I agree with. But I'd suggest for 1.0 we just get the existing PR in with the formula and the implementation, as adding a more detailed description is presumably allowable after "code complete".

jstone-lucasfilm commented 2 months ago

This change looks good to me, @portsmouth, and I'm fine to merge this work once we've validated rendering of the updated implementation in MaterialX 1.39.

jstone-lucasfilm commented 2 months ago

@portsmouth I addressed two very minor typos in the reference implementation, and I'm now getting successful renders of coated materials in the MaterialX Viewer:

OpenPBR_Carpaint

One unusual behavior that I should call out, though, is that neither specular_roughness_anisotropy nor coat_roughness_anisotropy seem to have any visual effect on our open_pbr_carpaint.mtlx example.

If I set coat_weight to zero, then specular_roughness_anisotropy has the expected effect on the shape of specular highlights, but when coat_weight is set to one, neither of these anisotropy controls have any noticeable effect on the rendered output.

Could this be a bug in our reference implementation, or is this behavior actually expected given the latest OpenPBR specification?

portsmouth commented 2 months ago

Thanks for the typo fixes.

If I load the "car-paint" material, and dial the coat anisotropy, then indeed the anisotropy doesn't seem to show up (i.e. the sun reflection doesn't smear). But this just seems to be a case of what Zap was complaining about, i.e. when the coat roughness is zero, coat anisotropy doesn't do anything.. (i.e. the roughness stays at zero, in both directions). That is by design, yes.

Also... in this material, the specular and coat IORs are equal (!). So the specular lobe is not showing up at all. So that accounts for the specular anisotropy not showing up either.

If you make the roughnesses non-zero and the IORs differ, then you will see the effect of the anisotropy in both lobes.

If you think that's unintuitive, we can discuss in the meeting (but it's the behavior of the spec, correctly implemented, I think).

So anyway, I think the PR is good to go, as we've verified it now in MaterialX.

jstone-lucasfilm commented 2 months ago

Got it, and I'm fine to merge this latest pull request, as the specification and reference implementation are aligned. It's arguable that we can do a better job of making these behaviors intuitive, e.g. making sure there's always a visual impact associated with moving the anisotropy slider, but I'll leave that judgement to artists with more production experience.

portsmouth commented 2 months ago

Here is an example of the coat roughening behaving correctly (IMO), including anisotropy.

The coat and spec IORs are different. The coat is slightly rougher than the spec. Both are highly anisotropic.

Without the coat (just the spec lobe, tight stretched highlight):

image

With the coat (highlight of coat and spec superimposed, coat highlight much broader):

image

This does show one defect of the roughening formula, in that the anisotropy of the base lobe is not affected. In fact, an isotropic rough coat should cause the base lobe to become less anisotropic. But we opted to neglect such anisotropy effects for now. (Later we could try to improve that though, for sure).

portsmouth commented 2 months ago

Another thing to note, is that the weird brightening around the edge of the lower image above, is the "spurious TIR artifact" -- which shows up here because the coat IOR is higher than the spec.

So the spec lobe "thinks" that the rays are incoming from a higher IOR, so TIR occurs. Which is false, as physically the rays should actually pass through the coat and bend, eliminating that TIR.

The PR https://github.com/AcademySoftwareFoundation/OpenPBR/pull/166 proposes a discussion of this with a suggested practical solution. I will attempt to fix this up in time for the meeting, to incorporate Peter's suggestions.