回顾

书接上文,Scene.js 模块内的 render 函数会将控制权交给 WebGL,执行 CesiumJS 自己封装的指令对象,画出每一帧来。

模块内的 render 函数首先会更新一批状态信息,譬如帧状态、雾效、Uniform 值、通道状态、三维场景中的环境信息等,然后就开始更新并执行指令,调用的是 Scene 原型链上的 updateAndExecuteCommands 方法。

整个过程大致的调用链是这样的(function 关键字简写为 fn):

  1. [Module Scene.js]
  2. - fn render()
  3. - Scene.prototype.updateAndExecuteCommands()
  4. - fn executeCommandsInViewport()
  5. - fn updateAndRenderPrimitives()
  6. [Module Primitive.js]
  7. - fn createCommands()
  8. - fn updateAndQueueCommands()
  9. - fn executeCommands()
  10. - fn executeCommand()

本篇讲解的是从 Scene 原型链上的 updateAndExcuteCommands() 方法开始,期间 Scene 中的 Primitives 是如何创建指令,又最终如何被 WebGL 执行的。

这个过程涉及非常多细节代码,但是为了快速聚焦整个过程,本篇先介绍两个 CesiumJS 封装的概念:指令和通道。

预备知识:指令

WebGL 是一种依赖于“全局状态”的绘图 API,面向对象特征比较弱,为了修改全局状态上的顶点数据、着色器程序、帧缓冲、纹理等“资源”,必须通过 gl.XXX 函数调用触发全局状态的改变。

而图形编程基础提出的渲染管线、通道等概念偏向于面向对象,显然 WebGL 这种偏过程的风格需要被 JavaScript 运行时引擎封装。

CesiumJS 将 WebGL 的绘制过程,也就是行为,封装成了“指令”,不同的指令对象有不同的用途。指令对象保存的行为,具体就是指由 Primitive 对象(不一定全是 Primitive)生成的 WebGL 所需的数据资源(缓冲、纹理、唯一值等),以及着色器对象。数据资源和着色器对象仍然是 CesiumJS 封装的对象,而不是 WebGL 原生的对象,这是为了更好地与 CesiumJS 各种对象结合去绘图。

CesiumJS 有三类指令:

  • DrawCommand 绘图指令
  • ClearCommand 清屏指令
  • ComputeCommand 计算指令

绘图指令最终会把控制权交给 Context 对象,根据自身的着色器对象,绘制自己身上的数据资源。

清屏指令比较简单,目的就是调用 WebGL 的清屏函数,清空各类缓冲区并填充清空后的颜色值,依旧会把控制权传递给 Context 对象。

计算指令则借助 WebGL 并行计算的特点,将指令对象上的数据在着色器中编码、计算、解码,最后写入到输出缓冲(通常是帧缓冲的纹理上),同样控制权会给到 Context 对象。

预备知识:通道

一帧是由多个通道顺序绘制构成的,在 CesiumJS 中,通道英文单词是 Pass

既然通道的绘制是有顺序的,其顺序保存在 Renderer/Pass.js 模块导出的冻结对象中,目前(1.92版本)有 10 个优先顺序等级:

  1. const Pass = {
  2. ENVIRONMENT: 0,
  3. COMPUTE: 1,
  4. GLOBE: 2,
  5. TERRAIN_CLASSIFICATION: 3,
  6. CESIUM_3D_TILE: 4,
  7. CESIUM_3D_TILE_CLASSIFICATION: 5,
  8. CESIUM_3D_TILE_CLASSIFICATION_IGNORE_SHOW: 6,
  9. OPAQUE: 7,
  10. TRANSLUCENT: 8,
  11. OVERLAY: 9,
  12. NUMBER_OF_PASSES: 10,
  13. }

以上为例,第一优先被绘制的指令,是环境(ENVIRONMENT: 0)方面的对象、物体。而不透明(OPAQUE: 7)的三维对象的绘制指令,则要先于透明(TRANSLUCENT: 8)物体被执行。

CesiumJS 会在每一帧即将开始绘制前,对所有已经收集好的指令根据通道进行排序,实现顺序绘制,下文会细谈。

1. 生成并执行指令

原型链上的 updateAndExecuteCommands 方法会做模式判断,我们一般使用的是三维模式(SceneMode.SCENE3D),所以只需要看 else if 分支即可,也就是

  1. executeCommandsInViewport(true, this, passState, backgroundColor);

此处,this 就是 Scene 自己。

executeCommandsInViewport() 是一个 Scene.js 模块内的函数,这个函数比较短,对于当前我们关心的东西,只需要看它调用的 updateAndRenderPrimitives() 和最后的 executeCommands() 函数调用即可。

1.1. Primitive 生成指令

  1. [Module Scene.js]
  2. - fn updateAndRenderPrimitives()
  3. [Module Primitive.js]
  4. - fn createCommands()
  5. - fn updateAndQueueCommands()

Scene.js 模块内的函数 updateAndRenderPrimitives() 负责更新 Scene 上所有的 Primitive。

期间,控制权会通过 PrimitiveCollection 转移到 Primitive 类(或者有类似结构的类,譬如 Cesium3DTileset 等)上,令其更新本身的数据资源,根据情况创建新的着色器,并随之创建 绘图指令,最终在 Primitive.js 模块内的 updateAndQueueCommands() 函数排序、并推入帧状态对象的指令列表上。

1.2. Context 对象负责执行 WebGL 底层代码

  1. [Module Scene.js]
  2. - fn executeCommands()
  3. - fn executeCommand() // 收到 Command 和 Context
  4. [Module Context.js]
  5. - Context.prototype.draw()

另一个模块内的函数 executeCommands() 则负责执行这些指令(中间还有一些小插曲,下文再提)。

此时,上文的“通道”再次起作用,此函数内会根据 Pass 的优先顺序依次更新唯一值状态(UniformState),然后下发给 executeCommand() 函数(注意少了个s)以具体的某个指令对象以及 Context 对象。

除了 executeCommand() 函数外,Scene.js 模块内仍还有其它类似的函数,例如 executeIdCommand() 负责执行绘制 ID 信息纹理的指令,executeTranslucentCommandsBackToFront() / executeTranslucentCommandsFrontToBack() 函数负责透明物体的指令等。甚至在 Scene 对象自己的属性中,就有清屏指令字段,会在 executeCommands() 函数中直接执行,不经过上述几个执行具体指令的函数。

Context 对象是对 WebGL(2)RenderingContext 等 WebGL 原生接口、参数的封装,所有指令对象最终都会由其进行拆包装、组装 WebGL 函数调用并执行绘图、计算、清屏(见上文介绍的预备知识:指令)。

2. 多段视锥体技术

先介绍一个概念,WebGL 中的深度。

简单的说,屏幕朝里,三维物体的前后顺序就是深度。CesiumJS 的深度非常大,即使不考虑远太空,只考虑地球表面附近的范围,WebGL 的数值范围也不太够用。聪明的前辈们想到了使用对数函数压缩深度的值域,因为一个非常大的数字只需取其对数,很快就能小下来。

Scene 对象上有一个可读可写访问器 logarithmicDepthBuffer,它指示是否启用对数深度。

现在,CesiumJS 通常使用的就是对数深度。

对数深度解决的不仅仅只是普通深度值值域不够的问题,还解决了不支持对数深度技术之前使用的多段视锥技术问题。

再次简单的说,多段视锥技术将视锥体由远及近切成多个段,保证了相机近段的指令足够多以保证效果,远段尽量少满足性能。

你在 Scene.js 模块中的 executeCommands() 函数的最后能找到一个循环体:

  1. // Execute commands in each frustum in back to front order
  2. let j;
  3. for (let i = 0; i < numFrustums; ++i) {
  4. // ...
  5. }

打开调试工具,很容易击中这个断点,查看 numFrustums 变量的值,有启用对数深度的 CesiumJS 程序,一般 numFrustums 都是 1。

3. 指令对象的转移

在本文第 1 节中,详细说明了指令对象的生成与被执行。

上述其实忽略了很多细节,现在捡起来说。

指令对象在 Primitive(或类似的类)生成后,由 模块内的 updateAndQueueCommands() 函数排序并推入帧状态对象的指令列表上:

  1. // updateAndQueueCommands 函数中,函数接收来自 Scene 逐级传入的帧状态对象 -- frameState
  2. const commandList = frameState.commandList;
  3. const passes = frameState.passes;
  4. if (passes.render || passes.pick) {
  5. // ... 省略部分代码
  6. commandList.push(colorCommand);
  7. }

frameState.commandList 就是个普通的数组。

而在执行时,却是从 View 对象上的 frustumCommandsList 上取的指令:

  1. // Scene.js 模块的 executeCommands 函数中
  2. const frustumCommandsList = view.frustumCommandsList;
  3. const numFrustums = frustumCommandsList.length;
  4. let j;
  5. for (let i = 0; i < numFrustums; ++i) {
  6. const index = numFrustums - i - 1;
  7. const frustumCommands = frustumCommandsList[index];
  8. // ...
  9. // 截取不透明物体的通道指令执行代码片段
  10. us.updatePass(Pass.OPAQUE);
  11. commands = frustumCommands.commands[Pass.OPAQUE];
  12. length = frustumCommands.indices[Pass.OPAQUE];
  13. for (j = 0; j < length; ++j) {
  14. executeCommand(commands[j], scene, context, passState);
  15. }
  16. // ...
  17. }

其中,下发给 executeCommand() 函数的 commands[j] 参数,就是具体的某个指令对象。

所以这两个过程之间,是怎么做指令对象传递的?

答案就在 View 原型链上的 createPotentiallyVisibleSet 方法中。

筛选可见集

View 对象是 Scene、Camera 之间的纽带,负责“眼睛”与“世界”之间信息的处理,即视图。

View 原型链上的 createPotentiallyVisibleSet 方法的作用,就是把 Scene 上的计算指令、覆盖物指令,帧状态上的指令列表,根据 View 的可见范围做一次筛选,减少要执行指令个数提升性能。

具体来说,就是把分散在各处的指令,筛选后绑至 View 对象的 frustumCommandsList 列表中,借助 View.js 模块内的 insertIntoBin() 函数完成:

  1. // View.js 模块内
  2. function insertIntoBin(view, scene, command, commandNear, commandFar) {
  3. // ...
  4. const frustumCommandsList = view.frustumCommandsList;
  5. const length = frustumCommandsList.length;
  6. for (let i = 0; i < length; ++i) {
  7. // ...
  8. frustumCommands.commands[pass][index] = command;
  9. // ...
  10. }
  11. // ...
  12. }

这个函数内做了对指令的筛选判断。

4. 本篇总结

本篇调查清楚了 Scene 对象上各种三维物体是如何绘制的,即借助 指令 对象保存待绘制的信息,最后交由 Context 对象完成 WebGL 代码的执行。

期间,发生了指令的分类和可见集的筛选;篇幅原因,CesiumJS 在这漫长的渲染过程中还做了很多细节的事情。

不过,Cesium 的三维物体的渲染架构就算讲完了。

接下来,则是另两个比较头痛的话题:

  • 地球的渲染架构(瓦片四叉树)
  • 具备创建指令的各路数据源(Entity、DataSource、Model、Cesium3DTileset等)

指令和通道的概念仍然会继续使用,敬请期待。

CesiumJS 2022^ 原理[2] 渲染架构之三维物体 - 创建并执行指令的更多相关文章

  1. CesiumJS 2022^ 原理[3] 渲染原理之从 Entity 看 DataSource 架构 - 生成 Primitive 的过程

    目录 API 用法回顾 1. 为什么要从 Viewer 访问 Entity API 1.1. 高层数据模型的封装 - DataSource API 1.2. 显示管理器 DataSourceDispl ...

  2. CesiumJS 2022^ 源码解读[0] - 文章目录与源码工程结构

    很高兴你能在浮躁的年代里还有兴趣阅读源代码,CesiumJS 至今已有十年以上,代码量也积累了三十多万行(未压缩状态). 我也很荣幸自己的文章能被读者看到,如果对你有帮助.有启发,点个赞就是对我最大的 ...

  3. CesiumJS 2022^ 原理[5] - 着色器相关的封装设计

    目录 1. 对 WebGL 接口的封装 1.1. 缓冲对象封装 1.2. 纹理与采样参数封装 1.3. 着色器封装 1.4. 上下文对象与渲染通道 1.5. 统一值(uniform)封装 1.6. 渲 ...

  4. CesiumJS 2022^ 源码解读[7] - 3DTiles 的请求、加载处理流程解析

    目录 1. 3DTiles 数据集的类型 2. 创建瓦片树 2.1. 请求入口文件 2.2. 创建树结构 2.3. 瓦片缓存机制带来的能力 3. 瓦片树的遍历更新 3.1. 三个大步骤 3.2. 遍历 ...

  5. Prerender.io - 预渲染架构,提高AngularJS SEO

    近些年来,越来越多的JavaScript框架(即AngularJS,BackboneJS,ReactJS)变得越来越流行.许多公司和开发人员使用这些JavaScript框架开发应用程序.这些框架有很多 ...

  6. 如何让手游更省带宽,耗电量更少?TBR渲染架构解析!

    如何让手游更省带宽,耗电量更少?渲染或是其中一个可突破的点.本文中,腾讯游戏学院专家Hailong将从为大家解析TBR渲染架构的特点. 什么是TBR? 全称是Tile Based Rendering, ...

  7. angularjs如何在视图渲染结束之后,或者render之后执行指令中的link方法呢?

    angularjs如何在视图渲染结束之后,或者render之后执行指令中的link方法 关键字: $timeout app.directive("myDirective",func ...

  8. CesiumJS 2022^ 源码解读[6] - 三维模型(ModelExperimental)新架构

    目录 1. ModelExperimental 的缓存机制 1.1. 缓存池 ResourceCache 1.2. 缓存对象的键设计 ResourceCacheKey 2. 三维模型的加载与解析 2. ...

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

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

随机推荐

  1. atoi atof atol

    在c语言中提供了把字符串转化为整数的函数,并没有提供把整数转化为字符串的函数 atoi是标准的库函数 itoa不是标准的库函数(在vs可编译,其它系统中未知) atol把一个字符串转化为long类型 ...

  2. Linux移植到自己的开发板(四)问题汇总

    @ 目录 1 使ubuntu支持两个版本的编译链: 2 版本问题: 3 ubuntu版本的vscode下载网速太慢: 4 ubuntu占用空间过大 5 执行make zImage 出错 lzop: n ...

  3. 同一局域网ping ip 失败(可能是你的路由器坑了你)

    事件起源:手机需要通过fiddler代理抓包 一顿操作猛如虎,手机输入ip+端口,芭比q了,连接不上.. 解决思路: 一.保证双方处于同一局域网内 二.查看电脑配置,找到 防火墙,关闭防火墙,再试 三 ...

  4. (leetcode)二叉树的前序遍历-c语言实现

    给定一个二叉树,返回它的 前序 遍历. 示例: 输入: [1,null,2,3] 1 \ 2 / 3 输出: [1,2,3] 进阶: 递归算法很简单,你可以通过迭代算法完成吗? 前序遍历 前序遍历首先 ...

  5. java中对集合操作的易错点01

    今天用for循环遍历集合,对集合中满足条件的元素进行remove操作报错:ConcurrentModificationException 所以,在遍历集合进行增.删操作时,要使用迭代器的方式 publ ...

  6. 2022IDEA配置启动lilishop的swagger展示

    目录 一.概述 二.基本构建 三.Git 导入编译器 四.模块描述浅析 五.配置文档 1.注释配置文件 2.添加配置 3.暂时关闭权限 4.浏览器测试访问 5.其他需要修改模块 六.参考文献 结语 一 ...

  7. sharding-jdbc教程 看这一篇就够了

    ​ Sharding-JDBC是ShardingSphere的第一个产品,也是ShardingSphere的前身. 它定位为轻量级Java框架,在Java的JDBC层提供的额外服务.它使用客户端直连数 ...

  8. Mybatis 动态 sql 有什么用?执行原理?有哪些动态 sql?

    Mybatis 动态 sql 可以在 Xml 映射文件内,以标签的形式编写动态 sql,执行原理 是根据表达式的值 完成逻辑判断并动态拼接 sql 的功能. Mybatis 提供了 9 种动态 sql ...

  9. Thread 类中的 yield 方法有什么作用?

    使当前线程从执行状态(运行状态)变为可执行态(就绪状态). 当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可 能是当前线程,也可能是其他线程,看系统的分配了.

  10. Java 中是如何支持正则表达式操作的?

    Java 中的 String 类提供了支持正则表达式操作的方法,包括:matches(). replaceAll().replaceFirst().split().此外,Java 中可以用 Patte ...