mcpField
T
threejs-textures

threejs-textures

github.com/cloudai-x创意与媒体3.8k13.3k 次安装1 周前更新
代码仓库

关于 threejs-textures

面向 Three.js 开发者的纹理参考技能,讲解纹理加载、色彩空间、映射与渲染目标,并涵盖 WebGLRenderer 和 WebGPURenderer。

主要功能

  1. 01加载图像、视频及原始数据纹理
  2. 02设置色彩空间、平铺和过滤
  3. 03处理 KTX2 压缩纹理与环境贴图
  4. 04使用渲染目标和动态反射
  5. 05说明 UV 通道、纹理图集与释放

适用场景

  1. 01为材质配置颜色和数据贴图
  2. 02调节纹理平铺、偏移及过滤
  3. 03在场景中使用 HDR 环境贴图
  4. 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。

评论

支持 Markdown