关于 threejs-textures
面向 Three.js 开发者的纹理参考技能,讲解纹理加载、色彩空间、映射与渲染目标,并涵盖 WebGLRenderer 和 WebGPURenderer。
主要功能
- 01加载图像、视频及原始数据纹理
- 02设置色彩空间、平铺和过滤
- 03处理 KTX2 压缩纹理与环境贴图
- 04使用渲染目标和动态反射
- 05说明 UV 通道、纹理图集与释放
适用场景
- 01为材质配置颜色和数据贴图
- 02调节纹理平铺、偏移及过滤
- 03在场景中使用 HDR 环境贴图
- 04创建动态反射或渲染目标
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cloudai-x/ threejs-skills --skill threejs-textures安装后在 Claude Code 中输入/threejs-textures确认已加载。
详细介绍
Three.js 纹理
适用于 three.js r186。示例使用 WebGLRenderer;WebGPU 和 TSL 部分使用 WebGPURenderer。
快速入门
import * as THREE from "three";
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 2);
// Color images must be tagged sRGB or they render too bright and washed out
const texture = new THREE.TextureLoader().load("diffuse.jpg");
texture.colorSpace = THREE.SRGBColorSpace;
const mesh = new THREE.Mesh(
new THREE.PlaneGeometry(2, 2),
new THREE.MeshBasicMaterial({ map: texture }),
);
scene.add(mesh);
renderer.setAnimationLoop(animate);
function animate() {
mesh.rotation.z += 0.01;
renderer.render(scene, camera);
}
加载
// load() returns the texture at once; its image arrives asynchronously
const texture = new THREE.TextureLoader().load("diffuse.jpg");
texture.colorSpace = THREE.SRGBColorSpace;
mesh.material.map = texture; // before the first render; afterwards also set mesh.material.needsUpdate = true
// When the next step needs the loaded image, await it
const colorMap = await new THREE.TextureLoader().loadAsync("diffuse.jpg");
colorMap.colorSpace = THREE.SRGBColorSpace;
TextureLoader 的 onProgress 参数不受支持,也不会触发;LoadingManager 会在每个文件加载完成时报告进度:
const manager = new THREE.LoadingManager();
manager.onProgress = (url, loaded, total) => console.log(`${loaded}/${total}: ${url}`);
const loader = new THREE.TextureLoader(manager);
色彩空间
默认启用色彩管理,每个纹理的初始值都是 NoColorSpace:
- LDR 色彩图像使用
SRGBColorSpace:map、emissiveMap、specularMap、sheen 颜色和 specular 颜色贴图、LDR matcap,以及用作颜色的 canvas 或视频。 - 标量数据使用
NoColorSpace:法线、凹凸、位移、粗糙度、金属度、环境光遮蔽和 alpha 贴图。 - HDR 环境使用
LinearSRGBColorSpace;HDR 加载器会自行设置该值。GLTFLoader 会设置其加载的纹理的色彩空间。
const colorMap = new THREE.TextureLoader().load("color.jpg");
colorMap.colorSpace = THREE.SRGBColorSpace; // authored color image
const normalMap = new THREE.TextureLoader().load("normal.jpg");
// normalMap stays at NoColorSpace: it is data, not color
变换:环绕、重复、偏移、旋转
const texture = new THREE.TextureLoader().load("diffuse.jpg");
// Tiling requires repeat wrapping; the default clamps edge pixels
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4); // tile 4x4
texture.offset.set(0.5, 0.5); // shift in UV units
texture.center.set(0.5, 0.5); // pivot for rotation
texture.rotation = Math.PI / 4; // radians
MirroredRepeatWrapping 会在每次重复时镜像翻转进行平铺。
过滤、Mipmaps、各向异性
const texture = new THREE.TextureLoader().load("diffuse.jpg");
// Minification (many texture pixels per screen pixel)
texture.minFilter = THREE.LinearMipmapLinearFilter; // default: trilinear blend between mip levels
texture.minFilter = THREE.NearestFilter; // hard pixel look, samples only the base level
// Magnification (texture magnified on screen)
texture.magFilter = THREE.LinearFilter; // default
texture.magFilter = THREE.NearestFilter; // pixelated
// Mipmaps are generated whenever generateMipmaps is true, whatever the filters.
// Turn generation off to save memory (UI sprites, data textures):
texture.generateMipmaps = false;
texture.minFilter = THREE.LinearFilter; // and do not request a mipmap filter
// Sharper at grazing angles; clamp to what the hardware supports
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
纹理类型
适用于任何图像元素的基类:
const image = new Image();
image.src = "sprite.png";
const imageTexture = new THREE.Texture(image);
imageTexture.colorSpace = THREE.SRGBColorSpace;
image.onload = () => (imageTexture.needsUpdate = true); // upload once the pixels exist
从原始类型化数组像素创建的 DataTexture。它的默认值与 Texture 不同:两个过滤器都使用 NearestFilter,flipY 为 false,且禁用 mipmaps:
const size = 256;
const data = new Uint8Array(size * size * 4); // RGBA, one byte per channel
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
const index = (i * size + j) * 4;
data[index] = i;
data[index + 1] = j;
data[index + 2] = 128;
data[index + 3] = 255;
}
}
const dataTexture = new THREE.DataTexture(data, size, size);
dataTexture.needsUpdate = true;
用于动态绘制纹理的 CanvasTexture(构造时会设置 needsUpdate):
const canvas = document.createElement("canvas");
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#404040";
ctx.fillRect(0, 0, 256, 256);
ctx.fillStyle = "#ffffff";
ctx.font = "48px sans-serif";
ctx.fillText("Hello", 40, 150);
const canvasTexture = new THREE.CanvasTexture(canvas);
canvasTexture.colorSpace = THREE.SRGBColorSpace; // canvas colors are sRGB
// after each redraw of the canvas:
canvasTexture.needsUpdate = true;
用于播放中的 HTMLVideoElement 的 VideoTexture(每帧重新上传,禁用 mipmaps):
const video = document.createElement("video");
video.src = "video.mp4";
video.loop = true;
video.muted = true;
video.play();
const videoTexture = new THREE.VideoTexture(video);
videoTexture.colorSpace = THREE.SRGBColorSpace;
适用于 WebCodecs 的 VideoFrameTexture:手动推送每个解码后的帧:
const frameTexture = new THREE.VideoFrameTexture();
frameTexture.colorSpace = THREE.SRGBColorSpace;
const decoder = new VideoDecoder({
output(frame) {
if (frameTexture.image instanceof VideoFrame) frameTexture.image.close(); // free the replaced frame, or decoding stalls
frameTexture.setFrame(frame);
},
error: (e) => console.error(e),
});
HTMLTexture 通过浏览器的 HTML-in-Canvas API 绘制 HTML 元素:渲染器会将该元素移入自己的画布,并在绘制事件发生时重新上传;如果浏览器不支持该 API,纹理将保持空白。
压缩纹理(KTX2)
import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js";
const ktx2Loader = new KTX2Loader();
// No setTranscoderPath call: the transcoder loads from the three package.
// Set one only when self-hosting the decoders, pinned to this release.
ktx2Loader.detectSupport(renderer); // required before loading; works with both renderers
const compressedMap = await ktx2Loader.loadAsync("diffuse.ktx2");
// colorSpace comes from the file's transfer function; nothing to set by hand
Basis Universal KTX2 会转码为 GPU 支持的压缩格式:与以未压缩形式上传的 PNG 或 JPEG 相比,上传内容更小、占用内存更少(如果 GPU 不支持任何目标格式,KTX2Loader 也会回退到未压缩的 RGBA)。压缩纹理无法在上传时翻转或生成 mipmaps:flipY 始终为 false,因此应按 UV 所需的方向制作文件;mipmaps 必须嵌入文件中。
环境贴图
立方体贴图(加载器会自行将结果标记为 sRGB):
const cubeTexture = new THREE.CubeTextureLoader().load([
"px.jpg", "nx.jpg", // +X, -X
"py.jpg", "ny.jpg", // +Y, -Y
"pz.jpg", "nz.jpg", // +Z, -Z
]);
scene.background = cubeTexture;
scene.environment = cubeTexture; // lights Standard and Physical materials (WebGLRenderer also Lambert and Phong)
HDR 等距柱状文件——设置 mapping 后再赋值;两个渲染器都会自行对环境进行预过滤:
import { HDRLoader } from "three/addons/loaders/HDRLoader.js";
const env = await new HDRLoader().loadAsync("environment.hdr");
env.mapping = THREE.EquirectangularReflectionMapping; // without it the background draws as a flat screen quad
scene.environment = env;
scene.background = env;
EXRLoader(three/addons/loaders/EXRLoader.js)以相同方式加载 .exr 文件,UltraHDRLoader 则加载 Ultra HDR JPEG;两者都会自行设置线性色彩空间。
场景级控制项:
scene.environmentIntensity = 0.5; // dim the image-based lighting
scene.environmentRotation.y = Math.PI / 4;
scene.backgroundBlurriness = 0.5; // 0-1
scene.backgroundIntensity = 1.0;
scene.backgroundRotation.y = Math.PI;
两个渲染器都会在内部将等距柱状环境转换为更大的立方体贴图(采样效果更好,但占用更多内存);如果内存用量很重要,请使用较低分辨率的等距柱状文件。
PMREMGenerator 用于生成环境——例如使用 fromScene 捕获 RoomEnvironment 这样的场景:
import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
const pmrem = new THREE.PMREMGenerator(renderer);
const room = new RoomEnvironment();
scene.environment = pmrem.fromScene(room, 0.04).texture;
room.dispose(); // frees the room's geometries and materials
pmrem.dispose();
不要为了“获得更好的反射”而将已加载的等距柱状文件传入 PMREMGenerator——设置好 mapping 后直接赋值即可获得相同的预过滤效果。
渲染目标
const renderTarget = new THREE.WebGLRenderTarget(512, 512);
const rtScene = new THREE.Scene(); // must not contain the mesh that samples the target
rtScene.add(new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshNormalMaterial())); // unlit, visible without lights
renderer.setRenderTarget(renderTarget);
renderer.render(rtScene, camera);
renderer.setRenderTarget(null); // back to the canvas
// the output is a texture like any other
mesh.material.map = renderTarget.texture;
将深度缓冲区用作纹理:
const depthTarget = new THREE.WebGLRenderTarget(512, 512);
depthTarget.depthTexture = new THREE.DepthTexture(512, 512); // default type UnsignedIntType
MSAA 和多个附件:
// MSAA: resolved into .texture automatically after rendering
const msaaTarget = new THREE.WebGLRenderTarget(512, 512, { samples: 4 });
// MRT: N color attachments, read back through .textures
const mrtTarget = new THREE.WebGLRenderTarget(512, 512, { count: 2 });
const firstAttachment = mrtTarget.textures[0];
const secondAttachment = mrtTarget.textures[1];
动态反射
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, {
generateMipmaps: true,
minFilter: THREE.LinearMipmapLinearFilter,
});
const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget);
scene.add(cubeCamera);
mesh.material.envMap = cubeRenderTarget.texture;
// each frame: hide the reflective mesh so it does not see itself
mesh.visible = false;
cubeCamera.position.copy(mesh.position);
cubeCamera.update(renderer, scene);
mesh.visible = true;
UV 通道
每个使用 UV 映射的材质纹理都会采样 texture.channel 指定的 UV 属性:0 读取 uv(默认值),1 读取 uv1,2 读取 uv2,3 读取 uv3。因此,aoMap 和 lightMap 使用默认的 uv 集;单独展开的 UV 集应放在 uv1 中,并将纹理指向它:
const aoTexture = new THREE.TextureLoader().load("ao.jpg");
const geometry = new THREE.BoxGeometry(1, 1, 1);
const uvAttribute = geometry.attributes.uv;
const aoUvs = new Float32Array(uvAttribute.count * 2); // fill with the second unwrap
geometry.setAttribute("uv1", new THREE.BufferAttribute(aoUvs, 2));
const material = new THREE.MeshStandardMaterial({ aoMap: aoTexture });
material.aoMap.channel = 1; // sample "uv1" instead of "uv"
const aoMesh = new THREE.Mesh(geometry, material);
scene.add(aoMesh);
纹理图集
一张图像包含多个精灵,每个精灵通过偏移和重复进行选择:
const atlas = await new THREE.TextureLoader().loadAsync("atlas.png");
atlas.colorSpace = THREE.SRGBColorSpace;
// filtering and mipmaps sample across sprite borders: leave padding around each sprite
// one clone per sprite: clones share the image source, so WebGLRenderer uploads it once
function spriteTexture(row, col, gridSize = 2) {
const sprite = atlas.clone();
sprite.offset.set(col / gridSize, 1 - (row + 1) / gridSize);
sprite.repeat.set(1 / gridSize, 1 / gridSize);
return sprite;
}
mesh.material.map = spriteTexture(0, 0); // top-left sprite of a 2x2 sheet
标准材质槽
值得了解的通道约定:aoMap 读取红色通道,roughnessMap 读取绿色通道,metalnessMap 读取蓝色通道(因此可以使用一张打包的遮蔽-粗糙度-金属度图像);alphaMap 读取绿色通道。
const colorMap = new THREE.TextureLoader().load("color.jpg");
const emissiveMap = new THREE.TextureLoader().load("emissive.jpg");
const normalMap = new THREE.TextureLoader().load("normal.jpg");
colorMap.colorSpace = THREE.SRGBColorSpace;
emissiveMap.colorSpace = THREE.SRGBColorSpace;
// normalMap keeps NoColorSpace, as do roughness, metalness, ao, alpha and displacement maps
const material = new THREE.MeshStandardMaterial({
map: colorMap,
normalMap: normalMap,
normalScale: new THREE.Vector2(1, 1),
emissiveMap: emissiveMap,
emissive: 0xffffff, // non-black, so the emissive map contributes
roughness: 1, // scalar multipliers for the matching maps
metalness: 0,
});
material.aoMap = new THREE.TextureLoader().load("ao.jpg");
material.aoMapIntensity = 1;
alphaMap 需配合 transparent: true(混合)或 alphaTest(裁切)才会生效;displacementMap 的缩放由 displacementScale 和 displacementBias 控制,并需要细分网格;normalMapType 默认为 TangentSpaceNormalMap,对象空间法线贴图则设为 ObjectSpaceNormalMap。
释放资源与内存
当没有其他未释放的纹理共享该 GPU 上传内容时,texture.dispose() 会释放它(上面的图集克隆共享同一份内容);图像对象仍会保留。仅释放没有其他材质使用的纹理:
function disposeMaterial(material) {
for (const key of Object.keys(material)) {
const value = material[key];
if (value && value.isTexture) {
value.dispose();
}
}
material.dispose();
}
console.log(renderer.info.memory.textures); // texture count currently on the GPU
console.log(renderer.capabilities.maxTextureSize); // largest supported dimension
在多个材质间共享同一个纹理实例,而不是为每个网格分别加载。非 2 的幂次尺寸也没问题:WebGL 2 支持对这类纹理使用 mipmap 和重复环绕。
WebGPU 和 TSL
import * as THREE from "three/webgpu";
import { texture, uv } from "three/tsl";
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // before any other use of the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111); // the WebGPU canvas starts transparent
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 2);
const map = new THREE.TextureLoader().load("diffuse.jpg");
map.colorSpace = THREE.SRGBColorSpace;
map.anisotropy = renderer.getMaxAnisotropy(); // no capabilities object under WebGPU
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), new THREE.MeshBasicNodeMaterial());
mesh.material.colorNode = texture(map, uv().mul(0.5).add(0.25)); // custom UV transform in TSL: the center half, zoomed 2x
scene.add(mesh);
renderer.setAnimationLoop(animate);
function animate() {
mesh.rotation.z += 0.01;
renderer.render(scene, camera);
}
TSL 采样:texture(map) 会使用 uv() 以及纹理自身的偏移、重复和旋转;显式节点会完全接管坐标。经典槽(map、normalMap、roughnessMap、aoMap)在节点材质中无需更改即可使用;colorNode 用于自定义采样。
import * as THREE from "three/webgpu";
import { texture, uv } from "three/tsl";
const map = new THREE.TextureLoader().load("diffuse.jpg");
map.colorSpace = THREE.SRGBColorSpace;
mesh.material.colorNode = texture(map); // uv() plus the texture's offset/repeat/rotation
mesh.material.colorNode = texture(map, uv(1)); // explicit node: samples the "uv1" attribute
KTX2Loader.detectSupport(renderer) 会查询设备功能,因此请在 await renderer.init() 之后调用。动态反射请使用 CubeRenderTarget(而不是 WebGLCubeRenderTarget),并采用相同的隐藏-更新-显示 CubeCamera 模式。
import * as THREE from "three/webgpu";
const cubeRenderTarget = new THREE.CubeRenderTarget(256); // replaces WebGLCubeRenderTarget
const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget);
scene.add(cubeCamera);
mesh.material.envMap = cubeRenderTarget.texture;
mesh.visible = false; // hide so the mesh does not reflect itself
cubeCamera.position.copy(mesh.position);
cubeCamera.update(renderer, scene);
mesh.visible = true;
常见错误
geometry.setAttribute("uv2", geometry.attributes.uv)→material.aoMap = aoTexture;:aoMap 会对texture.channel指定的属性进行采样(默认为 0,即uv),因此无需额外属性;只有channel为 2 的纹理才会读取uv2。单独烘焙的展开 UV 应放在uv1中,并设置aoTexture.channel = 1。new RGBELoader()→new HDRLoader():在 r180 中,RGBELoader 更名为 HDRLoader;旧类仍可运行,但会记录弃用警告。import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js"→import { EXRLoader } from "three/addons/loaders/EXRLoader.js":浏览器导入映射将three/addons/映射到 examples/jsm/,且未定义three/examples/键,因此 examples 路径无法解析。texture.encoding = THREE.sRGBEncoding→texture.colorSpace = THREE.SRGBColorSpace:encoding和sRGBEncoding常量已移除;它们已由colorSpace取代。const texture = loader.load("color.jpg")→const texture = loader.load("color.jpg"); texture.colorSpace = THREE.SRGBColorSpace;:未设置颜色空间(仍为NoColorSpace)的 LDR 颜色图像渲染后会过亮、褪色。texture.minFilter = THREE.LinearFilter; // no mipmaps→texture.generateMipmaps = false;:只要generateMipmaps为 true,就会生成 mipmap,与过滤器无关;因此仅设置 LinearFilter 仍会上传 mipmap。texture.generateMipmaps = false; texture.wrapS = THREE.ClampToEdgeWrapping; // non-power-of-two size→texture.wrapS = THREE.RepeatWrapping;:WebGL 2 支持对非 2 的幂次尺寸纹理使用 mipmap 和重复环绕,因此无需禁用这些功能。texture.anisotropy = renderer.capabilities.getMaxAnisotropy()→texture.anisotropy = renderer.getMaxAnisotropy():WebGPURenderer 没有capabilities对象;使用 WebGPU 时应调用此方法。material.envMapIntensity = 0.5→scene.environmentIntensity = 0.5:envMapIntensity只会调整材质自身envMap的强度,不会调整scene.environment。scene.environment = pmrem.fromEquirectangular(hdr).texture→hdr.mapping = THREE.EquirectangularReflectionMapping; scene.environment = hdr;:渲染器会自行预过滤等距柱状环境贴图,因此手动对已加载文件执行 PMREM 不会带来任何效果。scene.background = hdr→hdr.mapping = THREE.EquirectangularReflectionMapping; scene.background = hdr;:如果未设置mapping,等距柱状纹理会绘制成平面的屏幕空间四边形,而不是环绕场景。ktx2Loader.detectSupportAsync(renderer)→await renderer.init(); ktx2Loader.detectSupport(renderer);:异步版本已在 r181 弃用,且支持检测需要已初始化的设备。ktx2Loader.setTranscoderPath("libs/basis/")→const ktx2Loader = new KTX2Loader(); ktx2Loader.detectSupport(renderer);:转码器会从包中加载,因此只有在自行托管并固定使用 [email protected] 时才需要指定路径。new THREE.WebGLMultipleRenderTargets(width, height, 2)→new THREE.WebGLRenderTarget(width, height, { count: 2 }):该类已在 r162 移除;附件位于renderTarget.textures[0..count-1]。new THREE.WebGLCubeRenderTarget(256)→new THREE.CubeRenderTarget(256):使用 WebGPURenderer 时,应从three/webgpu导入CubeRenderTarget;WebGLCubeRenderTarget仅适用于 WebGL。loader.load(url, onLoad, onProgress, onError)→manager.onProgress = (url, loaded, total) => console.log(url, loaded, total):TextureLoader 不支持onProgress参数,该参数永远不会触发。mesh.material.map = texture; // after the mesh has rendered→mesh.material.map = texture; mesh.material.needsUpdate = true;:WebGLRenderer 会在材质首次渲染时,根据当时已有的贴图为其构建着色器。因此,之后添加的map会保持不可见,直到重建着色器;设置needsUpdate可强制重建。normalMap.colorSpace = THREE.LinearSRGBColorSpace→normalMap.colorSpace = THREE.NoColorSpace:法线、粗糙度、金属度、AO、透明度和位移贴图存储的是数据,而非颜色,因此应保留默认的NoColorSpace。const texture = await new Promise((resolve, reject) => loader.load(url, resolve, undefined, reject))→const texture = await loader.loadAsync(url):loadAsync就是该 Promise,由Loader基类内置提供。cubeCamera.update(renderer, scene)→mesh.visible = false; cubeCamera.update(renderer, scene); mesh.visible = true;:如果反射网格处于可见状态,它就会被绘制到自身envMap所读取的立方体贴图中。
另请参阅
threejs-materials— 纹理槽之外的材质属性、混合和物理材质。threejs-loaders— 加载管理器、glTF 模型以及加载器系列的常规介绍。threejs-shaders— 在 WebGLRenderer 下自定义 GLSL 纹理采样。threejs-lighting— 环境光照和光照单位。threejs-asset-pipeline— 使用 gltf-transform 将模型纹理编码为 KTX2、WebP 或 AVIF。