关于webgl:WebGPU-中消失的-FBO-和-RBO

54次阅读

共计 8841 个字符,预计需要花费 23 分钟才能阅读完成。

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_ATTACHMENT0gl.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> 为以后的 RBO
  • gl.renderbufferStorage(gl.RENDERBUFFER, <rbo_format>, width, height):设置以后绑定的 RBO 的数据格式以及长宽

上面是三个创立的办法:

  • gl.createFramebuffer()
  • gl.createRenderbuffer()
  • gl.createTexture()

顺带回顾一下纹理的参数设置、纹理绑定与数据传递函数:

  • gl.texParameteri():设置以后绑定的纹理对象的参数
  • gl.bindTexture():绑定纹理对象为以后作用纹理
  • gl.texImage2D():向以后绑定的纹理对象传递数据,最初一个参数即数据

2 WebGPU 中的对等概念

WebGPU 曾经没有 WebGLFramebufferWebGLRenderbuffer 这种相似的 API 了,也就是说,你找不到 WebGPUFramebufferWebGPURenderbuffer 这俩类。

然而,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.drawArraysgl.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_DSTMAP_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 离屏渲染,最常见的还是做成果。

正文完
 0