Closed portsmouth closed 2 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.
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:
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)
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).
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}$
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
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.
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} $$
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).
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
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).
The MaterialX implementation of this is needed. (Just need to decide which formula..).
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 |
---|---|
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).
Current PR adds roughening as a short sub-section of the coat discussion:
Ready to merge.
@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".
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.
@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:
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?
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.
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.
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):
With the coat (highlight of coat and spec superimposed, coat highlight much broader):
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).
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.
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