前两篇文章介绍了 WebGL 和 WebGPU 是如何筹备顶点和数字型 Uniform 数据的(纹理留到下一篇),当渲染所需的原材料筹备实现后,就要进入逻辑组装的过程。
WebGL 在这方面通过指定“WebGLProgram”,最终触发“drawArrays”或“drawElements”来启动渲染 / 计算。全局状态为特色的 WebGL 显然做多步骤渲染来说会麻烦一些,WebGPU 改善了渲染计算过程的接口设计,容许开发者组装更简单的渲染、计算流程。
以所有的“draw”函数调用为分界线,调用后,就认为 CPU 端的工作曾经实现,开始移交筹备好的渲染、计算原材料(数据与着色器程序)至 GPU,进而运行起渲染管线,直至输入到帧缓冲 /Canvas,我称 draw 这个行为是“一个通道”。
WebGPU 的呈现,除了渲染的性能,还呈现了通用计算性能,draw 也有了兄弟概念:dispatch(调度),下文会比照介绍。
1. WebGL
1.1. 应用 WebGLProgram 示意一个计算过程
WebGL 的整个渲染管线(尽管没有管线 API)中,能染指编程的就两处:顶点着色阶段 和 片元着色阶段,别离应用顶点着色器和片元着色器实现渲染过程的定制。
很多书或入门教程都会说,顶点着色器和片元着色器是成对呈现的,而能治理这两个着色器的下层容器对象,就叫做程序对象(接口 WebGLProgram
)。
const vertexShader = gl.createShader(gl.VERTEX_SHADER) // WebGLShader
gl.shaderSource(vertexShader, vertexShaderSource)
gl.compileShader(vertexShader)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER) // WebGLShader
gl.shaderSource(fragmentShader, fragmentShaderSource)
gl.compileShader(fragmentShader)
const program = gl.createProgram() // WebGLProgram
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
其实,真正的渲染管线是有很多步骤的,顶点着色和片元着色只是比拟有代表性:
- 顶点着色器大多数时候负责取色、图形变换
- 片元着色大多数时候负责计算并输入屏幕空间的片元色彩
既然 WebGL 只能定制这两个阶段,又因为这俩 WebGLShader
是被程序对象(WebGLProgram
)治理的,所以,一个程序对象所代表的那个“管线”,通常用于执行一个通道的计算。
在简单的 Web 三维开发中,一个通道还不足以将想要的一帧画面渲染实现,这个时候要切换着色器程序,再进行 drawArrays/drawElements
,绘制下一个通道,这样组合多个通道的绘制后果,就能在一个 requestAnimationFrame 中实现想要的渲染。
1.2. WebGL 没有通道 API
上文提及,在一帧的渲染过程中,有可能须要多个通道共同完成渲染。最初一次 gl.drawXXX
的调用会应用一个绘制到指标帧缓冲的 WebGLProgram
,这么说可能很形象,无妨思考这样一帧的渲染过程:
- 渲染法线、漫反射信息到 FBO1 中;
- 渲染光照信息到 FBO2 中;
- 应用 FBO1 和 FBO2,把最初后果渲染到 Canvas 上。
每一步都须要本人的 WebGLProgram
,而且每一步都要全局切换各种 Buffer、Texture、Uniform 的绑定,这样就须要一个封装对象来实现这些状态的切换,惋惜的是 WebGL 并没有这种对象,大多数时候是第三方库应用相似的类实现的。
因而,如果你不必第三方库(ThreeJS 等),那么你就要思考设计本人的通道类来治理通道了。
当然,随着古代 GPU 的个性开掘,一个通道不肯定是为了绘制一张“画”,因为有通用计算技术的呈现,所以我更乐意称一个通道为“一个计算汇合,由一系列计算过程有逻辑地形成”。在 WebGPU 也就是上面要介绍的内容中会提及计算通道,那个就是为通用计算筹备的。
2. WebGPU
2.1. 应用 Pipeline 组装管线中各个阶段
在 WebGPU 中,一个计算过程的工作就交由“管线”实现,也就是咱们在各种材料里见失去的“可编程管线”的具象化 API;在 WebGPU 中,可编程管线有两类:
- 渲染管线,
GPURenderPipeline
- 计算管线,
GPUComputePipeline
管线对象在创立时,会传递一个参数对象,用不同的状态属性配置不同的管线阶段。
回顾,WebGL 是应用
gl.attachShader()
办法配置两个 WebGLShader 附着到程序对象上的。
对渲染管线来说,除了能够配置顶点着色器、片元着色器之外,还容许应用其它的状态来配置管线中的其它状态:
- 应用
GPUPrimitiveState
对象设置 primitive 状态,配置图元的拆卸阶段和光栅化阶段; - 应用
GPUDepthStencilState
对象设置 depthStencil 状态,配置深度、模板测试以及光栅化阶段; - 应用
GPUMultisampleState
对象设置 multisample 状态,配置光栅化阶段中的多重采样。
具体内容须要参考 WebGPU 规范的文档。上面举个例子:
const renderPipeline = device.createRenderPipeline({
// --- 布局 ---
layout: pipelineLayout,
// --- 五大状态用于配置渲染管线的各个阶段
vertex: {module: device.createShaderModule({ /* 顶点着色器参数 */}),
// ...
},
fragment: {module: device.createShaderModule({ /* 片元着色器参数 */}),
// ...
},
primitive: {/* 设置图元状态 */},
depthStencil: {/* 设置深度模板状态 */},
multisample: {/* 设置多重采样状态 */}
})
而后再看一个异步创立计算管线的例子:
const computePipeline = await device.createComputePipelineAsync({
// --- 布局 ---
layout: pipelineLayout,
// --- 计算管线只需配置计算状态 ---
compute: {module: device.createShaderModule({ /* 计算着色器参数 */}),
// ...
}
})
读者可自行比对 WebGL 中 WebGLProgram
+ WebGLShader
的组合。
题外话,我在我的另一文还提到过,管线还具备了 WebGL 中的 VAO 的作用,感兴趣的能够找找看看。管线的片元状态还承当了 MRT 的信息。
2.2. 应用 PassEncoder 调度管线内的行为
由上一大节可知,管线对象收集了对应管线各个阶段所需的参数。这阐明了管线是一个具备行为的过程。
光有武林秘籍,没有人练,文治是体现不进去的。
所以,PassEncoder(通道编码器)就起了这么一个作用,它负责记录 GPU 计算一个通道的前后逻辑,能够对其设置管线、顶点相干的缓冲对象、资源绑定组,最初触发计算。
计算通道编码器(GPUComputePassEncoder
)的触发动作是调用 dispatch()
办法,这个办法译作“调度”;渲染通道编码器(GPURenderPassEncoder
)的触发动作是它的各个 “draw”
办法,即触发绘制。
这个时候就体现出面向对象编程的威力了,你能够将一个通道内的行为(即管线)、数据(即资源绑定组和各种缓冲对象)别离创立,独立于通道编码器之外,这样,面对不同的通道计算时,就能够按需选用不同的管线和数据,进而甚至能够实现管线或者资源的共用。
通道编码器这一大节没有示例代码,示例代码在下一大节。
2.3. 应用 CommandEncoder 编码多个通道
WebGPU 应用古代图形 API 的思维,将所有 GPU 该做的操作、须要信息当时编码至一个叫“CommandBuffer(指令缓冲)”的容器上,最初对立由 CPU 提交至 GPU,GPU 拿到就吭哧吭哧执行。
编码指令缓冲的对象叫做 GPUCommandEncoder
,即指令编码器,它最大的作用就是创立两种通道编码器(commandEncoder.begin[Render/Compute]Pass()
),以及收回提交动作(commandEncoder.finish()
),最终生成这一帧所需的所有指令。
话不多说,这里间接借用 austin-eng 的例子 ShadowMapping(暗影映射)
// 创立指令编码器
const commandEncoder = device.createCommandEncoder()
{
// 暗影通道的编码过程
const shadowPass = commandEncoder.beginRenderPass(shadowPassDescriptor)
// 应用暗影渲染管线
shadowPass.setPipeline(shadowPipeline)
shadowPass.setBindGroup(0, sceneBindGroupForShadow)
shadowPass.setBindGroup(1, modelBindGroup)
shadowPass.setVertexBuffer(0, vertexBuffer)
shadowPass.setIndexBuffer(indexBuffer, 'uint16')
shadowPass.drawIndexed(indexCount)
shadowPass.endPass()}
{
// 渲染通道惯例操作
const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor);
// 应用惯例渲染管线
renderPass.setPipeline(pipeline)
renderPass.setBindGroup(0, sceneBindGroupForRender)
renderPass.setBindGroup(1, modelBindGroup)
renderPass.setVertexBuffer(0, vertexBuffer)
renderPass.setIndexBuffer(indexBuffer, 'uint16')
renderPass.drawIndexed(indexCount)
renderPass.endPass()}
device.queue.submit([commandEncoder.finish()]);
为了实现三维物体的暗影渲染,在暗影映射无关的技术中个别会把暗影信息应用一个通道先绘制进去,而后把暗影信息传给下一个通道进而实现暗影的成果。
在下面的代码中,就应用了两个 RenderPassEncoder 进行暗影的先后步骤渲染。它们在 draw 之前就能够设置不同的渲染资料,包含代表行为的管线,以及代表资源的绑定组、各类缓冲等。
2.4. PassEncoder 和 Pipeline 的关系
WebGPU 中的 Pipeline 被划分成了多个阶段,其中有三个阶段是可编程的,其它的阶段是可配置的。管线因为在三个可编程阶段领有了着色器模块,所以管线对象更多的是表演一个“执行者”,它代表的是某个繁多计算过程的全副行为,而且是产生在 GPU 上。
而对于 PassEncoder,也就是通道编码器,它领有一系列 setXXX
办法,它的角色更多的是“调度者”。
通道编码器在完结编码后,整个被编码的过程就代表了一个 Pass(通道)的计算流程。
3. 总结
多个工夫很短的画面,就形成了动静的渲染后果。这每一个画面,叫做帧。而每一帧,在实时渲染技术中用多个“通道”,通过图形学或实时渲染常识有逻辑地组装在一起共同完成。
通道由行为和数据形成。
行为由着色器程序实现,也就是“你想在这一个通道做什么计算”,在 WebGL 中应用 WebGLProgram
附着两个着色器,而在 WebGPU 中应用 GPURenderPipeline/GPUComputePipeline
拆卸管线的各个阶段状态。
而数据,则心愿读者去看我写的 Uniform 和 顶点缓冲文章了。
每一帧,在 WebGL 代码中,其实就是一直切换 WebGLProgram
,绑定不同数据,最初收回 draw 动作实现;在 WebGPU 代码中,就是创立指令编码器、开始通道编码、完结通道编码、完结指令编码,最初提交指令缓冲实现。
WebGPU 把 WebGLProgram
与 WebGLShader
的行为职能抽离到 GPU[Render/Compute]Pipeline
和 GPUShaderModule
中去了,这样就能够在帧运算中独立出行为对象。