首页
/ three.js DataTexture 深度解析:用原始缓冲区数据创建 GPU 纹理的原理与实践

three.js DataTexture 深度解析:用原始缓冲区数据创建 GPU 纹理的原理与实践

2026-09-06 17:51:02作者:尤峻淳Whitney

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”(直接从原始缓冲区数据创建纹理)。关键在于:数据如何被解释,完全取决于 typeformat 两个参数

以最常见的组合 UnsignedByteType + RGBAFormat 为例,一个纹素(texel)需要 4 个字节,依次是 R、G、B、A(通常表示不透明度);因此一张 width × height 的纹理,其缓冲区长度应为 width * height * 4Uint8Array 元素。若换成 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 通道布局(如 RGBAFormatRGFormatRedFormat RGBAFormat 决定每纹素分量数
type 元素类型(如 UnsignedByteTypeFloatType UnsignedByteType 决定每个分量的字节与取值范围
mapping 纹理映射模式 UVMapping 通常为默认值
wrapS / wrapT U/V 方向超出 [0,1] 时的取样行为 ClampToEdgeWrapping 配合 RepeatWrapping 可实现平铺
magFilter 纹素覆盖多像素时的滤波 NearestFilter 与基类 TextureLinearFilter 不同
minFilter 纹素少于一个像素时的滤波 NearestFilter 与基类 LinearMipmapLinearFilter 不同
anisotropy 各向异性采样等级 1 Texture.DEFAULT_ANISOTROPY
colorSpace 颜色空间标注 NoColorSpace 颜色数据通常应标注 SRGBColorSpaceLinearSRGBColorSpace

值得注意的两个“非默认”默认值:DataTexturemagFilter/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 TextureisDataTexture === 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)。

继承来的其余能力(repeatoffsetrotationmatrixcolorSpaceuserData 等)在 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)
    ...
  }
}

由此得到三条实用结论:

  1. 手工 mipmaps 优先:往 texture.mipmaps 数组里塞入一组 { data, width, height } 对象(每级尺寸减半),渲染器会逐级调用 texSubImage2D/texImage2D 上传,并强制把 generateMipmaps 置为 falsesrc/renderers/webgl/WebGLTextures.js#L978-L1006)。这是浮点纹理获得多级细节的唯一可靠途径。
  2. 首次上传走 texStorage2D + texSubImage2D:WebGL2 路径先用 texStorage2D 一次性分配不可变存储,再写入首级数据;老路径则直接 texImage2D(..., image.data)src/renderers/webgl/WebGLTextures.js#L1026)。
  3. 更新走版本计数:修改 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),适合每帧只变化少数行的高频数据。

注意纹理属性变更(如改 wrapSformat)会影响渲染器的缓存键(getTextureCacheKeyunpackAlignmentflipYcolorSpace 等都参与拼接);而如文档所述,纹理尺寸、格式、类型在首次使用后不可更改——正确做法是 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”的桥梁,仓库内至少四类典型用法:

  1. GPU 计算GPUComputationRenderer 用 RGBA 浮点数据纹理表示每个计算单元的状态,通过 ping-pong 渲染目标迭代求解;webgl_gpgpu_* 系列示例(鸟群、流体、地形演化)都建立在这一模式上。
  2. 后期处理中间结果GTAOPassSSAOPass 等 pass 用 DataTexture 作为深度/法线探针与噪声查找表的容器。
  3. 光照查找表RectAreaLightTexturesLib 预计算片元辐射度函数表(DFG),本质就是一组 DataTexture
  4. 加载器输出KTX2LoaderIESLoaderHDRCubeTextureLoader 等解码压缩/特殊格式纹理后,都产出 DataTexture 供材质采样——压缩纹理的 mip 数据同样以 { data, width, height } 结构进入 texture.mipmaps

七、常见问题与注意事项

  • 缓冲区长度不匹配data 长度必须恰好等于 width * height * 通道数 * BYTES_PER_ELEMENT 对应容量,否则采样结果错乱或 WebGL 报错。
  • 改数据不重传:只改 TypedArray 内容而不设 needsUpdate = true,GPU 侧仍是旧数据(version 不增长,渲染器不会重新上传)。
  • 尺寸/格式不可变:首次上传后 widthheightformattype 均不可更改;需要变更时 dispose() 后重建纹理(基类注释明确提示,见 src/textures/Texture.js#L24-L30)。
  • 浮点纹理与 mipmap:不要依赖 generateMipmaps;需要多级细节就按 5.3 节手工构建 mipmaps 数组。
  • 色彩空间:数据纹理默认 NoColorSpace,除非你存放的确实是 sRGB 颜色数据,否则不要标注 SRGBColorSpace
  • 克隆与复制texture.copy() 会同时拷贝 flipYunpackAlignmentcolorSpace 等被覆盖属性(src/textures/Texture.js#L505-L509),并置 needsUpdate = truetoJSON 序列化输出中也包含这些字段(#L621-L625),可被 ObjectLoader 还原。

参考路径

登录后查看全文
热门项目推荐
相关项目推荐