Three.js Cheatsheet

Textures

Use this Three.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Loading Textures

// Basic load (async, fires callback when ready)
const loader = new THREE.TextureLoader();
const texture = loader.load(
  '/textures/diffuse.jpg',
  (tex) => console.log('loaded', tex),   // onLoad
  undefined,                             // onProgress (not supported for textures)
  (err) => console.error(err)            // onError
);

// Synchronous-style with async/await
const texture = await new Promise((resolve, reject) => {
  loader.load(url, resolve, undefined, reject);
});

LoadingManager

const manager = new THREE.LoadingManager(
  () => console.log('all loaded'),       // onLoad
  (url, loaded, total) => console.log(`${loaded}/${total} — ${url}`), // onProgress
  (url) => console.error('error', url)  // onError
);

const loader = new THREE.TextureLoader(manager);
const t1 = loader.load('/tex/a.jpg');
const t2 = loader.load('/tex/b.jpg');
// manager fires onLoad when both t1 and t2 are done

Applying Textures

const mat = new THREE.MeshStandardMaterial({
  map:         colorTex,
  normalMap:   normalTex,
  roughnessMap: roughTex,
  metalnessMap: metalTex,
  aoMap:       aoTex,
  emissiveMap: emissiveTex,
  alphaMap:    alphaTex,
  displacementMap: dispTex,
});

// Apply after creation
mat.map = texture;
mat.needsUpdate = true;

Texture Properties

const tex = loader.load('/tex/diffuse.jpg');

// UV repeat and offset
tex.wrapS      = THREE.RepeatWrapping;  // U axis
tex.wrapT      = THREE.RepeatWrapping;  // V axis
tex.repeat.set(4, 4);    // tile 4x4
tex.offset.set(0, 0);    // UV offset (0–1)
tex.center.set(0.5, 0.5); // rotation pivot
tex.rotation  = Math.PI / 4;

// Filtering
tex.magFilter  = THREE.LinearFilter;           // enlargement
tex.minFilter  = THREE.LinearMipmapLinearFilter; // reduction (default, uses mipmaps)

// Color space
tex.colorSpace = THREE.SRGBColorSpace;   // for color/albedo maps
// Leave as default LinearSRGBColorSpace for normal/roughness/metalness maps

// Flip
tex.flipY      = false;  // GLTF textures need flipY = false

// Mipmaps
tex.generateMipmaps = true;   // default true; set false for DataTexture / RenderTarget
tex.needsUpdate     = true;   // force GPU re-upload

Wrap Modes

ConstantDescription
THREE.RepeatWrappingTile/repeat (most common)
THREE.ClampToEdgeWrappingClamp to border pixel (default)
THREE.MirroredRepeatWrappingMirror every repetition

Filter Modes

FilterNotes
THREE.NearestFilterPixel-art / crisp (no blending)
THREE.LinearFilterBilinear
THREE.NearestMipmapNearestFilterFast, aliased
THREE.NearestMipmapLinearFilterMipmap blending, point sampling
THREE.LinearMipmapNearestFilterBilinear + abrupt mip levels
THREE.LinearMipmapLinearFilterTrilinear (best quality, default minFilter)

magFilter only supports NearestFilter or LinearFilter.

Cube Textures (Skybox)

const cubeLoader = new THREE.CubeTextureLoader();
const cubeTexture = cubeLoader.load([
  '/skybox/px.jpg', '/skybox/nx.jpg',
  '/skybox/py.jpg', '/skybox/ny.jpg',
  '/skybox/pz.jpg', '/skybox/nz.jpg',
  // order: +x, -x, +y, -y, +z, -z
]);
scene.background = cubeTexture;
scene.environment = cubeTexture; // IBL for PBR

HDR / EXR Environment Maps

import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { EXRLoader }  from 'three/addons/loaders/EXRLoader.js';

const pmrem = new THREE.PMREMGenerator(renderer);
pmrem.compileEquirectangularShader();

// RGBE (.hdr)
new RGBELoader().load('/env/studio.hdr', (hdrTex) => {
  const envMap = pmrem.fromEquirectangular(hdrTex).texture;
  scene.environment = envMap;
  scene.background  = envMap;
  hdrTex.dispose();
  pmrem.dispose();
});

// EXR
new EXRLoader().load('/env/scene.exr', (exrTex) => {
  const envMap = pmrem.fromEquirectangular(exrTex).texture;
  scene.environment = envMap;
  exrTex.dispose();
  pmrem.dispose();
});

Canvas Texture (dynamic)

const canvas  = document.createElement('canvas');
canvas.width  = 512;
canvas.height = 512;
const ctx     = canvas.getContext('2d');

ctx.fillStyle = '#000';
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = '#fff';
ctx.font = '72px sans-serif';
ctx.fillText('Hello!', 50, 260);

const tex = new THREE.CanvasTexture(canvas);
tex.needsUpdate = true; // call again whenever canvas is redrawn

// Live update in animate loop
function updateCanvas() {
  ctx.clearRect(0, 0, 512, 512);
  ctx.fillText(Date.now(), 50, 260);
  tex.needsUpdate = true;
}

Video Texture

const video    = document.createElement('video');
video.src      = '/video/clip.mp4';
video.loop     = true;
video.muted    = true;
video.autoplay = true;
video.play();

const videoTex = new THREE.VideoTexture(video);
videoTex.colorSpace = THREE.SRGBColorSpace;

const mat = new THREE.MeshBasicMaterial({ map: videoTex });
// VideoTexture auto-updates each frame while video plays

DataTexture (programmatic)

const size   = 256;
const data   = new Uint8Array(4 * size * size); // RGBA

for (let i = 0; i < size * size; i++) {
  const stride = i * 4;
  data[stride]     = Math.random() * 255; // R
  data[stride + 1] = Math.random() * 255; // G
  data[stride + 2] = Math.random() * 255; // B
  data[stride + 3] = 255;                 // A
}

const dataTex = new THREE.DataTexture(data, size, size);
dataTex.needsUpdate = true;
// Format defaults to THREE.RGBAFormat; use THREE.RGFormat, THREE.RedFormat etc. for single-channel

Render Targets

// Off-screen render target (render scene to texture)
const rt = new THREE.WebGLRenderTarget(1024, 1024, {
  minFilter: THREE.LinearFilter,
  magFilter: THREE.LinearFilter,
  format:    THREE.RGBAFormat,
});

// Render to it
renderer.setRenderTarget(rt);
renderer.render(scene, camera);
renderer.setRenderTarget(null); // back to screen

// Use as texture
material.map = rt.texture;

// Dispose when done
rt.dispose();

Cube Render Target (Reflections)

const cubeRT     = new THREE.WebGLCubeRenderTarget(256);
const cubeCamera = new THREE.CubeCamera(0.1, 100, cubeRT);
scene.add(cubeCamera);

// Update reflection each frame (or just once for static scenes)
cubeCamera.update(renderer, scene);

const mat = new THREE.MeshStandardMaterial({
  envMap: cubeRT.texture,
  metalness: 1,
  roughness: 0,
});

Compressed Textures

// KTX2 (Basis Universal — GPU-native compression, best option)
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';

const ktx2Loader = new KTX2Loader()
  .setTranscoderPath('/basis/')  // path to basis_transcoder.wasm
  .detectSupport(renderer);

ktx2Loader.load('/tex/diffuse.ktx2', (tex) => {
  material.map = tex;
  material.needsUpdate = true;
});

Texture Atlas / Sprite Sheet

const sheet = loader.load('/sprites/sheet.png');
sheet.wrapS = THREE.RepeatWrapping;
sheet.wrapT = THREE.RepeatWrapping;

const cols = 4, rows = 4;
sheet.repeat.set(1 / cols, 1 / rows);

// Set frame (col 2, row 1 in a 4x4 sheet)
const col = 2, row = 1;
sheet.offset.set(col / cols, row / rows);

Gotchas

Always set texture.colorSpace = THREE.SRGBColorSpace on albedo/color textures. Leave normal, roughness, metalness, and AO textures at the default LinearSRGBColorSpace to avoid incorrect shading.

Textures must be power-of-two dimensions (256, 512, 1024, 2048…) for mipmaps. Non-POT textures work but disable mipmaps and clamp wrapping automatically.

GLTF-exported textures should have texture.flipY = false (GLTF uses top-left origin; Three.js default is bottom-left).

needsUpdate = true triggers a full GPU re-upload — avoid calling every frame. VideoTexture and CanvasTexture handle this automatically only when content changes.

Large textures are the most common cause of memory issues. Dispose with texture.dispose() when no longer needed.

When using aoMap or lightMap, the material expects UV coordinates on the uv2 (or uv1 in r152+) attribute. Copy UVs if geometry has only one UV set: geo.setAttribute('uv2', geo.getAttribute('uv')).