Texture

An image on a 3D surface: a GPU texture that a Material samples (map, uniforms), decoded natively by the 3D engine. A 2D sprite's image is a Texture2D — the two are not interchangeable; load the same file into both if a project uses it in both worlds.

  • put an image on a 3D object
  • load a normal map or mask without colour conversion
  • use compressed KTX2 textures to save memory on mobile
  • make a texture from generated pixels
  • show a drawn canvas on a 3D surface
  • stop a texture from repeating at the edges
  • set texture quality: anisotropic filtering and a size cap
  • play a video on a 3D screen

Platforms: iOS, Android, desktop, the web, the web player, the le.codes viewer app. Android (and its viewer app) and the web store a PNG/JPG as colour whatever srgb says — a KTX2's own header decides there; the web has no Texture.fromCanvas and ignores anisotropy / maxSize; headless tests decode nothing and play no video.

At a glance

TypeScript
const main = async () => {
  const scene = new Scene()
  scene.add(Light.sun())
  scene.open()

  const tex = await Texture.load(asset('./crate.jpg'))
  const crate = Mesh.box({ material: Material.lit({ map: tex }) })
  scene.add(crate)
}
main()

Load an image as a texture: Texture.load {#loading}

TypeScript
Texture.load(source: string | FetchResponse | File, options?: {
  srgb?: boolean       // default true: COLOUR, stored sRGB; false = DATA (normal map, mask, heightmap), kept linear
  fullSize?: boolean   // ignore Texture.maxSize for this texture (a lookup table); default false
}): Promise<Texture>

Accepts an asset/remote URL string, an already-fetched FetchResponse, or a File (e.g. from openFilePicker). URL loads reject on HTTP errors (status ≥ 400); every load rejects on a decode failure. PNG / JPG are decoded by the platform, with a mip chain on hosts that build one.

Normal maps, masks and heightmaps: srgb: false

Load a normal map, a mask or a heightmap with srgb: false: it is DATA, kept linear, not colour. A flat normal read through sRGB bends by ~35°. The built-in lit / unlit sample only a colour map, so a data map goes to a uniform of your own shader.

TypeScript
const scene = new Scene()
scene.add(Light.sun())
scene.open()

const main = async () => {
  const brick = await Material.load(asset('./brick.mat'))                        // a shader with a normalMap sampler
  brick.set('normalMap', await Texture.load(asset('./brick_n.png'), { srgb: false }))   // data: kept linear
  scene.add(Mesh.box({ material: brick }))
}
main()
Note

Android and the web (Web Lite) store a PNG/JPG as colour whatever srgb says; a KTX2 carries its colour space in its header, so a data map that must be linear everywhere is a KTX2.

Compressed textures for mobile: KTX2

KTX2 (Basis Universal, detected by content, not extension) is transcoded on load to the best compressed format the GPU has (ASTC, BC7, ETC2 or BC3; uncompressed RGBA8 where none is available) and kept compressed in VRAM, with the file's mip chain. Use KTX2 for large textures on mobile: a 2K RGBA8 costs ~21 MB with mips, the same image compressed ~5 MB. lecodes assets doctor --ktx2 produces them; a KTX2's colour space (sRGB vs linear data map) comes from its own header, whatever srgb says.

A texture from raw pixels: Texture.fromPixels

TypeScript
Texture.fromPixels(width, height, data: Uint8Array, options?: {
  channels?: 1 | 2 | 3 | 4   // bytes per pixel; default 4
  srgb?: boolean             // default true (colour); false = data
}): Texture
tex.update(x, y, width, height, data: Uint8Array): void   // re-upload a rectangle (same channel count)

A texture from raw bytes — row-major, width × height × channels, no mips, copied on the call. fromPixels throws on a host without it, or when data is too short.

TypeScript
const scene = new Scene()
scene.add(Light.sun())
scene.open()

// an 8×8 checkerboard, one byte per pixel (grey)
const size = 8
const px = new Uint8Array(size * size)
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) px[y * size + x] = (x + y) % 2 ? 255 : 40
const checker = Texture.fromPixels(size, size, px, { channels: 1 })
scene.add(Mesh.box({ material: Material.unlit({ map: checker }) }))

px.fill(255, 0, size)                      // later: change the top row
checker.update(0, 0, size, 1, px.subarray(0, size))

A drawn canvas on a 3D surface: Texture.fromCanvas

TypeScript
Texture.fromCanvas(canvas: Canvas): Texture

Bakes a Canvas surface into a texture — synchronous, no decode. The GPU texture is cached on the canvas, so repeated calls (or several materials) share one, and canvas.update() re-uploads it. You rarely call this yourself: assigning a Canvas to material.map (or Material.lit({ map: canvas })) does it for you.

TypeScript
const scene = new Scene()
scene.add(Light.sun())
scene.open()

const label = new Canvas(256, 128)
label.fillStyle = '#1e293b'
label.fillRect(0, 0, 256, 128)
label.fillStyle = '#ffffff'
label.font = '48px sans-serif'
label.fillText('Score 0', 20, 80)
const sign = Mesh.box({ material: Material.unlit({ map: label }) })   // baked to a texture on assign
scene.add(sign)

// later: draw again, then
label.update()   // re-uploads the texture the material samples
Note

material.map = canvas does the same in one step (the material bakes the canvas).

Size and wrap mode (tiling): width, wrapS / wrapT {#properties}

TypeScript
tex.width, tex.height     // pixel dimensions (read-only)
tex.wrapS = 0             // horizontal (U) wrap: 0 clamp-to-edge, 1 repeat (default), 2 mirrored repeat
tex.wrapT = 0             // vertical (V) wrap, same values

wrapS / wrapT are applied when the texture is assigned to a material uniform — set them before material.set('baseColorMap', tex) (or Material.lit({ map: tex })).

TypeScript
const scene = new Scene()
scene.add(Light.sun())
scene.open()

const main = async () => {
  const tiles = await Texture.load(asset('./tiles.png'))
  tiles.wrapS = 0      // clamp-to-edge horizontally
  tiles.wrapT = 2      // mirrored repeat vertically
  scene.add(Mesh.box({ material: Material.lit({ map: tiles }) }))   // set the wrap BEFORE assigning
}
main()
Note

there is no dispose() on Texture. A texture is freed by itself once you drop every reference to it AND no material samples it any more; while a material uses it, it stays. Load big textures once and reuse them.

Texture quality settings: Texture.anisotropy / Texture.maxSize {#engine-wide-settings}

TypeScript
Texture.anisotropy = 4    // anisotropic filtering, 1 (off) … 16 (clamped); default 2
Texture.maxSize = 1024    // texture size cap in px, 0 = none (default)

A "texture quality" menu. Both reach only textures bound or loaded after they are set — a loaded level keeps its textures — so set them before the level loads (SceneOptions.anisotropy / maxTextureSize do it up front, see Scene). Under maxSize a KTX2 loses its top mip levels on load and a glTF image is downsampled; fullSize exempts one texture. A host may pin either (desktop CREATOR_TEXTURE_ANISOTROPY / CREATOR_TEXTURE_MAX_SIZE).

Play a video on a 3D surface: VideoPlayer.texture {#video-textures}

A texture can't be created from a video file directly — play it with a VideoPlayer and sample its texture:

TypeScript
const player = new VideoPlayer(asset('./clip.mp4'))
const screen = Mesh.plane({ material: Material.video(player.texture) })

player.texture reports width/height of 0 (the video's size isn't known at texture-creation time). See Media and Material.video.

Pitfalls

TypeScript
// ✗ feeding a 3D Texture to a Sprite (or a Texture2D to a Material)
new Sprite({ texture: await Texture.load(url) })       // wrong engine — use Texture2D.load
// ✓ each engine loads its own type
new Sprite({ texture: await Texture2D.load(url) })

// ✗ a data map (normal, mask, heightmap) loaded as colour — read back bent through sRGB
material.set('normalMap', await Texture.load(asset('./brick_n.png')))
// ✓ material.set('normalMap', await Texture.load(asset('./brick_n.png'), { srgb: false }))

See also

  • Material — where textures are consumed (map, uniforms)
  • Texture2D — the 2D-engine counterpart
  • Media — VideoPlayer.texture
  • Canvas — drawn surfaces as textures