diff --git a/examples_tests b/examples_tests index 0d1cd50d4d..fd8ebfeaca 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 0d1cd50d4d60611ae8d192cf43d719667193a559 +Subproject commit fd8ebfeacaf42f3cf63b1545cccce03809f9c8d6 diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index e22956cc37..c866763bd6 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2025 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2022-2025 - DevSH Graphics Programming Sp. z O.O. // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h #ifndef _NBL_ASSET_MATERIAL_COMPILER_V3_C_FRONTEND_IR_H_INCLUDED_ @@ -26,14 +26,51 @@ namespace nbl::asset::material_compiler3 // plan on ignoring every transmission through the microfacets within the statistical pixel footprint as given by the VNDF except the perfectly specular one. // The energy loss from that leads to pathologies like the glGTF Specular+Diffuse model, comparison: https://x.com/DS2LightingMod/status/1961502201228267595 // -// 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). -// 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). -// -// 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 +// 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 // 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 // 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 // through a medium layer, etc. // +// Our Frontend is built around the IR, which wants to perform the following canonicalization of a BSDF Layer (not including emission): +// +// f(w_i,w_o) = Sum_i^N Product_j^{N_i} h_{ij}(w_i,w_o) l_i(w_i,w_o) +// +// 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. +// 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. +// 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. +// +// 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 +// in convolution with the layer below or above. For emission this is equivalent to convolving the emission with BTDFs producing a custom emission profile. +// Some of these combinations can be approximated or solved outright without resolving to frequency space approaches or path tracing within the layers. +// +// To obtain a valid BxDF for the canonical expression, each product of weights also needs to exhibit Helmholtz Reciprocity: +// +// Product_j^{N_i} h(w_i,w_o) = Product_j^{N_i} h(w_o,w_i) +// +// 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: +// - Cook Torrance will use the Microfacet Normal for any calculation as that is symmetric between `w_o` and `w_i` +// - Diffuse will use both `NdotV` and `NdotL` (also known as `theta_i` and `theta_o`) symmetrically +// - A BTDF will use the compliments (`1-x`) of the Fresnels +// +// 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. +// +// Helmholtz Reciprocity allows us to use completely independent BRDFs per hemisphere, when `w_i` and `w_o` are in the same hemisphere (reflection). +// Note that transmission only occurs when `w_i` and `w_o` are in opposite hemispheres and the reciprocity forces one BTDF. +// +// 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), +// 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. +// This means that for modelling air-vs-glass you use the same Eta for the Top BRDF, the middle BTDF and Bottom BRDF. +// 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). +// +// 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 +// 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. +// Whether the backend actually generates a separate instruction stream depends on the impact of Instruction Cache misses due to not sharing streams for layers. +// +// Also note that a single null BTDF in the stack splits it into the two separate stacks, one per original interaction orientation. +// +// 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, +// 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. +// // Because we implement Schussler et. al 2017 we also ensure that signs of dot products with shading normals are identical to smooth normals. // 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. // Schussler can't help with disparity of Smooth Normal and Geometric Normal, it turns smooth surfaces into glistening "disco balls" really outlining the @@ -76,6 +113,18 @@ class CFrontendIR : public CNodePool { return abs(scale)::infinity() && (!view || viewChannelgetCreationParameters().format)); } + inline bool operator!=(const SParameter& other) const + { + if (scale!=other.scale) + return true; + if (viewChannel!=other.viewChannel) + return true; + // don't compare paddings! + if (view!=other.view) + return true; + return sampler!=other.sampler; + } + inline bool operator==(const SParameter& other) const {return !operator!=(other);} NBL_API void printDot(std::ostringstream& sstr, const core::string& selfID) const; @@ -86,6 +135,7 @@ class CFrontendIR : public CNodePool uint8_t padding[3] = {0,0,0}; core::smart_refctd_ptr view = {}; // shadow comparison functions are ignored + // NOTE: could take only things that matter from the sampler and pack the viewChannel and reduce padding ICPUSampler::SParams sampler = {}; }; // In the forest, this is not a node, we'll deduplicate later @@ -95,7 +145,7 @@ class CFrontendIR : public CNodePool private: friend class CSpectralVariable; template - inline void printDot(const uint8_t _count, std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}) const + inline void printDot(const uint8_t _count, std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) const { bool imageUsed = false; for (uint8_t i=0; i<_count; i++) @@ -110,10 +160,10 @@ class CFrontendIR : public CNodePool else sstr <<" [label=\"Param " << std::to_string(i) <<"\"]"; } - if (imageUsed) + if (uvRequired || imageUsed) { const auto uvTransformID = selfID+"_uvTransform"; - sstr << "\n\t" << uvTransformID << " [label=\""; + sstr << "\n\t" << uvTransformID << " [label=\"uvSlot = " << std::to_string(uvSlot()) << "\\n"; printMatrix(sstr,*reinterpret_cast(params+_count)); sstr << "\"]"; sstr << "\n\t" << selfID << " -> " << uvTransformID << "[label=\"UV Transform\"]"; @@ -128,20 +178,21 @@ class CFrontendIR : public CNodePool return false; return true; } - // Ignored if no modulator textures + // Ignored if no modulator textures and isotropic BxDF uint8_t& uvSlot() {return params[0].padding[0];} const uint8_t& uvSlot() const {return params[0].padding[0];} // Note: the padding abuse static_assert(sizeof(SParameter::padding)>0); template - inline void printDot(std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}) const + inline void printDot(std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) const { - printDot(Count,sstr,selfID,std::forward(paramNameBegin)); + printDot(Count,sstr,selfID,std::forward(paramNameBegin),uvRequired); } SParameter params[Count] = {}; // identity transform by default, ignored if no UVs + // NOTE: a transform could be applied per-param, whats important that the UV slot remains the smae across all of them. hlsl::float32_t2x3 uvTransform = hlsl::float32_t2x3( 1,0,0, 0,1,0 @@ -228,10 +279,22 @@ class CFrontendIR : public CNodePool bool isBTDF; // there's space for 7 more bools }; - virtual inline bool invalid(const SInvalidCheckArgs&) const {return false;} + // by default all children are mandatory + virtual inline bool invalid(const SInvalidCheckArgs& args) const + { + const auto childCount = getChildCount(); + for (uint8_t i=0u; i getChildHandle_impl(const uint8_t ix) const = 0; virtual inline core::string getLabelSuffix() const {return "";} + virtual inline std::string_view getChildName_impl(const uint8_t ix) const {return "";} virtual inline void printDot(std::ostringstream& sstr, const core::string& selfID) const {} }; @@ -252,16 +315,19 @@ class CFrontendIR : public CNodePool enum class Semantics : uint8_t { + NoneUndefined = 0, // 3 knots, their wavelengths are implied and fixed at color primaries - Fixed3_SRGB = 0, - Fixed3_DCI_P3 = 1, - Fixed3_BT2020 = 2, - Fixed3_AdobeRGB = 3, - Fixed3_AcesCG = 4, + Fixed3_SRGB = 1, + Fixed3_DCI_P3 = 2, + Fixed3_BT2020 = 3, + Fixed3_AdobeRGB = 4, + Fixed3_AcesCG = 5, // Ideas: each node is described by (wavelength,value) pair // PairsLinear = 5, // linear interpolation // PairsLogLinear = 5, // linear interpolation in wavelenght log space }; + + // template struct SCreationParams { @@ -275,28 +341,58 @@ class CFrontendIR : public CNodePool template requires (Enable==(Count>1)) const Semantics& getSemantics() const {return const_cast(const_cast*>(this)->getSemantics());} }; + // template - static inline uint32_t calc_size(const SCreationParams&) + inline CSpectralVariable(SCreationParams&& params) { - return sizeof(CSpectralVariable)+sizeof(SCreationParams); + // back up the count + params.knots.params[0].padding[1] = Count; + // set it correctly for monochrome + if constexpr (Count==1) + params.knots.params[0].padding[2] = static_cast(Semantics::NoneUndefined); + else + { + assert(params.getSemantics()!=Semantics::NoneUndefined); + } + std::construct_at(reinterpret_cast*>(this+1),std::move(params)); } - + + // encapsulation due to padding abuse + inline uint8_t& uvSlot() {return pWonky()->knots.uvSlot();} + inline const uint8_t& uvSlot() const {return pWonky()->knots.uvSlot();} + + // these getters are immutable inline uint8_t getKnotCount() const { static_assert(sizeof(SParameter::padding)>1); return paramsBeginPadding()[1]; } - inline uint32_t getSize() const override + inline Semantics getSemantics() const { - return sizeof(CSpectralVariable)+sizeof(SCreationParams<1>)+(getKnotCount()-1)*sizeof(SParameter); + static_assert(sizeof(SParameter::padding)>2); + const auto retval = static_cast(paramsBeginPadding()[2]); + assert((getKnotCount()==1)==(retval==Semantics::NoneUndefined)); + return retval; + } + + // + inline SParameter* getParam(const uint8_t i) + { + if (iknots.params[i]; + return nullptr; } + inline const SParameter* getParam(const uint8_t i) const {return const_cast(const_cast(this)->getParam(i));} + // template - inline CSpectralVariable(SCreationParams&& params) + static inline uint32_t calc_size(const SCreationParams&) { - // back up the count - params.knots.params[0].padding[1] = Count; - std::construct_at(reinterpret_cast*>(this+1),std::move(params)); + return sizeof(CSpectralVariable)+sizeof(SCreationParams); + } + inline uint32_t getSize() const override + { + return sizeof(CSpectralVariable)+sizeof(SCreationParams<1>)+(getKnotCount()-1)*sizeof(SParameter); } inline operator bool() const {return !invalid(SInvalidCheckArgs{.pool=nullptr,.logger=nullptr});} @@ -304,17 +400,15 @@ class CFrontendIR : public CNodePool protected: inline ~CSpectralVariable() { - auto pWonky = reinterpret_cast*>(this+1); - std::destroy_n(pWonky->knots.params,getKnotCount()); + std::destroy_n(pWonky()->knots.params,getKnotCount()); } inline _TypedHandle getChildHandle_impl(const uint8_t ix) const override {return {};} inline bool invalid(const SInvalidCheckArgs& args) const override { const auto knotCount = getKnotCount(); - auto pWonky = reinterpret_cast*>(this+1); // non-monochrome spectral variable - if (const auto semantic=pWonky->getSemantics(); knotCount>1) + if (const auto semantic=getSemantics(); knotCount>1) switch (semantic) { case Semantics::Fixed3_SRGB: [[fallthrough]]; @@ -333,7 +427,7 @@ class CFrontendIR : public CNodePool return true; } for (auto i=0u; iknots.params[i]) + if (!*getParam(i)) { args.logger.log("Knot %u parameters invalid",system::ILogger::ELL_ERROR,i); return true; @@ -345,7 +439,9 @@ class CFrontendIR : public CNodePool NBL_API void printDot(std::ostringstream& sstr, const core::string& selfID) const override; private: - const uint8_t* paramsBeginPadding() const {return reinterpret_cast*>(this+1)->knots.params[0].padding;} + SCreationParams<1>* pWonky() {return reinterpret_cast*>(this+1);} + const SCreationParams<1>* pWonky() const {return reinterpret_cast*>(this+1);} + const uint8_t* paramsBeginPadding() const {return pWonky()->knots.params[0].padding; } }; // class IUnaryOp : public IExprNode @@ -362,6 +458,7 @@ class CFrontendIR : public CNodePool { protected: inline TypedHandle getChildHandle_impl(const uint8_t ix) const override final {return ix ? rhs:lhs;} + inline std::string_view getChildName_impl(const uint8_t ix) const override final {return ix ? "rhs":"lhs";} public: inline uint8_t getChildCount() const override final {return 2;} @@ -403,34 +500,6 @@ class CFrontendIR : public CNodePool inline uint32_t getSize() const override { return calc_size(); } inline CComplement() = default; }; - // Compute Inifinite Scatter and extinction between two parallel infinite planes - // Reflective Component is: R, T E R E T, T E (R E)^3 T, T E (R E)^5 T, ... - // Transmissive Component is: T E T, T E (R E)^2 T, T E (R E)^4 T, ... - // Note: This node can be also used to model non-linear color shifts of Diffuse BRDF multiple scattering if one plugs in the albedo as the reflectance. - class CThinInfiniteScatterCorrection final : public IExprNode - { - protected: - inline TypedHandle getChildHandle_impl(const uint8_t ix) const override final {return ix ? (ix!=1 ? extinction:transmittance):reflectance;} - inline void printDot(std::ostringstream& sstr, const core::string& selfID) const override - { - sstr << "\n\t" << selfID << " -> " << selfID << "_computeTransmittance [label=\"computeTransmittance = " << (computeTransmittance ? "true":"false") << "\"]"; - } - - public: - inline uint8_t getChildCount() const override final {return 3;} - inline const std::string_view getTypeName() const override {return "nbl::CThinInfiniteScatterCorrection";} - - // you can set the children later - static inline uint32_t calc_size() { return sizeof(CThinInfiniteScatterCorrection); } - inline uint32_t getSize() const override { return calc_size(); } - inline CThinInfiniteScatterCorrection() = default; - - TypedHandle reflectance = {}; - TypedHandle transmittance = {}; - TypedHandle extinction = {}; - // Whether to compute reflectance or transmittance - uint8_t computeTransmittance : 1 = false; - }; // Emission nodes are only allowed in BRDF expressions, not BTDF. To allow different emission on both sides, expressed unambigously. // Basic Emitter - note that it is of unit radiance so its easier to importance sample class CEmitter final : public IContributor @@ -459,15 +528,20 @@ class CFrontendIR : public CNodePool NBL_API bool invalid(const SInvalidCheckArgs& args) const override; NBL_API void printDot(std::ostringstream& sstr, const core::string& selfID) const override; }; - //! 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. - //! However if the Leaf BXDF is Cook Torrance, the microfacet `H` normal will be used instead. - //! If there are two BxDFs with different normals, these nodes get split and duplicated into two in our Final IR. - //! ---------------------------------------------------------------------------------------------------------------- + //! Special nodes meant to be used as `CMul::rhs`, their behaviour depends on the IContributor in its MUL node relative subgraph. + //! If you use a different contributor node type or normal for shading, these nodes get split and duplicated into two in our Final IR. + //! Due to the Helmholtz Reciprocity handling outlined in the comments for the entire front-end you can usually count on these nodes + //! getting applied once using `VdotH` for Cook-Torrance BRDF, twice using `VdotN` and `LdotN` for Diffuse BRDF, and using their + //! complements before multiplication for BTDFs. + class IContributorDependant : public IExprNode + { + }; // Beer's Law Node, behaves differently depending on where it is: - // - to get a scattering medium, multiply it with CDeltaTransmission BTDF placed between two BRDFs in the same medium - // - to get a scattering medium between two Layers, create a layer with the above + // - to get an extinction medium, multiply it with CDeltaTransmission BTDF placed between two BRDFs in the same medium + // - to get a scattering medium between two Layers, create a layer with just a BTDF set up like above // - to apply the beer's law on a single microfacet or a BRDF or BTDF multiply it with a BxDF - class CBeer final : public IExprNode + // Note: Even it makes little sense, Beer can be applied to the most outermost BRDF to simulate a correllated "foggy" coating without an extra BRDF layer. + class CBeer final : public IContributorDependant { public: inline const std::string_view getTypeName() const override {return "nbl::CBeer";} @@ -478,17 +552,20 @@ class CFrontendIR : public CNodePool inline uint32_t getSize() const override {return calc_size();} inline CBeer() = default; - // Effective transparency = exp2(log2(perpTransparency)/dot(refract(V,X,eta),X)) = exp2(log2(perpTransparency)*inversesqrt(1.f+(LdotX-1)*rcpEta)) + // Effective transparency = exp2(log2(perpTransmittance)/dot(refract(V,X,eta),X)) = exp2(log2(perpTransmittance)*inversesqrt(1.f+(LdotX-1)*rcpEta)) // Absorption and thickness of the interface combined into a single variable, eta and `LdotX` is taken from the leaf BTDF node. // With refractions from Dielectrics, we get just `1/LdotX`, for Delta Transmission we get `1/VdotN` since its the same - TypedHandle perpTransparency = {}; + TypedHandle perpTransmittance = {}; protected: - inline TypedHandle getChildHandle_impl(const uint8_t ix) const override {return perpTransparency;} + inline TypedHandle getChildHandle_impl(const uint8_t ix) const override {return perpTransmittance;} + + inline std::string_view getChildName_impl(const uint8_t ix) const override {return "Perpendicular\\nTransmittance";} NBL_API bool invalid(const SInvalidCheckArgs& args) const override; }; // The "oriented" in the Etas means from frontface to backface, so there's no need to reciprocate them when creating matching BTDF for BRDF - class CFresnel final : public IExprNode + // @kept_secret TODO: Thin Film Interference Fresnel + class CFresnel final : public IContributorDependant { public: inline uint8_t getChildCount() const override {return 2;} @@ -500,7 +577,7 @@ class CFrontendIR : public CNodePool // Already pre-divided Index of Refraction, e.g. exterior/interior since VdotG>0 the ray always arrives from the exterior. TypedHandle orientedRealEta = {}; - // Specifying this turns your Fresnel into a conductor one + // Specifying this turns your Fresnel into a conductor one, note that currently these are disallowed on BTDFs! TypedHandle orientedImagEta = {}; // if you want to reuse the same parameter but want to flip the interfaces around uint8_t reciprocateEtas : 1 = false; @@ -508,11 +585,48 @@ class CFrontendIR : public CNodePool protected: inline TypedHandle getChildHandle_impl(const uint8_t ix) const override {return ix ? orientedImagEta:orientedRealEta;} NBL_API bool invalid(const SInvalidCheckArgs& args) const override; + inline std::string_view getChildName_impl(const uint8_t ix) const override {return ix ? "Real":"Imaginary";} NBL_API void printDot(std::ostringstream& sstr, const core::string& selfID) const override; }; - // @kept_secret TODO: Thin Film Interference Fresnel + // Compute Inifinite Scatter and extinction between two parallel infinite planes. + // It's a specialization of what would be a layer of two identical smooth BRDF and BTDF with arbitrary Fresnel function and beer's + // extinction on the BRDFs (not BTDFs), all applied on a per micro-facet basis (layering per microfacet, not whole surface). + // + // We actually allow you to use different reflectance nodes R_u and R_b, the NDFs of both layers remain the same but Reflectance Functions to differ. + // Note that e.g. using different Etas for the Fresnel used for the top and bottom reflectance will result in a compound Fresnel!=1.0 + // meaning that in such case you can no longer optimize the BTDF contributor into a DeltaTransmission but need a CookTorrance with + // an Eta equal to the ratio of the first Eta over the second Eta (note that when they're equal the ratio is 1 which turns into Delta Trans). + // + // Because we split BRDF and BTDF into separate expressions, what this node computes differs depending on where it gets used: + // BRDF: R_u + (1-R_u)^2 E^2 R_b Sum_{i=0}^{\Inf}{(R_b R_u E^2)^i} = R_u + (1-R_u)^2 E^2 R_b / (1 - R_u R_b E^2) = R_u + (1-R_u)^2 R_b / (E^-2 - R_u R_b) + // BTDF: (1-R_u) E (1-R_b) Sum_{i=0}^{\Inf}{(R_b R_u E^2)^i} = (1-R_u) E^2 (1-R_b) / (1 - R_u R_b E^2) = (1-R_u) (1-R_b) / (E^-2 - R_u R_b) + // Note the transformation at the end just makes the prevention of 0/0 or 0*INF same as for a non-extinctive equation, just check `R_u*R_b < Threshold` + // + // Note: This node can be also used to model non-linear color shifts of Diffuse BRDF multiple scattering if one plugs in the albedo as the extinction. + class CThinInfiniteScatterCorrection final : public IExprNode + { + protected: + inline TypedHandle getChildHandle_impl(const uint8_t ix) const override final {return ix ? (ix>1 ? reflectanceBottom:extinction):reflectanceTop;} + NBL_API bool invalid(const SInvalidCheckArgs& args) const override; + + inline std::string_view getChildName_impl(const uint8_t ix) const override {return ix ? (ix>1 ? "reflectanceBottom":"extinction"):"reflectanceTop";} + + public: + inline uint8_t getChildCount() const override final {return 3;} + inline const std::string_view getTypeName() const override {return "nbl::CThinInfiniteScatterCorrection";} + + // you can set the children later + static inline uint32_t calc_size() {return sizeof(CThinInfiniteScatterCorrection);} + inline uint32_t getSize() const override {return calc_size();} + inline CThinInfiniteScatterCorrection() = default; + + TypedHandle reflectanceTop = {}; + // optional + TypedHandle extinction = {}; + TypedHandle reflectanceBottom = {}; + }; //! Basic BxDF nodes - // Every BxDF leaf node is supposed to pass WFT test, color and extinction is added on later via multipliers + // Every BxDF leaf node is supposed to pass WFT test and must not create energy, color and extinction is added on later via multipliers class IBxDF : public IContributor { public: @@ -533,6 +647,19 @@ class CFrontendIR : public CNodePool param.scale = 0.f; } + // conservative check, checks if we can optimize certain things this way + inline bool definitelyIsotropic() const + { + // a derivative map from a texture allows for anisotropic NDFs at higher mip levels when pre-filtered properly + for (auto i=0; i<2; i++) + if (getDerivMap()[i].scale!=0.f && getDerivMap()[i].view) + return false; + // if roughness inputs are not equal (same scale, same texture) then NDF can be anisotropic in places + if (getRougness()[0]!=getRougness()[1]) + return false; + // if a reference stretch is used, stretched triangles can turn the distribution isotropic + return stretchInvariant(); + } // whether the derivative map and roughness is constant regardless of UV-space texture stretching inline bool stretchInvariant() const {return !(abs(hlsl::determinant(reference))>std::numeric_limits::min());} @@ -547,10 +674,10 @@ class CFrontendIR : public CNodePool // 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: // - Delta Reflection -> Any Cook Torrance BxDF with roughness=0 attached as BRDF // - Smooth Conductor -> above multiplied with Conductor-Fresnel - // - 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) - // - Thindielectric -> Any Cook Torrance BxDF multiplied with Dielectric-Fresnel as BRDF in both sides and a Delta Transmission BTDF - // - 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. - // If one wants to emulate non-linear diffuse TIR color shifts, abuse `CThinInfiniteScatterCorrection` + // - 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) + // - Thindielectric -> Any Cook Torrance BxDF multiplied with Dielectric-Fresnel as BRDF in both sides and a Delta Transmission BTDF with `CThinInfiniteScatterCorrection` on the fresnel + // - Plastic -> Similar to layering the above over Diffuse BRDF, its of uttmost importance that the BTDF is Delta Transmission. + // If one wants to emulate non-linear diffuse TIR color shifts, abuse `CThinInfiniteScatterCorrection`. class CDeltaTransmission final : public IBxDF { public: @@ -566,7 +693,8 @@ class CFrontendIR : public CNodePool protected: inline _TypedHandle getChildHandle_impl(const uint8_t ix) const override {return {};} }; - // Because of Schussler et. al 2017 every one of these nodes splits into 2 (if no L dependence) or 3 during canonicalization + //! Because of Schussler et. al 2017 every one of these nodes splits into 2 (if no L dependence) or 3 during canonicalization + // Base diffuse node class COrenNayar final : public IBxDF { public: @@ -618,6 +746,7 @@ class CFrontendIR : public CNodePool NBL_API bool invalid(const SInvalidCheckArgs& args) const override; inline core::string getLabelSuffix() const override {return ndf!=NDF::GGX ? "\\nNDF = Beckmann":"\\nNDF = GGX";} + inline std::string_view getChildName_impl(const uint8_t ix) const override {return "Oriented η";} NBL_API void printDot(std::ostringstream& sstr, const core::string& selfID) const override; }; @@ -650,6 +779,7 @@ class CFrontendIR : public CNodePool // To quickly make a matching backface material from a frontface or vice versa NBL_API TypedHandle reciprocate(const TypedHandle other); + NBL_API TypedHandle createNamedFresnel(const std::string_view name); // IMPORTANT: Two BxDFs are not allowed to be multiplied together. // NOTE: Right now all Spectral Variables are required to be Monochrome or 3 bucket fixed semantics, all the same wavelength. @@ -687,27 +817,11 @@ class CFrontendIR : public CNodePool } core::vector> m_rootNodes; - // TODO: named material Fresnels }; inline bool CFrontendIR::valid(const TypedHandle rootHandle, system::logger_opt_ptr logger) const { constexpr auto ELL_ERROR = system::ILogger::E_LOG_LEVEL::ELL_ERROR; - - core::stack layerStack; - auto pushLayer = [&](const TypedHandle layerHandle)->bool - { - const auto* layer = deref(layerHandle); - if (!layer) - { - logger.log("Layer node %u of type %s not a `CLayer` node!",ELL_ERROR,layerHandle.untyped.value,getTypeName(layerHandle).data()); - return false; - } - layerStack.push(layer); - return true; - }; - if (!pushLayer(rootHandle)) - return false; enum class SubtreeContributorState : uint8_t { @@ -720,8 +834,17 @@ inline bool CFrontendIR::valid(const TypedHandle rootHandle, syste TypedHandle handle; uint8_t contribSlot; SubtreeContributorState contribState = SubtreeContributorState::Required; + // using post-order like stack but with a pre-order DFS + uint8_t visited = false; }; core::stack exprStack; + // why a separate stack to the main one? Because we don't push siblings. + core::vector> ancestorPrefix; + // unused yet + core::unordered_set,HandleHash> visitedNodes; + // should probably size it better, if I knew total node count allocated or live + visitedNodes.reserve(m_rootNodes.size()<<3); + // auto validateExpression = [&](const TypedHandle exprRoot, const bool isBTDF) -> bool { if (!exprRoot) @@ -743,7 +866,19 @@ inline bool CFrontendIR::valid(const TypedHandle rootHandle, syste while (!exprStack.empty()) { const StackEntry entry = exprStack.top(); - exprStack.pop(); + if (entry.visited) + { + exprStack.pop(); + // this is the whole reason why we're using a post-order like stack + ancestorPrefix.pop_back(); + continue; + } + else + { + exprStack.top().visited = true; + // push self into prefix so children can check against it + ancestorPrefix.push_back(entry.handle); + } const auto* node = entry.node; const auto nodeType = node->getType(); const bool nodeIsMul = nodeType==IExprNode::Type::Mul; @@ -780,6 +915,15 @@ inline bool CFrontendIR::valid(const TypedHandle rootHandle, syste logger.log("Expression too complex, more than %d contributors encountered",ELL_ERROR,MaxContributors); return false; } + // detect cycles + const auto found = std::find(ancestorPrefix.begin(),ancestorPrefix.end(),childHandle); + if (found!=ancestorPrefix.end()) + { + logger.log("Expression contains a cycle involving node %d of type %s",ELL_ERROR,childHandle,getTypeName(childHandle).data()); + return false; + } + // cannot optimize with `unordered_set visitedNodes` because we need to check contributor slots, if we really wanted to we could do it with an + // `unordered_map` telling us the contributor slot range remapping (and presence of contributor) but right now it would be premature optimization. exprStack.push(newEntry); } else if (childHandle) @@ -808,15 +952,30 @@ inline bool CFrontendIR::valid(const TypedHandle rootHandle, syste } return true; }; - while (!layerStack.empty()) + + core::vector layerStack; + auto pushLayer = [&](const TypedHandle layerHandle)->bool { - const auto* layer = layerStack.top(); - layerStack.pop(); - if (layer->coated && !pushLayer(layer->coated)) + const auto* layer = deref(layerHandle); + if (!layer) + { + logger.log("Layer node %u of type %s not a `CLayer` node!",ELL_ERROR,layerHandle.untyped.value,getTypeName(layerHandle).data()); + return false; + } + auto found = std::find(layerStack.begin(),layerStack.end(),layer); + if (found!=layerStack.end()) { - logger.log("\tcoatee %d was specificed but is of wrong type",ELL_ERROR,layer->coated); + logger.log("Layer node %u is involved in a Cycle!",ELL_ERROR,layerHandle.untyped.value); return false; } + layerStack.push_back(layer); + return true; + }; + if (!pushLayer(rootHandle)) + return false; + while (true) + { + const auto* layer = layerStack.back(); if (!layer->brdfTop && !layer->btdf && !layer->brdfBottom) { logger.log("At least one BRDF or BTDF in the Layer is required.",ELL_ERROR); @@ -828,6 +987,13 @@ inline bool CFrontendIR::valid(const TypedHandle rootHandle, syste return false; if (!validateExpression(layer->brdfBottom,false)) return false; + if (!layer->coated) + break; + if (!pushLayer(layer->coated)) + { + logger.log("\tcoatee %d was specificed but is invalid!",ELL_ERROR,layer->coated); + return false; + } } return true; } diff --git a/include/nbl/asset/material_compiler3/CNodePool.h b/include/nbl/asset/material_compiler3/CNodePool.h index 941ce85145..e0d35f0f82 100644 --- a/include/nbl/asset/material_compiler3/CNodePool.h +++ b/include/nbl/asset/material_compiler3/CNodePool.h @@ -25,7 +25,8 @@ class CNodePool : public core::IReferenceCounted using value_t = uint32_t; constexpr static inline value_t Invalid = ~value_t(0); - inline operator bool() const {return value!=Invalid;} + explicit inline operator bool() const {return value!=Invalid;} + inline bool operator==(const Handle& other) const {return value==other.value;} // also serves as a byte offset into the pool value_t value = Invalid; @@ -64,11 +65,11 @@ class CNodePool : public core::IReferenceCounted inline CDebugInfo(const void* data, const uint32_t size) : m_size(size) { if (data) - memcpy(this+1,data,m_size); + memcpy(std::launder(this+1),data,m_size); } inline CDebugInfo(const std::string_view& view) : CDebugInfo(nullptr,view.length()+1) { - auto* out = reinterpret_cast(this+1); + auto* out = std::launder(reinterpret_cast(this+1)); if (m_size>1) memcpy(out,view.data(),m_size); out[m_size-1] = 0; @@ -76,7 +77,7 @@ class CNodePool : public core::IReferenceCounted inline const std::span data() const { - return {reinterpret_cast(this+1),m_size}; + return {std::launder(reinterpret_cast(this+1)),m_size}; } protected: @@ -89,7 +90,8 @@ class CNodePool : public core::IReferenceCounted { using node_type = T; - inline operator bool() const {return untyped;} + explicit inline operator bool() const {return bool(untyped);} + inline bool operator==(const TypedHandle& other) const {return untyped==other.untyped;} inline operator const TypedHandle&() const { @@ -117,6 +119,13 @@ class CNodePool : public core::IReferenceCounted } protected: + struct HandleHash + { + inline size_t operator()(const TypedHandle handle) const + { + return std::hash()(handle.untyped.value); + } + }; // save myself some typing using refctd_pmr_t = core::smart_refctd_ptr; @@ -174,6 +183,7 @@ class CNodePool : public core::IReferenceCounted const uint32_t size = ptr->getSize(); static_cast(ptr)->~INode(); // can't use `std::destroy_at(ptr);` because of destructor being non-public // wipe v-table to mark as dead (so `~CNodePool` doesn't run destructor twice) + // NOTE: This won't work if we start reusing memory, even zeroing out the whole node won't work! Then need an accurate record of live nodes! const void* nullVTable = nullptr; assert(memcmp(ptr,&nullVTable,sizeof(nullVTable))!=0); // double free memset(static_cast(ptr),0,sizeof(nullVTable)); @@ -196,6 +206,7 @@ class CNodePool : public core::IReferenceCounted for (auto handleOff=chunk.getAllocator().get_total_size(); handleOff(chunk.m_data+handleOff); + // NOTE: This won't work if we start reusing memory, even zeroing out the whole node won't work! Then need an accurate record of live nodes! if (auto* node=deref(*pHandle); node) node->~INode(); // can't use `std::destroy_at(ptr);` because of destructor being non-public } @@ -207,7 +218,7 @@ class CNodePool : public core::IReferenceCounted struct Chunk { // for now using KISS, we can use geeneralpupose allocator later - // Generalpurpose woudl require us to store the allocated handle list in a different way, so that handles can be quickly removed from it. + // Generalpurpose would require us to store the allocated handle list in a different way, so that handles can be quickly removed from it. // Maybe a doubly linked list around the original allocation? using allocator_t = core::LinearAddressAllocatorST; @@ -232,8 +243,9 @@ class CNodePool : public core::IReferenceCounted return invalid_address; } // clear vtable to mark as not initialized yet + // TODO: this won't work with reusable memory / not bump allocator memset(m_data+retval,0,sizeof(INode)); - *reinterpret_cast(m_data+newSize) = retval; + *std::launder(reinterpret_cast(m_data+newSize)) = retval; // shrink allocator getAllocator() = allocator_t(newSize, std::move(getAllocator()), nullptr); } @@ -264,9 +276,9 @@ class CNodePool : public core::IReferenceCounted return ptr; else { - if (*reinterpret_cast(ptr)) // vtable not wiped + if (*std::launder(reinterpret_cast(ptr))) // vtable not wiped { - auto* base = reinterpret_cast(ptr); + auto* base = std::launder(reinterpret_cast(ptr)); return dynamic_cast(base); } } diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 74b9fcbc70..a6937f00ab 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -12,7 +12,7 @@ namespace nbl::asset::material_compiler3 { // You make the Materials with a classical expression IR, one Root Node per material's interface layer, but here they're in "Accumulator Form" -// They appeared "flipped upside down" +// They appear "flipped upside down", its expected our backends will evaluate contributors first, and then bother with the attenuators. class CTrueIR : public CNodePool { public: @@ -46,10 +46,26 @@ class CTrueIR : public CNodePool { // TypedHandle root; CNodePool::TypedHandle debugInfo; + // + constexpr static inline uint8_t MaxUVSlots = 32; + std::bitset usedUVSlots; + // the tangent frames are a subset of used UV slots, unless there's an anisotropic BRDF involved + std::bitset usedTangentFrames; }; inline std::span getMaterials() const {return m_materials;} // We take the trees from the forest, and canonicalize them into our weird Domain Specific IR with Upside down expression trees. + // Process: + // 1. Schusslerization (for derivative map usage) and Decompression (duplicating nodes, etc.) + // 2. Canonicalize Expressions (Transform into Sum-Product form, DCE, etc.) + // 3. Split BTDFs (front vs. back part), reciprocate Etas + // 4. Simplify and Hoist Layer terms (delta sampling property) + // 5. Subexpression elimination + // It is the backend's job to handle: + // - constant encoding precision (scale factors, UV matrices, IoRs) + // - multiscatter compensation + // - compilation failure to unsupported complex layering + // - compilation failure to unsupported complex layering bool addMaterials(const CFrontendIR* forest); protected: @@ -63,4 +79,4 @@ class CTrueIR : public CNodePool } // namespace nbl::asset::material_compiler3 -#endif \ No newline at end of file +#endif diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index e9cc345abc..0bd4906419 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -3,6 +3,9 @@ // For conditions of distribution and use, see copyright notice in nabla.h #include "nbl/asset/material_compiler3/CFrontendIR.h" +#include "nbl/builtin/hlsl/complex.hlsl" +#include "nbl/builtin/hlsl/portable/vector_t.hlsl" + namespace nbl::asset::material_compiler3 { @@ -24,9 +27,9 @@ bool CFrontendIR::CEmitter::invalid(const SInvalidCheckArgs& args) const bool CFrontendIR::CBeer::invalid(const SInvalidCheckArgs& args) const { - if (!args.pool->deref(perpTransparency)) + if (!args.pool->deref(perpTransmittance)) { - args.logger.log("Perpendicular Transparency node of correct type must be attached, but is %u of type %s",ELL_ERROR,perpTransparency,args.pool->getTypeName(perpTransparency).data()); + args.logger.log("Perpendicular Transparency node of correct type must be attached, but is %u of type %s",ELL_ERROR,perpTransmittance,args.pool->getTypeName(perpTransmittance).data()); return true; } return false; @@ -39,9 +42,44 @@ bool CFrontendIR::CFresnel::invalid(const SInvalidCheckArgs& args) const 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()); return true; } - if (!args.pool->deref(orientedImagEta)) + if (const auto imagEta=args.pool->deref(orientedImagEta); imagEta) + { + if (args.isBTDF) + { + const auto knotCount = imagEta->getKnotCount(); + for (uint8_t i=0; igetParam(i); + if (param.scale==0.f) + continue; + args.logger.log("Fresnels used for BTDFs cannot have Imaginary Eta, scale must be 0.f for all knots, is %.*e at knot %u",ELL_ERROR,param.scale,i); + return true; + } + } + } + else if (orientedImagEta) + { + args.logger.log("Oriented Imaginary Eta node of incorrect type attached, but is %u of type %s",ELL_ERROR,orientedImagEta,args.pool->getTypeName(orientedImagEta).data()); + return true; + } + return false; +} + +bool CFrontendIR::CThinInfiniteScatterCorrection::invalid(const SInvalidCheckArgs& args) const +{ + if (!args.pool->deref(reflectanceTop)) + { + args.logger.log("Top reflectance node of correct type must be attached, but is %u of type %s",ELL_ERROR,reflectanceTop,args.pool->getTypeName(reflectanceTop).data()); + return true; + } + if (extinction && !args.pool->deref(extinction)) + { + args.logger.log("Extinction node of incorrect type attached, but is %u of type %s",ELL_ERROR,extinction,args.pool->getTypeName(extinction).data()); + return true; + } + if (!args.pool->deref(reflectanceBottom)) { - 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()); + args.logger.log("Top reflectance node of correct type must be attached, but is %u of type %s",ELL_ERROR,reflectanceBottom,args.pool->getTypeName(reflectanceBottom).data()); return true; } return false; @@ -75,14 +113,120 @@ bool CFrontendIR::CCookTorrance::invalid(const SInvalidCheckArgs& args) const auto CFrontendIR::reciprocate(const TypedHandle other) -> TypedHandle { + if (const auto* in=deref({.untyped=other.untyped}); in) + { + auto fresnelH = _new(); + auto* fresnel = deref(fresnelH); + *fresnel = *in; + fresnel->reciprocateEtas = ~in->reciprocateEtas; + return fresnelH; + } assert(false); // unimplemented return {}; } +auto CFrontendIR::createNamedFresnel(const std::string_view name) -> TypedHandle +{ + using complex32_t = hlsl::complex_t; + using spectral_complex_t = hlsl::portable_vector_t; + const static core::map creationLambdas = { +#define SPECTRUM_MACRO(R,G,B,X,Y,Z) spectral_complex_t(complex32_t(R,X),complex32_t(G,Y),complex32_t(B,Z)) + {"a-C", SPECTRUM_MACRO(1.6855f, 1.065f, 1.727f, 0.0f, 0.009f, 0.0263f)}, // there is no "a-C", but "a-C:H; data from palik" + {"Ag", SPECTRUM_MACRO(0.059481f, 0.055090f, 0.046878f, 4.1367f, 3.4574f, 2.8028f)}, + {"Al", SPECTRUM_MACRO(1.3404f, 0.95151f, 0.68603f, 7.3509f, 6.4542f, 5.6351f)}, + {"AlAs", SPECTRUM_MACRO(3.1451f, 3.2636f, 3.4543f, 0.0012319f, 0.0039041f, 0.012940f)}, + {"AlAs_palik", SPECTRUM_MACRO(3.145f, 3.273f, 3.570f, 0.0f, 0.000275f, 1.56f)}, + {"Au", SPECTRUM_MACRO(0.21415f, 0.52329f, 1.3319f, 3.2508f, 2.2714f, 1.8693f)}, + {"Be", SPECTRUM_MACRO(3.3884f, 3.2860f, 3.1238f, 3.1692f, 3.1301f, 3.1246f)}, + {"Be_palik", SPECTRUM_MACRO(3.46f, 3.30f, 3.19f, 3.18f, 3.18f, 3.16f)}, + {"Cr", SPECTRUM_MACRO(3.2246f, 2.6791f, 2.1411f, 4.2684f, 4.1664f, 3.9300f)}, + {"CsI", SPECTRUM_MACRO(1.7834f, 1.7978f, 1.8182f, 0.0f, 0.0f, 0.0f)}, + {"CsI_palik", SPECTRUM_MACRO(1.78006f, 1.79750f, 1.82315, 0.0f, 0.0f, 0.0f)}, + {"Cu", SPECTRUM_MACRO(0.32075f,1.09860f,1.2469f, 3.17900f,2.59220f,2.4562)}, + {"Cu_palik", SPECTRUM_MACRO(0.32000f, 1.04f, 1.16f, 3.15000f, 2.59f, 2.4f)}, + {"Cu20", SPECTRUM_MACRO(2.975f, 3.17f, 3.075f, 0.122f, 0.23f, 0.525f)}, // out of range beyond 2.5 um refractiveindex.info and similar websites, so data applied is same as from palik's data + {"Cu20_palik", SPECTRUM_MACRO(2.975f, 3.17f, 3.075f, 0.122f, 0.23f, 0.525f)}, + {"d-C", SPECTRUM_MACRO(2.4123f, 2.4246f, 2.4349f, 0.0f, 0.0f, 0.0f)}, + {"d-C_palik", SPECTRUM_MACRO(2.4137f, 2.4272f, 2.4446f, 0.0f, 0.0f, 0.0f)}, + {"Hg", SPECTRUM_MACRO(1.8847f, 1.4764f, 1.1306f, 5.1147f, 4.5410f, 3.9896f)}, + {"Hg_palik", SPECTRUM_MACRO(1.850f, 1.460f, 1.100f, 5.100f, 4.600f, 3.990f)}, + //{"HgTe", SPECTRUM_MACRO(,,, ,,)}, // lack of length wave range for our purpose https://www.researchgate.net/publication/3714159_Dispersion_of_refractive_index_in_degenerate_mercury_cadmium_telluride + //{"HgTe_palik", SPECTRUM_MACRO(,,, ,,)}, // the same in palik (wavelength beyond 2 um) + {"Ir", SPECTRUM_MACRO(2.4200f, 2.0795f, 1.7965f, 5.0665f, 4.6125f, 4.1120f)}, + {"Ir_palik", SPECTRUM_MACRO(2.44f, 2.17f, 1.87f, 4.52f, 4.24f, 3.79f)}, + {"K", SPECTRUM_MACRO(0.052350f, 0.048270f, 0.042580f, 1.6732f, 1.3919f, 1.1195f)}, + {"K_palik", SPECTRUM_MACRO(0.0525f, 0.0483f, 0.0427f, 1.67f, 1.39f, 1.12f)}, + {"Li", SPECTRUM_MACRO(0.14872f, 0.14726f, 0.19236f, 2.9594f, 2.5129f, 2.1144f)}, + {"Li_palik", SPECTRUM_MACRO(0.218f, 0.2093f, 0.229f, 2.848f, 2.369f, 2.226f)}, + {"MgO", SPECTRUM_MACRO(1.7357f, 1.7419f, 1.7501f, 0.0f, 0.0f, 0.0f)}, + {"MgO_palik", SPECTRUM_MACRO(1.7355f, 1.7414f, 1.74975f, 0.0f, 0.0f, 1.55f)}, // Handbook of optical constants of solids vol 2 page 951, weird k compoment alone, no measurements and resoults + {"Mo", SPECTRUM_MACRO(0.76709f, 0.57441f, 0.46711f, 8.5005f, 7.2352f, 6.1383f)}, // https://refractiveindex.info/?shelf=main&book=Mo&page=Werner comparing with palik - weird + {"Mo_palik", SPECTRUM_MACRO(3.68f, 3.77f, 3.175f, 3.51f, 3.624f, 3.56f)}, + {"Na_palik", SPECTRUM_MACRO(0.0522f, 0.061f, 0.0667f, 2.535f, 2.196f, 1.861f)}, + {"Nb", SPECTRUM_MACRO(2.2775f, 2.2225f, 2.0050f, 3.2500f, 3.1325f, 3.0100f)}, + {"Nb_palik", SPECTRUM_MACRO(2.869f, 2.9235f, 2.738f, 2.867f, 2.8764f, 2.8983f)}, + {"Ni_palik", SPECTRUM_MACRO(1.921f, 1.744f, 1.651f, 3.615f, 3.168f, 2.753f)}, + {"Rh", SPECTRUM_MACRO(2.8490f, 2.6410f, 2.4310f, 3.5450f, 3.3150f, 3.1190f)}, + {"Rh_palik", SPECTRUM_MACRO(2.092f, 1.934f, 1.8256f, 5.472f, 4.902f, 4.5181f)}, + {"Se", SPECTRUM_MACRO(1.4420f, 1.4759f, 1.4501f, 0.018713f, 0.10233f, 0.18418f)}, + {"Se_palik", SPECTRUM_MACRO(3.346f, 3.013f, 3.068f, 0.6402f, 0.6711f, 0.553f)}, + {"SiC", SPECTRUM_MACRO(2.6398f, 2.6677f, 2.7069f, 0.0f, 0.0f, 0.0f)}, + {"SiC_palik", SPECTRUM_MACRO(2.6412f, 2.6684f, 2.7077f, 0.0f, 0.0f, 0.0f)}, + {"SnTe", SPECTRUM_MACRO(3.059f, 1.813f, 1.687f, 5.144f, 4.177f, 3.555f)}, // no data except palik's resources, so data same as palik + {"SnTe_palik", SPECTRUM_MACRO(3.059f, 1.813f, 1.687f, 5.144f, 4.177f, 3.555f)}, + {"Ta", SPECTRUM_MACRO(1.0683f, 1.1379f, 1.2243f, 5.5047f, 4.7432f, 4.0988f)}, + {"Ta_palik", SPECTRUM_MACRO(1.839f, 2.5875f, 2.8211f, 1.997f, 1.8683f, 2.0514f)}, + {"Te", SPECTRUM_MACRO(4.1277f, 3.2968f, 2.6239f, 2.5658f, 2.8789f, 2.7673f)}, + {"Te_palik", SPECTRUM_MACRO(5.8101f, 4.5213f, 3.3682f, 2.9428f, 3.7289f, 3.6783f)}, + {"ThF4", SPECTRUM_MACRO(1.5113f, 1.5152f, 1.5205f, 0.0f, 0.0f, 0.0f)}, + {"ThF4_palik", SPECTRUM_MACRO(1.520f, 1.5125f, 1.524f, 0.0f, 0.0f, 0.0f)}, + {"TiC", SPECTRUM_MACRO(3.0460f, 2.9815f, 2.8864f, 2.6585f, 2.4714f, 2.3987f)}, + {"TiC_palik", SPECTRUM_MACRO(3.0454f, 2.9763, 2.8674f, 2.6589f, 2.4695f, 2.3959f)}, + {"TiO2", SPECTRUM_MACRO(2.1362f, 2.1729f, 2.2298f, 0.0f, 0.0f, 0.0f)}, + {"TiO2_palik", SPECTRUM_MACRO(2.5925f, 2.676f, 2.78f, 0.0f, 0.0f, 0.0f)}, + {"VC", SPECTRUM_MACRO(3.0033f, 2.8936f, 2.8138f, 2.4981f, 2.3046f, 2.1913f)}, + {"VC_palik", SPECTRUM_MACRO(3.0038f, 2.8951f, 2.8184f, 2.4923f, 2.3107f, 2.1902f)}, + {"V_palik", SPECTRUM_MACRO(3.512f, 3.671f, 3.2178f, 2.9337, 3.069f, 3.3667f)}, + {"VN", SPECTRUM_MACRO(2.3429f, 2.2268f, 2.1550f, 2.4506f, 2.1345f, 1.8753f)}, + {"VN_palik", SPECTRUM_MACRO(2.3418f, 2.2239f, 2.1539f, 2.4498f, 2.1371f, 1.8776f)}, + {"W", SPECTRUM_MACRO(0.96133f, 1.5474f, 2.1930f, 6.2902f, 5.1052f, 5.0325f)}, + {"none", SPECTRUM_MACRO(0.f,0.f,0.f, 0.f,0.f,0.f)} +#undef SPECTRUM_MACRO + }; + // + const auto found = creationLambdas.find(name); + if (found==creationLambdas.end()) + return {}; + // + const auto frH = _new(); + auto* fr = deref(frH); + fr->debugInfo = _new(found->first); + { + CSpectralVariable::SCreationParams<3> params = {}; + params.getSemantics() = CSpectralVariable::Semantics::Fixed3_SRGB; + params.knots.params[0].scale = found->second.x.real(); + params.knots.params[1].scale = found->second.y.real(); + params.knots.params[2].scale = found->second.z.real(); + fr->orientedRealEta = _new(std::move(params)); + } + { + CSpectralVariable::SCreationParams<3> params = {}; + params.getSemantics() = CSpectralVariable::Semantics::Fixed3_SRGB; + params.knots.params[0].scale = found->second.x.imag(); + params.knots.params[1].scale = found->second.y.imag(); + params.knots.params[2].scale = found->second.z.imag(); + fr->orientedImagEta = _new(std::move(params)); + } + return frH; +} + void CFrontendIR::printDotGraph(std::ostringstream& str) const { str << "digraph {\n"; - + + core::unordered_set,HandleHash> visitedNodes; + // should probably size it better, if I knew total node count allocated or live + visitedNodes.reserve(m_rootNodes.size()<<3); // TODO: track layering depth and indent accordingly? // assign in reverse because we want materials to print in order core::vector> layerStack(m_rootNodes.rbegin(),m_rootNodes.rend()); @@ -91,6 +235,11 @@ void CFrontendIR::printDotGraph(std::ostringstream& str) const { const auto layerHandle = layerStack.back(); layerStack.pop_back(); + // don't print layer nodes multiple times + const auto visited = visitedNodes.find(layerHandle); + if (visited!=visitedNodes.end()) + continue; + visitedNodes.insert(layerHandle); const auto* layerNode = deref(layerHandle); // const auto layerID = getNodeID(layerHandle); @@ -105,8 +254,14 @@ void CFrontendIR::printDotGraph(std::ostringstream& str) const { if (!root) return; + // print the link from the layer to the expression str << "\n\t" << layerID << " -> " << getNodeID(root) << "[label=\"" << edgeLabel << "\"]"; + // but not the expression again + const auto visited = visitedNodes.find(root); + if (visited!=visitedNodes.end()) + return; exprStack.push(root); + visitedNodes.insert(root); }; pushExprRoot(layerNode->brdfTop,"Top BRDF"); pushExprRoot(layerNode->btdf,"BTDF"); @@ -121,17 +276,19 @@ void CFrontendIR::printDotGraph(std::ostringstream& str) const const auto childCount = node->getChildCount(); if (childCount) { - str << "\n\t" << nodeID << " -> {"; for (auto childIx=0; childIxgetChildHandle(childIx); if (const auto child=deref(childHandle); child) { - str << getNodeID(childHandle) << " "; + str << "\n\t" << nodeID << " -> " << getNodeID(childHandle) << "[label=\"" << node->getChildName_impl(childIx) << "\"]"; + const auto visited = visitedNodes.find(childHandle); + if (visited!=visitedNodes.end()) + continue; exprStack.push(childHandle); + visitedNodes.insert(childHandle); } } - str << "}\n"; } // special printing node->printDot(str,nodeID); @@ -178,6 +335,7 @@ core::string CFrontendIR::CSpectralVariable::getLabelSuffix() const return ""; constexpr const char* SemanticNames[] = { + "", "\\nSemantics = Fixed3_SRGB", "\\nSemantics = Fixed3_DCI_P3", "\\nSemantics = Fixed3_BT2020", @@ -190,7 +348,7 @@ core::string CFrontendIR::CSpectralVariable::getLabelSuffix() const void CFrontendIR::CSpectralVariable::printDot(std::ostringstream& sstr, const core::string& selfID) const { auto pWonky = reinterpret_cast*>(this+1); - pWonky->knots.printDot(getKnotCount(),sstr,selfID); + pWonky->knots.printDot(getKnotCount(),sstr,selfID,{}); } void CFrontendIR::CEmitter::printDot(std::ostringstream& sstr, const core::string& selfID) const @@ -220,8 +378,8 @@ void CFrontendIR::IBxDF::SBasicNDFParams::printDot(std::ostringstream& sstr, con "alpha_u", "alpha_v" }; - SParameterSet<4>::printDot(sstr,selfID,paramSemantics); - if (hlsl::determinant(reference)>0.f) + SParameterSet<4>::printDot(sstr,selfID,paramSemantics,!definitelyIsotropic()); + if (!stretchInvariant()) { const auto referenceID = selfID+"_reference"; sstr << "\n\t" << referenceID << " [label=\"";