首页
/ three.js StorageTexture 全解析:借助 WebGPU Compute Shader 将数据写入纹理

three.js StorageTexture 全解析:借助 WebGPU Compute Shader 将数据写入纹理

2026-09-08 11:20:07作者:董宙帆

StorageTexture 是 three.js 中专门面向 Compute Shader(计算着色器) 设计的一类特殊纹理,它允许程序在 GPU 侧用计算着色器逐像素计算并写入纹理数据,随后在渲染管线中像普通纹理一样被采样。本指南以官方文档 docs/pages/StorageTexture.html.md 为骨架,结合源码与仓库示例,系统讲解其构造参数、属性、方法、WebGPU 底层绑定原理,并给出可运行的完整计算纹理实战方案。

three.js webgpu compute texture 示例截图:由 StorageTexture 承载的计算着色器输出被采样渲染在平面网格上

StorageTexture 是什么

在 WebGPU 渲染后端中,存在两类用途完全不同的纹理资源:

  • 传统纹理:作为只读采样资源喂给渲染着色器(fragment 阶段);
  • 存储纹理(storage texture):作为可写资源被绑定到 Compute Pass 中,供计算着色器直接写入像素数据。

StorageTexture 正是 three.js 对后者的抽象封装。官方文档明确说明:This special type of texture is intended for compute shaders. It can be used to compute the data of a texture with a compute shader.(这种特殊纹理专为计算着色器设计,可用计算着色器计算纹理数据。)

它的类继承关系为:

EventDispatcher → Texture → StorageTexture

即它本质上是 Texture 的子类,保留了普通纹理全部的基础能力(坐标、颜色空间、版本管理等),额外承担了"由 compute shader 输出数据"的职责。因此,计算完成后的 StorageTexture 既能通过 TSL 采样节点被普通材质读取,也能作为后续 Compute Pass 的输入参与迭代运算。

关键限制:仅适用于 WebGPURenderer

文档明确强调:StorageTexture 只能与 WebGPURenderer(WebGPU 后端)配合使用。原因在于"可写存储纹理"这一能力来自 WebGPU API 的 STORAGE_BINDING 使用标志与 WGSL 的 textureStore() 内建函数,而 WebGL 后端没有对应的 compute shader 存储绑定语义。在项目中使用时,应从 examples/webgpu_compute_texture.html 等方式先经 WebGPU 能力检测WebGPU.isAvailable())确认环境支持。

构造器:创建存储纹理

StorageTexture 的构造器不需要任何图像数据源,只需指定逻辑尺寸:

new StorageTexture( width, height )
  • width:纹理宽度,默认 1
  • height:纹理高度,默认 1

它没有沿用普通 Texture 需要 image 的载入方式,而是直接以宽高建立一张"空数据"的可写纹理,数据完全由随后的计算着色器填充。下面的源码印证了这一点,StorageTexture.js 构造器

constructor( width = 1, height = 1 ) {

    super();

    this.image = { width, height };   // image 仅承载宽高维度信息
    this.magFilter = LinearFilter;    // 过滤默认值被改写
    this.minFilter = LinearFilter;
    this.isStorageTexture = true;
    this.mipmapsAutoUpdate = true;

}

在创建更大尺寸(如 512×512)时只需:

const storageTexture = new THREE.StorageTexture( width, height );

属性详解

.image : Object

StorageTextureimage 字段不再是真实的像素数据源(如 HTMLImageElement 或 canvas),而仅仅是一个承载维度的占位对象:

this.image = { width, height };

它是构造器设置的尺寸映射,也是 setSize() 方法修改的目标。此项覆写了基类 Texture#image 的语义。

.isStorageTexture : boolean(只读)

类型判定标志,默认 true。该标志是 three.js 各处代码识别 StorageTexture 的统一手段,例如 WebGPU 后端创建底层纹理资源时会检查它(见下文"底层原理")。用户侧也可用于自检:

if ( texture.isStorageTexture === true ) {
    // 这是一张存储纹理
}

除本类外,同目录下的数组/三维变体 StorageArrayTexture.jsStorage3DTexture.js 也把该标志置为 true

.magFilter / .minFilter : number

StorageTexture 将过滤默认值统一改写为 THREE.LinearFilter(见 源码第 37/44 行),覆写了基类的默认配置。文档指出:

  • .magFilter 默认 THREE.LinearFilter
  • .minFilter 默认 THREE.LinearFilter

这是与"写入式"纹理的定位匹配的设计:默认线性过滤意味着采样不需要 mipmap 链,避免为尚未填好数据的纹理自动生成 mip 序列。若确实需要 mip 采样(如把 minFilter 改为 THREE.LinearMipMapLinearFilter),则应配合下面的 mipmapsAutoUpdate 使用——官方示例 webgpu_compute_texture.html 中即保留了这行注释掉的切换写法。

.mipmapsAutoUpdate : boolean

控制 compute 写入完成后 mipmap 链的更新策略:

  • true(默认):计算着色器写入后,mipmap 会被自动生成
  • false:mipmap 必须由你自己通过 compute shader 手动写入

自动生成机制在渲染管线的绑定更新流程中体现。当一张 StorageTexture 的绑定属性 store === true(本次作为写入目标)且 mipmapsAutoUpdate === true 时,Bindings.js 会将该纹理标记为需要重建 mip:

if ( texture.isStorageTexture === true && texture.mipmapsAutoUpdate === true ) {

    if ( binding.store === true ) {
        textureData.needsMipmap = true;
    } else if ( this.textures.needsMipmaps( texture ) && textureData.needsMipmap === true ) {
        this.backend.generateMipmaps( texture );
        textureData.needsMipmap = false;
    }
}

与此同时,Textures.js 在纹理更新分支用同一开关跳过默认自动生成路径,把 mip 生命周期完全交给上述绑定流程(或关闭开关后由手动 compute 写入):

const skipAutoGeneration = texture.isStorageTexture === true && texture.mipmapsAutoUpdate === false;
if ( options.needsMipmaps && texture.mipmaps.length === 0 && ! skipAutoGeneration ) {
    backend.generateMipmaps( texture );
}

使用提示:若你只把计算结果当平面纹理直接采样(无 mipmap 需求),保持默认即可;若需要高质量缩小采样,可开启 mip 过滤并依赖自动更新;若想完全自定义各 mip 层的数据,则关闭自动更新并利用 TSL 层 StorageTextureNode.mipLevel 在 shader 中逐级写入。

方法:setSize

.setSize( width, height )

动态调整存储纹理的尺寸。源码实现的关键在于——只有尺寸真正发生变化时才执行更新与重建,且重建通过调用 dispose() 触发底层资源重新分配:

setSize( width, height ) {

    if ( this.image.width !== width || this.image.height !== height ) {

        this.image.width = width;
        this.image.height = height;

        this.dispose();

    }

}

从源码结构看,该方法适合在计算分辨率动态变化(如视口尺寸调整、需要重建数据分辨率)时复用同一 StorageTexture 实例,而不是频繁新建对象。

WebGPU 底层:StorageTexture 如何被绑定为存储资源

理解 StorageTexture 为什么"特殊",需要看 WebGPU 后端为其铺设的三层机制:

第一层:纹理创建时附加 STORAGE_BINDING 使用标志。 WebGPUTextureUtils.js 在组装 GPUTextureDescriptor 时,对所有 isStorageTexture === true 的纹理追加存储绑定能力:

let usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC;

if ( texture.isStorageTexture === true ) {
    usage |= GPUTextureUsage.STORAGE_BINDING;
}

这保证了底层 GPUTexture 同时具备"被计算着色器写入"与"被普通着色器采样"的双重资格。

第二层:绑定布局 access 映射。 存储纹理在 WebGPU 中被描述为 GPUStorageTextureBindingLayout。在绑定生成处 WebGPUBindingUtils.js,three.js 会把 TSL 层的节点访问权限翻译为 WebGPU 的存储访问枚举:

TSL 节点访问模式 WebGPU 映射值 含义
NodeAccess.READ_WRITE GPUStorageTextureAccess.ReadWrite 可读写
NodeAccess.WRITE_ONLY(默认) GPUStorageTextureAccess.WriteOnly 只写
其他 GPUStorageTextureAccess.ReadOnly 只读

同时根据纹理类型设置视图维度(2D 数组纹理映射为 TwoDArray、3D 纹理映射为 ThreeD)。

第三层:WGSL 代码生成 textureStore() 调用。 当 TSL 图中的节点被判定为存储纹理写入时,WGSLNodeBuilder.js 会生成对应的 WGSL textureStore( ... ) 语句,实现计算着色器内对 texel 的直接写入。WGSL 类型解析器(WGSLNodeFunction.js)会把 texture_storage_2d / texture_storage_2d_array / texture_storage_3d 统一归入 storage texture 范畴。

用 TSL 在计算着色器中写入 StorageTexture

TSL 生态中,与 StorageTexture 配套的是节点 StorageTextureNode.js,对外提供 storageTexturetextureStore 两个便捷函数(导出见 Three.TSL.js)。其中 textureStore 会根据传入值自动包装为对应的存储纹理节点(见 StorageTextureNode.js 第 312 行附近)。

其官方注释给出了一份极简的写入模板(见 StorageTextureNode.js 顶部 JSDoc):

import * as THREE from 'three/webgpu';
import { textureStore, Fn, instanceIndex, uvec2, vec4 } from 'three/tsl';

const storageTexture = new THREE.StorageTexture( width, height );

const computeTexture = Fn( ( { storageTexture } ) => {

    const posX = instanceIndex.mod( width );
    const posY = instanceIndex.div( width );
    const indexUV = uvec2( posX, posY );

    // 生成像素颜色(r/g/b),此处仅为示意
    const r = 1, g = 1, b = 1;

    textureStore( storageTexture, indexUV, vec4( r, g, b, 1 ) ).toWriteOnly();

} );

const computeNode = computeTexture( { storageTexture } ).compute( width * height );
await renderer.computeAsync( computeNode );

其中要点:

  • instanceIndex 为内置的调度实例索引,配合宽高可推算出每个 texel 的坐标(第 i 个实例对应 x = i % widthy = i / width);
  • 写入坐标为 texel 整数坐标uvec2),因为 StorageTextureNode.getTransformedUV() 覆写了 UV 变换逻辑,坐标不会被纹理矩阵扰动;
  • .toWriteOnly() 把访问模式声明为只写,对应上文 GPUStorageTextureAccess.WriteOnly 绑定;
  • .compute( width * height ) 声明调度规模为覆盖全纹理的实例总数;
  • renderer.computeAsync( computeNode ) 执行计算 Pass(示例中亦可用 renderer.compute())。

写入完成后,计算产物即可作为普通纹理参与渲染,例如把 material.colorNode 指向它:

import { texture } from 'three/tsl';

const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = texture( storageTexture );   // 采样计算输出

完整实战解读:webgpu_compute_texture

仓库中的 examples/webgpu_compute_texture.html 是 StorageTexture 最直接的官方演示,运行流程如下:

  1. 环境与场景准备:使用正交相机与 1×1 平面网格承载纹理;WebGPU.isAvailable() 不满足时直接给出错误提示并中断;
  2. 创建存储纹理const storageTexture = new THREE.StorageTexture( 512, 512 );
  3. 定义计算函数:基于 instanceIndex 遍历每个 texel,按 shadertoy 公式合成 RGB 值,再以 textureStore( storageTexture, indexUV, vec4( r, g, b, 1 ) ).toWriteOnly(); 写入(示例第 74-97 行);
  4. 组织计算节点并执行const computeNode = computeTexture( { storageTexture } ).compute( width * height ); 随后 await renderer.init()renderer.compute( computeNode )示例第 101-117 行);
  5. 采样呈现material.colorNode = texture( storageTexture ); 让平面显示计算产出的图案。

该示例产出的效果即为本页开头截屏:一类热力流场般的程序化图案。整个过程印证了 StorageTexture 的完整生命周期——Compute Pass 写入 → 自动/手动维护 mip → 渲染 Pass 采样显示

进阶玩法:双缓冲 Ping-Pong 迭代

当需要基于上一帧结果进行迭代计算(如流体、热扩散等逐帧演化模拟)时,可用两个 StorageTexture 交替充当"读源"与"写目标",避免同一资源在读写作上冲突。examples/webgpu_compute_texture_pingpong.html 即为典型模板:

pingTexture = new THREE.StorageTexture( width, height );
pongTexture = new THREE.StorageTexture( width, height );

每次计算把当前写目标换成下一次的读源,帧间交换 ping/pong 的角色,从而实现持续演化的 GPU 模拟,同时仍以 textureStore( ... ) 写入、以普通纹理节点读取显示。

相关资源与使用注意事项

就近扩展阅读(仓库内相关实现与文档):

实践提醒

  1. 后端限制:StorageTexture 仅在 WebGPURenderer + WebGPU 后端下可用,WebGL 渲染器下无 compute 写入通道;
  2. 过滤器默认值minFilter/magFilter 默认 LinearFilter,启用 mip 过滤需显式调整并关注 mipmapsAutoUpdate
  3. mip 策略:需要自定义 mip 内容时把 mipmapsAutoUpdate 设为 false,并在计算着色器中逐 mip 层(借助 StorageTextureNode.mipLevel)手动写入;
  4. 尺寸变更:通过 setSize() 动态调整分辨率,内部仅在尺寸变化时触发重建(dispose()),应确保调用后重新派发一次计算;
  5. 访问模式:写入方向记得显式 .toWriteOnly()(或按需 READ_WRITE),以便后端正确生成对应的存储绑定布局。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389