three.js DataTexture 深度解析:用原始缓冲区数据创建 GPU 纹理的原理与实践
DataTexture 是 three.js 中直接基于原始 TypedArray 缓冲区创建纹理的类,它绕过了 <img>/<canvas> 等图像源,让开发者把任意数值数据(浮点场、噪声场、光追探针、物理仿真状态等)直接上传为 GPU 纹理。本文基于仓库中的 API 文档 docs/pages/DataTexture.html.md 与源码 src/textures/DataTexture.js,系统讲解其构造函数参数、与基类 Texture 的差异、GPU 上传路径,以及它在 GPU 计算、后期处理等模块中的真实应用方式。
一、核心概念:从缓冲区到纹素
官方文档对 DataTexture 的定义是:“Creates a texture directly from raw buffer data”(直接从原始缓冲区数据创建纹理)。关键在于:数据如何被解释,完全取决于 type 和 format 两个参数。
以最常见的组合 UnsignedByteType + RGBAFormat 为例,一个纹素(texel)需要 4 个字节,依次是 R、G、B、A(通常表示不透明度);因此一张 width × height 的纹理,其缓冲区长度应为 width * height * 4 个 Uint8Array 元素。若换成 FloatType + RGBAFormat,则一个纹素占 4 个 Float32Array 浮点值——这正是 GPU 计算场景中把每个像素位置存储 4 个浮点状态的原因。
从源码结构看,DataTexture 继承自 Texture(再向上是 EventDispatcher),构造时并不创建独立的存储,而是把三元组打包成一个 image 对象:
// src/textures/DataTexture.js
this.image = { data: data, width: width, height: height };
也就是说 DataTexture 的“图像”就是 { data, width, height } 结构。渲染器读取 texture.image.data 并配合 format/type 决定字节布局,这与 Texture 基类中 image 通常是 HTMLImageElement/Canvas 的约定不同(DataTexture 重写了 .image 属性,见 src/textures/DataTexture.js#L50)。
二、构造函数参数全解
文档给出的完整签名及默认值如下(与 src/textures/DataTexture.js#L32 中的实现一致):
new DataTexture(
data, // TypedArray,缓冲区数据,默认 null
width, // 纹理宽度,默认 1
height, // 纹理高度,默认 1
format, // 纹理格式,默认 RGBAFormat
type, // 数据类型,默认 UnsignedByteType
mapping, // 映射方式,默认 Texture.DEFAULT_MAPPING(UVMapping)
wrapS, // U 方向环绕方式,默认 ClampToEdgeWrapping
wrapT, // V 方向环绕方式,默认 ClampToEdgeWrapping
magFilter, // 放大滤波器,默认 NearestFilter
minFilter, // 缩小滤波器,默认 NearestFilter
anisotropy,// 各向异性值,默认 Texture.DEFAULT_ANISOTROPY(1)
colorSpace // 色彩空间,默认 NoColorSpace
)
各参数说明:
| 参数 | 含义 | 默认值 | 备注 |
|---|---|---|---|
data |
缓冲区数据(TypedArray) | null |
长度须等于 width * height * 每纹素通道数 |
width |
纹理宽度 | 1 |
使用后不可更改,需 dispose() 后重建 |
height |
纹理高度 | 1 |
同上 |
format |
通道布局(如 RGBAFormat、RGFormat、RedFormat) |
RGBAFormat |
决定每纹素分量数 |
type |
元素类型(如 UnsignedByteType、FloatType) |
UnsignedByteType |
决定每个分量的字节与取值范围 |
mapping |
纹理映射模式 | UVMapping |
通常为默认值 |
wrapS / wrapT |
U/V 方向超出 [0,1] 时的取样行为 |
ClampToEdgeWrapping |
配合 RepeatWrapping 可实现平铺 |
magFilter |
纹素覆盖多像素时的滤波 | NearestFilter |
与基类 Texture 的 LinearFilter 不同 |
minFilter |
纹素少于一个像素时的滤波 | NearestFilter |
与基类 LinearMipmapLinearFilter 不同 |
anisotropy |
各向异性采样等级 | 1 |
见 Texture.DEFAULT_ANISOTROPY |
colorSpace |
颜色空间标注 | NoColorSpace |
颜色数据通常应标注 SRGBColorSpace 或 LinearSRGBColorSpace |
值得注意的两个“非默认”默认值:DataTexture 的 magFilter/minFilter 默认是 NearestFilter,而基类 Texture 默认是 LinearFilter/LinearMipmapLinearFilter(见 src/textures/Texture.js#L48)。原因很直观:数据纹理(浮点场、查找表等)几乎从不需要插值平滑,取整采样才是安全默认;如果你要展示一个用 DataTexture 存的人眼可见灰度图,需要显式设置 texture.minFilter = LinearFilter。
由于默认不生成 mipmap 且滤波为 Nearest,非 2 的幂尺寸的数据纹理(如 100×100)也能正常工作,不需要像旧式 WebGL 1 约束那样取 2 的幂。
三、重写属性:flipY、generateMipmaps 与 unpackAlignment
文档中“Properties”一节列出了 DataTexture 相对基类的四处覆盖,源码位置在 src/textures/DataTexture.js#L43-L81:
.isDataTexture : boolean (readonly)— 恒为true的类型测试标记,渲染器内部正是靠它走专属上传分支(见下文texture.isDataTexture判断),单元测试也验证了这一点(test/unit/src/textures/DataTexture.tests.js 中分别断言了instanceof Texture与isDataTexture === true)。.flipY : boolean— 覆盖基类的true,默认false。图像纹理上传时通常要做垂直翻转以匹配 CSS 坐标系,而纯数据缓冲区的行顺序由写入者自己控制,翻转毫无意义且浪费一次 GPU 操作。.generateMipmaps : boolean— 覆盖基类的true,默认false。数据纹理(尤其浮点格式)在多数平台上不允许自动gl.generateMipmap;需要多级细节时要手工提供(见下节 mipmaps 机制)。.unpackAlignment— 覆盖基类的4,默认1。它对应gl.pixelStorei(gl.UNPACK_ALIGNMENT, ...),约束缓冲区中每一行的起始字节对齐(合法值 1/2/4/8)。基类 Texture.js#L291 注释中给出了取值含义;DataTexture默认按字节对齐(1),对任意width × 通道数的紧凑布局都安全。渲染器在上传前确实会执行state.pixelStorei( gl.UNPACK_ALIGNMENT, texture.unpackAlignment )(见 src/renderers/webgl/WebGLTextures.js#L932,WebGPU 回退路径同样处理,见 src/renderers/webgl-fallback/utils/WebGLTextureUtils.js#L335)。
继承来的其余能力(repeat、offset、rotation、matrix、colorSpace、userData 等)在 DataTexture 上依然有效,因为 Texture 的构造器会把它们全部初始化好。
四、GPU 上传路径:isDataTexture 分支与 mipmaps 机制
WebGLRenderer 上传纹理时,WebGLTextures.js 中有一条专门针对数据纹理的分支,可以印证文档中各项参数如何落到 GPU:
// src/renderers/webgl/WebGLTextures.js(uploadTexture 内)
} else if ( texture.isDataTexture ) {
// use manually created mipmaps if available
// if there are no manual mipmaps
// set 0 level mipmap and then use GL to generate other mipmap levels
if ( mipmaps.length > 0 ) {
// 用 texStorage2D 预分配,再逐级 texSubImage2D 上传 mipmap.data
...
texture.generateMipmaps = false;
} else {
// 无手工 mipmap:先分配存储,再 updateTexture(内部 texSubImage2D image.data)
...
}
}
由此得到三条实用结论:
- 手工 mipmaps 优先:往
texture.mipmaps数组里塞入一组{ data, width, height }对象(每级尺寸减半),渲染器会逐级调用texSubImage2D/texImage2D上传,并强制把generateMipmaps置为false(src/renderers/webgl/WebGLTextures.js#L978-L1006)。这是浮点纹理获得多级细节的唯一可靠途径。 - 首次上传走
texStorage2D + texSubImage2D:WebGL2 路径先用texStorage2D一次性分配不可变存储,再写入首级数据;老路径则直接texImage2D(..., image.data)(src/renderers/webgl/WebGLTextures.js#L1026)。 - 更新走版本计数:修改
data缓冲区后必须texture.needsUpdate = true,它会让version自增并触发下次渲染重新上传(src/textures/Texture.js#L754-L763)。此外Texture还提供addUpdateRange( start, count )/clearUpdateRanges()(src/textures/Texture.js#L442-L455)用于只更新部分数据,配合渲染器内的texSubImage2D区域写入(src/renderers/webgl/WebGLTextures.js#L805、#L886),适合每帧只变化少数行的高频数据。
注意纹理属性变更(如改 wrapS、format)会影响渲染器的缓存键(getTextureCacheKey 中 unpackAlignment、flipY、colorSpace 等都参与拼接);而如文档所述,纹理尺寸、格式、类型在首次使用后不可更改——正确做法是 dispose() 旧纹理再创建新的。
五、实战示例
5.1 创建一张 RGBA 浮点数据纹理
GPU 仿真的典型起点,与仓库中 GPUComputationRenderer 的变量纹理思路一致(其头注释明确说明:变量是“RGBA float textures that hold 4 floats for each compute element”):
import { DataTexture, FloatType, RGBAFormat, NearestFilter } from 'three';
const size = 256;
const data = new Float32Array( size * size * 4 );
// 初始化:把位置 (x, y) 写入每个纹素的前两个分量
for ( let i = 0; i < size * size; i ++ ) {
data[ i * 4 + 0 ] = ( i % size ) / size;
data[ i * 4 + 1 ] = Math.floor( i / size ) / size;
data[ i * 4 + 2 ] = 0;
data[ i * 4 + 3 ] = 1;
}
const field = new DataTexture( data, size, size, RGBAFormat, FloatType );
field.minFilter = NearestFilter;
field.magFilter = NearestFilter;
field.wrapS = field.wrapT = THREE.ClampToEdgeWrapping;
field.needsUpdate = true; // 首帧上传
// 之后每帧修改 data 内容后:
field.needsUpdate = true;
5.2 创建单通道字节纹理(如查找表 / 噪声图)
import { DataTexture, UnsignedByteType, RedFormat } from 'three';
const w = 8, h = 8;
const lut = new Uint8Array( w * h );
for ( let i = 0; i < w * h; i ++ ) lut[ i ] = i;
const table = new DataTexture( lut, w, h, RedFormat, UnsignedByteType );
table.colorSpace = NoColorSpace; // 数据纹理保持 NoColorSpace
table.minFilter = table.magFilter = THREE.NearestFilter;
table.needsUpdate = true;
5.3 手工提供 mipmaps
当数据纹理用于可视化的灰度场且希望缩小取样平滑时:
const mipmaps = [];
let levelData = data, levelW = size, levelH = size;
do {
mipmaps.push( { data: levelData, width: levelW, height: levelH } );
levelW = Math.max( 1, levelW >> 1 );
levelH = Math.max( 1, levelH >> 1 );
// 此处省略实际的降采样计算
} while ( levelW > 1 || levelH > 1 );
texture.mipmaps = mipmaps;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.generateMipmaps = false; // 渲染器检测到手工 mipmaps 后也会置 false
texture.needsUpdate = true;
六、仓库中的真实应用场景
DataTexture 是 three.js 生态中“数值 ↔ GPU”的桥梁,仓库内至少四类典型用法:
- GPU 计算:GPUComputationRenderer 用 RGBA 浮点数据纹理表示每个计算单元的状态,通过 ping-pong 渲染目标迭代求解;
webgl_gpgpu_*系列示例(鸟群、流体、地形演化)都建立在这一模式上。 - 后期处理中间结果:GTAOPass、SSAOPass 等 pass 用
DataTexture作为深度/法线探针与噪声查找表的容器。 - 光照查找表:RectAreaLightTexturesLib 预计算片元辐射度函数表(DFG),本质就是一组
DataTexture。 - 加载器输出:KTX2Loader、IESLoader、HDRCubeTextureLoader 等解码压缩/特殊格式纹理后,都产出
DataTexture供材质采样——压缩纹理的 mip 数据同样以{ data, width, height }结构进入texture.mipmaps。
七、常见问题与注意事项
- 缓冲区长度不匹配:
data长度必须恰好等于width * height * 通道数 * BYTES_PER_ELEMENT对应容量,否则采样结果错乱或 WebGL 报错。 - 改数据不重传:只改 TypedArray 内容而不设
needsUpdate = true,GPU 侧仍是旧数据(version不增长,渲染器不会重新上传)。 - 尺寸/格式不可变:首次上传后
width、height、format、type均不可更改;需要变更时dispose()后重建纹理(基类注释明确提示,见 src/textures/Texture.js#L24-L30)。 - 浮点纹理与 mipmap:不要依赖
generateMipmaps;需要多级细节就按 5.3 节手工构建mipmaps数组。 - 色彩空间:数据纹理默认
NoColorSpace,除非你存放的确实是 sRGB 颜色数据,否则不要标注SRGBColorSpace。 - 克隆与复制:
texture.copy()会同时拷贝flipY、unpackAlignment、colorSpace等被覆盖属性(src/textures/Texture.js#L505-L509),并置needsUpdate = true;toJSON序列化输出中也包含这些字段(#L621-L625),可被 ObjectLoader 还原。
参考路径
- API 文档:docs/pages/DataTexture.html.md
- 类实现:src/textures/DataTexture.js
- 基类:src/textures/Texture.js
- 上传实现:src/renderers/webgl/WebGLTextures.js
- 单元测试:test/unit/src/textures/DataTexture.tests.js
- 应用范例:examples/jsm/misc/GPUComputationRenderer.js、examples/jsm/postprocessing/GTAOPass.js、examples/jsm/lights/RectAreaLightTexturesLib.js
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00