计算通道编码器(GPUComputePassEncoder)与渲染通道编码器(GPURendePassEncoder)都属于可编程通道编码器(GPUProgrammablePassEncoder)的子类。
WebGPU 中各个类的名字真的是够长的了。
重复一遍,本文要学习的 3 个类:
可编程通道编码器(GPUProgrammablePassEncoder)
以及它的 2 个子类:
- 计算通道编码器(GPUComputePassEncoder)
- 渲染通道编码器(GPURenderPassEncoder)
编码 or 录制
在一些技术文章中,会将 “命令编码” 称呼为 “命令录制” ,无论使用 “编码” 还是 “录制”,其本身含义相同的,都是将 JS(发生在 CPU) 中我们定义好的各种管线操作 记录并转化为 GPU 可识别的命令(指令)。
什么是通道(pass)?
以下关于通道(pass)的解释,来源于 Orillusion 最新发布的一期 WebGPU 入门视频教程中。
通道(pass) 类似于 图层,很多绘图软件(例如 PhotoShop)都有 图层 的概念,允许我们一层一层的绘制图形内容。
每一个通道(pass)就相当于一个层级,我们可以将多个图层的结果最终叠加在一起,得到我们最终想要的结果。
注:这种不同图层之间的 叠加 并不是简单的 画家算法的那种叠加,而是像 PS 中 “图层叠加特效”的叠加。
这种方式更方便我们去管理每一个 管线(pipeline) 以及该管线所相应的资源、管线绘制细节等。
一个比较典型的场景:我们做特效的时候,一般会先将整个图形绘制一遍,然后再在这一层上面叠加一层或多层特效层。
另外一个较常用的场景:绘制阴影。同样操作套路,我们一般先绘制一个阴影图层,然后再绘制整个场景中的其他元素,最终将 两者(两个通道) 叠加在一起。
在学习本节之前,先介绍几个名词。
这几个名词来源于
-牧野-
的一篇文章:OpenGL图形渲染管线、VBO、VAO、EBO概念及用例
VBO(Vertex Buffer Objects):顶点缓冲对象
显存中开辟一块缓存区用来存储顶点的各类属性信息,例如 顶点坐标、顶点法向量、顶点颜色等。
在渲染时可直接从 VBO 中取出顶点的各类属性数据,且由于这些信息是存储在显存中,所以不需要通过 CPU 传输(读取)数据,因此处理效率更高。
VAO(Vertex Array Object):顶点数组对象
VAO 用于保存模型所有顶点数据属性的状态结合,它存储了顶点数据的格式以及顶点数据所需要的 VBO 对象的引用。
换句话说:VBO 是对顶点属性信息的绑定,而 VAO 是对很多个 VBO 的绑定。
EBO(Element Buffer Object):索引缓冲对象
EBO 相当于 “顶点数组” 的概念,假设同一个顶点被多次重复调用,那么为了减少内存空间浪费,提高执行效率,可以通过该顶点的位置索引来调用该顶点,而无需对重复的顶点信息重复记录多次。
在一些图形学文章中 VBO 可能最频繁被提及。
回到本文正题中。
由于之前的文章中,我们几乎都在侧重讲解每个类的背景知识、基本概念和属性方法,缺少实际的代码编写,所以本文也不会讲解太过详细,只需对通道编码器有个大致了解即可。
让我们快速的过完一遍类的基础概念后,等到实际编写示例时,很多地方自然才能真正理解其用法。现在讲再多也很难理解。
作为计算通道编码器和渲染通道编码器的父类,可编程通道编码器(GPUProgrammablePassEncoder) 目前只有一个方法:setBindGroup()
setBindGroup():设置绑定组
在 @webgpu/types
中对 setBindGroup() 方法进行了 2 种形式的定义。
setBindGroup(
index: GPUIndex32,
bindGroup: GPUBindGroup,
dynamicOffsets?: Iterable<GPUBufferDynamicOffset>
): undefined;
和
setBindGroup(
index: GPUIndex32,
bindGroup: GPUBindGroup,
dynamicOffsetsData: Uint32Array,
dynamicOffsetsDataStart: GPUSize64,
dynamicOffsetsDataLength: GPUSize32
): undefined;
可以看出前两个参数是相同的,区别在于剩余参数,即以哪种形式来定义绑定组中数据的偏移量。
计算通道编码器 顾名思义就是用来对 计算通道管线进行编码的。
它一共有 4 个方法,我们依次来说一下。
setPipeline(pipeline:GPUComputePipeline):设置计算管线
setPipeline(
pipeline: GPUComputePipeline
): undefined;
dispatch():使用当前 GPUComputePipeline 执行的调度工作
dispatch(
workgroupCountX: GPUSize32,
workgroupCountY?: GPUSize32,
workgroupCountZ?: GPUSize32
): undefined;
它的 3 个参数分别对应的含义为:
- workgroupCountX:要调度的工作组网格的 x 维度
- workgroupCountY:要调度的工作组网格的 y 维度
- workgroupCountZ:要调度的工作组网格的 z 维度
dispatchIndirect():使用从参数 GPUBuffer 中得到的数据与当前计算管线(GPUComputePipeline) 一起执行
indirect 单词的本意为:间接的、迂回的、不直截了当的
dispatchIndirect(
indirectBuffer: GPUBuffer,
indirectOffset: GPUSize64
): undefined;
endPass(): 结束当前通道编码
endPass(): undefined
一旦 .endPass() 方法被执行后,当前的 计算通道编码器就不能再使用了。
这个方法的作用和命令编码器(GPUCommandEncoder) 的 .finish() 非常相似。
渲染通道编码器是由 命令编码器(GPUCommandEncoder) 的 .beginRenderPass() 方法创建的。
渲染通道编码器 相对而言比较复杂。
接下来,我们看一下 .beginRenderPass() 方法所需要的参数:
beginRenderPass(
descriptor: GPURenderPassDescriptor
): GPURenderPassEncoder;
interface GPURenderPassDescriptor
extends GPUObjectDescriptorBase {
colorAttachments: Iterable<GPURenderPassColorAttachment>;
depthStencilAttachment?: GPURenderPassDepthStencilAttachment;
occlusionQuerySet?: GPUQuerySet;
timestampWrites?: GPURenderPassTimestampWrites;
}
Attachment 单词的翻译为 “附件”
colorAttachments(必填项):颜色附件
colorAttachments: Iterable<GPURenderPassColorAttachment>;
...
interface GPURenderPassColorAttachment {
view: GPUTextureView;
resolveTarget?: GPUTextureView;
clearValue?: GPUColor;
loadOp: GPULoadOp;
loadValue?: GPULoadOp | GPUColor;
storeOp: GPUStoreOp;
}
- view:颜色附件所要输出到的纹理视图(GPUTextView)
- resolveTarget:描述将为此颜色附件输入到纹理子资源的 纹理视图(GPUTextureView)
- celarValue:执行渲染通道之前要清除的颜色值,默认为(r:0, g:0, b:0, a:0)
- loadValue:执行渲染通道之前在 视图 上执行的加载操作
- storeOp:执行渲染通道后对 视图 执行的存储操作
depthStencilAttachment(可选):深度/模板附件
注:深度对应的英文单词为 depth、模板对应的英文单词为 stencil
depthStencilAttachment?: GPURenderPassDepthStencilAttachment;
...
interface GPURenderPassDepthStencilAttachment {
view: GPUTextureView;
depthClearValue?: number;
depthLoadOp?: GPULoadOp;
depthLoadValue?: GPULoadOp | number;
depthStoreOp?: GPUStoreOp;
depthReadOnly?: boolean;
stencilClearValue?: GPUStencilValue;
stencilLoadOp?: GPULoadOp;
stencilLoadValue?: GPULoadOp | GPUStencilValue;
stencilStoreOp?: GPUStoreOp;
stencilReadOnly?: boolean;
}
- view:深度模板附件所要输出到的纹理视图(GPUTextView),也可以从此深度模板附件中读取该纹理视图
- depthClearValue:执行渲染过程之前清除的 view 的深度组件的值,取值范围为 0.0 ~ 1.0
- depthLoadOp:在渲染过程之前要在 view 的深度组件上执行的加载操作
- depthStoreOp:执行渲染通道后对 view 深度组件执行的存储操作
- depthReadOnly:指示 view 的深度组件仅为只读模式
- stencilClearValue:执行渲染通道之前清除 view 的模板组件的值
- stencilLoadOp:执行渲染通道前要在 view 的模板组件上执行的加载操作
- stencilStoreOp:执行渲染通道后在 view 的模板组件上执行的存储操作
- stencilReadOnly:指示 view 的模板组件仅为只读模式
我们再看一下 GPULoadOp 和 GPUStoreOp 的类型定义:
type GPULoadOp = "load" | "clear";
type GPUStoreOp = "store" | "discard";
GPULoadOp:
- "load":将此附件的现有值加载到渲染通道中
- "clear":将此附件的明确值加载到渲染通道中
注:通常在手机设备的 GPU 中 "clear" 的成本要比 "load" 小很多,在 PC 设备的 GPU 中两者相差不大,建议使用 "clear"。
GPUStoreOp:
- "store":存储此附件的渲染通道的结果值
- "discard":丢弃此附件的渲染通道的结果值
补充:GPURenderPassLayout(渲染通道布局)
interface GPURenderPassLayout
extends GPUObjectDescriptorBase {
colorFormats: Iterable<GPUTextureFormat>;
depthStencilFormat?: GPUTextureFormat;
sampleCount?: GPUSize32;
}
渲染通道布局即当前通道渲染目标的布局,它决定了 通道 与 渲染管线的兼容性。
我也不是太理解它的具体含义和用法,暂时不做过多介绍。
上面讲的是渲染通道编码器(GPURenderPassEncoder) 的创建所牵涉的一些参数,接下来讲解它的其他一些方法。
以下是和 绘制 相关的方法:
setPipeline():设置当前的渲染管线
setPipeline(
pipeline: GPURenderPipeline
): undefined;
setIndexBuffer():设置当前索引缓冲区
setIndexBuffer(
buffer: GPUBuffer,
indexFormat: GPUIndexFormat,
offset?: GPUSize64,
size?: GPUSize64
): undefined;
setVertexBuffer():设置给定槽(slot)的当前顶点缓冲区
setVertexBuffer(
slot: GPUIndex32,
buffer: GPUBuffer,
offset?: GPUSize64,
size?: GPUSize64
): undefined;
draw():绘制图元
draw(
vertexCount: GPUSize32,
instanceCount?: GPUSize32,
firstVertex?: GPUSize32,
firstInstance?: GPUSize32
): undefined;
drawIndexed():绘制索引图元
drawIndexed(
indexCount: GPUSize32,
instanceCount?: GPUSize32,
firstIndex?: GPUSize32,
baseVertex?: GPUSignedOffset32,
firstInstance?: GPUSize32
): undefined;
drawIndirect():使用从 GPUBuffer 中读取的参数来绘制图元
drawIndirect(
indirectBuffer: GPUBuffer,
indirectOffset: GPUSize64
): undefined;
drawIndexedIndirect():使用从 GPUBuffer 读取的参数来绘制索引图元
drawIndexedIndirect(
indirectBuffer: GPUBuffer,
indirectOffset: GPUSize64
): undefined;
以下是和 光栅化状态 相关的方法:
setvViewport():将光栅化阶段使用的视口设置为从标准设备坐标线性映射到视口坐标
setViewport(
x: number,
y: number,
width: number,
height: number,
minDepth: number,
maxDepth: number
): undefined;
setScissorRect():设置在光栅化阶段使用的裁剪矩形
当转换为视口坐标后,任何处在剪裁矩形以外的片元都将被丢弃。
setScissorRect(
x: GPUIntegerCoordinate,
y: GPUIntegerCoordinate,
width: GPUIntegerCoordinate,
height: GPUIntegerCoordinate
): undefined;
setBlendConstant():设置与 constant 一起使用的常量混合颜色(含 alpha 值)
setBlendConstant(
color: GPUColor
): undefined;
setStencilReference():设置模板测试期间使用的模板参考值
setStencilReference(
reference: GPUStencilValue
): undefined;
以下是和 查询 相关的方法:
beginOcclusionQuery():开始查询
beginOcclusionQuery(
queryIndex: GPUSize32
): undefined;
endOcclusionQuery():结束查询
endOcclusionQuery(): undefined;
以下是和 绑定 相关的方法:
executeBundles():执行前先记录到给定 渲染包(GPURenderBundle) 集合中的命令,作为此渲染通道的一部分
executeBundles(
bundles: Iterable<GPURenderBundle>
): undefined;
以下适合 结束通道 相关的方法:
end():结束渲染通道编码器
用户完成记录命令后,结束当前渲染通道编码器。一旦执行该方法后,当前渲染通道编码器就不能再使用了。
end(): undefined;
endPass(): undefined;
注:endPass()
和 end()
的作用是相同的,目前官方计划将 endPass() 废弃,推荐使用 end()。
计算通道编码器与渲染通道编码器的众多方法,我们先不必去过分追查细节,因为这些方法需要大量的实际编写示例后才能慢慢掌握具体怎么使用。
再坚持一下,预计再有 2 个模块讲解完后,我们就该真正进入到实际示例编写中了。
本文到此结束,下一节我们将学习 渲染打包器(GPURenderBundleEncoder) 和 查询集(GPUQuerySet)。