Skip to content

Textures

Textures are multi-dimensional arrays of image data used for rendering and compute operations.

Creating Textures

javascript
const texture = device.createTexture({
  label: 'My Texture',
  size: { width: 512, height: 512, depthOrArrayLayers: 1 },
  format: 'rgba8unorm',
  usage: TextureUsage.TEXTURE_BINDING | TextureUsage.COPY_DST,
  dimension: '2d',
  mipLevelCount: 1,
  sampleCount: 1
})

Texture Formats

Common formats:

FormatDescriptionBytes per Pixel
'rgba8unorm'8-bit RGBA, normalized4
'rgba8unorm-srgb'8-bit RGBA, sRGB4
'rgba16float'16-bit float RGBA8
'rgba32float'32-bit float RGBA16
'r8unorm'8-bit red, normalized1
'rg8unorm'8-bit RG, normalized2
'depth24plus'24-bit depth4
'depth32float'32-bit float depth4

Usage Flags

javascript
const { TextureUsage } = require('@sylphx/webgpu')

const usage = TextureUsage.TEXTURE_BINDING |
              TextureUsage.COPY_DST |
              TextureUsage.RENDER_ATTACHMENT
FlagDescription
COPY_SRCCan be copied from
COPY_DSTCan be copied to
TEXTURE_BINDINGCan be sampled in shaders
STORAGE_BINDINGCan be used as storage texture
RENDER_ATTACHMENTCan be rendered to

Writing to Textures

Queue Write

javascript
const width = 256
const height = 256
const bytesPerPixel = 4 // RGBA

// Create RGBA data (red image)
const data = new Uint8Array(width * height * bytesPerPixel)
for (let i = 0; i < data.length; i += 4) {
  data[i] = 255      // R
  data[i + 1] = 0    // G
  data[i + 2] = 0    // B
  data[i + 3] = 255  // A
}

device.queueWriteTexture(
  {
    texture: texture,
    mipLevel: 0,
    origin: { x: 0, y: 0, z: 0 }
  },
  Buffer.from(data.buffer),
  {
    offset: 0,
    bytesPerRow: width * bytesPerPixel,
    rowsPerImage: height
  },
  {
    width: width,
    height: height,
    depthOrArrayLayers: 1
  }
)

Copy from Buffer

javascript
const buffer = device.createBuffer(
  width * height * 4,
  BufferUsage.COPY_SRC,
  true
)

// Write to buffer
const mapped = buffer.getMappedRange(0, width * height * 4)
const view = new Uint8Array(mapped.buffer, mapped.byteOffset)
view.set(data)
buffer.unmap()

// Copy to texture
const encoder = device.createCommandEncoder()
encoder.copyBufferToTexture(
  {
    buffer: buffer,
    offset: 0,
    bytesPerRow: width * 4,
    rowsPerImage: height
  },
  {
    texture: texture,
    mipLevel: 0,
    origin: { x: 0, y: 0, z: 0 }
  },
  {
    width: width,
    height: height,
    depthOrArrayLayers: 1
  }
)

device.queueSubmit(encoder.finish())

Reading from Textures

javascript
// Create buffer to read into
const buffer = device.createBuffer(
  width * height * 4,
  BufferUsage.COPY_DST | BufferUsage.MAP_READ,
  false
)

// Copy texture to buffer
const encoder = device.createCommandEncoder()
encoder.copyTextureToBuffer(
  {
    texture: texture,
    mipLevel: 0,
    origin: { x: 0, y: 0, z: 0 }
  },
  {
    buffer: buffer,
    offset: 0,
    bytesPerRow: width * 4,
    rowsPerImage: height
  },
  {
    width: width,
    height: height,
    depthOrArrayLayers: 1
  }
)

device.queueSubmit(encoder.finish())
device.poll(true)

// Read buffer
const mapped = await buffer.mapRead()
const pixels = new Uint8Array(mapped.buffer, mapped.byteOffset, width * height * 4)

// First pixel
console.log('RGBA:', pixels[0], pixels[1], pixels[2], pixels[3])

Texture Views

Texture views define how a texture is accessed in shaders:

javascript
const view = texture.createView({
  label: 'Texture View',
  format: 'rgba8unorm',
  dimension: '2d',
  baseMipLevel: 0,
  mipLevelCount: 1,
  baseArrayLayer: 0,
  arrayLayerCount: 1
})

View Dimensions

  • '1d' - 1D texture
  • '2d' - 2D texture
  • '2d-array' - Array of 2D textures
  • 'cube' - Cube map
  • 'cube-array' - Array of cube maps
  • '3d' - 3D texture

Samplers

Samplers control how textures are sampled in shaders:

javascript
const sampler = device.createSampler({
  label: 'Linear Sampler',
  addressModeU: 'repeat',
  addressModeV: 'repeat',
  addressModeW: 'repeat',
  magFilter: 'linear',
  minFilter: 'linear',
  mipmapFilter: 'linear',
  lodMinClamp: 0,
  lodMaxClamp: 32,
  compare: undefined,
  maxAnisotropy: 1
})

Address Modes

  • 'repeat' - Wrap coordinates
  • 'clamp-to-edge' - Clamp to edge
  • 'mirror-repeat' - Mirror and wrap

Filter Modes

  • 'nearest' - Nearest neighbor (pixelated)
  • 'linear' - Bilinear interpolation (smooth)

Depth Textures

For depth testing in rendering:

javascript
const depthTexture = device.createTexture({
  size: { width: 800, height: 600, depthOrArrayLayers: 1 },
  format: 'depth24plus',
  usage: TextureUsage.RENDER_ATTACHMENT,
  dimension: '2d',
  mipLevelCount: 1,
  sampleCount: 1
})

Multisampled Textures

For anti-aliasing:

javascript
const msaaTexture = device.createTexture({
  size: { width: 800, height: 600, depthOrArrayLayers: 1 },
  format: 'rgba8unorm',
  usage: TextureUsage.RENDER_ATTACHMENT,
  dimension: '2d',
  mipLevelCount: 1,
  sampleCount: 4  // 4x MSAA
})

3D Textures

javascript
const texture3D = device.createTexture({
  size: { width: 64, height: 64, depthOrArrayLayers: 64 },
  format: 'rgba8unorm',
  usage: TextureUsage.TEXTURE_BINDING | TextureUsage.COPY_DST,
  dimension: '3d',
  mipLevelCount: 1,
  sampleCount: 1
})

Best Practices

1. Use Appropriate Formats

javascript
// ✅ Do: Use efficient formats
const colorTexture = device.createTexture({
  format: 'rgba8unorm',  // 4 bytes per pixel
  // ...
})

// ❌ Don't: Waste memory
const colorTexture = device.createTexture({
  format: 'rgba32float',  // 16 bytes per pixel - overkill for color
  // ...
})

2. Power-of-Two Sizes

For mipmaps and better performance:

javascript
// ✅ Do: Power of two
const texture = device.createTexture({
  size: { width: 512, height: 512, depthOrArrayLayers: 1 },
  // ...
})

// ⚠️ Okay but no mipmaps
const texture = device.createTexture({
  size: { width: 500, height: 500, depthOrArrayLayers: 1 },
  // ...
})

3. Proper Bytes Per Row

Must be multiple of 256:

javascript
const width = 100
const bytesPerPixel = 4
const bytesPerRow = Math.ceil((width * bytesPerPixel) / 256) * 256

device.queueWriteTexture(
  { texture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
  data,
  { offset: 0, bytesPerRow, rowsPerImage: height },
  { width, height, depthOrArrayLayers: 1 }
)

4. Cleanup

javascript
texture.destroy()
sampler.destroy()

Complete Example

javascript
const { Gpu, TextureUsage } = require('@sylphx/webgpu')

async function textureExample() {
  const gpu = Gpu.create()
  const adapter = gpu.requestAdapter({ powerPreference: 'high-performance' })
  const device = adapter.requestDevice()

  // Create texture
  const width = 256
  const height = 256
  const texture = device.createTexture({
    size: { width, height, depthOrArrayLayers: 1 },
    format: 'rgba8unorm',
    usage: TextureUsage.TEXTURE_BINDING | TextureUsage.COPY_DST | TextureUsage.COPY_SRC,
    dimension: '2d',
    mipLevelCount: 1,
    sampleCount: 1
  })

  // Create gradient image
  const data = new Uint8Array(width * height * 4)
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const i = (y * width + x) * 4
      data[i] = (x / width) * 255      // R
      data[i + 1] = (y / height) * 255 // G
      data[i + 2] = 128                // B
      data[i + 3] = 255                // A
    }
  }

  // Upload to GPU
  device.queueWriteTexture(
    { texture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
    Buffer.from(data.buffer),
    { offset: 0, bytesPerRow: width * 4, rowsPerImage: height },
    { width, height, depthOrArrayLayers: 1 }
  )

  console.log('✅ Texture created and uploaded')

  // Cleanup
  texture.destroy()
  device.destroy()
}

textureExample()

Next Steps

Released under the MIT License.