The Weave Comes Into Focus
Imported textures now tile, rotate, and offset exactly as their author set them, and a second UV channel means occlusion maps stop stealing the wrong coordinates. The fine print from three releases ago is paid off.


Three releases back we shipped the seven glTF material extensions and left one line unresolved: KHR_texture_transform and second UV sets were accepted by the loader and then quietly ignored. Nothing crashed. Nothing warned twice. A material that tiled its fabric texture seven times across a cushion just… didn’t. The whole texture stretched once across the whole surface, upholstery rendered as a faint smear of whatever colour the fabric averaged out to.
That line is closed now. Every texture reference on an imported material, base colour, normal, occlusion, clearcoat, sheen, anisotropy, all of them, carries its own offset, rotation, and scale, applied per fragment at the point of sampling. A material can also point a channel at a second UV set, which turns out to matter more than the extension’s modest name suggests: exporters routinely bake occlusion onto its own UV island so the main channel stays free to tile, and until now that occlusion map was silently reading the wrong coordinates.
SheenChair pays its fine
The anchor asset for this one is Khronos’s SheenChair: a mango-velvet lounge chair whose fabric author scaled the weave 7x and offset it, whose wood grain is scaled 3x and rotated eight degrees, and whose occlusion sits on a second UV set on every material. It was practically written as a test case for the feature we hadn’t built yet. It renders at its authored scale now, weave and grain and all.
The transform itself is a small affine: offset + rotate(rotation) * (scale * uv), composed once on import and looked up per fragment through a small dedicated table rather than recomputed every pixel. Getting the composition order right mattered more than it sounds, an early pass built the rotation matrix column-major and got a transpose for free, which is the kind of bug that looks fine on anything with only scale and offset and only shows up once something actually rotates. SheenChair’s wood grain, being the one channel in the whole conformance set that rotates, caught it immediately.
Testing a real photograph is harder than testing a checkerboard
The synthetic gates are the easy half: a checkerboard tiled 4x has four times the transitions of one left alone, a stripe pattern rotated ninety degrees swaps which scanline carries the variance, a split-colour texture offset by half flips which side of the quad reads which colour. Clean signals, cheap to reason about.
Turning the same questions on SheenChair’s actual fabric photograph took a few tries. A checkerboard’s diagonal symmetry cancels itself out to a flat grey on the one scanline that happens to sample it symmetrically, which we discovered the fun way. And a real velvet close-up carries its own high-frequency weave regardless of how many times you tile it, so counting brightness transitions can’t tell “seven repeats of a detailed photo” apart from “one repeat of a detailed photo.” The regression this exists to catch renders bit-identical, diff exactly zero, so a before/after comparison alone is meaningful, but it’s still Lux checking Lux. The gate that stuck goes one step further: it scans the fabric photo’s own pixels, finds the patch where the authored transform should shift brightness the most, and checks that the actual render moves the same way. The camera math and the transform formula are both written fresh for the test rather than borrowed from the renderer, so the check can’t quietly agree with a bug it shares.
Related: Lacquer, Velvet, and Brushed Steel, where this fine print first got written down.