Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
b99ae6e
Fix small bug in GenericDataAccessor definition
Nov 11, 2025
b9537ea
First draft of Warpmap Generation workgroup implementation
Nov 11, 2025
a737173
Add warp concept
Nov 18, 2025
64349db
Add spherical warp
Nov 18, 2025
e44fcf4
Remove envmap accessors.hlsl
Nov 18, 2025
9b29dfd
Hierarchical image sampling implementation
Nov 18, 2025
06ae3e4
Merge branch 'master' into env_map_importance_sampling
Dec 19, 2025
16ecb52
Merge branch 'master' into env_map_importance_sampling
Dec 20, 2025
8d682b9
Remove envmap.hlsl
Dec 20, 2025
890f7c6
Move to sampling namespace and implement backward density
Dec 22, 2025
f99c63b
Remove private, public from hierarchical_image
Dec 22, 2025
3ff2791
Refactor hierarchical image to keep accessor and common data as member
Dec 22, 2025
76ef536
Refactor hierarchical image to separate binarySearch from Hierarchica…
Dec 26, 2025
ef773fd
Fix Spherical warp indentation
Dec 26, 2025
b9467fe
Add some comment why we add xi to the sample uvs
Dec 26, 2025
3682604
Merge branch 'master' into env_map_importance_sampling
Dec 30, 2025
ac1e2f3
WIP
Jan 6, 2026
baca1cf
Rename uv to coord for LuminanceAccessor concepts
Jan 9, 2026
f12b797
Fix hierarchical_image.hlsl
Jan 9, 2026
0957aed
Fix typo in spherical.hlsl
Jan 9, 2026
1b35d34
Implement gen_luma, gen_warpmap and measure_luma shaders
Jan 9, 2026
665bb8d
EnvmapImportanceSampling CMakeLists
Jan 9, 2026
b522b4f
Initial implementation of CEnvmapImportanceSampling
Jan 9, 2026
3e51c69
Initial implementation of CEnvmapImportanceSampling
Jan 12, 2026
c72d305
Small fixes
Jan 12, 2026
5ee2ce7
Initial implementation of computeWarpMap
Jan 20, 2026
867868c
Fix arithmetic config no const specifier for method
Jan 30, 2026
1a66157
Define config_t from outside
Jan 30, 2026
d4b8105
More fixes on computeWarpMap implementation
Jan 30, 2026
8853738
Fix chose second to be placed inside the loop
Jan 30, 2026
6bde489
LuminanceReadAccessor take ScalarT as template parameter
Jan 30, 2026
756fbb0
gen_warpmap to gen_warp
Jan 30, 2026
8d64a19
Move hierarchical_image concepts to sampling subdirectory
Feb 18, 2026
70d8423
Add some comment regarding corner sampling
Feb 18, 2026
2842d29
Parameterize spherical warp and make sure all literal is in the corre…
Feb 18, 2026
a51848c
Refactor CEnvmapImportanceSampling to block and calculate avgLuma
Feb 18, 2026
58c9c13
merge master, fix conflicts
keptsecret Feb 19, 2026
fa94ac2
Fix warp concept and add density type to warp concept
Feb 19, 2026
8494124
Rename luminanceScale to lumaRGBCoefficients
Feb 19, 2026
b273d87
Remove measure_luma.comp.hlsl
Feb 19, 2026
3bc0e57
Fix some bug in hierarchical_image.hlsl
Feb 19, 2026
1dadf92
Rename luminanceScales to lumaRGBCoefficients
Feb 19, 2026
f19cbe9
Move EnvmapImportanceSampling from ext to core
Feb 21, 2026
fde2bba
Fix binarySearch implementation. when last is 2x1 we should check for…
Feb 21, 2026
81cae21
Rename private member with underscore prefix
Feb 21, 2026
733a4ab
Merge branch 'master' into env_map_importance_sampling
Feb 22, 2026
ba6be93
Update submodule to follow master branch
Feb 22, 2026
f04d98b
Add missed EnvmapSampler.h and cpp
Feb 23, 2026
df2bfc3
Rename get and gather to texelFetch and texelGather
Feb 23, 2026
4930e25
Merge branch 'hlsl_path_tracer_example' into env_map_importance_sampling
Feb 23, 2026
05b862a
Include missing files into commit
Feb 23, 2026
d50b50f
Merge branch 'master' into env_map_importance_sampling
Feb 23, 2026
1498094
Update comment on sampleUvs
Mar 3, 2026
39da42b
Use bitfield for lumaMapResolution in push constant
Mar 3, 2026
47799ab
Remove NBL_BUILD_ENVMAP_IMPORTANCE_SAMPLING option
Mar 3, 2026
0f69171
Fix worgroup dim for gen_warp
Mar 3, 2026
43b88ef
Use constant workgroup dimension instead of WORKGROUP_DIM define
Mar 3, 2026
9aa3113
Remove passing WORKGROUP_DIM to shader
Mar 3, 2026
16c374e
Remove passing WORKGROUP_DIM for gen_luma
Mar 4, 2026
126aa21
Add upsscale parameter for EnvmapSampler
Mar 4, 2026
86a00f9
Pass warpMap width and height to luminanceSampler. Check For Out of B…
Mar 4, 2026
7509c83
Small fixes to gen_warp shader
Mar 4, 2026
833b388
gen_warp get image dimension from push constant instead of OpImageQue…
Mar 4, 2026
ac63441
Rename HierarchicalImage to WarpmapSampler
Mar 4, 2026
dacf493
Add todo comment for cube map
Mar 4, 2026
a2b57f9
Move EnvmapSampler from core/sampling to video/sampling
Mar 5, 2026
3343e64
Rename LuminanceSampler to HierarchicalLuminanceSampler
Mar 5, 2026
834163a
Fix corner sampling logic in gen_luma and hierarchical_image.hlsl
Mar 6, 2026
94d1f14
Fix previous commit
Mar 6, 2026
23a7e12
Add some todo comment for corner sampling flag
Mar 6, 2026
e8930ef
Add const modifier for binarySearch
Mar 7, 2026
de4807f
Add some temporary struct from pr #1001
Mar 7, 2026
a158057
Refactor hierarchical_image naming and concepts
Mar 10, 2026
4dd4e1f
Fix indentation of hierarchical_image.hlsl
Mar 10, 2026
fe14c93
Small fixes
Mar 10, 2026
bb41942
Remove superfluous member from WarpmapSampler
Mar 11, 2026
6b87e69
Merge branch 'master' into env_map_importance_sampling
Mar 11, 2026
9657c66
Fix resolve.hlsl case
Mar 12, 2026
fe91d9a
Merge branch 'master' into env_map_importance_sampling
Mar 26, 2026
48e16e3
Rename GenWarpWorkgroupDim and GenLumaWorkgroupDim
kevyuu Mar 26, 2026
1c7abcf
Fix include guard of warps/spherical.hlsl
kevyuu Mar 26, 2026
f05132d
Undo Embed built-in resources option to OFF
kevyuu Mar 27, 2026
08a7356
Gen Luma now support Texture Array
kevyuu Mar 27, 2026
df8e8d7
Remove unused member in HierarchicalWarpGenerator
kevyuu Mar 27, 2026
150fac0
Remove LuminanceReadAccessor concept and support Texture2DArray for g…
kevyuu Mar 27, 2026
858dbe8
Rename _mip2x1 to _lastMipLevel
kevyuu Mar 27, 2026
a977070
Fix envmapsampler header guard
kevyuu Mar 28, 2026
a2f2662
Remove unused defaults in EnvmapSampler
kevyuu Mar 28, 2026
58abf25
Undo cuda interop cmake option
kevyuu Mar 28, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ NBL_CONCEPT_END(
#include <nbl/builtin/hlsl/concepts/__end.hlsl>

template<typename T, typename V, typename I=uint32_t>
NBL_BOOL_CONCEPT GenericDataAccessor = GenericWriteAccessor<T,V,I> && GenericWriteAccessor<T,V,I>;
NBL_BOOL_CONCEPT GenericDataAccessor = GenericReadAccessor<T,V,I> && GenericWriteAccessor<T,V,I>;

}
}
Expand Down
314 changes: 314 additions & 0 deletions include/nbl/builtin/hlsl/sampling/hierarchical_image.hlsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
// Copyright (C) 2018-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_BUILTIN_HLSL_SAMPLING_HIERARCHICAL_IMAGE_INCLUDED_
#define _NBL_BUILTIN_HLSL_SAMPLING_HIERARCHICAL_IMAGE_INCLUDED_

#include <nbl/builtin/hlsl/concepts/accessors/loadable_image.hlsl>
#include <nbl/builtin/hlsl/sampling/basic.hlsl>
#include <nbl/builtin/hlsl/sampling/warp.hlsl>
#include <nbl/builtin/hlsl/sampling/hierarchical_image/accessors.hlsl>
#include <nbl/builtin/hlsl/cpp_compat/intrinsics.hlsl>

namespace nbl
{
namespace hlsl
{
namespace sampling
{

// TODO(kevinyu): Temporary struct before PR #1001 merged to master
template<typename V, typename P>
struct value_and_rcpPdf
{
using this_t = value_and_rcpPdf<V, P>;

static this_t create(const V _value, const P _rcpPdf)
{
this_t retval;
retval._value = _value;
retval._rcpPdf = _rcpPdf;
return retval;
}

V value() { return _value; }
P rcpPdf() { return _rcpPdf; }

V _value;
P _rcpPdf;
};

template<typename V, typename P>
struct value_and_pdf
{
using this_t = value_and_pdf<V, P>;

static this_t create(const V _value, const P _pdf)
{
this_t retval;
retval._value = _value;
retval._pdf = _pdf;
return retval;
}

V value() { return _value; }
P pdf() { return _pdf; }

V _value;
P _pdf;
};
Comment on lines +21 to +60
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

actually PR #1027 will have the replacements for these


// TODO: Add an option for corner sampling or centered sampling as boolean parameter
template <typename ScalarT, typename LuminanceAccessorT
Comment on lines +62 to +63
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

adjust the comment, its the LuminanceAccessor that should tell you if you're supposed to corner sample (because the luma image needs to take that into account at the borders so it had to be made "that way")

NBL_PRIMARY_REQUIRES(
is_scalar_v<ScalarT> &&
concepts::accessors::MipmappedLoadableImage<LuminanceAccessorT, ScalarT, 2, 1>
)
struct HierarchicalWarpGenerator
Copy link
Copy Markdown
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

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

since thanks to PR #1001 warps will be indistinguishable from BackwardTractableSampler, this can be called HierarchicalSampler or HierarchicalLuminanceSampler and meet BackwardTractableSampler constraints

{
using scalar_type = ScalarT;
using vector2_type = vector<scalar_type, 2>;
using vector4_type = vector<scalar_type, 4>;
using domain_type = vector2_type;
using codomain_type = vector2_type;
using sample_type = value_and_pdf<codomain_type, scalar_type>;
using density_type = scalar_type;

LuminanceAccessorT _map;
uint16_t2 _mapSize;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you shold really store _lastTexel (_mapSize-1) instead of mapSize

uint16_t _layerIndex;
uint16_t _lastMipLevel : 15;
uint16_t _aspect2x1 : 1;

static HierarchicalWarpGenerator<ScalarT, LuminanceAccessorT> create(NBL_CONST_REF_ARG(LuminanceAccessorT) lumaMap, uint16_t2 mapSize, uint16_t layerIndex)
{
HierarchicalWarpGenerator<ScalarT, LuminanceAccessorT> result;
result._map = lumaMap;
result._mapSize = mapSize;
result._layerIndex = layerIndex;
// Note: We use mapSize.y here because the currently the map aspect ratio can only be 1x1 or 2x1
result._lastMipLevel = _static_cast<uint16_t>(findMSB(_static_cast<uint32_t>(mapSize.y)));
result._aspect2x1 = mapSize.x != mapSize.y;
return result;
}

static bool __choseSecond(scalar_type first, scalar_type second, NBL_REF_ARG(scalar_type) xi, NBL_REF_ARG(scalar_type) rcpPmf) NBL_CONST_MEMBER_FUNC
{
// numerical resilience against IEEE754
scalar_type rcpChoiceProb = scalar_type(0);
PartitionRandVariable<scalar_type> partition;
partition.leftProb = scalar_type(1) / (scalar_type(1) + (second / first));
bool choseSecond = partition(xi, rcpChoiceProb);
rcpPmf *= rcpChoiceProb;
return choseSecond;
}

// Cannot use textureGather since we need to pass the mipLevel
vector4_type __texelGather(uint16_t2 coord, uint16_t level) NBL_CONST_MEMBER_FUNC
{
assert(coord.x < _mapSize.x - 1 && coord.y < _mapSize.y - 1);
vector<scalar_type, 1> p0, p1, p2, p3;
_map.get(p0, coord + uint16_t2(0, 1), _layerIndex, level);
_map.get(p1, coord + uint16_t2(1, 1), _layerIndex, level);
_map.get(p2, coord + uint16_t2(1, 0), _layerIndex, level);
_map.get(p3, coord + uint16_t2(0, 0), _layerIndex, level);
return vector4_type(p0, p1, p2, p3);
}

sample_type generate(vector2_type xi) NBL_CONST_MEMBER_FUNC
{
uint16_t2 p = uint16_t2(0, 0);

scalar_type rcpPmf = 1;
if (_aspect2x1) {
vector<scalar_type, 1> p0, p1;
// do one split in the X axis first cause penultimate full mip would have been 2x1
_map.get(p0, uint16_t2(0, 0), _layerIndex, _lastMipLevel);
_map.get(p1, uint16_t2(1, 0), _layerIndex, _lastMipLevel);
p.x = __choseSecond(p0.x, p1, xi.x, rcpPmf) ? 1 : 0;
}

for (int i = _lastMipLevel - 1; i >= 0; i--)
{
p <<= 1;
const vector4_type values = __texelGather(p, i);
scalar_type wx_0, wx_1;
{
const scalar_type wy_0 = values[3] + values[2];
const scalar_type wy_1 = values[1] + values[0];
if (__choseSecond(wy_0, wy_1, xi.y, rcpPmf))
{
p.y |= 1;
wx_0 = values[0];
wx_1 = values[1];
}
else
{
wx_0 = values[3];
wx_1 = values[2];
}
}
if (__choseSecond(wx_0, wx_1, xi.x, rcpPmf))
p.x |= 1;
}


// If we don`t add xi, the sample will clump to the lowest corner of environment map texel. Each time we call PartitionRandVariable(), the output xi is the new xi that determines how left and right(or top and bottom for y axis) to choose the child partition. It means that if for some input xi, the output xi = 0, then the input xi is the edge of choosing this partition and the previous partition, and vice versa, if output xi = 1, then the input xi is the edge of choosing this partition and the next partition. Hence, by adding xi to the lower corner of the texel, we create a gradual transition from one pixel to another. Without adding output xi, the calculation of jacobian using the difference of sample value would not work.
// Since we want to do corner sampling. We have to handle edge texels as corner cases. Remember, in corner sampling we map uv [0,1] to [center of first texel, center of last texel]. So when p is an edge texel, we have to remap xi. [0.5, 1] when p == 0, and [0.5, 1] when p == length - 1.
if (p.x == 0)
xi.x = xi.x * scalar_type(0.5) + scalar_type(0.5);
if (p.y == 0)
xi.y = xi.y * scalar_type(0.5) + scalar_type(0.5);
if (p.x == _mapSize.x - 1)
xi.x = xi.x * scalar_type(0.5);
if (p.y == _mapSize.y - 1)
xi.y = xi.y * scalar_type(0.5);

const vector2_type directionUV = (vector2_type(p.x, p.y) + xi) / _mapSize;
Comment on lines +159 to +168
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

there's a bit of a problem with what you're returning (corner sampled) because the PostWarp is resolution agnostic, it doesn't know about corner sampling and expects a [0,1]^2 Unit Square to map to the Sphere where the boundaries touch

So I'd divide by _mapSize - promote(1) and subtract 0.5 from the p+xi before that so you get a normalized coordinate within the corner sampling

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This also means that you return _lastTexel.x * _lastTexel.y /rcpPmf as the PDF instead because of how the UV domain gets shrunk

return sample_type::create(directionUV, (_mapSize.x * _mapSize.y) / rcpPmf);
}

density_type forwardPdf(domain_type xi) NBL_CONST_MEMBER_FUNC
{
return generate(xi).pdf();
}
Comment on lines +172 to +175
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

PR #1001 lets you pass a cache_Type from generate to forwardPdf

Copy link
Copy Markdown
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

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

and you'll be able to stuff rcpPmf in that cache, so here all you'd do would just be return (_mapSize.x*_mapSize.y)/cache.rcpPmf;

Edit: taking into account https://github.com/Devsh-Graphics-Programming/Nabla/pull/969/changes#r2991807597 it would be _lastTexel.x * _lastTexel.y / cache.rcpPmf


// Doesn't comply with sampler concept. This class is extracted so can be used on warpmap generation without passing in unnecessary information like avgLuma. So, need to pass in avgLuma when calculating backwardPdf.
density_type backwardPdf(codomain_type codomainVal, scalar_type rcpAvgLuma) NBL_CONST_MEMBER_FUNC
{
return _map.load(codomainVal) * rcpAvgLuma;
Comment on lines +178 to +180
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

but you already have a _rcpAvgLuma member ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also you need to use the texelFetch signature, same as your gather, to make sure you sample level 0 and with no bilinear interpolation, so this means turning your floating point cornerSampled normalized UV into integer texel coordinate

const uint16_t2 coord = codomainVal*vector2_type(_lastTexel) + promote<vector2_type>(0.5);

}

};

template <typename ScalarT, typename LuminanceAccessorT, typename PostWarpT
NBL_PRIMARY_REQUIRES(
is_scalar_v<ScalarT> &&
concepts::accessors::MipmappedLoadableImage<LuminanceAccessorT, ScalarT, 2, 1> &&
concepts::Warp<PostWarpT>
)
Copy link
Copy Markdown
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

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

when #1001 gets merged, this sampler will be a BackwardTractableSampler because PDF is known cheaply in both directions

and your PostWarpT will be required to meet BackwardTractableSampler as well to have a backwardPdf actually it will need to be BijectiveSampler because you need to call generateInverse on it to get a backwardPdf https://github.com/Devsh-Graphics-Programming/Nabla/pull/969/changes#r2991518272

struct HierarchicalWarpSampler
Copy link
Copy Markdown
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

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

just call it ComposedHierarchicalSampler, no warp in the name

{
using warp_generator_type = HierarchicalWarpGenerator<ScalarT, LuminanceAccessorT>;
using warp_sample_type = typename warp_generator_type::sample_type;
Copy link
Copy Markdown
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

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

could just assert that your PostWarpT::domain_type is same as the HierarhicalSampler::codomain_type (current warp_generator_type::codomain_type)

using scalar_type = ScalarT;
using density_type = scalar_type;
using vector2_type = vector<scalar_type, 2>;
using vector3_type = vector<scalar_type, 3>;
using vector4_type = vector<scalar_type, 4>;
using domain_type = vector2_type;
using codomain_type = vector3_type;
Comment on lines +200 to +201
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

domain_type = warp_generator_type::domain_type and codomain_type = PostWarp::codomain_type you can static_assert or require that they have vector_traits<>::dimension 2 and 3 respectively

using sample_type = value_and_pdf<codomain_type, density_type>;

warp_generator_type _warpGenerator;
scalar_type _rcpAvgLuma;

static HierarchicalWarpSampler<ScalarT, LuminanceAccessorT, PostWarpT> create(NBL_CONST_REF_ARG(LuminanceAccessorT) lumaMap, scalar_type avgLuma, uint16_t2 mapSize, uint16_t layerIndex)
{
HierarchicalWarpSampler result;
result._warpGenerator = warp_generator_type::create(lumaMap, mapSize, layerIndex);
result._rcpAvgLuma = scalar_type(1.0) / avgLuma;
return result;
}

sample_type generate(domain_type xi) NBL_CONST_MEMBER_FUNC
{
const warp_sample_type warpSample = _warpGenerator.generate(xi);
const WarpResult<codomain_type> postWarpResult = PostWarpT::warp(warpSample.value());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you need to store a postWarp member copied/initialized in create, because while the samplers will have constant methods, they will be allowed to be stateful

return sample_type::create(postWarpResult.dst, postWarpResult.density * warpSample.pdf());
}

density_type forwardPdf(domain_type xi) NBL_CONST_MEMBER_FUNC
Comment on lines +215 to +222
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

when #1001 comes, you'll need to put into this struct's cache:

  1. the _warpGenerator.generate's cache members
  2. the hierarchical output sample (since its hidden and intermediate value needed to call postWarp.forwardPdf)
  3. postWarp's cache

{
const warp_sample_type warpSample = _warpGenerator.generate(xi);
return PostWarpT::forwardDensity(warpSample.value()) * warpSample.pdf();
}

density_type backwardPdf(codomain_type codomainVal) NBL_CONST_MEMBER_FUNC
{
return PostWarpT::backwardPdf(codomainVal, _rcpAvgLuma) * _warpGenerator.backwardPdf(codomainVal);
}
Comment on lines +228 to +231
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

does your test even compile, its the _warpGenerator (hierarhical sampler) that currently cares about _rcpAvgLuma not the post-warp

anyway neither should care.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Anyway the function is wrong, because you'd need to call uv = _postWarp.generateInverse(codomainVal) to get the uv to feed into _warpGenerator.backwardPdf(uv);


};


template <typename ScalarT, typename LuminanceAccessorT, typename HierarchicalSamplerT, typename PostWarpT
NBL_PRIMARY_REQUIRES(is_scalar_v<ScalarT> &&
concepts::accessors::GenericReadAccessor<LuminanceAccessorT, ScalarT, float32_t2> &&
Copy link
Copy Markdown
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 26, 2026

Choose a reason for hiding this comment

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

use LuminanceRead accessor, and add a getAvgValue method to the concept

hierarchical_image::WarpAccessor<HierarchicalSamplerT, ScalarT> &&
concepts::Warp<PostWarpT>)
Comment on lines +236 to +240
Copy link
Copy Markdown
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

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

I think you can pretty much just template on LuminanceAccessorT for same reasons I outlined for the ComposedHierarchicalSampler you don't need PostWarpT or ScalarT

and I think there isn't much freedom in HierarchicalSamplerT , you can construct it from the other 3

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

wait, but your HierarchicalSamplerT isn't a HierarchicalWarpGenerator at alll, such a misleading name, its a WarpmapAccessorT

struct WarpmapSampler
{
using scalar_type = ScalarT;
using vector2_type = vector<ScalarT, 2>;
using vector3_type = vector<ScalarT, 3>;
using vector4_type = vector<ScalarT, 4>;
using domain_type = vector2_type;
using codomain_type = vector3_type;
Comment on lines +247 to +248
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same as with the ComposedHierarchicalSampler

using weight_type = scalar_type;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

after PR1001 you'll also need a density_type

using sample_type = value_and_pdf<codomain_type, weight_type>;

LuminanceAccessorT _lumaMap;
HierarchicalSamplerT _warpMap;
uint32_t _effectiveWarpArea;
scalar_type _rcpAvgLuma;

static WarpmapSampler create(NBL_CONST_REF_ARG(LuminanceAccessorT) lumaMap, NBL_CONST_REF_ARG(HierarchicalSamplerT) warpMap, uint16_t2 warpSize, scalar_type avgLuma)
{
WarpmapSampler<ScalarT, LuminanceAccessorT, HierarchicalSamplerT, PostWarpT> result;
result._lumaMap = lumaMap;
result._warpMap = warpMap;
result._effectiveWarpArea = (warpSize.x - 1) * (warpSize.y - 1);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

warpMap is using a custom concept anyway, add a resolution() getter to it

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also store _lastWarpTexel instead of _effectiveWarpArea

result._rcpAvgLuma = ScalarT(1.0) / avgLuma;
return result;
}

weight_type forwardWeight(domain_type xi) NBL_CONST_MEMBER_FUNC
{
return generate(xi).value();
}

weight_type backwardWeight(codomain_type direction) NBL_CONST_MEMBER_FUNC
{
vector2_type envmapUv = PostWarpT::inverseWarp(direction);
scalar_type luma;
_lumaMap.get(envmapUv, luma);
return luma * _rcpAvgLuma * PostWarpT::backwardDensity(direction);
}
Comment on lines +267 to +278
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

no the forward and backward weights need to be identical, so:

  1. forwardWeight must return lumaMap.get(generateCache.uv) * _rcpAvgLuma * generateCache.postWarpPdf
  2. backwardWeight must do what it does now*
  • except that right now it doesn't apply corner sampling to envmapUv which it must do


sample_type generate(vector2_type xi) NBL_CONST_MEMBER_FUNC
{
const vector2_type interpolant;
matrix<scalar_type, 4, 2> uvs;
_warpMap.gatherUv(xi, uvs, interpolant);
Comment on lines +282 to +284
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you know the warp size, make it your job to compute the interpolant and apply corner sampling, so you give the gatherUv(coord,uvs) method a coord thats in [0.5,WarpMapSize-0.5]^2

Lesss code for user to write and get wrong, see https://github.com/Devsh-Graphics-Programming/Nabla-Examples-and-Tests/pull/257/changes#r2991829432


const vector2_type xDiffs[] = {
uvs[2] - uvs[3],
uvs[1] - uvs[0]
};
const vector2_type yVals[] = {
xDiffs[0] * interpolant.x + uvs[3],
xDiffs[1] * interpolant.x + uvs[0]
};
const vector2_type yDiff = yVals[1] - yVals[0];
vector2_type uv = yDiff * interpolant.y + yVals[0];

const WarpResult<vector3_type> warpResult = PostWarpT::warp(uv);

const scalar_type detInterpolJacobian = determinant(matrix<scalar_type, 2, 2>(
lerp(xDiffs[0], xDiffs[1], interpolant.y), // first column dFdx
yDiff // second column dFdy
)) * _effectiveWarpArea;

const scalar_type pdf = abs(warpResult.density / detInterpolJacobian);
Comment on lines +299 to +304
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

postWarp density, xDiffs, yDiff and interpolant.y go in the cache between generate and forwardPdf

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also the uv put into the post warp (for the forward weight function)


return sample_type::create(warpResult.dst, pdf);
}
};

}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#ifndef _NBL_BUILTIN_HLSL_HIERARCHICAL_IMAGE_ACCESSORS_INCLUDED_
#define _NBL_BUILTIN_HLSL_CONCEPTS_ACCESSORS_HIERARCHICAL_IMAGE_INCLUDED_

#include "nbl/builtin/hlsl/concepts/accessors/generic_shared_data.hlsl"

namespace nbl
{
namespace hlsl
{
namespace sampling
{
namespace hierarchical_image
{

// gatherUvs return 4 UVs in a square for manual bilinear interpolation with differentiability
// declare concept
#define NBL_CONCEPT_NAME WarpAccessor
#define NBL_CONCEPT_TPLT_PRM_KINDS (typename)(typename)
#define NBL_CONCEPT_TPLT_PRM_NAMES (WarpAccessorT)(ScalarT)
// not the greatest syntax but works
#define NBL_CONCEPT_PARAM_0 (accessor,WarpAccessorT)
#define NBL_CONCEPT_PARAM_1 (coord,vector<uint32_t, 2>)
#define NBL_CONCEPT_PARAM_2 (val, matrix<ScalarT, 4, 2>)
#define NBL_CONCEPT_PARAM_3 (interpolant, vector<ScalarT, 2>)
// start concept
NBL_CONCEPT_BEGIN(4)
// need to be defined AFTER the concept begins
#define accessor NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_0
#define coord NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_1
#define val NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_2
#define interpolant NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_3
NBL_CONCEPT_END(
((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((accessor.gatherUv(coord, val, interpolant)), ::nbl::hlsl::is_same_v, void))
);
#undef accessor
#undef coord
#undef val
#undef interpolant
#include <nbl/builtin/hlsl/concepts/__end.hlsl>

}
}
}
}

#endif
Loading
Loading