Page MenuHome

Support for anisotropic random walk subsurface (and its albedo inversion)
ClosedPublic

Authored by Christophe Hery (chery) on Jul 19 2021, 7:19 PM.

Details

Summary

Following some discussion with Brecht, submitting this diff here.
This is about adding support for anisotropy (phase) in the random walk subsurface, essentially implementing https://graphics.pixar.com/library/PathTracedSubsurface/.

The important things are:
1/ bssrdf->anisotropy and bssrdf->ior parameters (we have to raise the number of parameters accordingly in kernel_types.h);
2/ handling of the anisotropic walk and the corresponding albedo inversion (as published in our tech memo at Pixar) in integrator_subsurface.h.

Diff Detail

Repository
rB Blender
Branch
arcpatch-D11965 (branched from master)
Build Status
Buildable 16449
Build 16449: arc lint + arc unit

Event Timeline

Christophe Hery (chery) requested review of this revision.Jul 19 2021, 7:19 PM
Christophe Hery (chery) created this revision.

Forgot to add that the diff is for Cycles-x.

Interesting!
Do you have renders done in Cycles already?

Did a quick pass of review. Generally seems fine, so far only cosmetic comments. BTW, do you have clang-format setup in your development environment?

intern/cycles/kernel/closure/bssrdf.h
417–418

I'd reshuffle the comment like this:

/* Mean free path length.
  * Note that we keep length in the same "unit space" as Burley. This also affects radius which should be `l` and not `l/s`. */

Avoiding the "above" which could easily get out of sync. Is better to mention what happens in the local code, without asking the reader to look around.

The name mention in the comment we also don't really do. The credits go via git blame and commit logs.

intern/cycles/kernel/integrator/integrator_state_template.h
100

There is already roughness defined. Guess this should be ior instead?

intern/cycles/kernel/integrator/integrator_subsurface.h
184

Kernel is one big header, essentially. To avoid possible conflicts suggest moving this into dipoleComputeAlphaPrime, and make it const, not define. Maybe constexpr const int max_num_iterations = 12.

P.S. Sure, there is undef down the road, but with a const you don't need that :)

194

I love newton solvers, yay! :)

225

Not sure about "now". Imagine reading the comment in half a year form now. Suggest to phrase it something like Anisotropy integration from:

But great to see links to papers, missed that a lot!

365

Similar to the MAX_ITERATIONS, suggest making it a constant. And replace ifdef with regular if. If the level is ever 0 the compiler will optimize code out.

Christophe Hery (chery) updated this revision to Diff 39764.EditedJul 21 2021, 1:19 AM

Updating the diff based on sergey's comments (thanks for these).
[The second KERNEL_STRUCT_MEMBER(subsurface, float, roughness, KERNEL_FEATURE_SUBSURFACE), in place of ior, was a typo when I created this new branch on my end - good catch!]

The only thing that I do not feel like addressing is the #define SIMILARITY_LEVEL 9 into a const (I did that change for MAX_ITERATIONS, as suggested). Having this define that way makes it a bit easier for debugging, since one can turn off the behavior fully (by commenting the line and recompiling), as opposed to a run-time test.

As for images in Cycles, I need to double check on permission. We have been rendering with this code in Cycles for the last 2 years. I may send some to you as private email. The main appearances changes are:
1/ the albedo inversion is more accurate, more precise, even with no anisotropy;
2/ adding anisotropy brings more details (and even less glow);
3/ the current inversion does not account for anisotropy at all (by definition, since the feature was not present); when using it (to be specific, when using the original subsurface_random_walk_coefficients calling the simpler Burley subsurface_random_walk_remap with its alpha exponential fit) in conjunction with the proposed random walk changes (with the anisotropy sampling and an anisotropic value different than 0), one simply gets, not surprisingly, the wrong energy.

Christophe Hery (chery) marked 4 inline comments as done.Jul 21 2021, 5:59 AM

The only thing that I do not feel like addressing is the #define SIMILARITY_LEVEL 9 into a const

Fine by me.

From reading the code the bigger missing part seems to be that it is not formatted b clang-format. We can do that when we'll be applying the patch. But it'll be great if you can look into setting it for your Cycles development as well. We have some quick notes at https://wiki.blender.org/wiki/Tools/ClangFormat and https://wiki.blender.org/wiki/Style_Guide/C_Cpp

Anyway, was playing around with the applied patch. Test test file with packed textures:


Few things I've noticed:

  • The compatibility seems to be lost: as if the radius is way smaller with this path. While the difference is inevitable, we are always trying to do-version existing files to avoid to make them render as close as possible. Not sure yet whether we can keep compatibility here, but some tweaks should be possible. Or, it needs to be explicitly stated somewhere that things are expected to break existing setups.
  • Not sure we really support Burley in the Cycles-X, but this patch seems to introduce artifacts related on non-initialized value somewhere. Easiest to see on my setup is to use CPU render.
  • Is it possible to have anisotropy support for Subsurface Scattering node (currently it is only available via Principled node) ?
  • There is something I don't understand in the anisotropy behavior. The model turns black for anistropy values range 0.8 - 0.96. Don't think this is expected?
intern/cycles/render/nodes.cpp
2962

Think in all other usages IOR is capitalized, so should be Subsurface IOR

  • Not sure we really support Burley in the Cycles-X, but this patch seems to introduce artifacts related on non-initialized value somewhere. Easiest to see on my setup is to use CPU render.

I re-rendered your scene with latest cycles-x and this patch applied and I do not see the artifacts you mention (in both OptiX, Cuda or CPU). Weird. See attached. This is F12-render.

  • There is something I don't understand in the anisotropy behavior. The model turns black for anistropy values range 0.8 - 0.96. Don't think this is expected?

I can confirm that here, but only with this scene. Not sure what is triggering that behavior. I will look into that.

  • There is something I don't understand in the anisotropy behavior. The model turns black for anistropy values range 0.8 - 0.96. Don't think this is expected?

I will look into that.

I think I have figured this one out. Your albedo is pretty high (at 0.8), and I guess the albedo inversion can produce slight numerical overflowing (result slightly above 1.0f) in this case.
The solution is simply to add a *alpha = clamp(*alpha, 0.0f, 0.99f) at line 271 in the new subsurface_random_walk_remap from integrator_subsurface.h.
Could you confirm that this solves it for you? Thanks.

Christophe Hery (chery) updated this revision to Diff 39844.EditedJul 23 2021, 4:42 AM

Incorporated the fix (alpha clamp) and ran the clang formatter.
Note that I moved the ior clamp (and added the anisotropy clamp) into svm_closure.h. I think this is cleaner.
Lastly, I'd rather keep the "Subsurface Ior" parameter (as opposed to "Subsurface IOR"), otherwise we have to upgrade all our scenes.
Thanks and let me know what you think.

Nothing here really. By accident, I had touched a comment which did not need editing (this was a search and replace missup, where I looked for constants 1 that should be floats as 1.0f).

I re-rendered your scene with latest cycles-x and this patch applied and I do not see the artifacts you mention (in both OptiX, Cuda or CPU).

You need to switch the node to Christensen-Burley mode. It currently falls-back to the Random-Walk in Cycles-X.
Seems that initializing IOR and anisotropy for BSSRDF in the svm_closure.h solves the artifacts.

The solution is simply to add a *alpha = clamp(*alpha, 0.0f, 0.99f) at line 271 in the new subsurface_random_walk_remap from integrator_subsurface.h.
Could you confirm that this solves it for you?

Testing the new state of the patch and can not reproduce the unexpected darkening anymore.

Lastly, I'd rather keep the "Subsurface Ior" parameter (as opposed to "Subsurface IOR"), otherwise we have to upgrade all our scenes.

If there are a lot of scenes it could be possible to deal with them as a do-version.

From the remaining parts I think we have:

  • Make the Subsurface BSDF node to support anisotropy.
  • Find a solution to preserve compatibility
  • Run benchmark, make sure we don't loose a lot of performance

You need to switch the node to Christensen-Burley mode. It currently falls-back to the Random-Walk in Cycles-X.
Seems that initializing IOR and anisotropy for BSSRDF in the svm_closure.h solves the artifacts.

This is really weird, right? As subsurface ior and anisotropy are new parameters and of course not used in Christensen-Burley.
In any case, glad it is solved.

Testing the new state of the patch and can not reproduce the unexpected darkening anymore.

Great. Thanks for confirming.

Lastly, I'd rather keep the "Subsurface Ior" parameter (as opposed to "Subsurface IOR"), otherwise we have to upgrade all our scenes.

If there are a lot of scenes it could be possible to deal with them as a do-version.

Not sure what a do-version is. But, we had talked about it with a few users internally, and we had opted originally to "Subsurface Ior" rather than "Subsurface IOR", bringing less attention to that parameter (since no capitalization) compared to others in the UI.

From the remaining parts I think we have:

  • Make the Subsurface BSDF node to support anisotropy.

I can do that.

  • Find a solution to preserve compatibility

Not sure about this. The albedo inversion is much truer to the input color. This is a big advantage, since now an input in base color or in subsurface color will produce very much the same color (minus the softness and backlighting of translucency with sss).

  • Run benchmark, make sure we don't loose a lot of performance

Because of the Similarity Theory thing I incorporated, you should not see major slow downs from it. But let me know.
Internally, we have used this method for 2 years and have been happy with its performance.

  • Make the Subsurface BSDF node to support anisotropy.

I can do that.

After looking at it, I wonder whether this is the right choice. That node is very simple. The albedo inversion and the changes in random walk will already work with it.
Default anisotropy is 0 and default ior 1.4. These seems reasonable in that context and, in my opinion, may not need to be exposed as parameters in that particular shader.
But let me know what you think.

Nothing new from my end. Just synchronizing to the upstream changes.

Not sure what a do-version is

Is a versioning code. It allows to "patch" .blend file on loading so that it behaves same as in older Blender version. In this case it means that we can change socket name without loosing compatibility with files you have.

But, we had talked about it with a few users internally, and we had opted originally to "Subsurface Ior" rather than "Subsurface IOR", bringing less attention to that parameter (since no capitalization) compared to others in the UI.

To my knowledge all abbreviations in Blender are capitalized. You can also argue that making IOR less visible things become more confusing because of having two "Subsurface" sliders.
So far I don't really see a strong argument for deviating of Blender's consistency.

After looking at it, I wonder whether this is the right choice. That node is very simple. The albedo inversion and the changes in random walk will already work with it.
Default anisotropy is 0 and default ior 1.4. These seems reasonable in that context and, in my opinion, may not need to be exposed as parameters in that particular shader.

To me it does make sense. Is not so much for being a simple node, but more of a node dedicated to BSSRDF. Don't think we gain much by hiding parameters in it, and forcing to use Principled node when one needs to mix some SSS to their shader.

Not sure what a do-version is

Is a versioning code. It allows to "patch" .blend file on loading so that it behaves same as in older Blender version. In this case it means that we can change socket name without loosing compatibility with files you have.

Great. Could you show me an example / how to do that?

But, we had talked about it with a few users internally, and we had opted originally to "Subsurface Ior" rather than "Subsurface IOR", bringing less attention to that parameter (since no capitalization) compared to others in the UI.

To my knowledge all abbreviations in Blender are capitalized. You can also argue that making IOR less visible things become more confusing because of having two "Subsurface" sliders.
So far I don't really see a strong argument for deviating of Blender's consistency.

Ok then. Easy to do.

After looking at it, I wonder whether this is the right choice. That node is very simple. The albedo inversion and the changes in random walk will already work with it.
Default anisotropy is 0 and default ior 1.4. These seems reasonable in that context and, in my opinion, may not need to be exposed as parameters in that particular shader.

To me it does make sense. Is not so much for being a simple node, but more of a node dedicated to BSSRDF. Don't think we gain much by hiding parameters in it, and forcing to use Principled node when one needs to mix some SSS to their shader.

Alright. Will do.

Overall I agree with Sergey's comments.

For compatibility, I think we need to add a new option in the menu if the new behavior is desired. I can see no other practical way to keep a compatible result if a texture map is used for the base color.

Here's an example to show why. These 3 strips have albedo 0.8, 0.5 and 0.1.

OldNew

In the old mapping the effective scattering distance varies a lot with the albedo, in the new one not so much. From what I can tell this is (entirely?) due to this code in bssrdf_burley_setup:

const float3 A = bssrdf->albedo;
const float3 s = make_float3(
    bssrdf_burley_fitting(A.x), bssrdf_burley_fitting(A.y), bssrdf_burley_fitting(A.z));

bssrdf->radius = l / s;

Also to give some background, a reason the Subsurface Scattering node is useful to support is because we have always let users build their own uber-shaders. Since we added the Principled BSDF this is less important, but if we add MaterialX import in the future it will become more important again to have all the components available as individual nodes.

Also to give some background, a reason the Subsurface Scattering node is useful to support is because we have always let users build their own uber-shaders. Since we added the Principled BSDF this is less important, but if we add MaterialX import in the future it will become more important again to have all the components available as individual nodes.

I am working on the Subsurface Scattering node right now. No worries.

In the old mapping the effective scattering distance varies a lot with the albedo, in the new one not so much. From what I can tell this is (entirely?) due to this code in bssrdf_burley_setup:

Yes, the albedo inversion is really what I am changing deep down. This bssrdf_burley_setup()/old subsurface_random_walk_remap() approach does not obviously account for anisotropy. You can try it, ie keep it called in subsurface_random_walk_coefficients, something like:

/* Support for anisotropy from:
 * "Path Traced Subsurface Scattering using Anisotropic Phase Functions
 * and Non-Exponential Free Flights".
 * Magnus Wrenninge, Ryusuke Villemin, Christophe Hery.
 * https://graphics.pixar.com/library/PathTracedSubsurface/ */

ccl_device void subsurface_monaco_walk_remap(
        const float albedo,
        const float d,
        const float eta,
        const float inv_eta,
        const float g,
        float *sigma_t,
        float *alpha)
{
  /* Compute attenuation and scattering coefficients from albedo. */
  /* the following #if should stay at 1: this is only meant to test/compare to the old albedo inversion (without anisotropy) */
#if 1
  const float g2 = g * g;
  const float g3 = g2 * g;
  const float g4 = g3 * g;
  const float g5 = g4 * g;
  const float g6 = g5 * g;
  const float g7 = g6 * g;

  const float A = 1.8260523782f + -1.28451056436f * g + -1.79904629312f * g2 +
                  9.19393289202f * g3 + -22.8215585862f * g4 + 32.0234874259f * g5 +
                  -23.6264803333f * g6 + 7.21067002658f * g7;
  const float B = 4.98511194385f +
                  0.127355959438f *
                      expf(31.1491581433f * g + -201.847017512f * g2 + 841.576016723f * g3 +
                           -2018.09288505f * g4 + 2731.71560286f * g5 + -1935.41424244f * g6 +
                           559.009054474f * g7);
  const float C = 1.09686102424f + -0.394704063468f * g + 1.05258115941f * g2 +
                  -8.83963712726f * g3 + 28.8643230661f * g4 + -46.8802913581f * g5 +
                  38.5402837518f * g6 + -12.7181042538f * g7;
  const float D = 0.496310210422f + 0.360146581622f * g + -2.15139309747f * g2 +
                  17.8896899217f * g3 + -55.2984010333f * g4 + 82.065982243f * g5 +
                  -58.5106008578f * g6 + 15.8478295021f * g7;
  const float E = 4.23190299701f +
                  0.00310603949088f *
                      expf(76.7316253952f * g + -594.356773233f * g2 + 2448.8834203f * g3 +
                           -5576.68528998f * g4 + 7116.60171912f * g5 + -4763.54467887f * g6 +
                           1303.5318055f * g7);
  const float F = 2.40602999408f + -2.51814844609f * g + 9.18494908356f * g2 +
                  -79.2191708682f * g3 + 259.082868209f * g4 + -403.613804597f * g5 +

  const float blend = powf(albedo, 0.25f);

  *alpha = (1.0f - blend) * A * powf(atanf(B * albedo), C) +
                   blend  * D * powf(atanf(E * albedo), F);
  *alpha = clamp(*alpha, 0.0f, 0.99f);  // because of numerical precision

  const float F_dr = inv_eta * (-1.440f * inv_eta + 0.710f) + 0.668f + 0.0636f * eta;
  const float fourthirdA = (4.0f / 3.0f)
                         * (1.0f + F_dr) / (1.0f - F_dr);  // from Jensen's Fdr ratio formula
  float alpha_prime = dipoleComputeAlphaPrime(albedo, fourthirdA);
  float z_r = sqrtf(3.0f * (1.0f - alpha_prime)) * d;

  float sigma_t_prime = 1.0f / fmaxf(z_r, 1e-16f);
  *sigma_t = sigma_t_prime / (1.0f - g);
#else
  /* Default Cycles' albedo inversion */
  /* From bssrdf.h: bssrdf_burley_setup() */
  /* Note that our radius d (for new anisotropic random walk) was scaled already with the bssrdf_burley_compatible_mfp to be "backwards-compatible" */
  *alpha = 1.0f - expf(albedo * (-5.09406f + albedo * (2.61188f - albedo * 4.31805f)));
  *sigma_t = 1.0f / fmaxf(d, 1e-16f); // d/s*s obviously simplifies out!
#endif
}

With anisotropy/g at 0, you will get a close result between the two inversion methods (though the new one is truer to the equivalent diffuse render, from same input albedo).
When you modify the anisotropy to go towards say 0.7, then you will see a strong energy loss with the old approach.

Renamed "Subsurface Ior" into "Subsurface IOR".
Added corresponding changes/parameters in shader_subsurface_scattering.

(Would still appreciate an example for the do_version stuff).

Brecht Van Lommel (brecht) edited the summary of this revision. (Show Details)
  • Rebase patch based on latest cycles-x, where diffusion BSSRDFs were removed.
  • Add legacy Random Walk mode for backwards compatibility.
  • Fix Subsurface Scattering node not using anisotropy and IOR correctly.
  • Minor code style changes.

I didn't find a good way to preserve compatibility besides adding another mode. I'm thinking now perhaps it's useful anyway to be able to specify a manual radius (maybe from measurement), or to have it automatically adjusted based on albedo. So I might rename the option to make it a feature more than a compatibility thing.

For converting existing .blend files, renaming a socket can be done like this (untested, based on the updated patch): P2327

I tested this with some more .blend files besides our tests, and found that it does not preserve the color well for negative anisotropy or higher values like 0.8 (the value of skin from the paper). See the attached .blend file for example, the skin turns green at 0.8.

Is this something that can be improved? Otherwise it seems only anisotropy in the range 0.0..0.5 preserves albedo well?

Christophe Hery (chery) marked 2 inline comments as done.EditedAug 17 2021, 9:12 AM

I tested this with some more .blend files besides our tests, and found that it does not preserve the color well for negative anisotropy or higher values like 0.8 (the value of skin from the paper). See the attached .blend file for example, the skin turns green at 0.8.
Is this something that can be improved? Otherwise it seems only anisotropy in the range 0.0..0.5 preserves albedo well?

We use routinely 0.7 to 0.8 for anisotropy (corresponding to measurements).
I think all the problems you seem to be facing (Sergey had a similar issue early on) is due to very high albedo inputs (for subsurface color). Here, the red channel goes above 0.9, even 0.99. These are not realistic values for organic albedos.
In any case, you can modify in subsurface_random_walk_remap the numerical protective clamp I had added, from:

*alpha = clamp(*alpha, 0.0f, 0.99f);  // because of numerical precision

into:

*alpha = clamp(*alpha, 0.0f, 0.999999f);  // because of numerical precision

This should take care of the hue shift.

(On the other hand, I am not sure about the negative anisotropy behavior. Probably our fits were never meant for that, since skin is very forward scattering. So maybe we should just clamp between 0 and 0.9 in svm_closure.h and in the interface)

  • Fix hue shift with high albedo as suggested.
  • Fix negative anisotropy turning black, clamp to 0 now for remapping.
  • Rename legacy to fixed radius.
  • Reduce size of closure and GPU state by doing radius adjustment as part of closure setup.
This revision is now accepted and ready to land.Aug 17 2021, 7:52 PM