three.js StorageTexture 全解析:借助 WebGPU Compute Shader 将数据写入纹理
StorageTexture 是 three.js 中专门面向 Compute Shader(计算着色器) 设计的一类特殊纹理,它允许程序在 GPU 侧用计算着色器逐像素计算并写入纹理数据,随后在渲染管线中像普通纹理一样被采样。本指南以官方文档 docs/pages/StorageTexture.html.md 为骨架,结合源码与仓库示例,系统讲解其构造参数、属性、方法、WebGPU 底层绑定原理,并给出可运行的完整计算纹理实战方案。
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
StorageTexture 的 image 字段不再是真实的像素数据源(如 HTMLImageElement 或 canvas),而仅仅是一个承载维度的占位对象:
this.image = { width, height };
它是构造器设置的尺寸映射,也是 setSize() 方法修改的目标。此项覆写了基类 Texture#image 的语义。
.isStorageTexture : boolean(只读)
类型判定标志,默认 true。该标志是 three.js 各处代码识别 StorageTexture 的统一手段,例如 WebGPU 后端创建底层纹理资源时会检查它(见下文"底层原理")。用户侧也可用于自检:
if ( texture.isStorageTexture === true ) {
// 这是一张存储纹理
}
除本类外,同目录下的数组/三维变体 StorageArrayTexture.js 与 Storage3DTexture.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,对外提供 storageTexture 与 textureStore 两个便捷函数(导出见 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 % width、y = 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 平面网格承载纹理;
WebGPU.isAvailable()不满足时直接给出错误提示并中断; - 创建存储纹理:
const storageTexture = new THREE.StorageTexture( 512, 512 );; - 定义计算函数:基于
instanceIndex遍历每个 texel,按 shadertoy 公式合成 RGB 值,再以textureStore( storageTexture, indexUV, vec4( r, g, b, 1 ) ).toWriteOnly();写入(示例第 74-97 行); - 组织计算节点并执行:
const computeNode = computeTexture( { storageTexture } ).compute( width * height );随后await renderer.init()与renderer.compute( computeNode )(示例第 101-117 行); - 采样呈现:
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( ... ) 写入、以普通纹理节点读取显示。
相关资源与使用注意事项
就近扩展阅读(仓库内相关实现与文档):
- 基类文档:Texture 属性与方法参考;
- 实现源码:StorageTexture.js;
- 数组/三维扩展:StorageArrayTexture.js、Storage3DTexture.js 及其 TSL 入口
storageTexture3D; - TSL 写入节点:StorageTextureNode.js;
- 官方示例:webgpu_compute_texture.html、webgpu_compute_texture_pingpong.html。
实践提醒:
- 后端限制:StorageTexture 仅在
WebGPURenderer+ WebGPU 后端下可用,WebGL 渲染器下无 compute 写入通道; - 过滤器默认值:
minFilter/magFilter默认LinearFilter,启用 mip 过滤需显式调整并关注mipmapsAutoUpdate; - mip 策略:需要自定义 mip 内容时把
mipmapsAutoUpdate设为false,并在计算着色器中逐 mip 层(借助StorageTextureNode.mipLevel)手动写入; - 尺寸变更:通过
setSize()动态调整分辨率,内部仅在尺寸变化时触发重建(dispose()),应确保调用后重新派发一次计算; - 访问模式:写入方向记得显式
.toWriteOnly()(或按需READ_WRITE),以便后端正确生成对应的存储绑定布局。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
