Textures
gui.texture(label, source) accepts:
- a raw WebGL2 texture descriptor
{ gl, texture, width, height }, withkind,depthandlevelswhen it isn't a 2D texture of one mip. Addsampler: 'int' | 'uint'for integer formats,isDepth: truefor depth, andcolorSpace: 'srgb'for values encoded for the screen already (sRGB image data in a plain 8-bit format), shown as they are - a WebGPU texture with its device,
{ device, texture }. The rest comes from the texture: size, mips, format (integer and depth formats are read as such). Addkind: 'cube'for a cube map. It needsTEXTURE_BINDINGusage. Row 0 is taken to be the top, as WebGPU renders; passtopDown: falsefor data laid out GL's way, andcolorSpace: 'srgb'as for WebGL2 - a canvas,
ImageBitmap, image or video - CPU data:
{ data, width, height, channels }, wheredatais a typed array or an array with rows from the top. AUint8ClampedArray's bytes read as 0–1.ImageDataworks too, its values taken as encoded for the screen; for other data say so withcolorSpace: 'srgb'. It's shaded in JS (again when the view or the data changes), and its numbers and probes read the array directly - with the three.js extension: any
Texture, render target (each attachment of an MRT target, and its depth), depth, cube, array, 3D or integer texture. It works withWebGLRendererand withWebGPURenderer(on WebGPU, or on its WebGL2 fallback)
GPU texture views need a WebGL2 context or a WebGPU device: a WebGL1 texture isn't shown.
Clicking a thumbnail opens an inspector:
- wheel to zoom about the cursor, drag (or the middle button, or Shift + the wheel) to pan, pixel grid when zoomed in; double-click to fit, again for 1:1, or fit over its corner (as every graph: gestures)
- opens at the texture's aspect (texels stay square); move by the title bar, resize from any edge or corner
- hover for exact float texel values; click (or tap) to pin up to four texels, with live values you can copy as JSON, a GLSL
vec4(…)or the whole row - NaN/Inf counted at full resolution, in the title bar and on the thumbnail. Find jumps to the first one and pins it
- mips: a mipmapped texture has a slider through its levels (a tick at each, the level's size beside it) and all, every level side by side;
[and]in the view step through them too, and the size line says which is shown - face/layer, exposure
- layouts:
- a cube as one face, a cross (4×3) or an equirect unwrap; the probe names the face, texel and direction
- a 3D texture sliced along x, y or z
- formats:
- integer textures probe exactly (32-bit values, through an integer read), with an ids colouring that gives each value its own colour
- depth can be shown as linear distance: near and far come from the camera that rendered it (three.js), or you set them
- an MRT render target has an attachment switcher
- black/white range, with auto fitted to the 0.1%–99.9% range of sampled values, so a firefly doesn't flatten the image (depth textures start this way)
- histogram & stats, collapsed until you open them (nothing is computed before): per-channel min, max and mean over every texel, and a histogram with linear or log bins. It samples about 65k texels, or every texel with exact.
- Drag across it to set black and white, or drag either line, or its value. A line lights up as the pointer nears it. Their values are at the lines (at an edge, with an arrow, when they're off it). Its axis is fitted to the values, not to black and white, so it holds still as you drag.
- It zooms and pans as every graph of devui's does (plots too): the wheel (or a pinch) zooms about the pointer, and the bins are worked out again for what's shown. Shift + the wheel, a sideways scroll or a drag with the middle button pans; focused, + and − zoom and 0 fits. A wheel's notch zooms by 1.25 and pans a tenth of the axis, and it keeps to the values: it can't be zoomed or panned off them.
- Over its corner, as a plot's controls are: first fit, while it's zoomed (or double-click it), then auto and 0–1 for black and white.
- tone mapping (clip, Reinhard, ACES, AgX)
- R/G/B/A isolation, rg only (velocity, normal xy)
- colour space, what the values are (each says so in its tooltip):
- Linear: light, as a renderer computes it (a render target's, a float texture's), encoded for the screen to show;
- sRGB: colour encoded for the screen already (a canvas, an image), shown as it is;
- Non-colour: numbers that aren't colour (normals, depth, masks, ids), shown as stored and fitted to their range.
A view starts as its texture's values are: sRGB for a canvas's, Non-colour for depths, integers, one- or two-channel floats and targets that hold data (three's normal, depth and distance materials write them), Linear otherwise. The thumbnail's header says how it's shown where that isn't plain light.
- NaN/Inf, negative and >1 overlays, false colour
- fullscreen (the title bar's button)
- HDR on HDR displays: the inspector can show values above 1 brighter than SDR white. It draws into the browser's HDR canvas (WebGPU's
rgba16floatwith extended tone mapping), on a WebGPU device of devui's own, whatever your app uses. clip keeps values up to a peak you set. The tone mappers reach that peak too: through the midtones they match SDR, and the highlights roll off to the peak instead of to white. Thumbnails stay SDR.createGUI({ hdr: 'force' })offers it anywhere, and'off'hides it.- It needs WebGPU: without it the view stays SDR and says so. A browser with no extended tone mapping clips at SDR white (the inspector says so too).
- It follows the window: moved to an SDR display, the view is SDR and HDR isn't offered; moved back, it's offered again, and shown if you chose it.
- If the HDR canvas fails, the view goes back to SDR and says why (in the note's tooltip, and the console). Choose SDR, then HDR, to try again.
- a live/frozen toggle with manual capture. Frozen is a GPU snapshot (integer textures stay integer), so probes and numbers read the frozen values too
- snapshots and compare: snapshot what you see (#1, #2, …), change the scene, then compare. Compare against a snapshot or another texture view:
- hold flips to B while you hold a button (or <kbd>`</kbd> over the view)
- split has a wipe you drag
- diff shows |A − B| × gain in false colour, with the largest difference over every texel
- Compare is per face and mip, so it shows in the plain layout. The texture's thumbnail shows it too, as the inspector does.
- Snapshots go when the inspector closes, unless kept (the pin), and are capped at 256 MB.
- linked cameras: the link button in two inspectors' title bars makes them zoom and pan together, side by side
A view can start set up: gui.texture('depth', source, { channels: 'r', autoRange: true }), and any of level, layer, exposure, tonemap (clip, reinhard, aces, agx), channels (rgb, rgba, rg, r, g, b, a), black, white, encodeSRGB, layout, axis, attachment, depth and capture. What you change in the inspector is saved with the layout, and wins over these.
Pixels are shaded in your own GPU context and copied to the panel on the GPU. The numbers are computed there too, then read back a few bytes at a time, asynchronously:
- WebGL2: a reduction pyramid, and a histogram whose bins are worked out on the GPU and counted on the CPU (every texel up to 2048² with exact). Values travel as their bits in integer targets, so they need no extension (devui enables none in your context), and come back exact: NaN, ±Inf and denormals too.
- WebGPU: compute passes on your device. Stats come from a workgroup reduction, and the histogram is exact counts. devui's own GPU work runs in error scopes, so a problem shows on its views rather than in your console.
Both backends share one set of display shading (tone mappers, overlays, false colour), so a texture looks the same in either.
renderer.render(scene, camera);
gui.update(); // optional: lets texture views copy from the frame you just drew
Without gui.update(), WebGL views fall back to asynchronous readback (a frame behind, never stalls). WebGPU textures outlive the frame, so WebGPU views don't need it.
Capture timing. A view normally shows a texture as it is at the end of the frame. A scratch target that's reused within a frame needs one of these:
const blur = gui.texture('blur 2', rt, { capture: 'manual' });
blurPass(rt);
blur.capture(); // a snapshot right now, so later passes into rt don't change what you see
gui.texture('ping', pingRT, { capture: 'on-write' }); // three.js: every write; `write: n` picks the nth
capture() takes a GPU snapshot at the moment it's called (with WebGPU, of the work submitted so far). With the three.js extension, on-write snapshots a render target each time rendering into it finishes, when setRenderTarget moves on. The view shows the last write of each frame, or the nth one. Snapshots happen only while the view is on screen.