Cycles: new Microfacet-based Hair BSDF with elliptical cross-section support #105600
No reviewers
Labels
No Label
Interest
Alembic
Interest
Animation & Rigging
Interest
Asset System
Interest
Audio
Interest
Automated Testing
Interest
Blender Asset Bundle
Interest
BlendFile
Interest
Code Documentation
Interest
Collada
Interest
Compatibility
Interest
Compositing
Interest
Core
Interest
Cycles
Interest
Dependency Graph
Interest
Development Management
Interest
EEVEE
Interest
Freestyle
Interest
Geometry Nodes
Interest
Grease Pencil
Interest
ID Management
Interest
Images & Movies
Interest
Import Export
Interest
Line Art
Interest
Masking
Interest
Metal
Interest
Modeling
Interest
Modifiers
Interest
Motion Tracking
Interest
Nodes & Physics
Interest
OpenGL
Interest
Overlay
Interest
Overrides
Interest
Performance
Interest
Physics
Interest
Pipeline, Assets & IO
Interest
Platforms, Builds & Tests
Interest
Python API
Interest
Render & Cycles
Interest
Render Pipeline
Interest
Sculpt, Paint & Texture
Interest
Text Editor
Interest
Translations
Interest
Triaging
Interest
Undo
Interest
USD
Interest
User Interface
Interest
UV Editing
Interest
VFX & Video
Interest
Video Sequencer
Interest
Viewport & EEVEE
Interest
Virtual Reality
Interest
Vulkan
Interest
Wayland
Interest
Workbench
Interest: X11
Legacy
Asset Browser Project
Legacy
Blender 2.8 Project
Legacy
Milestone 1: Basic, Local Asset Browser
Legacy
OpenGL Error
Meta
Good First Issue
Meta
Papercut
Meta
Retrospective
Meta
Security
Module
Animation & Rigging
Module
Core
Module
Development Management
Module
Grease Pencil
Module
Modeling
Module
Nodes & Physics
Module
Pipeline, Assets & IO
Module
Platforms, Builds & Tests
Module
Python API
Module
Render & Cycles
Module
Sculpt, Paint & Texture
Module
Triaging
Module
User Interface
Module
VFX & Video
Module
Viewport & EEVEE
Platform
FreeBSD
Platform
Linux
Platform
macOS
Platform
Windows
Severity
High
Severity
Low
Severity
Normal
Severity
Unbreak Now!
Status
Archived
Status
Confirmed
Status
Duplicate
Status
Needs Info from Developers
Status
Needs Information from User
Status
Needs Triage
Status
Resolved
Type
Bug
Type
Design
Type
Known Issue
Type
Patch
Type
Report
Type
To Do
No Milestone
No project
No Assignees
5 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: blender/blender#105600
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "weizhen/blender:microfacet_hair"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements the paper A Microfacet-based Hair Scattering Model by Weizhen Huang, Matthias B. Hullin and Johannes Hanika.
Features:
Energy loss is expected especially with high roughness values and very light hair colors. There is non-physical modulation factors (Reflection, Transmission, Secondary Reflection) which might help to mitigate this issue.Comparisons:
Using a modified version of the demo file from Daniel Bystedt, attached as
hair_nodes-female_hair_styles.blend
. Rendered with Mac M1 Ultra Metal, 256spp, original resolution 800x1200.* Cyber punk hair is rendered with non-physical scaling of Transmission = 2 for Microfacet Hair model
Missing:
9039f76928
) to default the orientation to align with the curve normal in the mathematical sense, but the stability (when animated) is unclear and it would be a hassle to generalise to all curve types. After the model is in main, we could experiment with the geometry nodes team to see what works the best as a default.pdf
than just1.0
.fresnel()
e5dc796da9sample_wh()
` 0c07582169fresnel()
beingNaN
sometimes 028918661afast_sincosf()
call 5e21a0909cmodel_type
intocross_section_type
anddistribution_type
f88b91fe61bsdf_microfacet.h
035ee375acLABEL_TRANSMIT
dfbe7c544fclosure.h
90cf06c874tilt
insetup()
instead ofeval()
andsample()
1c6d728248SD_BSDF_HAS_TRANSMISSION
flagWIP: Cycles: new Microfacet-base Hair BSDF with elliptical cross-section supportto WIP: Cycles: new Microfacet-based Hair BSDF with elliptical cross-section support@ -319,2 +323,4 @@
attr_intercept->add(time);
if (attr_normal) {
/* TODO: compute geometry normals. */
The curve normal (which is needed for orienting the cross-section) is not considered in the legacy hair curve. I don't want to put too much effort into this if the legacy curve is to be discarded at some point.
@ -469,3 +468,1 @@
ccl_device_inline float bsdf_principled_hair_albedo_roughness_scale(
const float azimuthal_roughness)
/* Hair Albedo. Also used by `bsdf_hair_microfacet.h` */
These are utitily functions for matching direct color with absorption coefficient. They are designed for Principled Hair BSDF, and don't seem to match very well with the Microfacet Hair BSDF. Currently I just "borrow" these functions so direct coloring is also available for the new model. I could move the three shared functions to
bsdf_util.h
, or duplicate them inbsdf_hair_microfacet.h
, if this matters.@ -1142,1 +1141,3 @@
num_closures += 4;
else if (closure_type == CLOSURE_BSDF_HAIR_PRINCIPLED_ID ||
closure_type == CLOSURE_BSDF_HAIR_MICROFACET_ID) {
num_closures += 2;
Not sure why this was 4 before, there are only 2 closures,
...HairBSDF
and...HairExtra
.WIP: Cycles: new Microfacet-based Hair BSDF with elliptical cross-section supportto Cycles: new Microfacet-based Hair BSDF with elliptical cross-section support@blender-bot package
Package build started. Download here when ready.
For the energy loss, do you think it would help a lot to add albedo scaling as we are planning to do for the regular microfacet BSDF, and would that be potentially straightforward or require more research? Or is it mainly a TRRT+ term like Principled Hair that would help?
From a user point of view, it would be great to have a far field model that both converges faster and avoids the energy loss somehow. I'm wondering what the best way to approach that is, make this model more energy conserving or add far field for Principled Hair.
I tried to isolate individual lobes. The TRRT+ term in Principled Hair doesn't seem to make a drastic difference:
Here are comparisons of the individual lobes:
So the main source of energy loss seems to be the single-scattering microfacet BSDF, especially for the TT lobe.
An albedo scaling could make sense, but I don't know how we are approaching this in the planned new microfacet BSDF, is there a reference I could look into?
Thanks for the comparison, very interesting to see it broken down like that.
I'm not sure which exact approach @LukasStockner used, couldn't find a reference quickly in the
principled-v2
branch, maybe he can clarify. But I think it's one of these two:https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
http://www.aconty.com/pdf/s2017_pbs_imageworks_slides.pdf
In
principled-v2
, seecycles_precompute.cpp
for the code.Yep, those are the two relevant approaches. For both, you compute the single-scattering albedo depending on angle and roughness. Then, you either add a second lobe based on that data so that the total albedo ends up being one (see Imageworks slides) or you just scale the single-scattering lobe up to normalize it to albedo one (see the Turquin paper).
The nontrivial part is accounting for Fresnel, since higher-order scattering leads to higher saturation, but the formula from the revised Imageworks slides works well.
I'm currently using the "just normalize it" approach for Principled v2 since it's much easier to implement and visually seems to be competitive with the secondary lobe approach (neither is perfect, but which one is better depends on the scene).
67da18dc22
to7d25982d7c
7d25982d7c
to9fd0c502d6
The last few commits aim to mitigate the energy loss issue.
Energy loss due to single-scattering microfacets
We refer to the paper Practical multiple scattering compensation for microfacet models, which proposes to simply scale up the reflectance, given that multi-scattering lobes have similar shapes as the single-scattering one. I find this approach very reasonable. Since our hair model already accounts for internal absorption, we also don't need to worry about the fresnel term. A possible optimization would be to use different scaling factors for reflection and transmission, but I'm going to follow whatever is used for the upcoming Multi-GGX for now.
Commit
91562e880e
includes all the relevant changes, all changes except for those inbsdf_hair_microfacet.h
are partially copied from the principled-v2 branch. For those, it's fine to wait until @LukasStockner push the Multi-GGX-related changes to main.Energy loss due to omitted terms beyond TRT
According to Fig 16 in the paper A Microfacet-based Hair Scattering Model, The terms beyond TRT are mainly distributed around the specular cone direction, with a roughly diffuse azimuthal component. Therefore, I followed the approach in the paper A practical and controllable hair and fur model for production path tracing (also used in principled hair BSDF) to approximate the TRRT+ terms by summing up a geometric series.
Disclamer: The purpose of commit
9fd0c502d6
is to evaluate how much energy loss we can recover by adding the TRRT+ terms, the adopted approach is not strictly verified. It's probably better to use specular reflection and absorption to approximate the rest of the interactions instead of reusing the value from the previous interaction to save computation time, also the scaling4 * bsdf->roughness
I just took from the principled hair code, not sure if it's reasonable.Comparison
Furnace test with
melanin=0
,roughness=1
melanin=0.2
,roughness=0.3
Therefore, albedo scaling recovers 8% of the energy with typical parameters for blond hair, and TRRT+ terms recovers 2% in addition.
It appears that we can achieve energy conservation with albedo scaling and TRRT+ terms, however, with only slightly positive absorption (
melanin=0.0001
), the energy loss is already ~12%:This is due to total internal reflection, similar as in optical fibers. The residual terms would be trapped inside the fiber, and with non-negative absorption, eventually all energy are absorbed. Such energy loss is "physical" and "expected", therefore, we can NOT expect the microfacet-based hair model to display the same saturation as the principled hair model.
Full blond hair
We can see that the hair appears slightly brighter with energy compensation, but still quite far from the energy level of principled hair BSDF. There is also a ~10% increase in rendering time.
GGX vs Beckmann
The albedo scaling is computed for GGX, there is energy gain when combined with Beckmann distribution. Without proper measurement it's hard to tell which distribution matches the reality well, maybe we should only use GGX for simplicity.
@LukasStockner, is this something you could help reviewing?
On the user interface level, I'm wondering if we shouldn't unify this with the Principled Hair BSDF, as a single node.
The reason being that we have Principled BSDF, Principled Hair and Principled Volume that we recommend as the main shaders for users. But then if this is the best hair shaders, it should be the Principled Hair.
There could be an enum on the node that switching between the two modes, hiding sockets that are not relevant to one or the other implementation. As far as the Cycles SVM and OSL code is concerned these could remain two completely separate nodes though, it's only about the UI.
What do you think? Does that make sense, or does it seem like a poor fit?
bsdf_util.h
b278dbadbcOverall looks great, just a few notes from a first pass.
@ -0,0 +16,4 @@
typedef struct MicrofacetHairExtra {
/* Optional modulation factors. */
float R, TT, TRT;
Do you think it could make sense to support colors here? It would increase the space usage, so we should only do it if there's a realistic use case I guess.
Currently if the parametrization is set to Melanin Concentration, an extra Tint socket is enabled, which corresponds to the realistic case of dyed hair (there is melanin present in natural hair, extra pigments from the dye is added). The R, TT, TRT are non-physical modulation factors, I thinking adding colors there would make the hair appearance quite difficult to control, using Tint should be a more proper way.
@ -0,0 +66,4 @@
* \{ */
/* Returns `sin(theta)` of the given direction. */
ccl_device_inline float sin_theta(const float3 w)
This is probably fine, but one consideration here is that all functions in the kernel share a namespace, so e.g.
sin_theta
might easily conflict with something else in the future.Then again, this is probably general enough that we could just move it to
util/
if that happens.@ -0,0 +479,4 @@
const float weight = (i == 0 || i == intervals) ? 0.5f : (i % 2 + 1);
const Spectrum A_t = exp(mu_a / cos_theta(wt) *
This appears 4 times I think, can we split it into a helper function?
@ -0,0 +608,4 @@
*eta = bsdf->eta;
/* Treat as transparent material if intersection lies outside of the projected radius. */
if (fabsf(bsdf->h) > bsdf->extra->radius) {
Since both of these are already known in the setup function at shader evaluation time, we might want to implement the "default to transparent" logic there?
@ -0,0 +616,4 @@
return LABEL_TRANSMIT | LABEL_TRANSPARENT;
}
if (bsdf->extra->R <= 0.0f && bsdf->extra->TT <= 0.0f && bsdf->extra->TRT <= 0.0f) {
Same here.
@ -0,0 +643,4 @@
const float h = sample_h * 2.0f - 1.0f;
const float gamma_mi = is_circular ?
asinf(h) :
Nitpicking, but we should probably try to not make this span five lines (maybe an
if (is_circular)
?)Indeed it doesn't look good, but adding
if (is_circular)
makes it span 8 lines, not sure of the benefits.@ -0,0 +771,4 @@
fast_sincosf(phi_o, &sin_phi_o, &cos_phi_o);
/* Compute outgoing direction. */
wtrrt = make_float3(sin_phi_o * cos_theta_o, sin_theta_o, cos_phi_o * cos_theta_o);
I think this is only used if we actually sample TRRT? The compiler should probably optimize that by itself though I guess.
@ -0,0 +853,4 @@
const float3 local_I = bsdf->extra->wi;
const float3 local_O = make_float3(dot(wo, X), dot(wo, Y), dot(wo, Z));
/* TODO: better estimation of the pdf */
Does this TODO refer to the PR, or it more general?
I think it's fine to keep it like this for now, just want to make sure it's not overlooked.
In the original implementation the pdf involves sampling microfacets, it is as costly as evaluating the BSDF itself, and it is also just an approximation, so not sure if it's worth it and how to do it better. As pdf is only used for MIS, it doesn't influence the correctness of the model, so left as a TODO as a future improvement.
@ -58,2 +58,3 @@
/* All closures contribute to the normal feature, but only diffuse-like ones to the albedo. */
normal += sc->N * sc->sample_weight;
/* If far-field hair, use fiber tangent as feature instead of normal. */
normal += (sc->type == CLOSURE_BSDF_HAIR_MICROFACET_ID ? safe_normalize(sd->dPdu) : sc->N) *
Is this conditional just due to compatibility?
If yes, and the tangent works better, I think we should just change it for the old model as well.
This piece of code is provided by @olivier.fx. I didn't came up with the logic myself, but for a far-field model the "normal" is always the incoming direction, not really a feature of the object itself, so tangent makes more sense here. For the previous near-field model the normal does influence the appearance and it makes sense to use normal as feature. Not sure if tangent works better, I'm not very knowledgeable about denoising.
@ -90,3 +95,3 @@
}
BSDF = principled_hair(Normal, sigma, roughness, radial_roughness, m0_roughness, Offset, IOR);
normal major_axis = Normal;
I think we can move this into the main
if
below?@ -3434,2 +3445,4 @@
void PrincipledHairBsdfNode::attributes(Shader *shader, AttributeRequestSet *attributes)
{
/* Make sure we have the normal for elliptical cross section tracking. */
if (model == NODE_PRINCIPLED_HAIR_HUANG && aspect_ratio != 1.0f) {
I think you also need to check whether the socket is connected here?
@ -1282,0 +1282,4 @@
typedef struct NodeShaderHairPrincipled {
short model;
short parametrization;
short cross_section;
Doesn't appear to be used?
@ -1282,0 +1284,4 @@
short parametrization;
short cross_section;
char _pad[2];
} NodeShaderHairPrincipled;
Not sure if we need this yet,
custom1
andcustom2
should be enough?For the current two enums yes, but named struct seems easier understandable. On the other hand, it introduces additional storage and is seen as a compromise for not having enum-typed sockets. I'm actually not sure what the current recommendation is.
I think it's fine to have better names for storage, custom1 / custom2 are not that great.
@ -4546,0 +4554,4 @@
0,
"Far-field Model",
"Microfacet-based hair scattering model by Huang et. al 2022, suitable for viewing from a "
"distance, supports elliptical cross-sections and has preciser highlight in forward "
"preciser"->"more precise"
@ -27,0 +47,4 @@
.description(
"For elliptical hair cross-section, the aspect ratio is the ratio of the minor axis to "
"the major axis (the major axis is aligned with the curve normal). Recommended values "
"are 0.8~1 for Asian hair, 0.65~0.9 for Caucasian hair, 0.5~0.65 for African hair. Set "
I'm not sure if the tooltips are the best place to put these values, might be better to keep it short and have them in the manual?
I don't see tooltips in any other shader nodes, so I'm not sure if there is a guideline here, and I also have no idea about UX. I thought it would be good to provide clear information here, so the users don't need to go to the manual to find it. Currently this spans three lines, if conciseness is desired I can shorten this of course.
I think it's fine to have longer tooltips in general, so this is fine with me.
@LukasStockner There is a problem with your change. Because
num_closure
is set to 2 inshader_graph.cpp
, when it tries to allocate a transparent closure inbsdf_microfacet_hair_setup()
it would fail and render black pixels. I basically replaced the hair closure with a transparent closure in7d14b8da7e
, but I'm not sure if this is the proper way to "de-allocate" a closure.principled_hair
->hair_chiang
,microfacet_hair
->hair_huang
e6b102efc9Ah, true, that's a problem. I had figured that the extra leftover closure doesn't hurt, but I forgot about the closure limit.
The linked commit seems like the correct way to do this. It only works if there hasn't been another allocation in between, but here that's the case.
One thing to note is that
sd->num_closure_left += 2
is only correct as long as the Extra storage is not larger than aShaderClosure
. We might want to either add a static assert for that, or add generaldealloc_extra
anddealloc_closure
toalloc.h
, wheredealloc_extra
could take a size argument.maybe something using realtime raytracing can mimic this for EEVEE-Next?