Optional ReadonlyanisotropyHow many anisotropic samples the backend may take when this texture is
minified at a steep angle (§77's "anisotropy"; R-30b, 2026-08-21). An
integer ≥ 1; defaults to 1, which is isotropic filtering and GL's own
default.
// A ground plane seen edge-on: mip levels alone blur it to mush.
new Texture({ ...ground, mipmaps: true, anisotropy: 8 });
Anisotropic filtering is an extension in WebGL 2 —
EXT_texture_filter_anisotropic — so a conformant device may not have it,
and every device that does has its own maximum. The policy here is §62's,
not §85's: the backend clamps the request to the device's maximum, and
where the extension is absent it uploads the texture with no anisotropy at
all rather than failing. Presence is the capability, the stance this
package takes for every optional renderer feature.
The asymmetry with TextureSource.filter, which is refused when
unknown, is deliberate and worth stating: "nearset" is a mistake in the
scene, and no device will ever honour it; anisotropy: 16 is a correct
request that some devices cannot fill, and a quality knob that turns a scene
into an error on half the fleet is worse than one that quietly costs less.
What §85 does refuse here is a value that is not an integer ≥ 1 — no
device could honour that either.
Anisotropy above 1 is only worth asking for together with mipmaps: true
(it selects among mip levels per axis); the combination is not required and
not refused, because a driver is free to do something useful with it and
the texture is complete either way.
Optional ReadonlycolorThe colour space these texels are in — §60a's first bullet: "color textures default to sRGB-encoded and are decoded to linear on sample; data maps (normal, roughness, occlusion) default to linear" (R-15, 2026-08-08).
// An albedo map authored in an image editor: sRGB-encoded bytes.
new Texture({ width, height, data, colorSpace: "srgb" });
"srgb" makes the backend allocate an sRGB internal format, so the GPU
decodes each sample to linear-light before the shader sees it and §60a's
"lighting and blending run in linear space" holds for textured surfaces
too. "linear" uploads the bytes as the numbers they are — the right tag
for a data map, a mask, or texels a program computed.
§60a defaults colour textures to sRGB. This field defaults to
"linear" when TextureSource.role is omitted, and the reason is
the one this repository applies to every default: flipping the engine-wide
default would silently darken every texture already authored against this
engine and move every pixel golden, for scenes that never asked for colour
management. Opt-in keeps §60a's behaviour available and every existing
frame byte-identical. Map roles (R-30c, 2026-09-09) made the faithful
default cheap per texture — role: "color" with no colorSpace resolves
to "srgb" — without flipping the engine-wide default. An owner call can
still change the omitted-role default later; this packet does not.
§77a's CanvasTexture deliberately answers the same question differently
— its default is "srgb" — because a class born after R-15 has no
authored content and no goldens to protect, and a host 2D canvas produces
sRGB-encoded bytes unambiguously (RFC 0004 Q3, adopted; the reason is
recorded at both defaults).
Optional ReadonlyfilterHow this texture is sampled between texel centres (§77; R-30,
2026-08-13). Defaults to "linear".
// A 6 × 12 bitmap font: every glyph texel stays a square.
const atlas = new Texture({ ...buildGlyphAtlas(), filter: "nearest" });
The default is "linear" because that is what this tier sampled with
before the field existed: a texture that names no filter issues the
identical texParameteri pair it always did, so no already-authored scene
moves a pixel (the rule R-15's colorSpace default follows, for the same
reason).
Optional ReadonlymaximumOptional ReadonlyminHow this texture is sampled when minified (§77; R-30b, 2026-08-21) — the mip-level-aware half of TextureSource.filter, which R-30 staged and this packet lands.
// Crisp within a level, snapped between levels — pixel art at a distance.
new Texture({ ...art, filter: "nearest", mipmaps: true,
minFilter: "nearest-mipmap-nearest" });
Defaults to the derived value, never to a constant:
texParameteri calls;mipmaps: true, to that filter's chain-aware form — "linear" →
"linear-mipmap-linear" (trilinear), "nearest" →
"nearest-mipmap-nearest". Building a chain and then not sampling it
would be the one combination nobody asks for on purpose.A mip-choosing value without mipmaps: true is refused (§85): GL calls
such a texture incomplete and samples it as opaque black, which is a
whole-surface visual failure with nothing anywhere saying why. Refusing is
this repository's rule for exactly that class of value.
Optional ReadonlymipmapsWhether the backend builds a full mip chain for this texture at upload
(§77's "mipmaps"; R-30b, 2026-08-21). Defaults to false — one level,
which is what every texture in the engine had before this field existed.
// A ground texture the camera flies away from.
new Texture({ width: 512, height: 512, data, mipmaps: true });
It buys minification: a 512² texture covering twenty screen pixels takes twenty samples out of a quarter-million texels without a chain, and which twenty depends on the sub-pixel position of the camera — which is the crawling shimmer that appears the moment anything moves. With a chain the hardware samples a level whose texels are about the size of a pixel, and the image holds still.
It costs a third more texture memory (Texture.byteLength counts the
chain) and one generateMipmap at upload — both paid once per upload, and
nothing at all on the draw path.
WebGL 1 could neither mipmap nor REPEAT a non-power-of-two texture. WebGL
2 is GLES 3.0 and lifts both restrictions for ordinary uncompressed
formats, so a 300 × 173 texture takes a chain here with no padding and no
resize. (Compressed containers keep restrictions of their own — they are
still deferred, see Texture.) A backend at §62's WebGL 1 tier is the
one that would have to refuse the combination, exactly as it would have to
refuse TextureSource.wrap's "repeat".
A backend whose context cannot generate mipmaps at all uploads the texture
with one level and an in-level min filter — degrading rather than leaving
a texture GL would treat as incomplete (see @fourjs/render-webgl).
Optional ReadonlyroleWhether these texels are a colour map or a data map (§60a, §77; R-30c, 2026-09-09). Optional: omitting it invents no default role.
// An albedo PNG: sRGB-encoded bytes, decoded on sample.
new Texture({ width, height, data, role: "color" });
// A roughness map: bytes are the numbers they are.
new Texture({ width, height, data, role: "data" });
role |
colorSpace authored |
resolved colorSpace |
|---|---|---|
| omitted | omitted | "linear" (R-15, goldens unchanged) |
| omitted | "srgb" / "linear" |
the authored tag |
"color" |
omitted | "srgb" |
"data" |
omitted | "linear" |
| either | "srgb" / "linear" |
the authored tag wins |
Backends still read Texture.colorSpace only. The role never changes a sampler type, and a source that never sets it is byte-identical to one built before this field existed.
ReadonlysurfaceOptional ReadonlywrapHow this texture is addressed outside [0, 1] (§77; R-30, 2026-08-13).
Defaults to "clamp-to-edge", this tier's previous fixed choice.
// A tiling ground texture, with uv running 0…8 across the plane.
new Texture({ width, height, data, wrap: "repeat" });
Note that "repeat" and "mirrored-repeat" are legal on a
non-power-of-two texture in WebGL 2 (they were not in WebGL 1), so a
backend at §62's WebGL 1 tier would have to refuse the combination rather
than silently sample black.
A supplied surface keeps DOM discovery, threading, and ownership in the host.