WebGL 与 WebGPU比对[5] - 渲染计算的过程
前两篇文章介绍了 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 中去了,这样就可以在帧运算中独立出行为对象。
WebGL 与 WebGPU比对[5] - 渲染计算的过程的更多相关文章
- WebGL 与 WebGPU 比对[1] 前奏
目录 1 为什么是 WebGPU 而不是 WebGL 3.0 显卡驱动 图形 API 的简单年表 WebGL 能运行在各个浏览器的原因 WebGPU 的名称由来 2 与 WebGL 比较编码风格 Op ...
- WebGL 与 WebGPU比对[6] - 纹理
目录 1. WebGL 中的纹理 1.1. 创建二维纹理与设置采样参数 1.2. 纹理数据写入与拷贝 1.3. 着色器中的纹理 1.4. 纹理对象 vs 渲染缓冲对象 1.5. 立方体六面纹理 1.6 ...
- 浏览器加载和渲染HTML的过程(标准定义的过程以及现代浏览器的优化)
先看一下标准定义的浏览器渲染过程(网上找的): 浏览器打开网页的过程 用户第一次访问网址,浏览器向服务器发出请求,服务器返回html文件: 浏览器开始载入html代码,发现 head 标签内有一个 l ...
- 理解WebKit和Chromium: 网页渲染的基本过程
转载请注明原文地址:http://blog.csdn.net/milado_nju ## 概述 前面介绍了一些渲染引擎的功能,包括网络,资源加载,DOM树,RenderObject树等等,但是,给人以 ...
- Java中在时间戳计算的过程中遇到的数据溢出问题
背景 今天在跑定时任务的过程中,发现有一个任务在设置数据的查询时间范围异常,出现了开始时间戳比结束时间戳大的奇怪现象,计算时间戳的代码大致如下. package com.lingyejun.authe ...
- WebGL 与 WebGPU比对[4] - Uniform
目录 1. WebGL 1.0 Uniform 1.1. 用 WebGLUniformLocation 寻址 1.2. 矩阵赋值用 uniformMatrix[234]fv 1.3. 标量与向量用 u ...
- PixiJS - 基于 WebGL 的超快 HTML5 2D 渲染引擎
Pixi.js 是一个开源的HTML5 2D 渲染引擎,使用 WebGL 实现,不支持的浏览器会自动降低到 Canvas 实现.PixiJS 的目标是提供一个快速且轻量级的2D库,并能兼容所有设备.此 ...
- ArcGIS api for javascript——渲染-计算相等间隔分级
描述 本例展示了如何配置分级渲染使用一个相等间隔分类.在这个分类类型中,断点被设置为相等的距离. 可以手工添加相等距离的断点:然而,如果数据被修改了,那些断点就会是不合理的.本例自动地计算断点,因此相 ...
- CesiumJS 2022^ 原理[4] - 最复杂的地球皮肤 影像与地形的渲染与下载过程
目录 API 回顾 1. 对象层级关系 1.1. Scene 中特殊的物体 - Globe 1.2. 地球 Globe 与椭球 Ellipsoid 1.3. 瓦片四叉树 - QuadtreePrimi ...
随机推荐
- 通过暗码去打开/关闭usb debug开关
通过暗码去打开/关闭usb debug开关 通过暗码去打开/关闭usb debug开关1. Description2. Analysis3. Solution4. Summary 1. Descrip ...
- JVM调优2-远程监控
监控远程JVM VisualJVM不仅是可以监控本地jvm进程,还可以监控远程的jvm进程,需要借助于JMX技术实现. 什么是JMX JMX(Java Management Extensions,即J ...
- JUC并发编程与高性能内存队列disruptor实战-上
JUC并发实战 Synchonized与Lock 区别 Synchronized是Java的关键字,由JVM层面实现的,Lock是一个接口,有实现类,由JDK实现. Synchronized无法获取锁 ...
- 《剑指offer》面试题56 - II. 数组中数字出现的次数 II
问题描述 在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次.请找出那个只出现一次的数字. 示例 1: 输入:nums = [3,4,3,3] 输出:4 示例 2: 输入:nums ...
- leetcode 24. 两两交换链表中的节点 及 25. K 个一组翻转链表
24. 两两交换链表中的节点 问题描述 给定一个链表,两两交换其中相邻的节点,并返回交换后的链表. 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换. 示例: 给定 1->2-> ...
- Mybatis 学习记录
1.先放上mybatis官网地址: https://mybatis.org/mybatis-3/zh/index.html 2.mybatis源码和有关包下载地址(GitHub): https://g ...
- php的CI框架相关数据库操作
在使用之前首先应该配置数据库的参数,详见文件application/config/database.php,里面包含主机名,登陆用户名,登录密码,数据库名,编码信息等. 在配置成功后,可以简单的调用: ...
- FilterConfig接口(Servlet)
Javax.Servet 包中提供了一个 FilterCofig 接口,它与 ServletConfig 接口相似,用于在过滤器初始化期间向其传递信息.FilterConfig 接口由容器实现,容器将 ...
- ☕【Java深层系列】「并发编程系列」让我们一起探索一下CyclicBarrier的技术原理和源码分析
CyclicBarrier和CountDownLatch CyclicBarrier和CountDownLatch 都位于java.util.concurrent这个包下,其工作原理的核心要点: Cy ...
- linux面试题(重点)
1.No space left on device ,但df -h,磁盘空间还很富余?原因是 Inode 耗尽.可以使用df -i检查.磁盘中中产生了很多小的临时文件,造成在磁盘空间耗尽之前文件系统的 ...