Skip to content

@typegpu/kitimages

Textures from images

Decode image files and HDR panoramas, then put them on the GPU as colour or data, with mips, layers and samplers.

kit.images

module

images.create(root, picture, options?)

images.decodeBitmap(image)

images.decode(image, custom?)

images.decodeHdr(bytes) → HdrImage

and images.array, images.cube, images.sampler, images.decodesItself, images.rgbm

rootborrows
the root
picturetakes
pixels from decode, decodeHdr or your own code, or a bitmap
imagetakes
an encoded PNG, JPEG or WebP, such as one from gltf.read
imagetakes
the encoded file and its MIME type
custom?takes
decodes files other than HDR, such as on a worker without createImageBitmap
bytestakes
the whole file
HdrImagereturns
pixels for create
constants
HDR
types
EncodedImageDecodedImageHdrImagePictureImageSourceDecodeFormatUnpackTexture
Optionscreate, array, cube · 4 fields
color?

'srgb' | 'linear'= 'srgb'

Says what 8-bit images hold: 'srgb' for colour, which sampling turns into linear light, or 'linear' for data such as normals, roughness or height.

mips?

boolean= true

Generates every mip level down to 1 × 1.

packed?

Unpack= none

Converts light stored in an 8-bit image, such as RGBM, to linear light once, at upload, so the texture filters and mips like any HDR texture.

format?

'rgba16float' | 'rg11b10ufloat'= 'rgba16float'

Sets the format of a texture of light, from packed or half-float images.

SamplerOptionssampler · 3 fields
wrap?

GPUAddressMode= 'repeat'

Says what happens past the edge: WebGPU's address mode on every axis.

filter?

GPUFilterMode= 'linear'

Filters within and between mip levels.

anisotropy?

number= 1

Sets the samples at grazing angles, 1 to 16.

submits

while setting up

create, array and cube generate mips with TypeGPU's generateMipmaps, which submits its own commands, unless mips is false. With packed, or a format other than the image's, they first draw each layer through a pipeline of their own and submit it.

owns

textures ·samplers ·a baker with `packed` or a narrower `format`

The textures and samplers each call returns, of which you destroy the textures; a staging texture it destroys and a baker that draws it into the texture, used once while creating.

requires
  • rg11b10ufloat-renderableoption

    format: 'rg11b10ufloat' needs it.

One sand image on a plane to the horizon. Without mips, the distant sand turns grainy. At 1× anisotropy, it blurs where the ground is seen at a slant. Read as linear, the colour bytes come out washed out.

A WebGPU texture holds texels in a format, and an image file holds encoded bytes. Between them sit a decode, a choice of format that says whether the bytes are colour or data, an upload and the smaller copies sampling reads at a distance. images does each step, so a texture is two calls: decode while loading, then create.

import {
import images
images
} from '@typegpu/kit';
const
const file: Response
file
= await
function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
fetch
('/textures/bark.webp');
const
const bytes: Uint8Array<ArrayBuffer>
bytes
= new
var Uint8Array: Uint8ArrayConstructor
new <ArrayBuffer>(buffer: ArrayBuffer, byteOffset?: number, length?: number) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
(await
const file: Response
file
.
Body.arrayBuffer(): Promise<ArrayBuffer>
arrayBuffer
());
const
const picture: images.Picture
picture
= await
import images
images
.
function decode(image: images.EncodedImage, custom?: images.Decode): Promise<images.Picture>

Decodes an image file. A Radiance .hdr file, by its MIME type images.HDR, decodes with decodeHdr on any thread. Other files decode with custom, or with decodeBitmap where createImageBitmap exists. Rejects on a runtime without either.

@param ― image the encoded file and its MIME type

@param ― custom decodes files other than HDR, such as on a worker without createImageBitmap

@returns ― the pixels or bitmap for create

decode
({
EncodedImage.bytes: Uint8Array<ArrayBufferLike>
bytes
,
EncodedImage.mimeType: string
mimeType
: 'image/webp' });
const
const bark: images.Texture<[number, number]>
bark
=
import images
images
.
function create(root: TgpuRoot, picture: images.Picture, options?: images.Options): images.Texture

Creates a sampled texture from one image. 8-bit images become rgba8unorm-srgb, or rgba8unorm with color: 'linear'. Half-float images, and 8-bit ones with packed, become format. Throws if the pixel count doesn't match the size, the image is empty or larger than the device allows, packed comes with color, or format with 8-bit colour or data.

@param ― picture pixels from decode, decodeHdr or your own code, or a bitmap

@returns ― a texture you destroy, with every mip level unless mips is false

create
(
const root: TgpuRoot
root
,
const picture: images.Picture
picture
); // sRGB colour, every mip level
const
const view: TgpuTextureView<d.WgslTexture2d<d.F32>>
view
=
const bark: images.Texture<[number, number]>
bark
.
TgpuTexture<{ size: [number, number]; format: Format; }>.createView<d.WgslTexture2d<d.F32>>(schema: d.WgslTexture2d<d.F32>, viewDescriptor?: (TgpuTextureViewDescriptor & {
sampleType?: "float" | "unfilterable-float" | undefined;
}) | undefined): TgpuTextureView<d.WgslTexture2d<d.F32>> (+3 overloads)
createView
(
import d
d
.
texture2d<d.F32>(sampleType: d.F32): d.WgslTexture2d<d.F32> (+1 overload)
export texture2d
texture2d
(
import d
d
.
const f32: d.F32
export f32

A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)

Can also be called to cast a value to an f32.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const value = f32(true); // 1

f32
));
const
const smooth: TgpuSampler
smooth
=
import images
images
.
function sampler(root: TgpuRoot, options?: images.SamplerOptions): TgpuSampler

Creates a filtering sampler for textures from this module.

@returns ― a sampler, which needs no destroy

sampler
(
const root: TgpuRoot
root
, {
SamplerOptions.anisotropy?: number | undefined

Sets the samples at grazing angles, 1 to 16. WebGPU allows more than 1 only with linear filtering, so 'nearest' uses 1. Default 1.

anisotropy
: 8 });
const
const barkAt: (uv: d.v2f) => d.v3f
barkAt
= (
uv: d.v2f
uv
:
import d
d
.
export v2f

Interface representing its WGSL vector type counterpart: vec2f or vec2. A vector with 2 elements of type f32

v2f
) => {
'use gpu';
return
import std
std
.
textureSample<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, sampler: d.sampler, coords: d.v2f, offset?: d.v2i): d.v4f (+8 overloads)
export textureSample
textureSample
(
const view: TgpuTextureView<d.WgslTexture2d<d.F32>>
view
.
TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$
,
const smooth: TgpuSampler
smooth
.
TgpuSampler.$: d.sampler
$
,
uv: d.v2f
uv
).
rgb: d.v3f
rgb
; // linear light
};
  • create makes the texture at the image’s size, writes it and generates its mips.
  • bark.destroy() frees it. A sampler needs no destroy.

An 8-bit image holds either colour, which a screen showed in sRGB, or data, such as normals, roughness or height. Colour goes into an rgba8unorm-srgb texture, which sampling turns into linear light. Data goes into rgba8unorm with color: 'linear', which samples the bytes as they are:

const
const normals: images.Texture<[number, number]>
normals
=
import images
images
.
function create(root: TgpuRoot, picture: images.Picture, options?: images.Options): images.Texture

Creates a sampled texture from one image. 8-bit images become rgba8unorm-srgb, or rgba8unorm with color: 'linear'. Half-float images, and 8-bit ones with packed, become format. Throws if the pixel count doesn't match the size, the image is empty or larger than the device allows, packed comes with color, or format with 8-bit colour or data.

@param ― picture pixels from decode, decodeHdr or your own code, or a bitmap

@returns ― a texture you destroy, with every mip level unless mips is false

create
(
const root: TgpuRoot
root
,
const picture: images.Picture
picture
, {
Options.color?: "srgb" | "linear" | undefined

Says what 8-bit images hold: 'srgb' for colour, which sampling turns into linear light, or 'linear' for data such as normals, roughness or height. Half-float images are linear either way. Default 'srgb'.

color
: 'linear' });
const
const sharp: images.Texture<[number, number]>
sharp
=
import images
images
.
function create(root: TgpuRoot, picture: images.Picture, options?: images.Options): images.Texture

Creates a sampled texture from one image. 8-bit images become rgba8unorm-srgb, or rgba8unorm with color: 'linear'. Half-float images, and 8-bit ones with packed, become format. Throws if the pixel count doesn't match the size, the image is empty or larger than the device allows, packed comes with color, or format with 8-bit colour or data.

@param ― picture pixels from decode, decodeHdr or your own code, or a bitmap

@returns ― a texture you destroy, with every mip level unless mips is false

create
(
const root: TgpuRoot
root
,
const icon: images.Picture
icon
, {
Options.mips?: boolean | undefined

Generates every mip level down to 1 × 1. Default true.

mips
: false });

Pixels you made yourself are a { width, height, pixels } with 4 bytes per pixel, unpremultiplied, in rows from the top. create takes them like a decoded file and throws if the count doesn’t match the size.

A Radiance .hdr file holds light brighter than white, such as a panorama of the sky with the sun in it. images.decodeHdr(bytes) decodes it into half floats, and create stores them in rgba16float:

const
const file: Response
file
= await
function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
fetch
('/skies/meadow.hdr');
const
const meadow: images.HdrImage
meadow
=
import images
images
.
function decodeHdr(bytes: Uint8Array): images.HdrImage

Decodes a Radiance .hdr file into linear half floats for an rgba16float texture. It reads flat and run-length encoded RGBE scanlines in the standard -Y +X order, and clamps values above 65 504, a half float's largest. It runs on any thread, a React Native worker included. Throws for another orientation, an XYZE file, a side above 16 384 or a truncated or corrupt file.

@param ― bytes the whole file

@returns ― pixels for create

decodeHdr
(new
var Uint8Array: Uint8ArrayConstructor
new <ArrayBuffer>(buffer: ArrayBuffer, byteOffset?: number, length?: number) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
(await
const file: Response
file
.
Body.arrayBuffer(): Promise<ArrayBuffer>
arrayBuffer
()));
const
const sky: images.Texture<[number, number]>
sky
=
import images
images
.
function create(root: TgpuRoot, picture: images.Picture, options?: images.Options): images.Texture

Creates a sampled texture from one image. 8-bit images become rgba8unorm-srgb, or rgba8unorm with color: 'linear'. Half-float images, and 8-bit ones with packed, become format. Throws if the pixel count doesn't match the size, the image is empty or larger than the device allows, packed comes with color, or format with 8-bit colour or data.

@param ― picture pixels from decode, decodeHdr or your own code, or a bitmap

@returns ― a texture you destroy, with every mip level unless mips is false

create
(
const root: TgpuRoot
root
,
const meadow: images.HdrImage
meadow
); // rgba16float, linear
  • decode reads a file whose MIME type is images.HDR, image/vnd.radiance, the same way.
  • An environment bakes such a panorama into the light around a scene.
  • Values above 65 504, the largest half float, clamp to it. The sun in a photographed sky can be brighter.
  • The decoder reads flat and run-length encoded RGBE rows from the top, as Poly Haven and most tools write them. It throws for other orientations and for XYZE files.

Some HDR files store light in 8-bit channels. RGBM, for example, stores rgb × a × range. Sampling such a texture interpolates the stored values, which gives the wrong light between texels whose a differs. packed converts each texel to linear light once, when the texture is created:

const
const light: images.Texture<[number, number]>
light
=
import images
images
.
function create(root: TgpuRoot, picture: images.Picture, options?: images.Options): images.Texture

Creates a sampled texture from one image. 8-bit images become rgba8unorm-srgb, or rgba8unorm with color: 'linear'. Half-float images, and 8-bit ones with packed, become format. Throws if the pixel count doesn't match the size, the image is empty or larger than the device allows, packed comes with color, or format with 8-bit colour or data.

@param ― picture pixels from decode, decodeHdr or your own code, or a bitmap

@returns ― a texture you destroy, with every mip level unless mips is false

create
(
const root: TgpuRoot
root
,
const picture: images.Picture
picture
, {
Options.packed?: images.Unpack | undefined

Converts light stored in an 8-bit image, such as RGBM, to linear light once, at upload, so the texture filters and mips like any HDR texture. A 'use gpu' function that takes each texel as stored, unfiltered, and returns linear RGBA. The texture takes format. It reads raw values, so it takes no color. Default none.

packed
:
import images
images
.
const rgbm: (range: number) => images.Unpack

Unpacks RGBM, light stored as rgb × a × range in linear values, for packed.

@param ― range the brightest light the encoding holds

@returns ― a function for packed

rgbm
(8) }); // rgba16float
const
const smaller: images.Texture<[number, number]>
smaller
=
import images
images
.
function create(root: TgpuRoot, picture: images.Picture, options?: images.Options): images.Texture

Creates a sampled texture from one image. 8-bit images become rgba8unorm-srgb, or rgba8unorm with color: 'linear'. Half-float images, and 8-bit ones with packed, become format. Throws if the pixel count doesn't match the size, the image is empty or larger than the device allows, packed comes with color, or format with 8-bit colour or data.

@param ― picture pixels from decode, decodeHdr or your own code, or a bitmap

@returns ― a texture you destroy, with every mip level unless mips is false

create
(
const root: TgpuRoot
root
,
const picture: images.Picture
picture
, {
Options.packed?: images.Unpack | undefined

Converts light stored in an 8-bit image, such as RGBM, to linear light once, at upload, so the texture filters and mips like any HDR texture. A 'use gpu' function that takes each texel as stored, unfiltered, and returns linear RGBA. The texture takes format. It reads raw values, so it takes no color. Default none.

packed
:
import images
images
.
const rgbm: (range: number) => images.Unpack

Unpacks RGBM, light stored as rgb × a × range in linear values, for packed.

@param ― range the brightest light the encoding holds

@returns ― a function for packed

rgbm
(8),
Options.format?: "rgba16float" | "rg11b10ufloat" | undefined

Sets the format of a texture of light, from packed or half-float images. 'rg11b10ufloat' takes half the memory and drops alpha. It needs the device feature 'rg11b10ufloat-renderable'. 8-bit images without packed take the format color picks. Default 'rgba16float'.

format
: 'rg11b10ufloat' });
  • packed is any 'use gpu' function from a stored texel to linear RGBA, and images.rgbm(range) is one. It reads the raw values, so it takes no color.
  • 'rg11b10ufloat' takes half the memory and drops alpha. It needs the device feature 'rg11b10ufloat-renderable'.
  • Mips, arrays and cube maps work as for any image.

images.array(root, pictures) makes a 2D array with one image per layer, such as terrain materials or an animation’s frames. All layers share a size and a kind. Sample a view of d.texture2dArray(d.f32) with the layer index.

images.cube(root, faces) makes a cube map from six square faces in WebGPU’s order: +x, −x, +y, −y, +z, −z. Sample a view of d.textureCube(d.f32) with a direction.

images.sampler(root, options) filters linearly within and between mip levels and repeats past the edge. wrap takes WebGPU’s address modes, such as 'clamp-to-edge'. anisotropy sharpens surfaces seen at grazing angles, up to 16 samples. WebGPU allows it only with linear filtering, so filter: 'nearest' uses one sample.

React Native’s createImageBitmap runs on the JS thread only, and a scene on a worker has none. Decode PNG, JPEG and WebP files on the JS thread with images.decodeBitmap, then pass the bitmaps to the scene, where create takes them. A Decode of your own, such as one built on Skia, can return { width, height, pixels } instead: pass it to decode as its second argument.

React Native’s decoder premultiplies alpha and then divides it back, so colour where alpha is near 0 is lost. It also ignores colorSpaceConversion: 'none', so a colour profile in the file can change the bytes. Keep data out of alpha here, or decode to pixels with your own Decode. For packed HDR, use an encoding that leaves alpha at 255 rather than RGBM, or a .hdr file.

decodeHdr is plain JavaScript and runs on any thread, the worker included.

  • Every image of an array or a cube has one size and one kind: 8-bit or half float.
  • create, array and cube submit to generate mips, and with packed to unpack. Call them while loading, not in a frame.
  • 8-bit images keep 4 channels and half-float images keep 4 half floats per texel. A 2048 × 1024 HDR panorama takes 22 MB with its mips, and the same image in 8 bits takes 11 MB. A packed image takes the HDR size, or the 8-bit size in rg11b10ufloat.