Skip to content

Commit 3ce8ea8

Browse files
author
devsh
committed
Write down the final design of the layering!
1 parent 4dbc948 commit 3ce8ea8

File tree

2 files changed

+63
-15
lines changed

2 files changed

+63
-15
lines changed

include/nbl/asset/material_compiler3/CFrontendIR.h

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,51 @@ namespace nbl::asset::material_compiler3
2626
// plan on ignoring every transmission through the microfacets within the statistical pixel footprint as given by the VNDF except the perfectly specular one.
2727
// The energy loss from that leads to pathologies like the glGTF Specular+Diffuse model, comparison: https://x.com/DS2LightingMod/status/1961502201228267595
2828
//
29-
// There's an implicit Top and Bottom on the layer stack, but thats only for the purpose of interpreting the Etas (predivided ratios of Indices of Refraction).
30-
// We don't track the IoRs per layer because that would deprive us of the option to model each layer interface as a mixture of materials (metalness workflow).
31-
//
32-
// If you don't plan on ignoring the actual convolution of incoming light by the VNDF, such an assumption only speeds up the Importance Sampling slightly as
29+
// If you don't plan on ignoring the actual convolution of incoming light by the BSDF, such an assumption only speeds up the Importance Sampling slightly as
3330
// on the way back through a layer we don't consume another 2D random variable, instead transforming the ray deterministically. This however would require one
3431
// to keep a stack of cached interactions with each layer, and its just simpler to run local path tracing through layers which can account for multiple scattering
3532
// through a medium layer, etc.
3633
//
34+
// Our Frontend is built around the IR, which wants to perform the following canonicalization of a BSDF Layer (not including emission):
35+
//
36+
// f(w_i,w_o) = Sum_i^N Product_j^{N_i} h_{ij}(w_i,w_o) l_i(w_i,w_o)
37+
//
38+
// Where `l(w_i,w_o)` is a Contributor Node BxDF such as Oren Nayar or Cook-Torrance, which is doesn't model absorption and is usually Monochrome.
39+
// These are assumed to be 100% valid BxDFs with White Furnace Test <= 1 and obeying Helmholtz Reciprocity. This is why you can't multiply two "Contributor Nodes" together.
40+
// We make an attempt to implement Energy Normalized versions of `l_i` but not always, so there might be energy loss due to single scattering assumptions.
41+
//
42+
// This convention greatly simplifies the Layering of BSDFs as when we model two layers combined we need only consider the Sum terms which are Products of a BTDF contibutor
43+
// in convolution with the layer below or above. For emission this is equivalent to convolving the emission with BTDFs producing a custom emission profile.
44+
// Some of these combinations can be approximated or solved outright without resolving to frequency space approaches or path tracing within the layers.
45+
//
46+
// To obtain a valid BxDF for the canonical expression, each product of weights also needs to exhibit Helmholtz Reciprocity:
47+
//
48+
// Product_j^{N_i} h(w_i,w_o) = Product_j^{N_i} h(w_o,w_i)
49+
//
50+
// Which means that direction dependant weight nodes need to know the underlying contributor they are weighting to determine their semantics, e.g. a Fresnel on:
51+
// - Cook Torrance will use the Microfacet Normal for any calculation as that is symmetric between `w_o` and `w_i`
52+
// - Diffuse will use both `NdotV` and `NdotL` (also known as `theta_i` and `theta_o`) symmetrically
53+
// - A BTDF will use the compliments (`1-x`) of the Fresnels
54+
//
55+
// We cannot derive BTDF factors from top and bottom BRDF as the problem is underconstrained, we don't know which factor models absorption and which part transmission.
56+
//
57+
// Helmholtz Reciprocity allows us to use completely independent BRDFs per hemisphere, when `w_i` and `w_o` are in the same hemisphere (reflection).
58+
// Note that transmission only occurs when `w_i` and `w_o` are in opposite hemispheres and the reciprocity forces one BTDF.
59+
//
60+
// There's an implicit Top and Bottom on the layer stack, but thats only for the purpose of interpreting the Etas (predivided ratios of Indices of Refraction),
61+
// both the Top and Bottom BRDF treat the Eta as being the speed of light in the medium above over the speed of light in the medium below.
62+
// This means that for modelling air-vs-glass you use the same Eta for the Top BRDF, the middle BTDF and Bottom BRDF.
63+
// We don't track the IoRs per layer because that would deprive us of the option to model each layer interface as a mixture of materials (metalness workflow).
64+
//
65+
// The backend can expand the Top BRDF, Middle BTDF, Bottom BRDF into 4 separate instruction streams for Front-Back BRDF and BTDF. This is because we can
66+
// throw away the first or last BRDF+BTDF in the stack, as well as use different pre-computed Etas if we know the sign of `cos(theta_i)` as we interact with each layer.
67+
// Whether the backend actually generates a separate instruction stream depends on the impact of Instruction Cache misses due to not sharing streams for layers.
68+
//
69+
// Also note that a single null BTDF in the stack splits it into the two separate stacks, one per original interaction orientation.
70+
//
71+
// I've considered expressing the layers using only a BTDF and BRDF (same top and bottom hemisphere) but that would lead to more layers in for materials,
72+
// requiring the placing of a mirror then vantablack layer for most one-sided materials, and most importantly disallow the expression of certain front-back correlations.
73+
//
3774
// Because we implement Schussler et. al 2017 we also ensure that signs of dot products with shading normals are identical to smooth normals.
3875
// However the smooth normals are not identical to geometric normals, we reserve the right to use the "normal pull up trick" to make them consistent.
3976
// Schussler can't help with disparity of Smooth Normal and Geometric Normal, it turns smooth surfaces into glistening "disco balls" really outlining the
@@ -459,9 +496,11 @@ class CFrontendIR : public CNodePool
459496
NBL_API bool invalid(const SInvalidCheckArgs& args) const override;
460497
NBL_API void printDot(std::ostringstream& sstr, const core::string& selfID) const override;
461498
};
462-
//! Special nodes meant to be used as `CMul::rhs`, as for the `N`, they use the normal used by the Leaf ContributorLeafs in its MUL node relative subgraph.
463-
//! However if the Leaf BXDF is Cook Torrance, the microfacet `H` normal will be used instead.
464-
//! If there are two BxDFs with different normals, these nodes get split and duplicated into two in our Final IR.
499+
//! Special nodes meant to be used as `CMul::rhs`, their behaviour depends on the IContributor in its MUL node relative subgraph.
500+
//! If you use a different contributor node type or normal for shading, these nodes get split and duplicated into two in our Final IR.
501+
//! Due to the Helmholtz Reciprocity handling outlined in the comments for the entire front-end you can usually count on these nodes
502+
//! getting applied once using `VdotH` for Cook-Torrance BRDF, twice using `VdotN` and `LdotN` for Diffuse BRDF, and using their
503+
//! complements before multiplication for BTDFs.
465504
//! ----------------------------------------------------------------------------------------------------------------
466505
// Beer's Law Node, behaves differently depending on where it is:
467506
// - to get a scattering medium, multiply it with CDeltaTransmission BTDF placed between two BRDFs in the same medium
@@ -500,7 +539,7 @@ class CFrontendIR : public CNodePool
500539

501540
// Already pre-divided Index of Refraction, e.g. exterior/interior since VdotG>0 the ray always arrives from the exterior.
502541
TypedHandle<CSpectralVariable> orientedRealEta = {};
503-
// Specifying this turns your Fresnel into a conductor one
542+
// Specifying this turns your Fresnel into a conductor one, note that currently these are disallowed on BTDFs!
504543
TypedHandle<CSpectralVariable> orientedImagEta = {};
505544
// if you want to reuse the same parameter but want to flip the interfaces around
506545
uint8_t reciprocateEtas : 1 = false;
@@ -512,7 +551,7 @@ class CFrontendIR : public CNodePool
512551
};
513552
// @kept_secret TODO: Thin Film Interference Fresnel
514553
//! Basic BxDF nodes
515-
// Every BxDF leaf node is supposed to pass WFT test, color and extinction is added on later via multipliers
554+
// Every BxDF leaf node is supposed to pass WFT test and must not create energy, color and extinction is added on later via multipliers
516555
class IBxDF : public IContributor
517556
{
518557
public:
@@ -547,10 +586,10 @@ class CFrontendIR : public CNodePool
547586
// Delta Transmission is the only Special Delta Distribution Node, because of how useful it is for compiling Anyhit shaders, the rest can be done easily with:
548587
// - Delta Reflection -> Any Cook Torrance BxDF with roughness=0 attached as BRDF
549588
// - Smooth Conductor -> above multiplied with Conductor-Fresnel
550-
// - Smooth Dielectric -> Any Cook Torrance BxDF with roughness=0 attached as BRDF on both sides (bottom side having a reciprocated Eta) of a Layer and BTDF multiplied with Dielectric-Fresnel (no imaginary component)
551-
// - Thindielectric -> Any Cook Torrance BxDF multiplied with Dielectric-Fresnel as BRDF in both sides and a Delta Transmission BTDF
552-
// - Plastic -> Can layer the above over Diffuse BRDF, but its faster to cook a mixture of Diffuse and Smooth Conductor BRDFs, weighing the diffuse by Fresnel complements.
553-
// If one wants to emulate non-linear diffuse TIR color shifts, abuse `CThinInfiniteScatterCorrection`
589+
// - Smooth Dielectric -> Any Cook Torrance BxDF with roughness=0 attached as BRDF on both sides of a Layer and BTDF multiplied with Dielectric-Fresnel (no imaginary component)
590+
// - Thindielectric -> Any Cook Torrance BxDF multiplied with Dielectric-Fresnel as BRDF in both sides and a Delta Transmission BTDF with `CThinInfiniteScatterCorrection` on the fresnel
591+
// - Plastic -> Similar to layering the above over Diffuse BRDF, its of uttmost importance that the BTDF is Delta Transmission.
592+
// If one wants to emulate non-linear diffuse TIR color shifts, abuse `CThinInfiniteScatterCorrection`.
554593
class CDeltaTransmission final : public IBxDF
555594
{
556595
public:
@@ -566,7 +605,8 @@ class CFrontendIR : public CNodePool
566605
protected:
567606
inline _TypedHandle<IExprNode> getChildHandle_impl(const uint8_t ix) const override {return {};}
568607
};
569-
// Because of Schussler et. al 2017 every one of these nodes splits into 2 (if no L dependence) or 3 during canonicalization
608+
//! Because of Schussler et. al 2017 every one of these nodes splits into 2 (if no L dependence) or 3 during canonicalization
609+
// Base diffuse node
570610
class COrenNayar final : public IBxDF
571611
{
572612
public:

src/nbl/asset/material_compiler3/CFrontendIR.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,15 @@ bool CFrontendIR::CFresnel::invalid(const SInvalidCheckArgs& args) const
4242
args.logger.log("Oriented Real Eta node of correct type must be attached, but is %u of type %s",ELL_ERROR,orientedRealEta,args.pool->getTypeName(orientedRealEta).data());
4343
return true;
4444
}
45-
if (!args.pool->deref(orientedImagEta))
45+
if (const auto imagEta = args.pool->deref(orientedImagEta); imagEta)
46+
{
47+
if (args.isBTDF)
48+
{
49+
const auto knotCount = imagEta->getKnotCount();
50+
// TODO: check all knots have a scale of 0
51+
}
52+
}
53+
else
4654
{
4755
args.logger.log("Oriented Imaginary Eta node of correct type must be attached, but is %u of type %s",ELL_ERROR,orientedImagEta,args.pool->getTypeName(orientedImagEta).data());
4856
return true;

0 commit comments

Comments
 (0)