Skip to content

Conversation

devshgraphicsprogramming
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming commented Sep 16, 2025

Description

Continues #899 , #916 and #919

Testing

TODO list:

Comment on lines +807 to +808
retval.iso_cache.VdotL = hlsl::mix(scalar_type(2.0) * retval.iso_cache.VdotH * retval.iso_cache.VdotH - scalar_type(1.0),
VdotH * (LdotH - rcpOrientedEta.value[0] + VdotH * rcpOrientedEta.value[0]), transmitted);
Copy link
Member Author

Choose a reason for hiding this comment

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

there's a more graceful way to compute this via mix, see my other comment

}

OrientedEtas<T> getRefractionOrientedEta() NBL_CONST_MEMBER_FUNC { return orientedEta; }
scalar_type getRefractionOrientedEta() NBL_CONST_MEMBER_FUNC { return orientedEta.value[0]; } // expect T to be monochrome?
Copy link
Member Author

Choose a reason for hiding this comment

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

its a bit of a complicated topic, its possible to have RGB fresnel without dispersion by fixing the refraction Eta to be something else than the etas used to compute RGB reflectance or some sort of interpolation of them.

This is a good default cause the Etas wont differ that much anyway

Comment on lines +92 to +93
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!!!!

Comment on lines 338 to 363
template<class Interaction, class MicrofacetCache, typename C=bool_constant<!IsAnisotropic> NBL_FUNC_REQUIRES(RequiredInteraction<Interaction> && RequiredMicrofacetCache<MicrofacetCache>)
enable_if_t<C::value && !IsAnisotropic, dg1_query_type> createDG1Query(NBL_CONST_REF_ARG(Interaction) interaction, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
dg1_query_type dg1_query;
dg1_query.ndf = __ndf_base.template D<MicrofacetCache>(cache);
scalar_type clampedNdotV = interaction.getNdotV(_clamp);
dg1_query.G1_over_2NdotV = base_type::G1_wo_numerator_devsh_part(clampedNdotV, __ndf_base.devsh_part(interaction.getNdotV2()));
return dg1_query;
}
template<class LS, class Interaction, typename C=bool_constant<!IsAnisotropic> NBL_FUNC_REQUIRES(LightSample<LS> && RequiredInteraction<Interaction>)
enable_if_t<C::value && !IsAnisotropic, g2g1_query_type> createG2G1Query(NBL_CONST_REF_ARG(LS) _sample, NBL_CONST_REF_ARG(Interaction) interaction)
{
g2g1_query_type g2_query;
g2_query.devsh_l = __ndf_base.devsh_part(_sample.getNdotL2());
g2_query.devsh_v = __ndf_base.devsh_part(interaction.getNdotV2());
return g2_query;
}
template<class Interaction, class MicrofacetCache, typename C=bool_constant<IsAnisotropic> NBL_FUNC_REQUIRES(RequiredInteraction<Interaction> && RequiredMicrofacetCache<MicrofacetCache>)
enable_if_t<C::value && IsAnisotropic, dg1_query_type> createDG1Query(NBL_CONST_REF_ARG(Interaction) interaction, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
dg1_query_type dg1_query;
dg1_query.ndf = __ndf_base.template D<MicrofacetCache>(cache);
scalar_type clampedNdotV = interaction.getNdotV(_clamp);
dg1_query.G1_over_2NdotV = base_type::G1_wo_numerator_devsh_part(clampedNdotV, __ndf_base.devsh_part(interaction.getTdotV2(), interaction.getBdotV2(), interaction.getNdotV2()));
return dg1_query;
}
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.

if you made createDG1Query use createG2G1Query and grab the devsh_v from the return value you could get away with only having one createDG1Query method I think

Comment on lines 383 to 384
dmq.microfacetMeasure = d;
dmq.projectedLightMeasure = d * _sample.getNdotL(BxDFClampMode::BCM_MAX);
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 wrong, the projectedLightMeasure needs a full transform same as beckmann

Copy link
Member Author

Choose a reason for hiding this comment

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

because its the PDF of V getting reflected through H which has a PDF of microfacetmeasure

{
scalar_type d = __ndf_base.template D<MicrofacetCache>(cache);
quant_type dmq;
dmq.microfacetMeasure = d; // note: microfacetMeasure/2NdotV
Copy link
Member Author

Choose a reason for hiding this comment

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

huh? then the method shouldn't be called D but D_over_2NdotV

but the comment is wrong, cause your __ndf_base.D is actually the NDF, not anythingelse

Comment on lines 397 to 400
scalar_type NdotL_over_denominator = _sample.getNdotL(BxDFClampMode::BCM_ABS);
if (transmitted)
NdotL_over_denominator *= -scalar_type(4.0) * VdotHLdotH / (VdotH_etaLdotH * VdotH_etaLdotH);
dmq.projectedLightMeasure = d * NdotL_over_denominator;
Copy link
Member Author

Choose a reason for hiding this comment

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

wrong the denominator is 4 NdotV NdotL, so NdotL over denominator must be 0.25 / NdotV(ABS)

Copy link
Member Author

Choose a reason for hiding this comment

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

for the transmission case its - VdotHLdotH / (VdotH_etaLdotH*VdotH_etaLdotH*NdotV(abs))

So its NdotL_over_denominator = mix(0.25,VdotHLdotH*neg_rcp2_VdotH_etaLdotH,transmitted)/NdotV(ABS);

dmq.microfacetMeasure = d; // note: microfacetMeasure/2NdotV

const scalar_type VdotHLdotH = quant_query.getVdotHLdotH();
const scalar_type VdotH_etaLdotH = quant_query.getVdotH_etaLdotH();
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.

should already take this a neg_rcp2_VdotH_etaLdotH // -(VdotH+eta*LdotH)^(-2)

Comment on lines 74 to 75
// help avoid preprocessor splitting template declarations by comma
#define SINGLE_ARG(...) __VA_ARGS__
Copy link
Member Author

Choose a reason for hiding this comment

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

if you want a global macro, make it NDF prefixed.

Also all macros need NBL_HLSL prefix

Comment on lines +286 to +287
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

Comment on lines 320 to 322
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

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

template<class MicrofacetCache>
NBL_CONSTEXPR_STATIC_INLINE bool RequiredMicrofacetCache = IsAnisotropic ? AnisotropicMicrofacetCache<MicrofacetCache> : ReadableIsotropicMicrofacetCache<MicrofacetCache>;
NDF_CONSTEXPR_DECLS(_IsAnisotropic,reflect_refract);
NDF_TYPE_ALIASES(SINGLE_ARG(Beckmann<T,IsAnisotropic,SupportedPaths>), SINGLE_ARG(impl::BeckmannCommon<T,IsAnisotropic>), impl::SBeckmannDG1Query<scalar_type>, impl::SBeckmannG2overG1Query<scalar_type>, DualMeasureQuantQuery<scalar_type>);
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 could structure the macro to take a BOOST_PP tuple

NDF_TYPE_ALIASES((Beckmann<T,IsAnisotropic,SupportedPaths>)(impl::BeckmannCommon<T,IsAnisotropic>)(impl::SBeckmannDG1Query<scalar_type>)(impl::SBeckmannG2overG1Query<scalar_type>)(DualMeasureQuantQuery<scalar_type>))

and then get the args via BOOST_PP_TUPLE_ELEM

quant_query_type quant_query; // only has members for refraction
if (SupportsTransmission)
{
quant_query.VdotHLdotH = cache.getVdotHLdotH();
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 that even though in PBRT its abs(VdotH)*abs(LdotH) we leverage that under transmission the sign must always be negattive and rest of the code accounts for that.

Copy link
Member Author

Choose a reason for hiding this comment

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

or where the getVdotHLdotH concept check is defined

Comment on lines 32 to 35
scalar_type getVdotH_etaLdotH() NBL_CONST_MEMBER_FUNC { return VdotH_etaLdotH; }

scalar_type VdotHLdotH;
scalar_type VdotH_etaLdotH;
Copy link
Member Author

Choose a reason for hiding this comment

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

the VdotH_etaLdotH needs to change (name and what they store) as per #930 (comment)

and a comment needs to be left here or in the concept #930 (comment)

Comment on lines +62 to +66
retval.projectedLightMeasure = microfacetMeasure*mix<scalar_type>(scalar_type(0.25),VdotHLdotH,transmitted);
scalar_type denominator = clampedNdotV;
if (transmitted) // VdotHLdotH is negative under transmission, so thats denominator is negative
denominator *= -VdotH_etaLdotH * VdotH_etaLdotH;
}
return pdf * (transmitted ? VdotHLdotH : scalar_type(0.25)) / denominator;
}

scalar_type pdf;
scalar_type absNdotV;
bool transmitted;
scalar_type VdotH;
scalar_type LdotH;
scalar_type VdotHLdotH;
scalar_type orientedEta;
retval.projectedLightMeasure /= denominator;
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 correct, unlike #930 (comment)

although you may want to reformulate it same way into mix(0.25,VdotHLdotH*neg_rcp2_VdotH_etaLdotH,transmitted)/NdotV(ABS);

Comment on lines 409 to 418
dmq.microfacetMeasure = dg1;
dmq.projectedLightMeasure = dg1;// TODO: figure this out * _sample.getNdotL(BxDFClampMode::BCM_MAX);
return dmq;
}
template<class LS, class Interaction, typename C=bool_constant<IsBSDF> NBL_FUNC_REQUIRES(LightSample<LS> && RequiredInteraction<Interaction>)
enable_if_t<C::value && IsBSDF, quant_type> DG1(NBL_CONST_REF_ARG(dg1_query_type) query, NBL_CONST_REF_ARG(quant_query_type) quant_query, NBL_CONST_REF_ARG(LS) _sample, NBL_CONST_REF_ARG(Interaction) interaction)
{
scalar_type dg1 = base_type::template DG1<dg1_query_type>(query);
quant_type dmq;
dmq.microfacetMeasure = dg1; // note: microfacetMeasure/2NdotV
Copy link
Member Author

Choose a reason for hiding this comment

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

if your base DG1 returns something else than actual DG1 like DG1_over_4NdotV, then it should be called DG1_over_4NdotV

then you assign straight to projectedLightMeasure in the reflective case and properly multiply the 4*NdotV back in for the microfacetMeasure

Copy link
Member Author

Choose a reason for hiding this comment

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

ideally you should really just return DG1_over_2NdotV cause extra 0.5 mul is kinda pointless inside

Comment on lines 420 to 426
const scalar_type VdotHLdotH = quant_query.getVdotHLdotH();
const scalar_type VdotH_etaLdotH = quant_query.getVdotH_etaLdotH();
const bool transmitted = reflect_refract==MTT_REFRACT || (reflect_refract!=MTT_REFLECT && VdotHLdotH < scalar_type(0.0));
scalar_type NdotL_over_denominator = _sample.getNdotL(BxDFClampMode::BCM_ABS);
if (transmitted)
NdotL_over_denominator *= -scalar_type(4.0) * VdotHLdotH / (VdotH_etaLdotH * VdotH_etaLdotH);
dmq.projectedLightMeasure = dg1;// TODO: figure this out * NdotL_over_denominator;
Copy link
Member Author

Choose a reason for hiding this comment

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

for the projectedLightMeasure transmittance you'd just do mix(0.5,2.0,transmitted)*DG1_over_2NdotV and then

if (transmitted) 
   dmq.projectedLightMeasure *= VdotHLdotH*neg_rcp2_VdotH_etaLdotH;
dmq.microfacetMeasure *= 2.0*clampedNdotV;

Comment on lines 378 to 428
template<class LS, class Interaction, class MicrofacetCache, typename C=bool_constant<!IsBSDF> NBL_FUNC_REQUIRES(LightSample<LS> && RequiredInteraction<Interaction> && RequiredMicrofacetCache<MicrofacetCache>)
enable_if_t<C::value && !IsBSDF, quant_type> D(NBL_CONST_REF_ARG(quant_query_type) quant_query, NBL_CONST_REF_ARG(LS) _sample, NBL_CONST_REF_ARG(Interaction) interaction, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
scalar_type d = __ndf_base.template D<MicrofacetCache>(cache);
quant_type dmq;
dmq.microfacetMeasure = d;
dmq.projectedLightMeasure = d * _sample.getNdotL(BxDFClampMode::BCM_MAX);
return dmq;
}
template<class LS, class Interaction, class MicrofacetCache, typename C=bool_constant<IsBSDF> NBL_FUNC_REQUIRES(LightSample<LS> && RequiredInteraction<Interaction> && RequiredMicrofacetCache<MicrofacetCache>)
enable_if_t<C::value && IsBSDF, quant_type> D(NBL_CONST_REF_ARG(quant_query_type) quant_query, NBL_CONST_REF_ARG(LS) _sample, NBL_CONST_REF_ARG(Interaction) interaction, NBL_CONST_REF_ARG(MicrofacetCache) cache)
{
scalar_type d = __ndf_base.template D<MicrofacetCache>(cache);
quant_type dmq;
dmq.microfacetMeasure = d; // note: microfacetMeasure/2NdotV

const scalar_type VdotHLdotH = quant_query.getVdotHLdotH();
const scalar_type VdotH_etaLdotH = quant_query.getVdotH_etaLdotH();
const bool transmitted = reflect_refract==MTT_REFRACT || (reflect_refract!=MTT_REFLECT && VdotHLdotH < scalar_type(0.0));
scalar_type NdotL_over_denominator = _sample.getNdotL(BxDFClampMode::BCM_ABS);
if (transmitted)
NdotL_over_denominator *= -scalar_type(4.0) * VdotHLdotH / (VdotH_etaLdotH * VdotH_etaLdotH);
dmq.projectedLightMeasure = d * NdotL_over_denominator;
return dmq;
}

template<class LS, class Interaction, typename C=bool_constant<!IsBSDF> NBL_FUNC_REQUIRES(LightSample<LS> && RequiredInteraction<Interaction>)
enable_if_t<C::value && !IsBSDF, quant_type> DG1(NBL_CONST_REF_ARG(dg1_query_type) query, NBL_CONST_REF_ARG(quant_query_type) quant_query, NBL_CONST_REF_ARG(LS) _sample, NBL_CONST_REF_ARG(Interaction) interaction)
{
scalar_type dg1 = base_type::template DG1<dg1_query_type>(query);
quant_type dmq;
dmq.microfacetMeasure = dg1;
dmq.projectedLightMeasure = dg1;// TODO: figure this out * _sample.getNdotL(BxDFClampMode::BCM_MAX);
return dmq;
}
template<class LS, class Interaction, typename C=bool_constant<IsBSDF> NBL_FUNC_REQUIRES(LightSample<LS> && RequiredInteraction<Interaction>)
enable_if_t<C::value && IsBSDF, quant_type> DG1(NBL_CONST_REF_ARG(dg1_query_type) query, NBL_CONST_REF_ARG(quant_query_type) quant_query, NBL_CONST_REF_ARG(LS) _sample, NBL_CONST_REF_ARG(Interaction) interaction)
{
scalar_type dg1 = base_type::template DG1<dg1_query_type>(query);
quant_type dmq;
dmq.microfacetMeasure = dg1; // note: microfacetMeasure/2NdotV

const scalar_type VdotHLdotH = quant_query.getVdotHLdotH();
const scalar_type VdotH_etaLdotH = quant_query.getVdotH_etaLdotH();
const bool transmitted = reflect_refract==MTT_REFRACT || (reflect_refract!=MTT_REFLECT && VdotHLdotH < scalar_type(0.0));
scalar_type NdotL_over_denominator = _sample.getNdotL(BxDFClampMode::BCM_ABS);
if (transmitted)
NdotL_over_denominator *= -scalar_type(4.0) * VdotHLdotH / (VdotH_etaLdotH * VdotH_etaLdotH);
dmq.projectedLightMeasure = dg1;// TODO: figure this out * NdotL_over_denominator;
return dmq;
}
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 stuff maybe we can rethink, I'd be tempted to allow an NDF "skip" providing D and correlated if it has both the DG1 and Dcorrelated methods (so the cook torrance can leverage that and skip multiplying D with correlated by itself for eval)

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


// help avoid preprocessor splitting template declarations by comma
#define SINGLE_ARG(...) __VA_ARGS__
#define NDF_SINGLE_ARG(...) __VA_ARGS__
Copy link
Member Author

Choose a reason for hiding this comment

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

imho you wont need it if you rework NDF_TYPE_ALIASES into taking a boost PP Tuple

Comment on lines +275 to +276
else
dmq.projectedLightMeasure = d * _sample.getNdotL(BxDFClampMode::BCM_MAX);
Copy link
Member Author

Choose a reason for hiding this comment

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

btw you can always use ABS, because it should be the callee's job to not call you on backfacing BRDF if it chooses to

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 just the NDF

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants