Skip to content

Address path tracer merge leftover comments#991

Open
keptsecret wants to merge 26 commits intomasterfrom
path_tracer_leftover_cleanup
Open

Address path tracer merge leftover comments#991
keptsecret wants to merge 26 commits intomasterfrom
path_tracer_leftover_cleanup

Conversation

@keptsecret
Copy link
Contributor

Makes changes left from #966

@keptsecret
Copy link
Contributor Author

hlsl/matrix_utils/transformation_matrix_utils.hlsl removed because of https://github.com/Devsh-Graphics-Programming/Nabla/pull/966/files#r2618467323

return pdf * bilinear.backwardPdf(u);
}

scalar_type pdf(const vector3_type receiverNormal, bool receiverWasBSDF, const vector3_type L)

Choose a reason for hiding this comment

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

NBL_UNROLL for (int m=1; m<=2; ++m)
{
Cm *= r123s;
Sm = hlsl::promote<vector_type>(2.0) * evalSensitivity(hlsl::promote<vector_type>(scalar_type(m))*D, hlsl::promote<vector_type>(scalar_type(m)) *(phi23s+phi21s));

Choose a reason for hiding this comment

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

at least awrite a TODO about hoisting this common subexpression

Comment on lines 92 to 93

Choose a reason for hiding this comment

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

only the sampling::SphericalTriangle is needed #966 (comment)

Choose a reason for hiding this comment

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

most parameters should be members #966 (comment)

Choose a reason for hiding this comment

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

return retval;
}

vector4_type computeBilinearPatch(const vector3_type receiverNormal, bool isBSDF)

Choose a reason for hiding this comment

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

why is this returning vector4_type instead of sampling::bilinear

Comment on lines 29 to 34
static ProjectedSphericalTriangle<T> create(NBL_CONST_REF_ARG(shapes::SphericalTriangle<T>) tri)
{
ProjectedSphericalTriangle<T> retval;
retval.tri = tri;
return retval;
}

Choose a reason for hiding this comment

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

remove, see #966 (comment)

Comment on lines 67 to 68
vector3_type cos_vertices, sin_vertices;
const scalar_type solidAngle = tri.solidAngleOfTriangle(cos_vertices, sin_vertices, cos_a, cos_c, csc_b, csc_c);
const scalar_type solidAngle = tri.solidAngle(cos_vertices, sin_vertices);

Choose a reason for hiding this comment

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

should be precomputed and stored as members #966 (comment)

Choose a reason for hiding this comment

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

again function with bazillion parameters that are really local members

using matrix3x3_type = matrix<Scalar, 3, 3>;

static SphericalRectangle<scalar_type> create(const vector3_type observer, const vector3_type rectangleOrigin, const matrix3x3_type basis)
static SphericalRectangle<Scalar> create(const vector3_type rectangleOrigin, const vector3_type right, const vector3_type up)

Choose a reason for hiding this comment

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

why are you not taking CompressedSphericalRectangle as input instead of 3 variables ?

cos_vertices[2] = hlsl::dot<vector3_type>(awayFromEdgePlane0, awayFromEdgePlane1);
// TODO: above dot products are in the wrong order, either work out which is which, or try all 6 permutations till it works
cos_vertices = hlsl::clamp<vector3_type>((cos_sides - cos_sides.yzx * cos_sides.zxy) * csc_sides.yzx * csc_sides.zxy, hlsl::promote<vector3_type>(-1.0), hlsl::promote<vector3_type>(1.0));
cos_vertices = hlsl::clamp((cos_sides - cos_sides.yzx * cos_sides.zxy) * csc_sides.yzx * csc_sides.zxy, hlsl::promote<vector3_type>(-1.0), hlsl::promote<vector3_type>(1.0));

Choose a reason for hiding this comment

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

this should be in a common private method, its same as above

retval.extents = vector2_type(hlsl::length(right), hlsl::length(up));
retval.basis[0] = right / retval.extents[0];
retval.basis[1] = up / retval.extents[1];
retval.basis[2] = hlsl::normalize(hlsl::cross(retval.basis[0], retval.basis[1]));

Choose a reason for hiding this comment

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

add assert that the absolute value of dot product of cross product args is >0.f

// The matrix should be a matrix<scalar_type,3,4> where the last column is the translation, a 3x3 matrix with a pre-transform translation (worldSpace rectangle origin to be subtracted).

// You can compute it from an OBB matrix (as given by/to imguizmo to position a [0,1]^2 rectangle mesh where Z+ is the front face.

Choose a reason for hiding this comment

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

add the code snippet to ocomments

Choose a reason for hiding this comment

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

Comment on lines 28 to 51

Choose a reason for hiding this comment

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

the observer should be passed during create because while we can have our shape::SphericalRectangle and query its solid angle from multiple observes, the sole purpose of sampling::SphericalRectangle is to draw multiple samples

As much as possible should be precomputed, like the cosGamma etc,

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

Comment on lines +153 to +154
// We don't allow non watertight transmitters in this renderer
bool validPath = nee_sample.getNdotL() > numeric_limits<scalar_type>::min && nee_sample.isValid();

Choose a reason for hiding this comment

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

this is to implement in your NEE system

bxdf::fresnel::OrientedEtas<monochrome_type> orientedEta = bxdf::fresnel::OrientedEtas<monochrome_type>::create(interaction.getNdotV(), hlsl::promote<monochrome_type>(monochromeEta));
anisocache_type _cache = anisocache_type::template create<anisotropic_interaction_type, sample_type>(interaction, nee_sample, orientedEta);
validPath = validPath && _cache.getAbsNdotH() >= 0.0;
bxdf.params.eta = monochromeEta;

Choose a reason for hiding this comment

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

this should be done in your NEE system, and return a pdf thats NaN or 0

Choose a reason for hiding this comment

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

probably 0 so we dont have issues with MIS weights

validPath = validPath && _cache.getAbsNdotH() >= 0.0;
bxdf.params.eta = monochromeEta;

if (neeContrib_pdf.pdf < numeric_limits<scalar_type>::max)

Choose a reason for hiding this comment

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

this seems off, you should be checking for pdf>0.f

Comment on lines 170 to 171
quotient_pdf_type bsdf_quotient_pdf = materialSystem.quotient_and_pdf(bxdf.materialType, bxdf.params, nee_sample, interaction.isotropic, _cache.iso_cache);
neeContrib_pdf.quotient *= bxdf.albedo * throughput * bsdf_quotient_pdf.quotient;
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Feb 12, 2026

Choose a reason for hiding this comment

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

why is there an albedo thing going on?

This should literally be nee.quotient *= materialSystem.eval*rcpChoiceProb;

Choose a reason for hiding this comment

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

you definitely don't want to call quotient, because that picks up contributions and pdfs from dirac lobes which you shouldn't be able to pick up with other samplers

Comment on lines +172 to +174
const scalar_type otherGenOverChoice = bsdf_quotient_pdf.pdf * rcpChoiceProb;
const scalar_type otherGenOverLightAndChoice = otherGenOverChoice / bsdf_quotient_pdf.pdf;
neeContrib_pdf.quotient *= otherGenOverChoice / (1.f + otherGenOverLightAndChoice * otherGenOverLightAndChoice); // balance heuristic
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Feb 12, 2026

Choose a reason for hiding this comment

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

This is completely wrong, the MIS weight should be a ratio of PDFs

generator^2/(generator^2+notGenerator^2)

which is

1/(1+(notGenerator/generator)^2)

You've used the BSDF's PDF as the GENERATOR! The MIS weights are wrong.

in the code this should have the form

const scalar_type otherGenOverLightAndChoice = bsdf.pdf * rcpChoiceProb/ neeContrib.pdf;
nee.quotient /= (1.f+otherGenOverLightAndChoice*otherGenOverLightAndChoice);

and only get applied if (neeContrib_pdf.pdf < numeric_limits<scalar_type>::max) because when it -> INF you'll get the MIS weight going to 1.f you can always apply, its numerically stable

Comment on lines +179 to +182
ray_type nee_ray;
nee_ray.origin = intersection + nee_sample.getL().getDirection() * t * Tolerance<scalar_type>::getStart(depth);
nee_ray.direction = nee_sample.getL().getDirection();
nee_ray.intersectionT = t;

Choose a reason for hiding this comment

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

can package inside intersector.traceShadowRay

nee_ray.origin = intersection + nee_sample.getL().getDirection() * t * Tolerance<scalar_type>::getStart(depth);
nee_ray.direction = nee_sample.getL().getDirection();
nee_ray.intersectionT = t;
if (bsdf_quotient_pdf.pdf < numeric_limits<scalar_type>::max && getLuma(neeContrib_pdf.quotient) > lumaContributionThreshold && intersector_type::traceRay(nee_ray, scene).id == -1)

Choose a reason for hiding this comment

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

you shouldn't be checking bsdf_quotient_pdf.pdf < numeric_limits<scalar_type>::max here... eval should never return a huge pdf really (dirac lobes should be skipped both for eval and pdf of eval - they differ with quotient), you could assert it

Choose a reason for hiding this comment

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

btw the traceRay should be a traceShadowRay and it should take the objectID of what you intend to hit and return a floating point "fuzzy" shadow (because I can make it attenuate NEE rays when there are diffuse translucents or thindielectrics)

// example only uses isotropic bxdfs
// the value of the bsdf divided by the probability of the sample being generated
quotient_pdf_type bsdf_quotient_pdf = materialSystem.quotient_and_pdf(bxdf.materialType, bxdf.params, bsdf_sample, interaction.isotropic, _cache.iso_cache);
throughput *= bxdf.albedo * bsdf_quotient_pdf.quotient;

Choose a reason for hiding this comment

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

what's the albedo doing here!? its literally part of the material!

const float lumaThroughputThreshold = lumaContributionThreshold;
if (bxdfPdf > bxdfPdfThreshold && getLuma(throughput) > lumaThroughputThreshold)
{
ray.payload.throughput = throughput;

Choose a reason for hiding this comment

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

shouldn't be touching a member directly you should have a method to multiply in a new factor

Comment on lines +217 to +224
// trace new ray
ray.origin = intersection + bxdfSample * (1.0/*kSceneSize*/) * Tolerance<scalar_type>::getStart(depth);
ray.direction = bxdfSample;
NBL_IF_CONSTEXPR (nee_type::IsPolygonMethodProjectedSolidAngle)
{
ray.normalAtOrigin = interaction.getN();
ray.wasBSDFAtOrigin = isBSDF;
}

Choose a reason for hiding this comment

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

this needs to be tucked inside your ray as a method or something similar

Comment on lines +214 to +215
scalar_type otherTechniqueHeuristic = neeProbability / bxdfPdf; // numerically stable, don't touch
ray.payload.otherTechniqueHeuristic = otherTechniqueHeuristic * otherTechniqueHeuristic;

Choose a reason for hiding this comment

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

do as a method to set it, something with namign that connects with with foundEmissionMIS

@devshgraphicsprogramming
Copy link
Member

Merge #989 into this PR


const scalar_type LdotH = hlsl::mix(VdotH, ieee754::copySign(hlsl::sqrt(rcpEta.value2[0]*VdotH*VdotH + scalar_type(1.0) - rcpEta.value2[0]), -VdotH), transmitted);
if (hlsl::isnan(LdotH))
return sample_type::createInvalid();

Choose a reason for hiding this comment

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

do it like this then

scalar_type LdotH;
if (transmitted)
{
   float det = rcpEta.value2[0]*VdotH*VdotH + scalar_type(1.0) - rcpEta.value2[0];
   if (det<0.f)
      return sample_type::createInvalid();
   LdotH = ieee754::copySign(hlsl::sqrt(det), -VdotH);
}
else
   LdotH = VdotH;

Comment on lines 154 to 162
// We don't allow non watertight transmitters in this renderer
bool validPath = nee_sample.getNdotL() > numeric_limits<scalar_type>::min && nee_sample.isValid();
// but if we allowed non-watertight transmitters (single water surface), it would make sense just to apply this line by itself
bxdf::fresnel::OrientedEtas<monochrome_type> orientedEta = bxdf::fresnel::OrientedEtas<monochrome_type>::create(interaction.getNdotV(), hlsl::promote<monochrome_type>(monochromeEta));
anisocache_type _cache = anisocache_type::template create<anisotropic_interaction_type, sample_type>(interaction, nee_sample, orientedEta);
validPath = validPath && _cache.getAbsNdotH() >= 0.0;
bxdf.params.eta = monochromeEta;

if (neeContrib_pdf.pdf < numeric_limits<scalar_type>::max)

Choose a reason for hiding this comment

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

shouldn't this be tucked inside the if statement here?

Also you can just assert the nee_sample.isValid() if the neeContrib.pdf>0.f

Choose a reason for hiding this comment

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

drop the _pdf suffix

Comment on lines 114 to 117
measure_type emissive = nee.deferredEvalAndPdf(_pdf, scene, lightID, ray) * throughput;
scalar_type _pdfSq = hlsl::mix(_pdf, _pdf * _pdf, _pdf < numeric_limits<scalar_type>::max);
emissive /= (1.0 + _pdfSq * ray.payload.otherTechniqueHeuristic);
ray.payload.accumulation += emissive;
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Feb 15, 2026

Choose a reason for hiding this comment

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

I think the reason you got NaN is because _pdfSq was Max*Max giving you Inf, and otherTechniqueHeuristic was 0

Its a bit weird that your Area Light's PDF is near Max or INF, because this can only happen when Solid Angle or Projected Solid Angle is absolutely tiny, or when during area sampling the distance is huge or the angle of the ray to emitter normal is near orthogonal.

Because you only support 2 generators in this Path Tracer, I can allow you to not square your PDFs until you compue their ratios (square the ratio at the end), this way you'll have more numerical precision.

If you had 4 generators in your MIS weight then you'd have one MUL extra than doing it the way we did it until now.

Because the BxDF sample was generated and we've arrived here, it must be that the BxDF PDF was larger than 0.f, so no division by 0.

The other technique heuristic is neeProbability / bxdfPdf .

What could have gone wrong ? Your BxDF could have been a delta ditribution or close to, so the PDF would have been huge.

Would that have mattered? Probably not because neeProbability would be between 0 and 1, so you're divising a finite number by a non-zero number, so you can get between 0 and not infinity.

However what can happen is that the otherTechniqnueHeuristic is 0 due to delta distribution or zero NEE probability, then in the code I gave #991 (comment)

the MIS condition should be

if (otherTechniqueHeuristic>min)
if (const uint lightID=scene.getLightID(objectID); lightID)

instead of just

if (const uint lightID=scene.getLightID(objectID); lightID)

meaning there was nothing to MIS in the first place

Choose a reason for hiding this comment

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

you should also probably assert that the _pdf of the light is not zero outside of the if-statement

if (bxdfPdf > bxdfPdfThreshold && getLuma(throughput) > lumaThroughputThreshold)
{
ray.payload.throughput = throughput;
scalar_type otherTechniqueHeuristic = neeProbability / bxdfPdf; // numerically stable, don't touch

Choose a reason for hiding this comment

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

assert that otherTechniqueHeuristic is finite (less than infinity)

const scalar_type neeProbability = bxdf.getNEEProb();
scalar_type rcpChoiceProb;
sampling::PartitionRandVariable<scalar_type> partitionRandVariable;
partitionRandVariable.leftProb = neeProbability;

Choose a reason for hiding this comment

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

assert neeProbability is between 0 and 1 inclusive

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