前两篇文章介绍了 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 把 WebGLProgramWebGLShader 的行为职能抽离到 GPU[Render/Compute]PipelineGPUShaderModule 中去了,这样就可以在帧运算中独立出行为对象。

WebGL 与 WebGPU比对[5] - 渲染计算的过程的更多相关文章

  1. WebGL 与 WebGPU 比对[1] 前奏

    目录 1 为什么是 WebGPU 而不是 WebGL 3.0 显卡驱动 图形 API 的简单年表 WebGL 能运行在各个浏览器的原因 WebGPU 的名称由来 2 与 WebGL 比较编码风格 Op ...

  2. WebGL 与 WebGPU比对[6] - 纹理

    目录 1. WebGL 中的纹理 1.1. 创建二维纹理与设置采样参数 1.2. 纹理数据写入与拷贝 1.3. 着色器中的纹理 1.4. 纹理对象 vs 渲染缓冲对象 1.5. 立方体六面纹理 1.6 ...

  3. 浏览器加载和渲染HTML的过程(标准定义的过程以及现代浏览器的优化)

    先看一下标准定义的浏览器渲染过程(网上找的): 浏览器打开网页的过程 用户第一次访问网址,浏览器向服务器发出请求,服务器返回html文件: 浏览器开始载入html代码,发现 head 标签内有一个 l ...

  4. 理解WebKit和Chromium: 网页渲染的基本过程

    转载请注明原文地址:http://blog.csdn.net/milado_nju ## 概述 前面介绍了一些渲染引擎的功能,包括网络,资源加载,DOM树,RenderObject树等等,但是,给人以 ...

  5. Java中在时间戳计算的过程中遇到的数据溢出问题

    背景 今天在跑定时任务的过程中,发现有一个任务在设置数据的查询时间范围异常,出现了开始时间戳比结束时间戳大的奇怪现象,计算时间戳的代码大致如下. package com.lingyejun.authe ...

  6. WebGL 与 WebGPU比对[4] - Uniform

    目录 1. WebGL 1.0 Uniform 1.1. 用 WebGLUniformLocation 寻址 1.2. 矩阵赋值用 uniformMatrix[234]fv 1.3. 标量与向量用 u ...

  7. PixiJS - 基于 WebGL 的超快 HTML5 2D 渲染引擎

    Pixi.js 是一个开源的HTML5 2D 渲染引擎,使用 WebGL 实现,不支持的浏览器会自动降低到 Canvas 实现.PixiJS 的目标是提供一个快速且轻量级的2D库,并能兼容所有设备.此 ...

  8. ArcGIS api for javascript——渲染-计算相等间隔分级

    描述 本例展示了如何配置分级渲染使用一个相等间隔分类.在这个分类类型中,断点被设置为相等的距离. 可以手工添加相等距离的断点:然而,如果数据被修改了,那些断点就会是不合理的.本例自动地计算断点,因此相 ...

  9. CesiumJS 2022^ 原理[4] - 最复杂的地球皮肤 影像与地形的渲染与下载过程

    目录 API 回顾 1. 对象层级关系 1.1. Scene 中特殊的物体 - Globe 1.2. 地球 Globe 与椭球 Ellipsoid 1.3. 瓦片四叉树 - QuadtreePrimi ...

随机推荐

  1. vue传参子传父

    vue子传父用$emit实现 1.文件目录结构 2.parent父组件内容 <template> <div class="wrap"> <div> ...

  2. 【Java】获取两个字符串中最大相同子串

    题目 获取两个字符串中最大相同子串 前提 两个字符串中只有一个最大相同子串 解决方案 public class StringDemo { public static void main(String[ ...

  3. day 19 C语言顺序结构基础2

    (1).算术运算符和圆括号有不同的运算优先级,对于表达式:a+b+c*(d+e),关于执行步骤,以下说法正确的是[A] (A).先执行a+b的r1,再执行(d+e)的r2,再执行c*r2的r3,最后执 ...

  4. leetcode刷题目录

    leetcode刷题目录 1. 两数之和 2. 两数相加 3. 无重复字符的最长子串 4. 寻找两个有序数组的中位数 5. 最长回文子串 6. Z 字形变换 7. 整数反转 8. 字符串转换整数 (a ...

  5. migrate 和makemigrations 命令

    在你改动了app下 models.py的内容之后执行下面的命令: Python manger.py makemigrations 相当于 在该app下建立 migrations目录,并记录下你所有的关 ...

  6. MySQL 5.7主从搭建(同一台机器)

    主从复制原理:复制是 MySQL 的一项功能,允许服务器将更改从一个实例复制到另一个实例. 1)主服务器将所有数据和结构更改记录到二进制日志中. 2)从属服务器从主服务器请求该二进制日志并在本地应用其 ...

  7. kubernetes之配置Metrics Server

    Kubernetes 1.8 关于资源使用情况的 metrics,可以通过 Metrics API 获取到, Kubernetes 1.11 已经废弃 heapster.这里我们基于 Kubernet ...

  8. static关键字的一些使用

    百度百科定义static关键字 通常情况下,类成员必须通过它的类的对象访问,但是可以创建这样一个成员,它能够被它自己使用,而不必引用特定的实例.在成员的声明前面加上关键字static(静态的)就能创建 ...

  9. Spring系列8:bean的作用域

    本文内容 bean定义信息的意义 介绍6种bean的作用域 bean定义信息的意义 Spring中区分下类.类定义信息,类实例对象的概念?不容易理解,以餐馆中点炒饭为例. 类: 相当于你看到菜单上炒饭 ...

  10. Java编程中标识符注意点以及注释

    标识符注意点 所有的标识符都应该以字母(A-Z或者a-z),美元符($),或者下划线(_)开始 首字符之后可以是字母(A-Z或者a-z),美元符($),下划线(_)或数 字的任何字符组合 不能使用关键 ...