Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
5218545
changes to dual measure quant, old stuff commented out for now
keptsecret Sep 4, 2025
f7525af
create query structs in beckmann ndf, removed query concepts, funcs r…
keptsecret Sep 5, 2025
c50db68
did the same for ggx ndf, some fixes to beckmann
keptsecret Sep 5, 2025
c49bedb
Merge branch 'master' into bxdf_fixes_cook_torrance
keptsecret Sep 8, 2025
319c954
fixes to ndf concept
keptsecret Sep 8, 2025
4eeacf1
moved eval, quotient/pdf into cook torrance base
keptsecret Sep 9, 2025
e7bc784
moved generate H into ndfs, generate impl in cook torrance base
keptsecret Sep 9, 2025
72226bb
use new cook torrance base in microfacet bxdfs
keptsecret Sep 9, 2025
f34b348
Merge branch 'master' into bxdf_fixes_cook_torrance
keptsecret Sep 10, 2025
ad13044
numerous typo bug fixes
keptsecret Sep 10, 2025
d8d2116
fixes to ggx ndf
keptsecret Sep 10, 2025
238b08e
fixed conductor fresnel naming ior as eta
keptsecret Sep 10, 2025
8bc707f
minor fixes to beckmann ndf, removed obsolete comments
keptsecret Sep 10, 2025
9fb28cf
added luminosity contribution hint to cook torrance bsdfs
keptsecret Sep 11, 2025
4cb8a0a
added aniso overloads to isotropic bxdf methods
keptsecret Sep 11, 2025
392dc31
checks for invalid generate sample
keptsecret Sep 11, 2025
02de86e
removed obsolete commented out stuff
keptsecret Sep 12, 2025
4a7f532
added cartesian-polar conversions from unit tests
keptsecret Sep 12, 2025
80c4f67
Merge branch 'fix_rotation_mat' into bxdf_fixes_cook_torrance
keptsecret Sep 15, 2025
6233bd1
Merge branch 'master' into bxdf_fixes_cook_torrance
keptsecret Sep 15, 2025
ce5fbac
vector hashes
keptsecret Sep 15, 2025
47e814b
Merge branch 'fix_rotation_mat' into bxdf_fixes_cook_torrance
keptsecret Sep 15, 2025
c983975
Merge branch 'master' into bxdf_fixes_cook_torrance
keptsecret Sep 16, 2025
0f2ee0b
fix fresnel usage in bxdf pdf
keptsecret Sep 17, 2025
b5f02e6
fix F calc in generate
keptsecret Sep 17, 2025
a111415
merge master, fix conflicts
keptsecret Sep 22, 2025
9655049
minor fixes to non cook torrance bxdf generate
keptsecret Sep 22, 2025
a1743d2
split out bxdf concept typdefs
keptsecret Sep 22, 2025
e6d663b
removed redundant thin_smooth_dielectric create
keptsecret Sep 22, 2025
340cee3
added and use notEqual spirv intrinsic
keptsecret Sep 22, 2025
a3733b1
moved polar coord stuff into its own file
keptsecret Sep 22, 2025
3e3589b
made smith functions return measureless
keptsecret Sep 22, 2025
4bdf199
use type alias macro from config
keptsecret Sep 22, 2025
4faecc3
adjust ndf concept, change fresnel conductor ior to eta
keptsecret Sep 23, 2025
407da2f
combine brdf/bsdf cook torrance into same struct, old stuff commented…
keptsecret Sep 23, 2025
638b8b5
moved duplicate code in eval, pdf, quotient_pdf into templated intera…
keptsecret Sep 23, 2025
c587820
some changes to fresnel, cook torrance base
keptsecret Sep 24, 2025
d438360
reverted ggx ndf to use optimizations
keptsecret Sep 24, 2025
5f49f11
pdf function checks for backfacing V
keptsecret Sep 25, 2025
627074b
reduced beckmann, ggx ndfs to single struct with enable_ifs
keptsecret Sep 25, 2025
3896231
put concepts back in for ndf impl
keptsecret Sep 26, 2025
91b39d5
ggx ndf determine clamp with template bool
keptsecret Sep 26, 2025
2a3cda3
moved A out of ndf base into generate base
keptsecret Sep 26, 2025
c9f9366
added create methods to ndfs, cook torrance base; slight changes to c…
keptsecret Sep 26, 2025
fd128e6
make beckmann + ggx bxdfs typedefs of cooktorrance base, functionalit…
keptsecret Sep 26, 2025
882375e
added flipSign func that copies sign of rhs
keptsecret Sep 29, 2025
b22d570
remove unused var
keptsecret Sep 29, 2025
c0586f5
moved cook torrance base into base folder, removed commented out in b…
keptsecret Sep 29, 2025
9eeb248
make lambertian + oren nayar bxdfs typedefs of corresponding base, fu…
keptsecret Sep 29, 2025
26c76e4
fix microfacet bxdf concept
keptsecret Sep 29, 2025
58e2a0b
fixes to cook torrance generate
keptsecret Sep 30, 2025
2a08728
templated ray_dir_info reflect/refract funcs
keptsecret Sep 30, 2025
b993e47
pdf should return inf when smooth cook torrance bxdf
keptsecret Sep 30, 2025
f3cb6ff
set pdf=0 in specific cases
keptsecret Oct 1, 2025
7389c9a
added checking for TIR in bsdfs
keptsecret Oct 1, 2025
ec5913b
change fresnel orientedeta based on NdotV, user needs to pass front f…
keptsecret Oct 2, 2025
bccdb0b
fixes flipSign and flipSignIfRHSNeg
keptsecret Oct 3, 2025
1e9e407
fix bsdf generate: LdotH and NdotL same sign, otherwise invalid sample
keptsecret Oct 3, 2025
dab51c3
cook torrance brdf returns invalid sample in cases, fix missing return
keptsecret Oct 3, 2025
53ab934
more util funcs for ray_dir_info, avoid setting vals directly
keptsecret Oct 3, 2025
804014f
added IsMicrofacet to bxdf_traits
keptsecret Oct 3, 2025
260f7a3
projectedSphere generate should take v by ref
keptsecret Oct 6, 2025
fd54ac4
removed vector std::hash because glm already implemented it
keptsecret Oct 6, 2025
5caf006
Merge branch 'master' into bxdf_fixes_cook_torrance
keptsecret Oct 6, 2025
7508b82
get orientedeta and rcp methods in fresnel + concept
keptsecret Oct 6, 2025
2b4a9c1
slight change to flipSignIfRHSNeg struct
keptsecret Oct 6, 2025
d3fa872
added two-sided fresnel concept
keptsecret Oct 6, 2025
5765e87
removed cook torrance param structs/create funcs
keptsecret Oct 6, 2025
23763e2
cache.isValid func, schlick fresnel reorient
keptsecret Oct 7, 2025
069f499
big fixes to cook torrance bxdf generate
keptsecret Oct 7, 2025
22c4918
moved cook torrance checks into private func, changed checks in quoti…
keptsecret Oct 8, 2025
dcc2464
getRefractionOrientedEta returns a scalar, but now I have to turn it …
keptsecret Oct 8, 2025
7aa071b
moved ndf type defs and constexpr declaration into macro
keptsecret Oct 8, 2025
97e5ce3
moved out quant query, adjusted ndf macro usage
keptsecret Oct 8, 2025
6272281
reduced duplicate code in beckmann, ggx ndfs
keptsecret Oct 9, 2025
b55bdef
fix calculations in cook torrance eval, generate
keptsecret Oct 9, 2025
444b656
removed passthrough funcs, template directly on required interaction/…
keptsecret Oct 9, 2025
971e140
precompute more stuff in quant query
keptsecret Oct 10, 2025
6a0e28d
various fixes to ggx ndf, cook torrance base
keptsecret Oct 10, 2025
f93772e
change ndf type alias macro usage
keptsecret Oct 10, 2025
3519866
overwrite DG concept, added Dcorrelated to ggx ndf with optimizations
keptsecret Oct 13, 2025
2a7cf4b
fix pdf call
keptsecret Oct 13, 2025
79aea6d
minor fixes to cook torrance generate
keptsecret Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 132 additions & 81 deletions include/nbl/builtin/hlsl/bxdf/base/cook_torrance_base.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,13 @@ struct check_TIR_helper<F, false>
template<class F>
struct check_TIR_helper<F, true>
{
using vector_type = typename F::vector_type; // expect monochrome

template<class MicrofacetCache>
static bool __call(NBL_CONST_REF_ARG(F) fresnel, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
return cache.isValid(fresnel.getRefractionOrientedEta());
fresnel::OrientedEtas<vector_type> orientedEta = fresnel::OrientedEtas<vector_type>::create(typename F::scalar_type(1.0), hlsl::promote<vector_type>(fresnel.getRefractionOrientedEta()));
return cache.isValid(orientedEta);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you promoting to a vector!? Cache.isValid MUST take the Eta as a scalar!!!!

Copy link
Contributor

@keptsecret keptsecret Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ComputeMicrofacetNormal::isValid that cache.isValid uses is called with OrientedEta type. Does that need to take eta (and rcp_eta) as scalar(s) then? Or just move construction of OrientedEta into cache.isValid

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrientedEta<scalar_type> or scalar_type yes

}
};

Expand Down Expand Up @@ -131,33 +134,40 @@ struct SCookTorrance
NBL_CONSTEXPR_STATIC_INLINE bool IsAnisotropic = ndf_type::IsAnisotropic;
NBL_CONSTEXPR_STATIC_INLINE bool IsBSDF = ndf_type::NDFSurfaceType != ndf::MTT_REFLECT;

template<class Interaction, class MicrofacetCache>
static bool __checkValid(NBL_CONST_REF_ARG(fresnel_type) f, NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(Interaction) interaction, NBL_CONST_REF_ARG(MicrofacetCache) cache)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why make static and pass the f if its your member?

Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it because the fresnel is already properly oriented? then give f a clear name like reorientedFresnel

and since its static you may as well make the __checkValid into a standalone struct functor with a partial spec on IsBSDF, that way you don't call impl::check_TIR_helper but you can call cache.isValid(reorientedFresnel.getRefractionOrientedEta()) directly

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually if you use impl::checkValid<IsBSDF>::template __call() here

const bool notTIR = impl::check_TIR_helper<fresnel_type, IsBSDF>::template __call<MicrofacetCache>(_f, cache);
assert(notTIR);

then there's no need to have an impl::check_TIR_helper at all

{
NBL_IF_CONSTEXPR(IsBSDF)
return impl::check_TIR_helper<fresnel_type, IsBSDF>::template __call<MicrofacetCache>(f, cache);
else
return _sample.getNdotL() > numeric_limits<scalar_type>::min && interaction.getNdotV() > numeric_limits<scalar_type>::min;
}

template<class Interaction, class MicrofacetCache>
spectral_type __eval(NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(Interaction) interaction, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
fresnel_type _f = impl::getOrientedFresnel<fresnel_type, IsBSDF>::__call(fresnel, interaction.getNdotV());
const bool notTIR = impl::check_TIR_helper<fresnel_type, IsBSDF>::template __call<MicrofacetCache>(_f, cache);
if ((IsBSDF && notTIR) || (!IsBSDF && _sample.getNdotL() > numeric_limits<scalar_type>::min && interaction.getNdotV() > numeric_limits<scalar_type>::min))
{
using quant_query_type = typename ndf_type::quant_query_type;
using g2g1_query_type = typename ndf_type::g2g1_query_type;
fresnel_type _f = fresnel;
NBL_IF_CONSTEXPR(IsBSDF)
_f = impl::getOrientedFresnel<fresnel_type, IsBSDF>::__call(fresnel, interaction.getNdotV());
if (!__checkValid<Interaction, MicrofacetCache>(_f, _sample, interaction, cache))
return hlsl::promote<spectral_type>(0.0);

scalar_type dummy;
quant_query_type qq = ndf.template createQuantQuery<MicrofacetCache>(cache, dummy);
using quant_query_type = typename ndf_type::quant_query_type;
scalar_type dummy;
quant_query_type qq = ndf.template createQuantQuery<MicrofacetCache>(cache, dummy);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats the dummy in place of?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment please

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I erm, since you're using this for BRDF and BSDF, this can't be dummy, needs to be the actual refractive index thing I think!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should've been replaced with the quant_query_helper


quant_type D = ndf.template D<sample_type, Interaction, MicrofacetCache>(qq, _sample, interaction, cache);
scalar_type DG = D.projectedLightMeasure;
if (D.microfacetMeasure < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity))
{
g2g1_query_type gq = ndf.template createG2G1Query<sample_type, Interaction>(_sample, interaction);
DG *= ndf.template correlated<sample_type, Interaction>(gq, _sample, interaction);
}
NBL_IF_CONSTEXPR(IsBSDF)
return impl::__implicit_promote<spectral_type, typename fresnel_type::vector_type>::__call(_f(hlsl::abs(cache.getVdotH()))) * DG;
else
return impl::__implicit_promote<spectral_type, typename fresnel_type::vector_type>::__call(_f(cache.getVdotH())) * DG;
quant_type D = ndf.template D<sample_type, Interaction, MicrofacetCache>(qq, _sample, interaction, cache);
scalar_type DG = D.projectedLightMeasure;
if (D.microfacetMeasure < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity))
{
using g2g1_query_type = typename ndf_type::g2g1_query_type;
g2g1_query_type gq = ndf.template createG2G1Query<sample_type, Interaction>(_sample, interaction);
DG *= ndf.template correlated<sample_type, Interaction>(gq, _sample, interaction);
Comment on lines 175 to 178
Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we must let NDF provide a Dcorrelated method, and if detected, use that instead of this, because we miss an optimization opportunity with GGX

We could actually do this in the following way

        using quant_query_type = typename ndf_type::quant_query_type;
        scalar_type dummy;
        quant_query_type qq = ndf.template createQuantQuery<MicrofacetCache>(cache, dummy);

        quant_type D = ndf.template D<sample_type, Interaction, MicrofacetCache>(qq, _sample, interaction, cache);
        scalar_type DG = D.projectedLightMeasure;
        if (D.microfacetMeasure < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity))
        {
            using g2g1_query_type = typename ndf_type::g2g1_query_type;
            g2g1_query_type gq = ndf.template createG2G1Query<sample_type, Interaction>(_sample, interaction);
            DG *= ndf.template correlated<sample_type, Interaction>(gq, _sample, interaction);
        }
        
        // overwrites DG with NDF's `Dcorrelated` method, also `asserts` that the optimal method returns similar to above
        impl::overwrite_DG<ndf_type,sample_type,Interaction,Cache>(/*NBL_REF_ARG*/DG,ndf,...);

continues #930 (comment)

}
else
return hlsl::promote<spectral_type>(0.0);
scalar_type clampedVdotH = cache.getVdotH();
NBL_IF_CONSTEXPR(IsBSDF)
clampedVdotH = hlsl::abs(clampedVdotH);
return impl::__implicit_promote<spectral_type, typename fresnel_type::vector_type>::__call(_f(clampedVdotH)) * DG;
}
template<typename C=bool_constant<!IsAnisotropic> >
enable_if_t<C::value && !IsAnisotropic, spectral_type> eval(NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(isotropic_interaction_type) interaction, NBL_CONST_REF_ARG(isocache_type) cache)
Expand All @@ -172,79 +182,109 @@ struct SCookTorrance
template<typename C=bool_constant<!IsBSDF> >
enable_if_t<C::value && !IsBSDF, sample_type> generate(NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction, const vector2_type u, NBL_REF_ARG(anisocache_type) cache)
{
ray_dir_info_type localV_raydir = interaction.getV().transform(interaction.getToTangentSpace());
if (interaction.getNdotV() > numeric_limits<scalar_type>::min)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either rewrite the if so its > and put the valid case code inside, or assert that getNdotV is not NaN, as it stands a NaN will proceed

Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you've now made the valid case return invalid!?

{
ray_dir_info_type invalidL;
invalidL.makeInvalid();
return sample_type::createFromTangentSpace(invalidL, interaction.getFromTangentSpace());
Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's probably a faster way to make an invalid sample, require a createInvalid factory on the sample concept

}

const vector3_type localV = interaction.getTangentSpaceV();
const vector3_type localH = ndf.generateH(localV, u);
const scalar_type VdotH = hlsl::dot(localV, localH);
const vector3_type H = hlsl::mul(interaction.getFromTangentSpace(), localH);

cache = anisocache_type::createForReflection(localV, localH);
struct reflect_wrapper
ray_dir_info_type L;
if (scalar_type(2.0) * VdotH * localH.z > localV.z) // NdotL>0, compiler's Common Subexpression Elimination pass should re-use 2*VdotH later
{
vector3_type operator()() NBL_CONST_MEMBER_FUNC
assert(VdotH >= scalar_type(0.0));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can asset this as soon as VdotH is known with a comment about VNDF sampling

ray_dir_info_type V = interaction.getV();
struct reflect_wrapper // so we don't recalculate VdotH
{
return r(VdotH);
}
bxdf::Reflect<scalar_type> r;
scalar_type VdotH;
};
reflect_wrapper r;
r.r = bxdf::Reflect<scalar_type>::create(localV, localH);
r.VdotH = cache.getVdotH();
ray_dir_info_type localL = localV_raydir.template reflect<reflect_wrapper>(r);
vector3_type operator()() NBL_CONST_MEMBER_FUNC
{
return r(VdotH);
}
bxdf::Reflect<scalar_type> r;
scalar_type VdotH;
};
reflect_wrapper rw;
rw.r = bxdf::Reflect<scalar_type>::create(V.getDirection(), H);
rw.VdotH = VdotH;
L = V.template reflect<reflect_wrapper>(rw);

cache = anisocache_type::createForReflection(localV, localH);
}
else // fail if samples have invalid paths
L.makeInvalid(); // should check if sample direction is invalid

// fail if samples have invalid paths
if (localL.getDirection().z < scalar_type(0.0)) // NdotL<0
localL.makeInvalid(); // should check if sample direction is invalid
const vector3_type T = interaction.getT();
const vector3_type B = interaction.getB();
const vector3_type _N = interaction.getN();

return sample_type::createFromTangentSpace(localL, interaction.getFromTangentSpace());
return sample_type::create(L, T, B, _N);
}
template<typename C=bool_constant<IsBSDF> >
enable_if_t<C::value && IsBSDF, sample_type> generate(NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction, const vector3_type u, NBL_REF_ARG(anisocache_type) cache)
{
fresnel_type _f = impl::getOrientedFresnel<fresnel_type, IsBSDF>::__call(fresnel, interaction.getNdotV());
const vector3_type localV = interaction.getTangentSpaceV();
const scalar_type NdotV = localV.z;

fresnel_type _f = impl::getOrientedFresnel<fresnel_type, IsBSDF>::__call(fresnel, NdotV);
fresnel::OrientedEtaRcps<monochrome_type> rcpEta = _f.getOrientedEtaRcps();

ray_dir_info_type V = interaction.getV();
const vector3_type localV = interaction.getTangentSpaceV();
const vector3_type upperHemisphereV = ieee754::flipSignIfRHSNegative<vector3_type>(localV, hlsl::promote<vector3_type>(interaction.getNdotV()));
const vector3_type upperHemisphereV = ieee754::flipSignIfRHSNegative<vector3_type>(localV, hlsl::promote<vector3_type>(NdotV));
const vector3_type localH = ndf.generateH(upperHemisphereV, u.xy);
const scalar_type VdotH = hlsl::dot(localV, localH);
const vector3_type H = hlsl::mul(interaction.getFromTangentSpace(), localH);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

push calculation till last moment


const scalar_type VdotH = hlsl::dot(V.getDirection(), H);
assert(NdotV * VdotH > scalar_type(0.0));
const scalar_type reflectance = _f(hlsl::abs(VdotH))[0];

scalar_type rcpChoiceProb;
scalar_type z = u.z;
bool transmitted = math::partitionRandVariable(reflectance, z, rcpChoiceProb);

ray_dir_info_type V = interaction.getV();
const vector3_type _N = interaction.getN();
Refract<scalar_type> r = Refract<scalar_type>::create(V.getDirection(), H);
const scalar_type LdotH = hlsl::mix(VdotH, r.getNdotT(rcpEta.value2[0]), transmitted);
cache = anisocache_type::createPartial(VdotH, LdotH, hlsl::dot(_N, H), transmitted, rcpEta);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need to compute dot(_N,H) its literally localH.z

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also assert that the cache.isValid() right away after this (it should be, by construction)


struct reflect_refract_wrapper // so we don't recalculate LdotH
{
vector3_type operator()(const bool doRefract, const scalar_type rcpOrientedEta) NBL_CONST_MEMBER_FUNC
{
return rr(NdotTorR, rcpOrientedEta);
}
bxdf::ReflectRefract<scalar_type> rr;
Comment on lines +276 to +278
Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leave a comment that the call rr makes to getNdotTorR and mix as well as a good part of the comuptations should CSE with our computation of NdotL (which should be hoisted above the reflect/refract code)

scalar_type NdotTorR;
};
bxdf::ReflectRefract<scalar_type> rr;
rr.refract = r;
ray_dir_info_type L = V.reflectRefract(rr, transmitted, rcpEta.value[0]);
reflect_refract_wrapper rrw;
rrw.rr = rr;
rrw.NdotTorR = LdotH;
ray_dir_info_type L = V.template reflectRefract<reflect_refract_wrapper>(rrw, transmitted, rcpEta.value[0]);

const vector3_type T = interaction.getT();
const vector3_type B = interaction.getB();
const vector3_type _N = interaction.getN();

// fail if samples have invalid paths
const vector3_type Ldir = L.getDirection();
const scalar_type LdotH = hlsl::dot(Ldir, H);
if ((ComputeMicrofacetNormal<scalar_type>::isTransmissionPath(VdotH, LdotH) != transmitted) || (LdotH * hlsl::dot(_N, Ldir) < scalar_type(0.0)))
const scalar_type NdotL = scalar_type(2.0) * VdotH * localH.z - localV.z;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not a valid computation of NdotL for the BSDF, this is the reflection equation

Copy link
Member Author

@devshgraphicsprogramming devshgraphicsprogramming Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you follow

vector_type operator()(const bool doRefract, const scalar_type rcpOrientedEta, NBL_REF_ARG(scalar_type) out_IdotTorR) NBL_CONST_MEMBER_FUNC
{
scalar_type NdotI = getNdotR();
const scalar_type a = hlsl::mix<scalar_type>(1.0f, rcpOrientedEta, doRefract);
const scalar_type b = NdotI * a + getNdotTorR(doRefract, rcpOrientedEta);
// assuming `I` is normalized
out_IdotTorR = NdotI * b - a;
return refract.N * b - refract.I * a;
}

then using the NdotH==localH.z, NdotV and the LdotH you already compute for cache.isValid() you can compute NdotL as:

const float viewShortenFactor = hlsl::mix<scalar_type>(1.0f, rcpOrientedEta, transmitted);
const float NdotL = localH.z * (VdotH*viewShortenFactor + LdotH) - NdotV * viewShortenFactor;

if ((ComputeMicrofacetNormal<scalar_type>::isTransmissionPath(NdotV, NdotL) != transmitted))
L.makeInvalid(); // should check if sample direction is invalid
else
cache = anisocache_type::create(VdotH, Ldir, H, T, B, _N, transmitted);
{
fresnel::OrientedEtas<monochrome_type> orientedEta = fresnel::OrientedEtas<monochrome_type>::create(scalar_type(1.0), hlsl::promote<monochrome_type>(fresnel.getRefractionOrientedEta()));
assert(ComputeMicrofacetNormal::isValidMicrofacet(transmitted,cache.getVdotL(),localH.z,orientedEta));
cache.fillTangents(T, B, H);
}

return sample_type::create(L, T, B, _N);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as for the BRDF generation, NdotL can be overwriten by whatever you had to compute to check that L is in the correct hemisphere as per transmitted and the sign of VdotN

}
template<typename C=bool_constant<!IsAnisotropic>, typename D=bool_constant<!IsBSDF> >
enable_if_t<C::value && !IsAnisotropic && D::value && !IsBSDF, sample_type> generate(NBL_CONST_REF_ARG(isotropic_interaction_type) interaction, const vector2_type u, NBL_REF_ARG(isocache_type) cache)
{
anisocache_type aniso_cache;
sample_type s = generate(anisotropic_interaction_type::create(interaction), u, aniso_cache);
cache = aniso_cache.iso_cache;
return s;
}
template<typename C=bool_constant<!IsAnisotropic>, typename D=bool_constant<IsBSDF> >
enable_if_t<C::value && !IsAnisotropic && D::value && IsBSDF, sample_type> generate(NBL_CONST_REF_ARG(isotropic_interaction_type) interaction, const vector3_type u, NBL_REF_ARG(isocache_type) cache)
template<typename C=bool_constant<!IsAnisotropic>, typename T=conditional_t<IsBSDF, vector3_type, vector2_type> >
enable_if_t<C::value && !IsAnisotropic, sample_type> generate(NBL_CONST_REF_ARG(isotropic_interaction_type) interaction, const T u, NBL_REF_ARG(isocache_type) cache)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use conditional_t directly in a function signature, there's no need to add another template parameter T

P.S. also try to use NBL_FUNC_REQUIRES instead of the enable_if_t

{
anisocache_type aniso_cache;
sample_type s = generate(anisotropic_interaction_type::create(interaction), u, aniso_cache);
Expand Down Expand Up @@ -277,46 +317,57 @@ struct SCookTorrance
template<typename C=bool_constant<!IsAnisotropic> >
enable_if_t<C::value && !IsAnisotropic, scalar_type> pdf(NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(isotropic_interaction_type) interaction, NBL_CONST_REF_ARG(isocache_type) cache)
{
if (IsBSDF || (_sample.getNdotL() > numeric_limits<scalar_type>::min && interaction.getNdotV() > numeric_limits<scalar_type>::min))
{
scalar_type _pdf = __pdf<isotropic_interaction_type, isocache_type>(_sample, interaction, cache);
return hlsl::mix(scalar_type(0.0), _pdf, _pdf < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity));
}
else
fresnel_type _f = fresnel;
NBL_IF_CONSTEXPR(IsBSDF)
_f = impl::getOrientedFresnel<fresnel_type, IsBSDF>::__call(fresnel, interaction.getNdotV());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either don't passIsBSDF to impl::getOrientedFresnel or skip the NBL_IF_CONSTEXPR

if (!__checkValid<isotropic_interaction_type, isocache_type>(_f, _sample, interaction, cache))
return scalar_type(0.0);

scalar_type _pdf = __pdf<isotropic_interaction_type, isocache_type>(_sample, interaction, cache);
return hlsl::mix(scalar_type(0.0), _pdf, _pdf < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity));
}
scalar_type pdf(NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction, NBL_CONST_REF_ARG(anisocache_type) cache)
{
if (IsBSDF || (_sample.getNdotL() > numeric_limits<scalar_type>::min && interaction.getNdotV() > numeric_limits<scalar_type>::min))
{
scalar_type _pdf = __pdf<anisotropic_interaction_type, anisocache_type>(_sample, interaction, cache);
return hlsl::mix(scalar_type(0.0), _pdf, _pdf < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity));
}
else
fresnel_type _f = fresnel;
NBL_IF_CONSTEXPR(IsBSDF)
_f = impl::getOrientedFresnel<fresnel_type, IsBSDF>::__call(fresnel, interaction.getNdotV());
if (!__checkValid<anisotropic_interaction_type, anisocache_type>(_f, _sample, interaction, cache))
return scalar_type(0.0);

scalar_type _pdf = __pdf<anisotropic_interaction_type, anisocache_type>(_sample, interaction, cache);
return hlsl::mix(scalar_type(0.0), _pdf, _pdf < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity));
}

template<class Interaction, class MicrofacetCache>
quotient_pdf_type __quotient_and_pdf(NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(Interaction) interaction, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
if (!_sample.isValid())
return quotient_pdf_type::create(scalar_type(0.0), scalar_type(0.0)); // set pdf=0 when quo=0 because we don't want to give high weight to sampling strategy that yields 0 contribution

scalar_type _pdf = __pdf<Interaction, MicrofacetCache>(_sample, interaction, cache);
fresnel_type _f = impl::getOrientedFresnel<fresnel_type, IsBSDF>::__call(fresnel, interaction.getNdotV());

spectral_type quo = hlsl::promote<spectral_type>(0.0);
const bool notTIR = impl::check_TIR_helper<fresnel_type, IsBSDF>::template __call<MicrofacetCache>(_f, cache);
if (notTIR)
assert(notTIR);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a comment why


scalar_type G2_over_G1 = scalar_type(1.0);
if (_pdf < bit_cast<scalar_type>(numeric_limits<scalar_type>::infinity))
{
using g2g1_query_type = typename N::g2g1_query_type;
g2g1_query_type gq = ndf.template createG2G1Query<sample_type, Interaction>(_sample, interaction);
scalar_type G2_over_G1 = ndf.template G2_over_G1<sample_type, Interaction, MicrofacetCache>(gq, _sample, interaction, cache);
NBL_IF_CONSTEXPR(IsBSDF)
quo = hlsl::promote<spectral_type>(G2_over_G1);
else
quo = _f(cache.getVdotH()) * G2_over_G1;
G2_over_G1 = ndf.template G2_over_G1<sample_type, Interaction, MicrofacetCache>(gq, _sample, interaction, cache);
}

spectral_type quo;
NBL_IF_CONSTEXPR(IsBSDF)
quo = hlsl::promote<spectral_type>(G2_over_G1);
else
{
const scalar_type VdotH = cache.getVdotH();
assert(VdotH > scalar_type(0.0));
quo = _f(VdotH) * G2_over_G1;
}

// set pdf=0 when quo=0 because we don't want to give high weight to sampling strategy that yields 0 contribution
_pdf = hlsl::mix(_pdf, scalar_type(0.0), hlsl::all(quo < hlsl::promote<spectral_type>(numeric_limits<scalar_type>::min)));
return quotient_pdf_type::create(quo, _pdf);
}
template<typename C=bool_constant<!IsAnisotropic> >
Expand Down
Loading
Loading