Texture Basics

Creating and configuring textures

A texture is a structured array of data that lives on the GPU. While we often think of textures as images, they are far more general—a texture can hold colors, depth values, normals, arbitrary floating-point data, or anything else you need to access efficiently in a shader.

The GPU hardware is specifically optimized for texture access. When a fragment shader samples a texture, it benefits from dedicated texture units, specialized caches, and built-in filtering operations that would be expensive to replicate with raw buffer reads.

What Textures Really Are

At the hardware level, a texture is a block of GPU memory with associated metadata that tells the GPU how to interpret that memory. The metadata includes the texture's dimensions, its pixel format, how many mipmap levels it contains, and what operations are allowed on it.

Unlike CPU memory, which is typically accessed linearly, texture memory is organized in a way that optimizes for 2D locality. When you sample pixel (100, 100), the hardware assumes you will likely sample nearby pixels soon. This spatial locality is exploited by texture caches to dramatically improve performance.

The fundamental difference between a texture and a buffer is that textures support hardware-accelerated filtering. When you sample between pixels, the texture unit automatically interpolates values. When you sample a distant surface, mipmaps provide pre-filtered versions at appropriate resolutions. Buffers offer none of this—they are raw arrays where you handle everything yourself.

Texture Formats

The format of a texture defines how each pixel's data is stored and interpreted. WebGPU supports dozens of formats, each optimized for different use cases.

Interactive: Texture Format Explorer

rgba8unorm
1.00 MB
rgba16float
2.00 MB
rgba32float
4.00 MB
r8unorm
256.0 KB

Click a format to see details. Memory usage scales quadratically with texture dimensions.

The most common format is rgba8unorm—four 8-bit channels (red, green, blue, alpha), each interpreted as an unsigned normalized value mapping 0-255 to the floating-point range [0.0, 1.0]. This format covers most standard image use cases: photos, UI elements, diffuse textures.

For high dynamic range rendering, you need more precision. rgba16float provides 16 bits per channel as floating-point values, capable of representing values far outside the [0, 1] range. This is essential for HDR lighting, bloom effects, and any computation where values can exceed standard display range.

Normal maps often use rgba8snorm—signed normalized values mapping to [-1, 1]. This matches the natural range of unit vectors, where each component can be negative.

Single-channel formats like r8unorm save memory when you only need one value per pixel—grayscale images, masks, or height maps. Two-channel rg8unorm works well for 2D vectors like velocity fields.

Depth textures use specialized formats. depth24plus provides 24-bit depth precision, the standard for most 3D rendering. depth32float offers maximum precision for cases where 24 bits cause z-fighting artifacts. The depth24plus-stencil8 format combines depth with an 8-bit stencil buffer for masking operations.

Texture Dimensions

Textures come in several dimensional varieties, each suited to different applications.

Interactive: Texture Dimensions

2D Texture

The most common type. A rectangular grid of pixels indexed by UV coordinates.

Images, normal maps, sprites, render targets

1D textures are single rows of pixels. They excel as lookup tables—feed in a value, get back a color or other data. Gradient ramps, material property curves, and color grading LUTs all work naturally as 1D textures.

2D textures are the workhorses. Every image, sprite, normal map, and render target is a 2D texture. They are indexed by UV coordinates in the [0, 1] range, with the hardware handling coordinate-to-pixel mapping.

3D textures add a third dimension, creating volumes of voxels. They are used for volumetric effects like fog, clouds, and medical imaging. 3D textures consume memory rapidly—a 256³ volume is 16 million voxels—but provide natural trilinear filtering in three dimensions.

Cube maps consist of six 2D textures arranged as the faces of a cube. Rather than UV coordinates, you sample them with a direction vector. The hardware figures out which face and location to sample. Cube maps are the standard for environment reflections and skyboxes.

2D texture arrays stack multiple 2D textures of the same size. Each layer is a complete texture, selectable by index in the shader. Terrain systems use arrays for blending between ground materials. Shadow mapping uses arrays for cascaded shadow maps.

Creating Textures

Creating a texture in WebGPU requires specifying its dimensions, format, and usage flags.

Interactive: Texture Configuration

Dimensions
512 × 512
Mip Levels
1
Memory
1.00 MB
Generated Code
const texture = device.createTexture({
  size: [512, 512],
  format: "rgba8unorm",
  dimension: "2d",
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
});

Configure texture properties and see the WebGPU code needed to create it.

The usage flags are particularly important. They tell the GPU how you intend to use the texture, allowing it to optimize memory placement:

TEXTURE_BINDING means the texture will be sampled in shaders. Most textures need this flag.

COPY_DST means you will upload data to this texture. Required for any texture that receives image data from the CPU.

COPY_SRC means you will read data from this texture. Needed if you want to download GPU results back to the CPU.

RENDER_ATTACHMENT means this texture can be a render target—the output of a render pass. Frame buffers, shadow maps, and post-processing buffers need this flag.

STORAGE_BINDING means compute shaders can write directly to this texture. Unlike render attachments, storage textures allow random writes from any thread.

You must specify all flags up front. A texture created without RENDER_ATTACHMENT cannot later be used as a render target. This constraint enables the driver to optimize memory placement for your declared usage patterns.

Mipmaps

Mipmaps are pre-computed, progressively smaller versions of a texture. Level 0 is the original image. Level 1 is half the size in each dimension. Level 2 is half again, and so on, down to 1×1.

Interactive: Mipmap Pyramid

Mip Levels
9
Total Memory
341.3 KB
Overhead
+33%

Mipmaps add ~33% memory overhead but dramatically improve rendering quality and performance at distance.

Mipmaps solve two problems. First, they prevent aliasing. When a textured surface is far from the camera, many texels map to a single screen pixel. Without mipmaps, the texture "shimmers" as tiny movements select different texels. Mipmaps provide pre-averaged versions at appropriate scales.

Second, mipmaps improve performance. Sampling from a small mipmap level requires less memory bandwidth than sampling from the full-resolution texture. For distant objects, the GPU automatically selects smaller mip levels, reducing cache pressure.

The memory cost is modest—about 33% more than the base texture alone. This is because the series 1 + 1/4 + 1/16 + ... converges to 4/3.

When creating a texture with mipmaps, set mipLevelCount to the desired number of levels. The maximum is floor(log2(max(width, height))) + 1. Unlike some older APIs, WebGPU does not automatically generate mipmap contents—you must generate them yourself, typically using a compute shader or a series of render passes that downsample each level.

Uploading Image Data

Getting image data onto the GPU typically uses copyExternalImageToTexture, which handles browser image sources efficiently.

Interactive: Image Upload Pipeline

Step 1: Load Image
const img = new Image();
img.src = "texture.png";
await img.decode();

First, load the image from a URL or file. The decode() call ensures the image data is ready.

copyExternalImageToTexture works with HTMLImageElement, HTMLCanvasElement, ImageBitmap, and HTMLVideoElement.

The function accepts several source types. HTMLImageElement is the most common—load an image from a URL, then copy it to the GPU. HTMLCanvasElement lets you generate images programmatically. ImageBitmap provides the fastest path when you need to process the image before upload. HTMLVideoElement enables video textures updated each frame.

For raw pixel data in an ArrayBuffer, use writeTexture instead:

device.queue.writeTexture(
  { texture: texture },
  pixelData,
  { bytesPerRow: width * 4 },
  [width, height]
);
typescript

The bytesPerRow parameter specifies the stride between rows in your source data. It must be a multiple of 256 bytes for optimal performance, though WebGPU will handle non-aligned data with a potential performance penalty.

Sample Count and Multisampling

Textures can have multiple samples per pixel for antialiasing. When sampleCount is 4, each pixel stores four independent color values. During rasterization, the GPU evaluates coverage at four sub-pixel locations, storing results in the corresponding samples.

Multisampled textures cannot be directly sampled in shaders—they must be resolved to a single-sample texture first. This is typically done as part of the render pass:

const renderPass = encoder.beginRenderPass({
  colorAttachments: [{
    view: multisampledTexture.createView(),
    resolveTarget: finalTexture.createView(),
    loadOp: "clear",
    storeOp: "store",
  }]
});
typescript

The resolveTarget receives the averaged result of all samples.

Texture Views

A texture view is a window into a texture's data with a specific interpretation. Views let you access sub-regions, specific mip levels, or reinterpret the format.

const view = texture.createView({
  format: "rgba8unorm",       // Can reinterpret format
  dimension: "2d",
  baseMipLevel: 0,
  mipLevelCount: 1,
  baseArrayLayer: 0,
  arrayLayerCount: 1,
});
typescript

You can create multiple views of the same texture. A shadow mapping system might have one view of the entire shadow map array for reading in the main pass, and separate views of each layer for writing in the shadow passes.

Views are cheap to create—they are just metadata describing how to interpret existing memory.

Key Takeaways

  • Textures are structured GPU memory with hardware-accelerated sampling and filtering
  • The format determines how pixel data is stored: rgba8unorm for standard images, rgba16float for HDR, depth formats for 3D rendering
  • Texture dimensions include 1D (lookup tables), 2D (images), 3D (volumes), cube (environment), and arrays (layers)
  • Usage flags must be declared at creation time: TEXTURE_BINDING for sampling, COPY_DST for uploads, RENDER_ATTACHMENT for render targets
  • Mipmaps prevent aliasing and improve performance at the cost of ~33% more memory
  • copyExternalImageToTexture efficiently uploads browser image sources to the GPU
  • Texture views provide different interpretations of the same underlying data