OpenGL 体系给图形开发留下了不少的技术积攒,其中就有不少的“Buffer”,耳熟能详的就有顶点缓冲对象(VertexbufferObject,VBO),帧缓冲对象(FramebufferObject,FBO)等。
切换到以三大古代图形开发技术体系为根底的 WebGPU 之后,这些经典的缓冲对象就在 API 中“隐没了”。其实,它们的职能被更迷信地扩散到新的 API 去了。
本篇讲一讲 FBO 与 RBO,这两个通常用于离屏渲染逻辑中,以及到了 WebGPU 后为什么没有这两个 API 了(用什么作为了代替)。
1 WebGL 中的 FBO 与 RBO
WebGL 其实更多的角色是一个绘图 API,所以在 gl.drawArrays 函数收回时,必须确定将数据资源画到哪里去。
WebGL 容许 drawArrays 到两个中央中的任意一个:canvas 或 FramebufferObject. 很多材料都有介绍,canvas 有一个默认的帧缓冲,若不显式指定本人创立的帧缓冲对象(或者指定为 null)那就默认绘制到 canvas 的帧缓冲上。
换句话说,只有应用 gl.bindFramebuffer()
函数指定一个本人创立的帧缓冲对象,那么就不会绘制到 canvas 上。
本篇探讨的是 HTMLCanvasElement,不波及 OffscreenCanvas
1.1 帧缓冲对象(FramebufferObject)
FBO 创立起来简略,它大多数时候就是一个负责点名的头儿,出汗水的都是小弟,也即它下辖的两类附件:
- 色彩附件(在 WebGL1 中有 1 个,在 WebGL 2 能够有 16 个)
- 深度模板附件(能够只用深度,也能够只用模板,也能够两个一起应用)
对于 MRT 技术(MultiRenderTarget),也就是容许输入到多个色彩附件的技术,WebGL 1.0 应用
gl.getExtension('WEBGL_draw_buffers')
获取扩大来应用;而 WebGL 2.0 原生就反对,所以色彩附件的数量上有所区别。
而这两大类附件则通过如下 API 进行设置:
// 设置 texture 为 0 号色彩附件
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, color0Texture, 0)
// 设置 rbo 为 0 号色彩附件
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, color0Rbo)
// 设置 texture 为 仅深度附件
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0)
// 设置 rbo 为 深度模板附件(须要 WebGL2 或 WEBGL_depth_texture)gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthStencilRbo)
实际上,在须要进行 MRT 时,gl.COLOR_ATTACHMENT0
、gl.COLOR_ATTACHMENT1
… 这些属性只是一个数字,能够通过计算属性进行色彩附件的地位索引,也能够间接应用明确的数字代替:
console.log(gl.COLOR_ATTACHMENT0) // 36064
console.log(gl.COLOR_ATTACHMENT1) // 36065
let i = 1
console.log(gl[`COLOR_ATTACHMENT${i}`]) // 36065
1.2 色彩附件与深度模板附件的真正载体
色彩附件与深度模板附件是须要明确指定数据载体的。WebGL 若改将绘图后果绘制到非 canvas 的 FBO,那么就须要明确指定具体画在哪。
如 1.1 大节的示例代码所示,每个附件都能够抉择如下二者之一作为真正的数据载体容器:
- 渲染缓冲对象(
WebGLRenderbuffer
) - 纹理对象(
WebGLTexture
)
有前辈在博客中指出,渲染缓冲对象会比纹理对象稍好,然而要具体问题具体分析。
实际上,在大多数古代 GPU 以及显卡驱动程序上,这些性能差别没那么重要。
简略的说,如果离屏绘制的后果不须要再进行下一个绘制中作为纹理贴图应用,用 RBO 就能够,因为只有纹理对象能向着色器传递。
对于 RBO 和纹理作为两类附件的区别的材料就没那么多了,而且这篇次要是比对 WebGL 和 WebGPU 二者的不同,就不再开展了。
1.3 FBO/RBO/WebGLTexture 相干办法收集
gl.framebufferTexture2D(gl.FRAMEBUFFER, <attachment_type>, <texture_type>, <texture>, <mip_level>)
:将 WebGLTexture 关联到 FBO 的某个附件上gl.framebufferRenderbuffer(gl.FRAMEBUFFER, <attachment_type>, gl.RENDERBUFFER, <rbo>)
:将 RBO 关联到 FBO 的某个附件上gl.bindFramebuffer(gl.FRAMEBUFFER, <fbo | null>)
:设置帧缓冲对象为以后渲染指标gl.bindRenderbuffer(gl.RENDERBUFFER, <rbo>)
:绑定<rbo>
为以后的 RBOgl.renderbufferStorage(gl.RENDERBUFFER, <rbo_format>, width, height)
:设置以后绑定的 RBO 的数据格式以及长宽
上面是三个创立的办法:
gl.createFramebuffer()
gl.createRenderbuffer()
gl.createTexture()
顺带回顾一下纹理的参数设置、纹理绑定与数据传递函数:
gl.texParameteri()
:设置以后绑定的纹理对象的参数gl.bindTexture()
:绑定纹理对象为以后作用纹理gl.texImage2D()
:向以后绑定的纹理对象传递数据,最初一个参数即数据
2 WebGPU 中的对等概念
WebGPU 曾经没有 WebGLFramebuffer
和 WebGLRenderbuffer
这种相似的 API 了,也就是说,你找不到 WebGPUFramebuffer
和 WebGPURenderbuffer
这俩类。
然而,gl.drawArray
的对等操作还是有的,那就是渲染通道编码器(令其为 renderPassEncoder)收回的 renderPassEncoder.draw
动作。
2.1 渲染通道编码器(GPURenderPassEncoder)承当 FBO 的职能
WebGPU 的绘制指标在哪呢?因为 WebGPU 与 canvas 元素不是强关联的,所以必须显式指定绘制到哪里去。
通过学习可编程通道以及指令编码等概念,理解到 WebGPU 是通过一些指令缓冲来向 GPU 传递“我将要干啥”的信息的,而指令缓冲(Command Buffer)则由指令编码器(也即 GPUCommandEncoder
)实现创立。指令缓冲由若干个 Pass
(通道)形成,绘制相干的通道,叫做渲染通道。
渲染通道则是由渲染通道编码器来设置的,一个渲染通道就设定了这个通道的绘制后果要置于何处(这个形容就类比了 WebGL 要绘制到哪儿)。具体到代码中,其实就是创立 renderPassEncoder 时,传递的 GPURenderPassDescriptor
参数对象里的 colorAttachments 属性:
const renderPassEncoder = commandEncoder.beginRenderPass({
// 是一个数组,能够设置多个色彩附件
colorAttachments: [
{
view: textureView,
loadValue: {r: 0.0, g: 0.0, b: 0.0, a: 1.0},
storeOp: 'store',
}
]
})
留神到,colorAttachments[0].view 是一个 textureView,也即 GPUTextureView
,换言之,意味着这个渲染通道要绘制到某个纹理对象上。
通常状况下,如果你不须要离屏绘制或者应用 msaa,那么应该是画到 canvas 上的,从 canvas 中获取其配置好的纹理对象如下操作:
const context = canvas.getContext('webgpu')
context.configure({
gpuDevice,
format: presentationFormat, // 此参数能够应用画布的客户端长宽 × 设施像素缩放比例失去,是一个两个元素的数组
size: presentationSize, // 此参数能够调用 context.getPreferredFormat(gpuAdapter) 获取
})
const textureView = context.getCurrentTexture().createView()
上述代码片段实现了渲染通道与屏幕 canvas 的关联,即把 canvas 视作一块 GPUTexture
,应用其 GPUTextureView
与渲染通道的关联。
其实,更谨严的说法是 渲染通道 承当了 FBO 的局部职能(因为渲染通道还有收回其它动作的职能,例如设置管线等),因为没有 GPURenderPass 这个 API,所以只能冤屈 GPURenderPassEncoder 代替一下了。
2.2 多指标渲染
为了进行多指标渲染,也即片元着色器要输入多个后果的状况(代码中体现为返回一个构造体),就意味着要多个色彩附件来承载渲染的输入。
此时,要配置渲染管线的片元着色阶段(fragment)的 targets 属性。
相干的从创立纹理、创立管线、指令编码例子代码如下所示,用到两个纹理对象来充当色彩附件的容器:
// 一、创立渲染指标纹理 1 和 2,以及其对应的纹理视图对象
const renderTargetTexture1 = device.createTexture({size: [/* 略 */],
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
format: 'rgba32float',
})
const renderTargetTexture2 = device.createTexture({size: [/* 略 */],
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
format: 'bgra8unorm',
})
const renderTargetTextureView1 = renderTargetTexture1.createView()
const renderTargetTextureView2 = renderTargetTexture2.createView()
// 二,创立管线,配置片元着色阶段的多个对应指标的纹素输入格局
const pipeline = device.createRenderPipeline({
fragment: {
targets: [
{format: 'rgba32float'},
{format: 'bgra8unorm'}
]
// ... 其它属性省略
},
// ... 其它阶段省略
})
const renderPassEncoder = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: renderTargetTextureView1,
// ... 其它参数
},
{
view: renderTargetTextureView2,
// ... 其它参数
}
]
})
这样,两个色彩附件别离用上了两个纹理视图对象作为渲染指标,而且在管线对象的片元着色阶段也明确指定了两个 target 的格局。
于是,你能够在片元着色器代码中指定输入构造:
struct FragmentStageOutput {@location(0) something: vec4<f32>;
@location(1) another: vec4<f32>;
}
@stage(fragment)
fn main(/* 省略输出 */) -> FragmentStageOutput {
var output: FragmentStageOutput;
// 轻易写俩数字,没什么意义
output.something = vec4<f32>(0.156);
output.another = vec4<f32>(0.67);
return output;
}
这样,位于 location 0 的 something 这个 f32 型四维向量就写入了 renderTargetTexture1 的一个纹素,而位于 location 1 的 another 这个 f32 型四维向量则写入了 renderTargetTexture2 的一个纹素。
只管,在 pipeline 的片元阶段中 target 指定的 format 略有不一样,即 renderTargetTexture2 指定为 'bgra8unorm'
,而着色器代码中构造体的 1 号 location 数据类型是 vec4<f32>
,WebGPU 会帮你把 f32 这个 [0.0f, 1.0f]
范畴内的输入映射到 [0, 255]
这个 8bit 整数区间上的。
事实上,如果没有多输入(也即多指标渲染),WebGPU 中大部分片元着色器的返回类型就是一个繁多的
vec4<f32>
,而最常见的 canvas 最佳纹理格局是bgra8unorm
,总归要产生[0.0f, 1.0f]
通过放大 255 倍再取整到[0, 255]
这个映射过程的。
2.3 深度附件与模板附件
GPURenderPassDescriptor
还反对传入 depthStencilAttachment,作为深度模板附件,代码举例如下:
const renderPassDescriptor = {
// 色彩附件设置略
depthStencilAttachment: {view: depthTexture.createView(),
depthLoadValue: 1.0,
depthStoreOp: 'store',
stencilLoadValue: 0,
stencilStoreOp: 'store',
}
}
与单个色彩附件相似,也须要一个纹理对象的视图对象为 view,须要特地留神的是,作为深度或模板附件,肯定要设置与深度、模板无关的纹理格局。
若对深度、模板的纹理格局在额定的设施性能(Device feature)中,在申请设施对象时肯定要加上对应的 feature 来申请,例如有 "depth24unorm-stencil8"
这个性能能力用 "depth24unorm-stencil8"
这种纹理格局。
深度模板的计算,还须要留神渲染管线中深度模板阶段参数对象的配置,例如:
const renderPipeline = device.createRenderPipeline({
// ...
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus',
}
})
2.4 非 canvas 的纹理对象作为两种附件的留神点
除了深度模板附件里提及的纹理格局、申请设施的 feature 之外,还须要留神非 canvas 的纹理若作为某种附件,那它的 usage 肯定蕴含 RENDER_ATTACHMENT
这一项。
const depthTexture = device.createTexture({
size: presentationSize,
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
})
const renderColorTexture = device.createTexture({
size: presentationSize,
format: presentationFormat,
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
})
3 读取数据
3.1 从 FBO 中读像素值
从 FBO 读像素值,实际上就是读色彩附件的色彩数据到 TypedArray 中,想读取以后 fbo(或 canvas 的帧缓冲)的后果,只需调用 gl.readPixels
办法即可。
//#region 创立 fbo 并将其设为渲染指标容器
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
//#endregion
//#region 创立离屏绘制的容器:纹理对象,并绑定它成为以后要解决的纹理对象
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// -- 若不须要作为纹理再次被着色器采样,其实这里能够用 RBO 代替
//#endregion
//#region 绑定纹理对象到 0 号色彩附件
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
//#endregion
// ... gl.drawArrays 进行渲染
//#region 读取到 TypedArray
const pixels = new Uint8Array(imageWidth * imageHeight * 4);
gl.readPixels(0, 0, imageWiebdth, imageHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
//#endregion
gl.readPixels()
办法是把以后绑定的 FBO 及以后绑定的色彩附件的像素值读取到 TypedArray 中,无论载体是 WebGLRenderbuffer 还是 WebGLTexture.
惟一须要留神的是,如果你在写引擎,那么读像素的操作得在绘制指令(个别指 gl.drawArrays
或 gl.drawElements
)收回后的代码中编写,否则可能会读不到值。
3.2 WebGPU 读 GPUTexture 中的数据
在 WebGPU 中将渲染指标,也即纹理中拜访像素是比较简单的,应用到指令编码器的 copyTextureToBuffer 办法,将纹理对象的数据读取到 GPUBuffer,而后通过解映射、读范畴的形式获取 ArrayBuffer.
//#region 创立色彩附件关联的纹理对象
const colorAttachment0Texture = device.createTexture({/* ... */})
//#endregion
//#region 创立用于保留纹理数据的缓冲对象
const readPixelsResultBuffer = device.createBuffer({
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
size: 4 * textureWidth * textureHeight,
})
//#endregion
//#region 图像拷贝操作,将 GPUTexture 拷贝到 GPUBuffer
const encoder = device.createCommandEncoder()
encoder.copyTextureToBuffer({ texture: colorAttachment0Texture},
{buffer: readPixelsResultBuffer},
[textureWidth, textureHeight],
)
device.queue.submit([encoder.finish()])
//#endregion
//#region 读像素
await readPixelsResultBuffer.mapAsync()
const pixels = new Uint8Array(readPixelsResultBuffer.getMappedRange())
//#endregion
要额定留神,如果要拷贝到 GPUBuffer 并且要交给 CPU 端(也就是 JavaScript)来读取,那这块 GPUBuffer 的 usage 肯定要有 COPY_DST
和 MAP_READ
这两项;而且,这个纹理对象的 usage 也必须要有 COPY_SRC
这一项(作为色彩附件的关联纹理,它还得有 RENDER_ATTACHMENT
这一个 usage)。
4 总结
从 WebGL(也即 OpenGL ES 体系)到 WebGPU,离屏绘制技术、多指标渲染技术都有了接口和用法上的降级。
首先是勾销了 RBO 这个概念,一律应用 Texture 作为绘制指标。
其次,更替了 FBO 的职权至 RenderPass,由 GPURenderPassEncoder
负责承载原来 FBO 的两类附件。
因为勾销了 RBO 概念,所以 RTT(RenderToTexture)
和 RTR(RenderToRenderbuffer)
就不再存在了,然而离屏绘制技术仍旧是存在的,你在 WebGPU 中能够应用多个 RenderPass 实现多个绘制成绩,Texture 作为绘制载体能够自在地通过资源绑定组穿梭在不同的 RenderPass 的某个 RenderPipeline 中。
对于如何从 GPU 的纹理中读取像素(色彩值),第 3 节也有浅显的探讨,这部分大多数用处是 GPU Picking;而对于 FBO 这个遗留概念,当初即 RenderPass 离屏渲染,最常见的还是做成果。