Skip to content

Commit d009951

Browse files
committed
Stochastic mipmap interpolation (AcademySoftwareFoundation#3127)
Suggested by Luke Emrose, the idea here is that we introduce two new MIP interpolation modes: StochasticTrilinear and StochasticAniso. These work similarly to Trilinear and Aniso, respectively, except that instead of sampling both of the bracketing MIP levels and blending between them, in stochastic mode we use a random variate to select ONE of the two MIP levels with probability proportional to the computed blending weight. If these mip modes are used, the caller needs to pass a well-stratified value in the new "rnd" field of TextureOpt. Also note that the method only really makes sense in the context of a renderer that is using many samples per pixel. As a single texture lookup, it looks horrible and noisy, as you can see by the testshade-based tests we do of this, which are 1spp and looks pretty rough. My benchmarks (in a hard stress test of the texture system using `testtex --threadtimes 8`) show that this approach has speedups of 35-40% when using between 1-4 threads, dropping to more like 25-30% speedup as threads are in the 12-32 range (that makes sense, it's doing the same amount of total I/O). In this first implementation, in order to preserve ABI compatibility if we decide to backport this feature to the release branch, TextureOpt::rnd is added as a union co-member with the unused "bias" field. For TextureOptBatch, we had to add it outright, but it's guarded by an `#if` and will not be backported to 2.3. (We may change our minds and not backport it at all, I just wanted to leave the option open with this initial patch. Also, there will probably be additional further ABI-breaking changes coming later that will definitely not be backported.) In this first phase, we only use the stochastic component to turn the 2-MIP-level lookup into one level lookup. It has been further suggested that we could apply stochastic techniques to anisotropic filtering (instead of many samples spaced across the major axis of the filter footprint, take one sample distributed across the axis), and even for the ordinary bilinear or bicubic within each level (instead of 4 or 16 weighted texels, just retrieve one texel chosen with the weights as probabilities). But these deserve to be benchmarked and have their quality evaluated separtely and deliberately, and so will be relegated to a later investigation and implementation.
1 parent 858bd5c commit d009951

File tree

12 files changed

+107
-11
lines changed

12 files changed

+107
-11
lines changed

src/cmake/testing.cmake

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ macro (oiio_add_all_tests)
161161
misnamed-file
162162
texture-crop texture-cropover
163163
texture-filtersize
164+
texture-filtersize-stochastic
164165
texture-overscan
165166
texture-wrapfill
166167
texture-res texture-maxres
@@ -191,6 +192,8 @@ macro (oiio_add_all_tests)
191192
texture-interp-closest
192193
texture-mip-nomip texture-mip-onelevel
193194
texture-mip-trilinear
195+
texture-mip-stochastictrilinear
196+
texture-mip-stochasticaniso
194197
texture-missing
195198
texture-pointsample
196199
texture-udim texture-udim2

src/include/OpenImageIO/texture.h

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
// features are supported.
2121
#define OIIO_TEXTURESYSTEM_SUPPORTS_CLOSE 1
2222

23+
#define OIIO_TEXTURESYSTEM_SUPPORTS_STOCHASTIC 1
2324

2425

2526
OIIO_NAMESPACE_BEGIN
@@ -93,7 +94,9 @@ enum class MipMode {
9394
NoMIP, ///< Just use highest-res image, no MIP mapping
9495
OneLevel, ///< Use just one mipmap level
9596
Trilinear, ///< Use two MIPmap levels (trilinear)
96-
Aniso ///< Use two MIPmap levels w/ anisotropic
97+
Aniso, ///< Use two MIPmap levels w/ anisotropic
98+
StochasticTrilinear, ///< Stochastic trilinear
99+
StochasticAniso, ///< Stochastic anisotropic
97100
};
98101

99102
/// Interp mode determines how we sample within a mipmap level
@@ -102,7 +105,7 @@ enum class InterpMode {
102105
Closest, ///< Force closest texel
103106
Bilinear, ///< Force bilinear lookup within a mip level
104107
Bicubic, ///< Force cubic lookup within a mip level
105-
SmartBicubic ///< Bicubic when maxifying, else bilinear
108+
SmartBicubic ///< Bicubic when magnifying, else bilinear
106109
};
107110

108111

@@ -190,7 +193,9 @@ class OIIO_API TextureOpt {
190193
MipModeNoMIP, ///< Just use highest-res image, no MIP mapping
191194
MipModeOneLevel, ///< Use just one mipmap level
192195
MipModeTrilinear, ///< Use two MIPmap levels (trilinear)
193-
MipModeAniso ///< Use two MIPmap levels w/ anisotropic
196+
MipModeAniso, ///< Use two MIPmap levels w/ anisotropic
197+
MipModeStochasticTrilinear, ///< Stochastic trilinear
198+
MipModeStochasticAniso, ///< Stochastic anisotropic
194199
};
195200

196201
/// Interp mode determines how we sample within a mipmap level
@@ -199,7 +204,7 @@ class OIIO_API TextureOpt {
199204
InterpClosest, ///< Force closest texel
200205
InterpBilinear, ///< Force bilinear lookup within a mip level
201206
InterpBicubic, ///< Force cubic lookup within a mip level
202-
InterpSmartBicubic ///< Bicubic when maxifying, else bilinear
207+
InterpSmartBicubic ///< Bicubic when magnifying, else bilinear
203208
};
204209

205210

@@ -212,10 +217,8 @@ class OIIO_API TextureOpt {
212217
anisotropic(32), conservative_filter(true),
213218
sblur(0.0f), tblur(0.0f), swidth(1.0f), twidth(1.0f),
214219
fill(0.0f), missingcolor(nullptr),
215-
// dresultds(nullptr), dresultdt(nullptr),
216-
time(0.0f), bias(0.0f), samples(1),
217-
rwrap(WrapDefault), rblur(0.0f), rwidth(1.0f), // dresultdr(nullptr),
218-
// actualchannels(0),
220+
time(0.0f), rnd(0.0f), samples(1),
221+
rwrap(WrapDefault), rblur(0.0f), rwidth(1.0f),
219222
envlayout(0)
220223
{ }
221224

@@ -237,7 +240,10 @@ class OIIO_API TextureOpt {
237240
float fill; ///< Fill value for missing channels
238241
const float* missingcolor; ///< Color for missing texture
239242
float time; ///< Time (for time-dependent texture lookups)
240-
float bias; ///< Bias for shadows
243+
union {
244+
float bias; ///< Bias for shadows (DEPRECATED?)
245+
float rnd; ///< Stratified sample value
246+
};
241247
int samples; ///< Number of samples for shadows
242248

243249
// For 3D volume texture lookups only:
@@ -291,6 +297,9 @@ class OIIO_API TextureOptBatch {
291297
alignas(Tex::BatchAlign) float twidth[Tex::BatchWidth];
292298
alignas(Tex::BatchAlign) float rwidth[Tex::BatchWidth];
293299
// Note: rblur,rwidth only used for volumetric lookups
300+
#if OIIO_VERSION_GREATER_EQUAL(2,4,0)
301+
alignas(Tex::BatchAlign) float rnd[Tex::BatchWidth];
302+
#endif
294303

295304
// Options that must be the same for all points we're texturing at once
296305
int firstchannel = 0; ///< First channel of the lookup
@@ -354,15 +363,17 @@ class OIIO_API TextureOptions {
354363
InterpClosest, ///< Force closest texel
355364
InterpBilinear, ///< Force bilinear lookup within a mip level
356365
InterpBicubic, ///< Force cubic lookup within a mip level
357-
InterpSmartBicubic ///< Bicubic when maxifying, else bilinear
366+
InterpSmartBicubic ///< Bicubic when magnifying, else bilinear
358367
};
359368

360369
/// Create a TextureOptions with all fields initialized to reasonable
361370
/// defaults.
371+
OIIO_DEPRECATED("no longer used since OIIO 1.8")
362372
TextureOptions();
363373

364374
/// Convert a TextureOpt for one point into a TextureOptions with
365375
/// uniform values.
376+
OIIO_DEPRECATED("no longer used since OIIO 1.8")
366377
TextureOptions(const TextureOpt& opt);
367378

368379
// Options that must be the same for all points we're texturing at once

src/libtexture/environment.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,8 @@ TextureSystemImpl::environment(TextureHandle* texture_handle_,
447447

448448
TextureOpt::MipMode mipmode = options.mipmode;
449449
bool aniso = (mipmode == TextureOpt::MipModeDefault
450-
|| mipmode == TextureOpt::MipModeAniso);
450+
|| mipmode == TextureOpt::MipModeAniso
451+
|| mipmode == TextureOpt::MipModeStochasticAniso);
451452

452453
float aspect, trueaspect, filtwidth;
453454
int nsamples;
@@ -612,6 +613,7 @@ TextureSystemImpl::environment(TextureHandle* texture_handle,
612613
opt.tblur = options.tblur[i];
613614
opt.swidth = options.swidth[i];
614615
opt.twidth = options.twidth[i];
616+
opt.rnd = options.rnd[i];
615617
Imath::V3f R_(R[i], R[i + Tex::BatchWidth],
616618
R[i + 2 * Tex::BatchWidth]);
617619
Imath::V3f dRdx_(dRdx[i], dRdx[i + Tex::BatchWidth],

src/libtexture/texture3d.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ TextureSystemImpl::texture3d(TextureHandle* texture_handle_,
112112
&TextureSystemImpl::texture3d_lookup_nomip,
113113
&TextureSystemImpl::texture3d_lookup_trilinear_mipmap,
114114
&TextureSystemImpl::texture3d_lookup_trilinear_mipmap,
115+
&TextureSystemImpl::texture3d_lookup,
116+
&TextureSystemImpl::texture3d_lookup_trilinear_mipmap,
115117
&TextureSystemImpl::texture3d_lookup
116118
};
117119
texture3d_lookup_prototype lookup = lookup_functions[(int)options.mipmode];

src/libtexture/texturesys.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,8 @@ TextureSystemImpl::texture(TextureHandle* texture_handle_,
10851085
&TextureSystemImpl::texture_lookup_nomip,
10861086
&TextureSystemImpl::texture_lookup_trilinear_mipmap,
10871087
&TextureSystemImpl::texture_lookup_trilinear_mipmap,
1088+
&TextureSystemImpl::texture_lookup,
1089+
&TextureSystemImpl::texture_lookup_trilinear_mipmap,
10881090
&TextureSystemImpl::texture_lookup
10891091
};
10901092
texture_lookup_prototype lookup = lookup_functions[(int)options.mipmode];
@@ -1273,6 +1275,7 @@ TextureSystemImpl::texture(TextureHandle* texture_handle,
12731275
opt.tblur = options.tblur[i];
12741276
opt.swidth = options.swidth[i];
12751277
opt.twidth = options.twidth[i];
1278+
opt.rnd = options.rnd[i];
12761279
// rblur, rwidth not needed for 2D texture
12771280
if (dresultds) {
12781281
ok &= texture(texture_handle, thread_info, opt, s[i], t[i],
@@ -1482,6 +1485,10 @@ compute_miplevels(TextureSystemImpl::TextureFile& texturefile,
14821485
float filtwidth_ras = minorlength
14831486
* std::min(subinfo.spec(m).width,
14841487
subinfo.spec(m).height);
1488+
// FIXME: We should store the min(width,height) of each level directly
1489+
// in an array in subinfo, so we're not rifling through the specs
1490+
// and taking mins in this loop every single time.
1491+
14851492
// Once the filter width is smaller than one texel at this level,
14861493
// we've gone too far, so we know that we want to interpolate the
14871494
// previous level and the current level. Note that filtwidth_ras
@@ -1521,6 +1528,18 @@ compute_miplevels(TextureSystemImpl::TextureFile& texturefile,
15211528
miplevel[0] = miplevel[1];
15221529
levelblend = 0;
15231530
}
1531+
if (options.mipmode == TextureOpt::MipModeStochasticTrilinear
1532+
|| options.mipmode == TextureOpt::MipModeStochasticAniso) {
1533+
// If using stochastic sampling, the random deviate is a threshold
1534+
// versus the levelblend to determine which ONE of the two MIP
1535+
// levels to use.
1536+
if (options.rnd > levelblend) {
1537+
miplevel[1] = miplevel[0];
1538+
} else {
1539+
miplevel[0] = miplevel[1];
1540+
}
1541+
levelblend = 0;
1542+
}
15241543
levelweight[0] = 1.0f - levelblend;
15251544
levelweight[1] = levelblend;
15261545
}

src/testtex/testtex.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ static std::string dataformatname = "half";
4242
static float sscale = 1, tscale = 1;
4343
static float sblur = 0, tblur = -1;
4444
static float width = 1;
45+
static float widthramp = 0;
4546
static float anisoaspect = 1.0; // anisotropic aspect ratio
4647
static std::string wrapmodes("periodic");
4748
static int anisomax = TextureOpt().anisotropic;
@@ -138,6 +139,8 @@ getargs(int argc, const char* argv[])
138139
.help("Add blur (s, t) to texture lookup");
139140
ap.arg("--width %f:WIDTH", &width)
140141
.help("Multiply filter width of texture lookup");
142+
ap.arg("--widthramp %f:WIDTH", &widthramp)
143+
.help("If set, ramp to this width on the right side");
141144
ap.arg("--fill %f:FILLVAL", &fill)
142145
.help("Set fill value for missing channels");
143146
ap.arg("--wrap %s:MODE", &wrapmodes)
@@ -578,6 +581,20 @@ plain_tex_region(ImageBuf& image, ustring filename, Mapping2D mapping,
578581
float s, t, dsdx, dtdx, dsdy, dtdy;
579582
mapping(p.x(), p.y(), s, t, dsdx, dtdx, dsdy, dtdy);
580583

584+
if (widthramp != 0.0f) {
585+
// If widthramp is set, we want to blend between the set width
586+
// and the ramp width from left to right.
587+
opt.swidth = OIIO::lerp(width, widthramp, s);
588+
opt.twidth = opt.swidth;
589+
}
590+
if (mipmode == TextureOpt::MipModeStochasticTrilinear
591+
|| mipmode == TextureOpt::MipModeStochasticAniso) {
592+
// Hash the pixel coords to get a pseudo-random variant
593+
constexpr float inv = 1.0f
594+
/ float(std::numeric_limits<uint32_t>::max());
595+
opt.rnd = bjhash::bjfinal(p.x(), p.y()) * inv;
596+
}
597+
581598
// Call the texture system to do the filtering.
582599
bool ok;
583600
if (use_handle)
@@ -694,6 +711,26 @@ plain_tex_region_batch(ImageBuf& image, ustring filename, Mapping2DWide mapping,
694711
for (int x = roi.xbegin; x < roi.xend; x += BatchWidth) {
695712
FloatWide s, t, dsdx, dtdx, dsdy, dtdy;
696713
mapping(IntWide::Iota(x), y, s, t, dsdx, dtdx, dsdy, dtdy);
714+
715+
if (widthramp != 0.0f) {
716+
// If widthramp is set, we want to blend between the set width
717+
// and the ramp width from left to right.
718+
for (int i = 0; i < BatchWidth; ++i) {
719+
opt.swidth[i] = OIIO::lerp(width, widthramp, s[i]);
720+
opt.twidth[i] = opt.swidth[i];
721+
}
722+
}
723+
if (mipmode == TextureOpt::MipModeStochasticTrilinear
724+
|| mipmode == TextureOpt::MipModeStochasticAniso) {
725+
// Hash the pixel coords to get a pseudo-random variant
726+
#if OIIO_VERSION_GREATER_EQUAL(2, 4, 0)
727+
constexpr float inv
728+
= 1.0f / float(std::numeric_limits<uint32_t>::max());
729+
for (int i = 0; i < BatchWidth; ++i)
730+
opt.rnd[i] = bjhash::bjfinal(x + i, y) * inv;
731+
#endif
732+
}
733+
697734
int npoints = std::min(BatchWidth, roi.xend - x);
698735
RunMask mask = RunMaskOn >> (BatchWidth - npoints);
699736
// Call the texture system to do the filtering.
Binary file not shown.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env python
2+
3+
# This test just views the "miplevels" texture straight on, but it uses
4+
# -widthramp to smoothly blend between mipmap levels from left to right
5+
# (wanting the 256^2 level at the left and the 64^2 level at the right).
6+
# Using MIPmode StochasticAniso and pseudo-random variate, this tests that
7+
# we are blending between the levels correctly.
8+
9+
10+
command = testtex_command (OIIO_TESTSUITE_IMAGEDIR + "/miplevels.tx",
11+
" -nowarp -res 256 256 -mipmode 6 -widthramp 4 -d uint8 -o out.tif")
12+
outputs = [ "out.tif" ]
Binary file not shown.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env python
2+
3+
command = testtex_command ("../common/textures/grid.tx",
4+
extraargs = "-mipmode 6 -d uint8 -o out.tif")
5+
outputs = [ "out.tif" ]
Binary file not shown.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env python
2+
3+
command = testtex_command ("../common/textures/grid.tx",
4+
extraargs = "-mipmode 5 -d uint8 -o out.tif")
5+
outputs = [ "out.tif" ]

0 commit comments

Comments
 (0)