Created concepts for samplers, added quotient_and_pdf variants to satisfy the concepts#1001
Created concepts for samplers, added quotient_and_pdf variants to satisfy the concepts#1001karimsayedre wants to merge 39 commits intomasterfrom
Conversation
…isfy the concepts
…concepts - Move codomain_and_*Pdf and domain_and_*Pdf structs into their own warp_and_pdf.hlsl header - Keeping quotient_and_pdf.hlsl focused on importance sampling quotients for BxDFs - Add SampleWithPDF, SampleWithRcpPDF, and SampleWithDensity concepts to validate sample types - Used concept composition (NBL_CONCEPT_REQ_TYPE_ALIAS_CONCEPT) to build ResamplableSampler on TractableSampler and BijectiveSampler on ResamplableSampler
… projected/spherical triangle
| vector3_type tri_vertices[3]; | ||
| scalar_type triCosC; | ||
| scalar_type triCscB; | ||
| scalar_type triCscC; |
There was a problem hiding this comment.
take a template whether you want the SphericalTriangle to be bijective (have a generateInverse method) and implement the Bijective in terms of Resamplable & Tractable Spherical Trangle + the triCscC member
There was a problem hiding this comment.
seems that triCscB doesn't need to be kept #1001 (comment)
include/nbl/builtin/hlsl/sampling/projected_spherical_triangle.hlsl
Outdated
Show resolved
Hide resolved
include/nbl/builtin/hlsl/sampling/projected_spherical_triangle.hlsl
Outdated
Show resolved
Hide resolved
include/nbl/builtin/hlsl/sampling/projected_spherical_triangle.hlsl
Outdated
Show resolved
Hide resolved
include/nbl/builtin/hlsl/sampling/projected_spherical_triangle.hlsl
Outdated
Show resolved
Hide resolved
include/nbl/builtin/hlsl/sampling/projected_spherical_triangle.hlsl
Outdated
Show resolved
Hide resolved
# Conflicts: # examples_tests # include/nbl/builtin/hlsl/shapes/spherical_triangle.hlsl
| // Builds a normalized cumulative histogram from an array of non-negative weights. | ||
| // Output has N-1 entries (last bucket implicitly 1.0). | ||
| template<typename T> | ||
| void computeNormalizedCumulativeHistogram(const T* weights, uint32_t N, T* outCumProb) |
There was a problem hiding this comment.
maybe take a span<T> weights then the N is implicit, also do a special path for size()<2 because right now it will crash
There was a problem hiding this comment.
resolved, alias table too?
There was a problem hiding this comment.
yea alias table too
| { | ||
| impl::bound_t<Accessor,Comparator> implementation = impl::bound_t<Accessor,Comparator>::setup(begin,end,value,comp); | ||
| return implementation(accessor); | ||
| const uint32_t retval = implementation(accessor); |
There was a problem hiding this comment.
WARNING impl::bound_t<Accessor,Comparator>::setup takes comp as const
need to change the impl and provide it brom a NBL_REF_ARG through the methods of impl::bound_t
| static weight_type backwardWeight(const codomain_type sample) | ||
| { | ||
| return backwardPdf(sample); | ||
| return T(0.5) * hemisphere_t::backwardWeight(sample); |
There was a problem hiding this comment.
remember sample is a HLSL keyword
| T z = T(1.0) - T(2.0) * _sample.x; | ||
| T r = hlsl::sqrt<T>(hlsl::max<T>(T(0.0), T(1.0) - z * z)); | ||
| T phi = T(2.0) * numbers::pi<T> * _sample.y; | ||
| return vector_t3(r * hlsl::cos<T>(phi), r * hlsl::sin<T>(phi), z); | ||
| // Map _sample.x from [0,1] into hemisphere sample + sign flip: | ||
| // upper hemisphere when _sample.x < 0.5, lower when >= 0.5 | ||
| const bool chooseLower = _sample.x >= T(0.5); | ||
| const T hemiX = chooseLower ? (T(2.0) * _sample.x - T(1.0)) : (T(2.0) * _sample.x); | ||
| vector_t3 retval = hemisphere_t::__generate(vector_t2(hemiX, _sample.y)); | ||
| retval.z = chooseLower ? (-retval.z) : retval.z; | ||
| return retval; |
There was a problem hiding this comment.
ugh no the code is definitely worse here with all the conditionals, I just need a shared common function between both that takes a z coord externally
| NBL_PRIMARY_REQUIRES( | ||
| concepts::accessors::GenericReadAccessor<ProbabilityAccessor, T, Codomain> && | ||
| concepts::accessors::GenericReadAccessor<AliasIndexAccessor, Codomain, Codomain> && | ||
| concepts::accessors::GenericReadAccessor<PdfAccessor, T, Codomain>) |
There was a problem hiding this comment.
require that Domain is an UnsignedIntegralScalar (or whatever the concept is called)
There was a problem hiding this comment.
Codomain you mean, resolved
There was a problem hiding this comment.
yeah codomain, sorry
| math::sincos<scalar_type>(scalar_type(2.0) * numbers::pi<scalar_type> * u.y - numbers::pi<scalar_type>, sinPhi, cosPhi); | ||
| const codomain_type outPos = vector2_type(cosPhi, sinPhi) * nbl::hlsl::sqrt(scalar_type(-2.0) * nbl::hlsl::log(u.x)) * stddev; | ||
| cache.pdf = backwardPdf(outPos); | ||
| cache.u_x = u.x; | ||
| return outPos; | ||
| } | ||
|
|
||
| density_type forwardPdf(const cache_type cache) | ||
| { | ||
| return cache.pdf; | ||
| return halfRcpStddev2 * numbers::inv_pi<scalar_type> * cache.u_x; |
There was a problem hiding this comment.
ok so you check my math from the comment and it was wrong ?
There was a problem hiding this comment.
sorry, It's actually correct, I brute forced it in python and it seems I miswrote stuff, it' much simpler now, resolved
There was a problem hiding this comment.
Since we don't really have a test for this did you plot or test somehow the new version works ?
There was a problem hiding this comment.
yes I tested it, this python stuff is great!
| phi += hlsl::mix(T(0.0), twopi, phi < T(0.0)); | ||
| return vector_t2(_sample.z, phi / twopi); |
There was a problem hiding this comment.
more accurate to divide phi by twopi first and add 1.0 if it was negative
There was a problem hiding this comment.
also use a select
| vector_t2 hemiUV = hemisphere_t::__generateInverse(hemiSample); | ||
| // Recover _sample.x: upper hemisphere maps [0,0.5], lower maps [0.5,1] | ||
| hemiUV.x = isLower ? (hemiUV.x * T(0.5) + T(0.5)) : (hemiUV.x * T(0.5)); | ||
| return hemiUV; |
There was a problem hiding this comment.
just overwrite the x coordinate, previous version was branchless
| cache_type c; | ||
| c.L_z = L.z; | ||
| return forwardPdf(c); |
There was a problem hiding this comment.
assert that L_z > 0
There was a problem hiding this comment.
resolved, in generate too
There was a problem hiding this comment.
generate spits it out, so should be no issue for it or forwardPdf
Its just special for backwardPdf because we need to clearly state that its invalid to call with such a parameter (since people can call backwardPdf with literally whatever)
There was a problem hiding this comment.
should probably assert everywhere then? (all other samplers? some of them?)
There was a problem hiding this comment.
only this one because its Hemisphere and the UniformHemisphere, because you take an L in R^3 and a negative L.z can be fed into it by accident if someone doesn't read the docs
| static domain_type generateInverse(const codomain_type L) | ||
| { | ||
| // NOTE: incomplete information to recover exact z component; we only know which hemisphere L came from, | ||
| // so we return a canonical value (0.0 for upper, 1.0 for lower) that round-trips correctly through __generate | ||
| return vector_t3(hemisphere_t::__generateInverse(L), hlsl::mix(T(1.0), T(0.0), L.z > T(0.0))); | ||
| } | ||
|
|
||
| static domain_type generateInverse(const codomain_type L, NBL_REF_ARG(cache_type) cache) | ||
| { | ||
| cache.pdf = __pdf(L.z); | ||
| return __generateInverse(L); | ||
| } | ||
|
|
||
| static T __pdf(T L_z) | ||
| { | ||
| return T(0.5) * hemisphere_t::__pdf(hlsl::abs(L_z)); | ||
| } | ||
|
|
||
| static scalar_type pdf(T L_z) | ||
| { | ||
| return __pdf(L_z); | ||
| return vector_t3(hemisphere_t::generateInverse(L), hlsl::mix(T(1.0), T(0.0), L.z > T(0.0))); | ||
| } |
There was a problem hiding this comment.
not bijective you can't tell the eact value of Z before it went in
| const bool chooseLower = _sample.z > T(0.5); | ||
| retval.z = chooseLower ? (-retval.z) : retval.z; | ||
| if (chooseLower) | ||
| _sample.z -= T(0.5); | ||
| _sample.z *= T(2.0); | ||
| return retval; |
There was a problem hiding this comment.
I wonder if
const scalar_type tmp = _sample.z - scalar_type(0.5);
retval.z *= sign(tmp);
if (tmp)
_sample.z = tmp;
_sample.z *= 2.0;
would be faster
| namespace concepts | ||
| { | ||
|
|
||
| // clang-format off | ||
| #define NBL_CONCEPT_NAME CumulativeProbabilityAccessor | ||
| #define NBL_CONCEPT_TPLT_PRM_KINDS (typename)(typename) | ||
| #define NBL_CONCEPT_TPLT_PRM_NAMES (T)(Scalar) | ||
| #define NBL_CONCEPT_PARAM_0 (accessor, T) | ||
| #define NBL_CONCEPT_PARAM_1 (index, uint32_t) | ||
| NBL_CONCEPT_BEGIN(2) | ||
| #define accessor NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_0 | ||
| #define index NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_1 | ||
| NBL_CONCEPT_END( | ||
| ((NBL_CONCEPT_REQ_TYPE)(T::value_type)) | ||
| ((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((accessor[index]), ::nbl::hlsl::is_same_v, Scalar))); | ||
| #undef index | ||
| #undef accessor | ||
| #include <nbl/builtin/hlsl/concepts/__end.hlsl> | ||
| // clang-format on | ||
|
|
||
| } // namespace concepts |
There was a problem hiding this comment.
why can't you use the existing Generic Read accessor ?
| // Stateful comparator that tracks the CDF values seen during binary search. | ||
| // upper_bound uses lower_to_upper_comparator_transform_t which calls !comp(rhs, lhs), | ||
| // so our operator() receives (value=u, rhs=cumProb[testPoint]). | ||
| struct CdfComparator | ||
| { | ||
| bool operator()(const density_type value, const density_type rhs) | ||
| { | ||
| const bool retval = value < rhs; | ||
| if (retval) | ||
| upperBound = rhs; | ||
| else | ||
| oneBefore = rhs; | ||
| return retval; | ||
| } | ||
|
|
||
| density_type oneBefore; | ||
| density_type upperBound; | ||
| }; |
There was a problem hiding this comment.
btw local structs are 100% okay in HLSL, and you can literally put its definition inside the generate so it doesn't leak :D
| const density_type cur = (v < storedCount) ? cumProbAccessor[v] : density_type(1.0); | ||
| const density_type prev = (v > 0u) ? cumProbAccessor[v - 1u] : density_type(0.0); | ||
| return cur - prev; |
There was a problem hiding this comment.
Without hlsl::select the ? wont compile to a cmov, because in C++ and HLSL the ? operator shortcircuits
but you can't pipe the memory accesses into select because you'll get OOB access
so you should write since you're going to have true if-statements anyway
density_type retval = 1.f;
if (v<storedCount)
cumProbAccessor.get(v,retval);
if (v)
{
density_type prev;
cumProbAccessor.get(v-1,prev);
retval -= prev;
}| if (pyramidAngles()) | ||
| return 0.f; |
There was a problem hiding this comment.
instead check for the solidAngle being 0 or below a threshold
There was a problem hiding this comment.
then you can fold pyramidAngles into the create
| const scalar_type u = subTriSolidAngleRatio > numeric_limits<scalar_type>::min ? subTriSolidAngleRatio : 0.0; | ||
|
|
||
| const scalar_type sinBC_s_product = sinB_ * sinC_; | ||
|
|
||
| // 1 ULP below 1.0, ensures (1.0 - cosBC_s) is strictly positive in float | ||
| const scalar_type one_below_one = bit_cast<scalar_type>(bit_cast<uint_type>(scalar_type(1)) - uint_type(1)); | ||
| const scalar_type one_below_one = ieee754::nextTowardZero(scalar_type(1.0)); | ||
| const scalar_type cosBC_s = sinBC_s_product > numeric_limits<scalar_type>::min ? (cosA + cosB_ * cosC_) / sinBC_s_product : triCosC; | ||
| const scalar_type v_denom = scalar_type(1.0) - (cosBC_s < one_below_one ? cosBC_s : triCosC); |
There was a problem hiding this comment.
replace the ?s with hlsl::select
| return backwardPdf(L); | ||
| cache_type cache; | ||
| // cache.bilinearCache; // unused | ||
| // approximate abs(cos_theta) via bilinear interpolation at the inverse-mapped point | ||
| const vector2_type u = sphtri.generateInverse(L); | ||
| cache.abs_cos_theta = bilinearPatch.backwardWeight(u); | ||
| return forwardWeight(cache); |
There was a problem hiding this comment.
you absolutely don't want to call generateInverse here!
whole function should be
cache.abs_cos_theta = math::conditionalAbsOrMax(receiverWasBSDF,dot(receiverNormal,L),minimumProjSolidAngle);
return forwardWeight(cache);
whoops seems you need to store receiverWasBSDF after all
There was a problem hiding this comment.
but as we've already established the Projected Solid Angle MIS weight is actually useless (to be confirmed by testing later), so leave a comment we shouldn't store receiver and rcpProj angle as members and will probably use a mix of just clampedNdotL and rcpSolidAngle
There was a problem hiding this comment.
add a template argument whether to use the Pdf as the weight (default would be to use the PDF, cause the fake MIS weight has been shown to be crap so far)
There was a problem hiding this comment.
resolved will obv. need work later
| density_type backwardPdf(const vector3_type L) | ||
| { | ||
| const density_type pdf = sphtri.backwardPdf(L); | ||
| typename SphericalTriangle<scalar_type>::cache_type dummyCache; | ||
| const vector2_type u = sphtri.generateInverse(L, dummyCache); | ||
| Bilinear<scalar_type> bilinear = computeBilinearPatch(); | ||
| return pdf * bilinear.backwardPdf(u); | ||
| const vector2_type u = sphtri.generateInverse(L); | ||
| return sphtri.rcpSolidAngle * bilinearPatch.backwardPdf(u); | ||
| } |
There was a problem hiding this comment.
you don't need a backwardPdf function, we won't be composing the warp further
but what you have now, you can move to backwardWeight for the WeightIsPdf template option because I want a toggle between fake MIS weight and true PDF https://github.com/Devsh-Graphics-Programming/Nabla/pull/1001/changes/254404b5cfa69581820833c5886dee5835a0bb42..edc3c3ed47d09ccf78eb3379fe7b7905c54495a4#r2988401827
| sampling::quotient_and_pdf<monochrome_type, scalar_type> qp; | ||
| scalar_type p; | ||
| NBL_IF_CONSTEXPR (IsBSDF) | ||
| qp = sampling::ProjectedSphere<scalar_type>::template quotientAndPdf(_sample.getNdotL(_clamp)); | ||
| p = sampling::ProjectedSphere<scalar_type>::backwardPdf(vector<scalar_type, 3>(0, 0, _sample.getNdotL(_clamp))); | ||
| else | ||
| qp = sampling::ProjectedHemisphere<scalar_type>::template quotientAndPdf(_sample.getNdotL(_clamp)); | ||
| return quotient_pdf_type::create(qp.quotient()[0], qp.pdf()); | ||
| p = sampling::ProjectedHemisphere<scalar_type>::backwardPdf(vector<scalar_type, 3>(0, 0, _sample.getNdotL(_clamp))); | ||
| return quotient_pdf_type::create(scalar_type(1.0), p); |
There was a problem hiding this comment.
why are you using bakwardPdf for a quotient function!? should be forwardPdf
Examples PR
Notes:
quotient_and_pdf()methods inUniformHemisphere,UniformSphere,ProjectedHemisphere, andProjectedSphereshadow the struct typesampling::quotient_and_pdf<Q, P>fromquotient_and_pdf.hlsl. DXC can't resolve the return type because the method name takes precedence over the struct name during lookup. Fixed by fully qualifying with::nbl::hlsl::sampling::quotient_and_pdf<U, T>.