threejs-textures
Three.js texture workflows for maps, UV mapping, environment maps, and texture configuration.
Install
npx skills add https://github.com/cloudai-x/threejs-skills --skill threejs-texturesThree.js Textures
Targets three.js r186. Examples use WebGLRenderer; the WebGPU and TSL section uses WebGPURenderer.
Quick Start
Loading
TextureLoader's onProgress parameter is unsupported and never fires; a LoadingManager reports progress as each file finishes:
Color Space
Color management is on by default and every texture starts at NoColorSpace:
- LDR color images get
SRGBColorSpace:map,emissiveMap,specularMap, sheen color and specular color maps, LDR matcaps, and canvas or video used as color. - Scalar data keeps
NoColorSpace: normal, bump, displacement, roughness, metalness, ambient-occlusion and alpha maps. - HDR environments are
LinearSRGBColorSpace; the HDR loaders set that themselves. GLTFLoader sets the color spaces of the textures it loads.
Transform: Wrap, Repeat, Offset, Rotation
MirroredRepeatWrapping tiles with a mirror flip on each repeat.
Filtering, Mipmaps, Anisotropy
Texture Types
Base class from any image element:
DataTexture from raw typed-array pixels. Its defaults differ from Texture: NearestFilter for both filters, flipY false, mipmaps off:
CanvasTexture for dynamically drawn textures (sets needsUpdate on construction):
VideoTexture for a playing HTMLVideoElement (re-uploads every frame, mipmaps off):
VideoFrameTexture for WebCodecs: each decoded frame is pushed by hand:
HTMLTexture draws an HTML element through the browser's HTML-in-Canvas API: the renderer moves the element into its own canvas and re-uploads it on paint events; where the browser lacks that API the texture stays empty.
Compressed Textures (KTX2)
Basis Universal KTX2 transcodes to a compressed format the GPU supports: smaller uploads and less memory than PNG or JPEG, which upload uncompressed (where the GPU supports none of the targets, KTX2Loader also falls back to uncompressed RGBA). Compressed textures cannot be flipped or mipmapped at upload: flipY stays false, so author the file in the orientation your UVs expect, and mipmaps must be embedded in the file.
Environment Maps
Cube maps (the loader tags the result sRGB itself):
HDR equirectangular files — set mapping, then assign; both renderers prefilter the environment themselves:
EXRLoader (three/addons/loaders/EXRLoader.js) loads .exr files the same way and UltraHDRLoader loads Ultra HDR JPEGs; both set their own linear color space.
Scene-level controls:
Both renderers convert an equirectangular environment into a cube map with a larger texture internally (better sampling, more memory); when memory matters, ship a lower-resolution equirect file.
PMREMGenerator is for generated environments — a scene such as RoomEnvironment captured with fromScene:
Do not run a loaded equirect file through PMREMGenerator "for better reflections" — direct assignment with mapping set produces the same prefiltering.
Render Targets
Depth buffer as a texture:
MSAA and multiple attachments:
Dynamic Reflections
UV Channels
Every UV-mapped material texture samples the UV attribute chosen by texture.channel: 0 reads uv (the default), 1 reads uv1, 2 reads uv2, 3 reads uv3. So aoMap and lightMap work on the default uv set; a separately unwrapped set goes in uv1 with the texture pointed at it:
Texture Atlas
One image, many sprites, each selected with offset and repeat:
Standard Material Slots
Channel conventions worth knowing: aoMap reads red, roughnessMap reads green, metalnessMap reads blue (so one packed occlusion-roughness-metalness image works); alphaMap reads green.
alphaMap takes effect with transparent: true (blended) or alphaTest (cut out); displacementMap scales with displacementScale and displacementBias and wants a subdivided mesh; normalMapType defaults to TangentSpaceNormalMap, set ObjectSpaceNormalMap for object-space normal maps.
Disposal and Memory
texture.dispose() frees the GPU upload once no other undisposed texture shares it (the atlas clones above share one); the image object remains. Dispose only textures that no other material still uses:
Share one texture instance across materials rather than loading per mesh. Non-power-of-two sizes are fine: WebGL 2 supports mipmaps and repeat wrapping on them.
WebGPU and TSL
TSL sampling: texture(map) uses uv() plus the texture's own offset/repeat/rotation; an explicit node takes over the coordinates entirely. The classic slots (map, normalMap, roughnessMap, aoMap) work unchanged on node materials; colorNode is for custom sampling.
KTX2Loader.detectSupport(renderer) queries device features, so call it after await renderer.init(). For dynamic reflections use CubeRenderTarget (not WebGLCubeRenderTarget) with the same hide-update-show CubeCamera pattern.
Common Mistakes
geometry.setAttribute("uv2", geometry.attributes.uv)→material.aoMap = aoTexture;: aoMap samples the attribute picked bytexture.channel(default 0,uv), so it needs no extra attribute anduv2is read only by textures whosechannelis 2; only a separate baked unwrap goes inuv1, withaoTexture.channel = 1.new RGBELoader()→new HDRLoader(): renamed from RGBELoader to HDRLoader in r180; the old class still runs but logs a deprecation warning.import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js"→import { EXRLoader } from "three/addons/loaders/EXRLoader.js": the browser import maps mapthree/addons/to examples/jsm/ and define nothree/examples/key, so the examples path fails to resolve there.texture.encoding = THREE.sRGBEncoding→texture.colorSpace = THREE.SRGBColorSpace:encodingand thesRGBEncodingconstant were removed;colorSpacereplaced them.const texture = loader.load("color.jpg")→const texture = loader.load("color.jpg"); texture.colorSpace = THREE.SRGBColorSpace;: an LDR color image left atNoColorSpacerenders too bright and washed out.texture.minFilter = THREE.LinearFilter; // no mipmaps→texture.generateMipmaps = false;: mipmaps are generated whenevergenerateMipmapsis true, whatever the filter, so LinearFilter alone still uploads them.texture.generateMipmaps = false; texture.wrapS = THREE.ClampToEdgeWrapping; // non-power-of-two size→texture.wrapS = THREE.RepeatWrapping;: WebGL 2 supports mipmaps and repeat wrapping on non-power-of-two textures, so nothing has to be disabled for them.texture.anisotropy = renderer.capabilities.getMaxAnisotropy()→texture.anisotropy = renderer.getMaxAnisotropy(): WebGPURenderer has nocapabilitiesobject; this is the call under WebGPU.material.envMapIntensity = 0.5→scene.environmentIntensity = 0.5:envMapIntensityscales only the material's ownenvMap, notscene.environment.scene.environment = pmrem.fromEquirectangular(hdr).texture→hdr.mapping = THREE.EquirectangularReflectionMapping; scene.environment = hdr;: the renderer prefilters an equirectangular environment itself, so manual PMREM of a loaded file adds nothing.scene.background = hdr→hdr.mapping = THREE.EquirectangularReflectionMapping; scene.background = hdr;: withoutmapping, an equirectangular texture draws as a flat screen-space quad instead of a surround.ktx2Loader.detectSupportAsync(renderer)→await renderer.init(); ktx2Loader.detectSupport(renderer);: the async variant was deprecated in r181, and support detection needs the initialized device.ktx2Loader.setTranscoderPath("libs/basis/")→const ktx2Loader = new KTX2Loader(); ktx2Loader.detectSupport(renderer);: the transcoder loads from the package, so a path is only needed when self-hosting, pinned to three@0.186.0.new THREE.WebGLMultipleRenderTargets(width, height, 2)→new THREE.WebGLRenderTarget(width, height, { count: 2 }): removed in r162; the attachments arerenderTarget.textures[0..count-1].new THREE.WebGLCubeRenderTarget(256)→new THREE.CubeRenderTarget(256): under WebGPURenderer useCubeRenderTargetimported fromthree/webgpu;WebGLCubeRenderTargetis WebGL-only.loader.load(url, onLoad, onProgress, onError)→manager.onProgress = (url, loaded, total) => console.log(url, loaded, total): TextureLoader'sonProgressparameter is unsupported and never fires.mesh.material.map = texture; // after the mesh has rendered→mesh.material.map = texture; mesh.material.needsUpdate = true;: WebGLRenderer builds a material's shader for the maps it has at its first render, so amapadded later stays invisible until the shader is rebuilt, whichneedsUpdateforces.normalMap.colorSpace = THREE.LinearSRGBColorSpace→normalMap.colorSpace = THREE.NoColorSpace: normal, roughness, metalness, ao, alpha and displacement maps hold data, not color, so they keep the defaultNoColorSpace.const texture = await new Promise((resolve, reject) => loader.load(url, resolve, undefined, reject))→const texture = await loader.loadAsync(url):loadAsyncis that promise, built into theLoaderbase class.cubeCamera.update(renderer, scene)→mesh.visible = false; cubeCamera.update(renderer, scene); mesh.visible = true;: while visible, the reflective mesh is drawn into the cube map that its ownenvMapreads.
See Also
threejs-materials— material properties, blending, and physical materials beyond texture slots.threejs-loaders— loading managers, glTF models, and the loader family generally.threejs-shaders— custom GLSL texture sampling under WebGLRenderer.threejs-lighting— environment lighting and light units.threejs-asset-pipeline— encoding a model's textures to KTX2, WebP or AVIF with gltf-transform.