sample method
Samples at u, v, choosing a mip level from how fast the coordinate is
moving.
du and dv are the change in the coordinate per screen pixel — the
derivatives a hardware rasteriser computes from a quad of neighbouring
fragments and this one derives per triangle. Zero means "no idea", which
selects the base level and is what every call that predates mip chains
passes.
The shader supplies them, because only the shader knows which varyings are a texture coordinate. This rasteriser interpolates a list of floats; nothing in it can tell a UV from a world position. That is a real difference from the hardware backends, where the derivative is a property of the fragment rather than of the call, and it is why this is a parameter rather than something read off the context.
Implementation
Vector4 sample(
double u,
double v, {
double du = 0.0,
double dv = 0.0,
double dudx = 0.0,
double dvdx = 0.0,
double dudy = 0.0,
double dvdy = 0.0,
}) {
if (_clampsLod) return _sampleClamped(u, v, du, dv);
final chain = texture.levels;
if (chain == null || chain.isEmpty || (du == 0.0 && dv == 0.0)) {
return _sampleLevel(texture, u, v);
}
// The footprint in texels: how much of the texture one pixel covers. A
// level is chosen so that footprint is about one texel, which is the whole
// of what a mip chain is for.
final along = du * width;
final across = dv * height;
final footprint = math.max(along, across);
if (footprint <= 1.0) return _sampleLevel(texture, u, v);
// **Anisotropy — `gfx-02n`.** A trilinear sampler has one footprint and
// must pick a level for it, so at a grazing angle it serves the long axis
// and blurs the short one: a floor receding to the horizon loses its
// checks long before perspective would. The fix the hardware has always
// had is to take several taps *along* the long axis and choose the level
// from the short one.
//
// **A sampler that did not ask is untouched**, which is what makes this
// safe to add to a backend 96 golden scenes are recorded on: with
// `anisotropy` at one — the default everywhere in this engine — the
// arithmetic below is not reached and the bytes are the ones that were
// recorded. `anisotropic-floor` is the one scene that asks.
final int wanted = sampler.anisotropy;
if (wanted > 1 && (dudx != 0.0 || dvdy != 0.0 || dudy != 0.0)) {
// **The two screen derivatives as vectors, not their axis maxima.** The
// footprint is the parallelogram they span; what a mip level cannot
// serve is its *ratio*, and `du`/`dv` have already thrown that away by
// taking a maximum per axis. The first version of this read them and
// measured a ratio of about one on a floor receding to the horizon —
// zero differing pixels, and the fixture said so.
final double lx = math.sqrt(
dudx * width * dudx * width + dvdx * height * dvdx * height,
);
final double ly = math.sqrt(
dudy * width * dudy * width + dvdy * height * dvdy * height,
);
final double major = math.max(lx, ly);
final double minor = math.min(lx, ly);
if (minor > 0.0 && major > 1.0) {
final int taps = math.min(wanted, math.max(1, (major / minor).round()));
if (taps > 1) {
return _anisotropic(
u,
v,
lx >= ly ? dudx : dudy,
lx >= ly ? dvdx : dvdy,
minor,
taps,
);
}
}
}
final lod = portableLog2(footprint);
final top = chain.length;
if (lod >= top) return _sampleLevel(chain[top - 1], u, v);
final lower = lod.floor();
final near = lower == 0 ? texture : chain[lower - 1];
if (sampler.mipFilter == MipFilter.nearest) return _sampleLevel(near, u, v);
final far = chain[lower];
final t = lod - lower;
final a = _sampleLevel(near, u, v);
final b = _sampleLevel(far, u, v);
return Vector4(
a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t,
a.z + (b.z - a.z) * t,
a.w + (b.w - a.w) * t,
);
}