The first image texture in the tree, and it differs from every existing one
in ways that are easy to get wrong silently:
the colormap LUTs (rendering/colormap-textures.ts) are 256x1 8-bit and
always LinearFilter/ClampToEdge — no wrap, no mipmaps, no colour space;
the element store (rendering/element-storage.ts) is float but always
NearestFilter, because it carries data rather than an image.
So this is the first texture that is simultaneously float-capable, linearly
filtered, mipmapped and colour-managed, and each of those four is a place where
the wrong choice renders plausibly rather than failing.
Three decisions worth reading before changing anything here
1. Three-channel data is expanded to RGBA. Not an oversight and not
laziness: WebGPU has no 3-channel texture formats at all, so RGBFormat would
work under WebGL2 and break the moment the same scene loads on the WebGPU
backend. rgbToRgba in colormap-textures.ts expands for the same reason.
Single-channel is kept as RedFormat, which both backends do support.
2. HDR uploads as HalfFloatType unless the device says otherwise. Core
WebGL2 lets a FloatType texture exist and be sampled with NEAREST, but LINEAR
filtering on one requires OES_texture_float_linear (float32-filterable on
WebGPU). When it is absent the sampler silently drops to nearest, so the
texture looks blocky with nothing to attribute it to. Half-float is linearly
filterable in core WebGL2 and carries ~11 bits of mantissa, which is ample for
an image; float32 is used only where the capability probe confirms it.
3. v = 0 is the FIRST row of the image, on all three arms.texture.flipY
is set to false explicitly rather than left at its default, because the
defaults disagree AND two of them are inert or misleading:
DataTexture defaults flipY = false;
a Texture over an ImageBitmap defaults flipY = true, and WebGL
silently ignores it — UNPACK_FLIP_Y_WEBGL has no effect on an
ImageBitmap upload, so the flag reads true while nothing is flipped.
a CompressedTexture defaults flipY = false, and WebGL likewise cannot
apply UNPACK_FLIP_Y_WEBGL to its already-compressed mip payloads.
Left alone, that made texture_encoding change the MEANING of a UV: the same
coordinates over the same pixels rendered upside down as raw vs as JPEG. Since
the raw path takes an (h, w, c) array, the convention that matches it —
v = 0 is arr[0] — is the one an author can predict, and it is also what
sample_equirect uses (row 0 is +90 latitude). So flipY is pinned off and
the authored v runs top-down.
Found by eye, not by the numeric check: a UV-vs-position residual test passed
because it MODELLED the shader's sampling from texture.flipY, and the flag it
trusted was the thing that was wrong.
4. sRGB is decoded HERE for float textures, and by the sampler for 8-bit
ones. THREE's SRGBColorSpace maps to the hardware SRGB8_ALPHA8 sampler,
which is exact and free — but only exists for 8-bit textures. Rather than
depend on what THREE does with colorSpace on a float texture (which has
varied across versions, and whose failure mode is a washed-out or over-dark
surface that still looks plausible), the float path linearizes explicitly with
the piecewise sRGB EOTF and declares itself linear. That makes the conversion
a known-value unit test instead of a visual judgement — which is the whole
reason to prefer it, since "looks about right" is exactly the check this class
of bug passes.
Upload a decoded mesh texture to the GPU.
The first image texture in the tree, and it differs from every existing one in ways that are easy to get wrong silently:
rendering/colormap-textures.ts) are 256x1 8-bit and alwaysLinearFilter/ClampToEdge— no wrap, no mipmaps, no colour space;rendering/element-storage.ts) is float but alwaysNearestFilter, because it carries data rather than an image.So this is the first texture that is simultaneously float-capable, linearly filtered, mipmapped and colour-managed, and each of those four is a place where the wrong choice renders plausibly rather than failing.
Three decisions worth reading before changing anything here
1. Three-channel data is expanded to RGBA. Not an oversight and not laziness: WebGPU has no 3-channel texture formats at all, so
RGBFormatwould work under WebGL2 and break the moment the same scene loads on the WebGPU backend.rgbToRgbaincolormap-textures.tsexpands for the same reason. Single-channel is kept asRedFormat, which both backends do support.2. HDR uploads as
HalfFloatTypeunless the device says otherwise. Core WebGL2 lets aFloatTypetexture exist and be sampled with NEAREST, but LINEAR filtering on one requiresOES_texture_float_linear(float32-filterableon WebGPU). When it is absent the sampler silently drops to nearest, so the texture looks blocky with nothing to attribute it to. Half-float is linearly filterable in core WebGL2 and carries ~11 bits of mantissa, which is ample for an image; float32 is used only where the capability probe confirms it.3.
v = 0is the FIRST row of the image, on all three arms.texture.flipYis set tofalseexplicitly rather than left at its default, because the defaults disagree AND two of them are inert or misleading:DataTexturedefaultsflipY = false;Textureover anImageBitmapdefaultsflipY = true, and WebGL silently ignores it —UNPACK_FLIP_Y_WEBGLhas no effect on anImageBitmapupload, so the flag readstruewhile nothing is flipped.CompressedTexturedefaultsflipY = false, and WebGL likewise cannot applyUNPACK_FLIP_Y_WEBGLto its already-compressed mip payloads.Left alone, that made
texture_encodingchange the MEANING of a UV: the same coordinates over the same pixels rendered upside down as raw vs as JPEG. Since the raw path takes an(h, w, c)array, the convention that matches it —v = 0isarr[0]— is the one an author can predict, and it is also whatsample_equirectuses (row 0 is +90 latitude). SoflipYis pinned off and the authoredvruns top-down.Found by eye, not by the numeric check: a UV-vs-position residual test passed because it MODELLED the shader's sampling from
texture.flipY, and the flag it trusted was the thing that was wrong.4. sRGB is decoded HERE for float textures, and by the sampler for 8-bit ones. THREE's
SRGBColorSpacemaps to the hardwareSRGB8_ALPHA8sampler, which is exact and free — but only exists for 8-bit textures. Rather than depend on what THREE does withcolorSpaceon a float texture (which has varied across versions, and whose failure mode is a washed-out or over-dark surface that still looks plausible), the float path linearizes explicitly with the piecewise sRGB EOTF and declares itself linear. That makes the conversion a known-value unit test instead of a visual judgement — which is the whole reason to prefer it, since "looks about right" is exactly the check this class of bug passes.