Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 170 additions & 55 deletions apps/typegpu-docs/src/content/docs/apis/textures.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@ TypeGPU textures serve as a wrapper that provides type safety and higher level u
Let's look at an example of creating and using a typed texture.

```ts twoslash
import { tgpu, d } from 'typegpu';
import { tgpu } from 'typegpu';

const root = await tgpu.init();

const texture = root.createTexture({
size: [256, 256],
format: 'rgba8unorm' as const,
}).$usage('sampled');

const response = await fetch('path/to/image.png');
const blob = await response.blob();
const image = await createImageBitmap(blob);
const imageBitmap = await createImageBitmap(blob);

// Uploading image data to the texture (will be resampled if sizes differ)
texture.write(image);
const texture = root.createTexture({
size: [imageBitmap.width, imageBitmap.height],
format: 'rgba8unorm' as const,
}).$usage('sampled', 'render');

// Uploading image data to the texture
texture.write(imageBitmap);

// Creating a view to use in shader
const sampledView = texture.createView();
Expand Down Expand Up @@ -80,7 +80,7 @@ const texture = root.createTexture({
})
.$usage('sampled') // Can be sampled in shaders
.$usage('storage') // Can be written or read to as storage texture
.$usage('render'); // Can be used as a render target
.$usage('render'); // Can be used as a render target or image upload target
```

You can also add multiple flags at once:
Expand Down Expand Up @@ -115,72 +115,127 @@ This is an escape hatch. It replaces the flags TypeGPU would normally infer and

## Writing to a texture

The `.write()` method provides multiple overloads for different data sources:
The `.write()` method accepts a variety of data sources. Here's a quick overview of the available options:

```ts
// Image sources (single or array)
write(source: ExternalImageSource | ExternalImageSource[]): void
| Source | Use when |
| --- | --- |
| `texture.write(imageBitmap)` | You already have a decoded image that matches the texture size. |
| `texture.write(imageBitmap, { fit: 'stretch' })` | You want the image resampled to fit the texture. |
| `texture.write(imageBitmap, { fit: 'clip' })` | You want a 1:1 copy of the overlapping region. |
| `texture.writeAsync(blob, { ... })` | You have a fetched `Blob` and don't need to keep the decoded bitmap around. |
| `texture.write([layer0, layer1])` | You want to fill a texture array or 3D texture one layer at a time. |
| `texture.write(bytes)` | You already have raw texel bytes. |
| `texture.write(imageBitmap, { origin, size, ... })` | You need a crop or a destination region. |
| `common.writeChannels(texture, { ... })` | You want to pack image sources into individual channels. |

// Raw binary data with optional mip level
write(source: ArrayBuffer | TypedArray | DataView, mipLevel?: number): void
```
Note that writing image sources requires the `'render'` usage flag, and that the source and destination sizes have to match unless you pick a mismatch behavior with the `fit` option.

### Writing image data

You can write various image sources to textures. `ExternalImageSource` includes:
- `HTMLCanvasElement`
- `HTMLImageElement`
- `HTMLVideoElement`
- `ImageBitmap`
- `ImageData`
- `OffscreenCanvas`
- `VideoFrame`
You can write various image sources to textures. If the image dimensions match the texture size, all it takes is a single `.write()` call:

```ts twoslash
import { tgpu } from 'typegpu';
const root = await tgpu.init();
const response = await fetch('path/to/image.png');
const blob = await response.blob();
const imageBitmap = await createImageBitmap(blob);
// ---cut---
const texture = root.createTexture({
size: [256, 256],
size: [imageBitmap.width, imageBitmap.height],
format: 'rgba8unorm',
}).$usage('sampled');
}).$usage('sampled', 'render');

// From an ImageBitmap
const response = await fetch('path/to/image.png');
const blob = await response.blob();
const imageBitmap = await createImageBitmap(blob);
texture.write(imageBitmap);
```

:::caution[Browser support]
Besides `ImageBitmap`, `GPUCopyExternalImageSource` also includes canvas, video, `ImageData`, `HTMLImageElement`, and `VideoFrame` - but browser support for these varies. For portable code, decode fetched images to `ImageBitmap` first.
:::

### Handling size mismatches

// From an HTMLCanvasElement
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// ... draw on canvas
texture.write(canvas);
If the image dimensions don't match the texture size, a plain `.write()` call throws. The `fit` option picks what happens instead: `'stretch'` resamples the source to fill the target region, while `'clip'` copies texels 1:1 and cuts off whatever doesn't overlap.

```ts
const texture = root.createTexture({
size: [512, 512],
format: 'rgba8unorm',
}).$usage('sampled', 'render');

texture.write(imageBitmap, { fit: 'stretch' });

texture.write(imageBitmap, {
sourceOrigin: [16, 16],
sourceSize: [128, 128],
size: [256, 256],
fit: 'stretch',
});

texture.write(tileBitmap, { origin: [128, 64], fit: 'clip' });
```

:::tip
If image dimensions don't match the texture size, the image will be automatically resampled to fit (requires 'render' usage).
:::note
Under the hood, `fit: 'stretch'` runs a render-pass blit, so the destination has to have a renderable, float-sampleable format. `fit: 'clip'` is a plain texel copy.
:::

### Writing blobs

If you fetched an image and don't need to keep the decoded `ImageBitmap` around, you can hand the `Blob` directly to `writeAsync` and let TypeGPU handle the decoding. With `fit: 'stretch'`, the resize options are passed along to `createImageBitmap` before uploading.

```ts twoslash
import { tgpu } from 'typegpu';
const root = await tgpu.init();
// ---cut---
const texture = root.createTexture({
size: [512, 512],
format: 'rgba8unorm',
}).$usage('sampled', 'render');

const blob = await (await fetch('path/to/image.png')).blob();

await texture.writeAsync(blob, { fit: 'stretch' });
```

If you don't specify a `size`, it defaults to the size of the written region - the target mip level minus the origin offset. Keep in mind that the decoded bitmap is still written as an image source, so the texture needs the `'render'` usage flag.

### Writing arrays of images

For 3D textures or texture arrays, you can write multiple images:
For texture arrays, you can write multiple images at once - one per layer:

```ts twoslash
import { tgpu } from 'typegpu';
const root = await tgpu.init();
declare const layer0: ImageBitmap;
declare const layer1: ImageBitmap;
declare const layer2: ImageBitmap;
// ---cut---
const texture = root.createTexture({
size: [256, 256, 3],
format: 'rgba8unorm',
}).$usage('sampled', 'render');

texture.write([layer0, layer1, layer2]);
```

Each image has to match the layer size, unless you pass a `fit` mode.

The same form works for 3D textures (created with `dimension: '3d'`) - each image in the array fills one depth slice:

```ts twoslash
import { tgpu } from 'typegpu';
const root = await tgpu.init();
declare const imageBitmap1: ImageBitmap;
declare const imageBitmap2: ImageBitmap;
declare const imageBitmap3: ImageBitmap;
declare const slice0: ImageBitmap;
declare const slice1: ImageBitmap;
declare const slice2: ImageBitmap;
// ---cut---
const texture3d = root.createTexture({
size: [256, 256, 3],
format: 'rgba8unorm',
dimension: '3d',
}).$usage('sampled');
}).$usage('sampled', 'render');

// Write array of images for each layer
texture3d.write([imageBitmap1, imageBitmap2, imageBitmap3]);
texture3d.write([slice0, slice1, slice2]);
```

### Writing raw binary data
Expand All @@ -194,6 +249,7 @@ const root = await tgpu.init();
const texture = root.createTexture({
size: [2, 2],
format: 'rgba8unorm',
mipLevelCount: 2,
}).$usage('sampled');

// Using Uint8Array for RGBA data (4 pixels, 4 bytes each)
Expand All @@ -206,29 +262,61 @@ const data = new Uint8Array([
texture.write(data);

// Write to a specific mip level
const mipData = new Uint8Array(4 * 128 * 128); // Data for 128x128
const mipData = new Uint8Array(4); // Data for 1 pixel
texture.write(mipData, 1); // Write to mip level 1

// Update just a region
const regionData = new Uint8Array(4); // Data for 1 pixel
texture.write(regionData, { origin: [1, 1], size: [1, 1] });
```

You can also copy from another texture:
Writing raw data doesn't require the `'render'` usage flag, but the byte length has to exactly match the written region for the given texture format.

### Regions and channel writes

If you only want to update a part of the texture, or copy just a fragment of the source image, you can pass options with source and destination regions. The options also let you flip the source vertically (`flipY`), premultiply alpha (`premultipliedAlpha`), or convert to a specific color space (`colorSpace`).

```ts twoslash
import { tgpu } from 'typegpu';
const root = await tgpu.init();
declare const imageBitmap: ImageBitmap;
// ---cut---
const sourceTexture = root.createTexture({
size: [256, 256],
const texture = root.createTexture({
size: [512, 512],
format: 'rgba8unorm',
}).$usage('sampled');
}).$usage('sampled', 'render');

const targetTexture = root.createTexture({
size: [256, 256],
texture.write(imageBitmap, {
sourceOrigin: [16, 16],
sourceSize: [128, 128],
origin: [128, 64],
size: [128, 128],
});
```

Sometimes you'll want to pack multiple grayscale maps into a single texture - for example combining separate roughness, metalness, and mask maps into one material texture. The `common.writeChannels` utility does exactly that. Each entry writes a single channel of the texture, with `from` selecting which channel of the source to read. Entries set to `undefined` are skipped.

```ts twoslash
import { tgpu, common } from 'typegpu';
const root = await tgpu.init();
declare const roughnessMap: ImageBitmap;
declare const metalnessMap: ImageBitmap;
declare const maskMap: ImageBitmap;
// ---cut---
const texture = root.createTexture({
size: [512, 512],
format: 'rgba8unorm',
}).$usage('sampled');
}).$usage('sampled', 'render');

targetTexture.copyFrom(sourceTexture);
common.writeChannels(texture, {
r: { source: roughnessMap, from: 'r' },
g: { source: metalnessMap, from: 'r' },
a: { source: maskMap, from: 'r' },
});
```

Region and channel writes follow the same rules as other image writes - they require the `'render'` usage flag, and sizes have to match unless you pass a `fit` mode.

### Mipmaps

TypeGPU provides automatic mipmap generation for textures:
Expand All @@ -252,6 +340,33 @@ texture.generateMipmaps(); // Generate all mip levels automatically
The `generateMipmaps()` method requires both `'sampled'` and `'render'` usage flags, as TypeGPU runs a downsampling pipeline behind the scenes to generate the mip levels.
:::

### Copying and clearing

You can also copy data from another texture. If both textures have the same size and format, a plain `targetTexture.copyFrom(sourceTexture)` is all you need. To copy a region, target a specific mip level, or copy between differently sized textures, pass copy options:

```ts twoslash
import { tgpu } from 'typegpu';
const root = await tgpu.init();
const sourceTexture = root.createTexture({ size: [64, 64], format: 'rgba8unorm' });
const targetTexture = root.createTexture({ size: [512, 512], format: 'rgba8unorm' });
// ---cut---
targetTexture.copyFrom(sourceTexture, {
sourceOrigin: [16, 16],
origin: [128, 64],
size: [32, 32],
});
```

To wipe a texture clean, use `texture.clear()`, which fills it with zeros. You can also pass an rgba color to fill the texture with instead (this requires the `'render'` usage flag):

```ts twoslash
import { tgpu } from 'typegpu';
const root = await tgpu.init();
const texture = root.createTexture({ size: [512, 512], format: 'rgba8unorm' }).$usage('render');
// ---cut---
texture.clear([0, 0.5, 0, 1]);
```

## Texture views

To create a view - which will also serve as fixed texture usage - you can use one of the available [texture schemas](/TypeGPU/apis/data-schemas/#textures). You can pass it to the `.createView` method of the texture.
Expand All @@ -272,7 +387,7 @@ const sampledView = texture.createView(d.texture2d(d.f32));
```

:::tip
If type information is available the view schema will be staticly checked against the texture properties.
If type information is available the view schema will be statically checked against the texture properties.

```ts twoslash
import { tgpu, d } from 'typegpu';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const carSpriteTexture = root
format: 'rgba8unorm',
})
.$usage('sampled', 'render');
carSpriteTexture.write(carBitmap);
carSpriteTexture.write(carBitmap, { fit: 'stretch' });
const carSpriteView = carSpriteTexture.createView();

const linearSampler = root.createSampler({
Expand Down
4 changes: 2 additions & 2 deletions apps/typegpu-docs/src/examples/simulation/gravity/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export async function loadSkyBox(root: TgpuRoot) {
}),
);

texture.write(bitmaps);
texture.write(bitmaps, { fit: 'stretch' });

return texture;
}
Expand Down Expand Up @@ -144,7 +144,7 @@ export async function loadSphereTextures(root: TgpuRoot) {
return await createImageBitmap(blob);
}),
);
texture.write(planets);
texture.write(planets, { fit: 'stretch' });

return texture;
}
Expand Down
13 changes: 2 additions & 11 deletions apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { defineControls } from '../../common/defineControls.ts';

// Initialize
const root = await tgpu.init();
const device = root.device;

// Setup canvas
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
Expand Down Expand Up @@ -87,20 +86,12 @@ let brushState: BrushState = {

// Load and create background texture
const response = await fetch('/TypeGPU/plums.jpg');
const plums = await createImageBitmap(await response.blob(), {
resizeWidth: p.N,
resizeHeight: p.N,
resizeQuality: 'high',
});
const plums = await response.blob();

const backgroundTexture = root
.createTexture({ size: [p.N, p.N], format: 'rgba8unorm' })
.$usage('sampled', 'render');
device.queue.copyExternalImageToTexture(
{ source: plums },
{ texture: root.unwrap(backgroundTexture) },
{ width: p.N, height: p.N, depthOrArrayLayers: 1 },
);
await backgroundTexture.writeAsync(plums, { size: [p.N, p.N], fit: 'stretch' });

// Create simulation textures
const velTex = [createField('velocity0'), createField('velocity1')];
Expand Down
Loading
Loading