书接上文,Scene.js 模块内的 render 函数会将控制权交给 WebGL,执行 CesiumJS 自己封装的指令对象,画出每一帧来。
模块内的 render 函数首先会更新一批状态信息,譬如帧状态、雾效、Uniform 值、通道状态、三维场景中的环境信息等,然后就开始更新并执行指令,调用的是 Scene 原型链上的 updateAndExecuteCommands 方法。
整个过程大致的调用链是这样的(function 关键字简写为 fn):
[Module Scene.js]
- fn render()
- Scene.prototype.updateAndExecuteCommands()
- fn executeCommandsInViewport()
- fn updateAndRenderPrimitives()
[Module Primitive.js]
- fn createCommands()
- fn updateAndQueueCommands()
- fn executeCommands()
- 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版本)有 9 个优先顺序等级(最后 NUMBER_OF_PASSES 是通道的数量):
const Pass = {
ENVIRONMENT: 0,
COMPUTE: 1,
GLOBE: 2,
TERRAIN_CLASSIFICATION: 3,
CESIUM_3D_TILE: 4,
CESIUM_3D_TILE_CLASSIFICATION: 5,
CESIUM_3D_TILE_CLASSIFICATION_IGNORE_SHOW: 6,
OPAQUE: 7,
TRANSLUCENT: 8,
OVERLAY: 9,
NUMBER_OF_PASSES: 10,
}
以上为例,第一优先被绘制的指令,是环境(ENVIRONMENT: 0)方面的对象、物体。而不透明(OPAQUE: 7)的三维对象的绘制指令,则要先于透明(TRANSLUCENT: 8)物体被执行。
CesiumJS 会在每一帧即将开始绘制前,对所有已经收集好的指令根据通道进行排序,实现顺序绘制,下文会细谈。
原型链上的 updateAndExecuteCommands 方法会做模式判断,我们一般使用的是三维模式(SceneMode.SCENE3D),所以只需要看 else if 分支即可,也就是
executeCommandsInViewport(true, this, passState, backgroundColor);
此处,this 就是 Scene 自己。
executeCommandsInViewport() 是一个 Scene.js 模块内的函数,这个函数比较短,对于当前我们关心的东西,只需要看它调用的 updateAndRenderPrimitives() 和最后的 executeCommands() 函数调用即可。
[Module Scene.js]
- fn updateAndRenderPrimitives()
[Module Primitive.js]
- fn createCommands()
- fn updateAndQueueCommands()
Scene.js 模块内的函数 updateAndRenderPrimitives() 负责更新 Scene 上所有的 Primitive。
期间,控制权会通过 PrimitiveCollection 转移到 Primitive 类(或者有类似结构的类,譬如 Cesium3DTileset 等)上,令其更新本身的数据资源,根据情况创建新的着色器,并随之创建 绘图指令,最终在 Primitive.js 模块内的 updateAndQueueCommands() 函数排序、并推入帧状态对象的指令列表上。
[Module Scene.js]
- fn executeCommands()
- fn executeCommand() // 收到 Command 和 Context
[Module Context.js]
- Context.prototype.draw()
另一个模块内的函数 executeCommands() 则负责执行这些指令(中间还有一些小插曲,下文再提)。
此时,上文的“通道”再次起作用,此函数内会根据 Pass 的优先顺序依次更新唯一值状态(UniformState),然后下发给 executeCommand() 函数(注意少了个s)以具体的某个指令对象以及 Context 对象。
除了
executeCommand()函数外,Scene.js模块内仍还有其它类似的函数,例如executeIdCommand()负责执行绘制 ID 信息纹理的指令,executeTranslucentCommandsBackToFront()/executeTranslucentCommandsFrontToBack()函数负责透明物体的指令等。甚至在 Scene 对象自己的属性中,就有清屏指令字段,会在executeCommands()函数中直接执行,不经过上述几个执行具体指令的函数。
Context 对象是对 WebGL(2)RenderingContext 等 WebGL 原生接口、参数的封装,所有指令对象最终都会由其进行拆包装、组装 WebGL 函数调用并执行绘图、计算、清屏(见上文介绍的预备知识:指令)。
先介绍一个概念,WebGL 中的深度。
简单的说,屏幕朝里,三维物体的前后顺序就是深度。CesiumJS 的深度非常大,即使不考虑远太空,只考虑地球表面附近的范围,WebGL 的数值范围也不太够用。聪明的前辈们想到了使用对数函数压缩深度的值域,因为一个非常大的数字只需取其对数,很快就能小下来。
Scene 对象上有一个可读可写访问器 logarithmicDepthBuffer,它指示是否启用对数深度。
现在,CesiumJS 通常使用的就是对数深度。
对数深度解决的不仅仅只是普通深度值值域不够的问题,还解决了不支持对数深度技术之前使用的多段视锥技术问题。
再次简单的说,多段视锥技术将视锥体由远及近切成多个段,保证了相机近段的指令足够多以保证效果,远段尽量少满足性能。
你在 Scene.js 模块中的 executeCommands() 函数的最后能找到一个循环体:
// Execute commands in each frustum in back to front order
let j;
for (let i = 0; i < numFrustums; ++i) {
// ...
}
打开调试工具,很容易击中这个断点,查看 numFrustums 变量的值,有启用对数深度的 CesiumJS 程序,一般 numFrustums 都是 1。
在本文第 1 节中,详细说明了指令对象的生成与被执行。
上述其实忽略了很多细节,现在捡起来说。
指令对象在 Primitive(或类似的类)生成后,由 模块内的 updateAndQueueCommands() 函数排序并推入帧状态对象的指令列表上:
// updateAndQueueCommands 函数中,函数接收来自 Scene 逐级传入的帧状态对象 -- frameState
const commandList = frameState.commandList;
const passes = frameState.passes;
if (passes.render || passes.pick) {
// ... 省略部分代码
commandList.push(colorCommand);
}
frameState.commandList 就是个普通的数组。
而在执行时,却是从 View 对象上的 frustumCommandsList 上取的指令:
// Scene.js 模块的 executeCommands 函数中
const frustumCommandsList = view.frustumCommandsList;
const numFrustums = frustumCommandsList.length;
let j;
for (let i = 0; i < numFrustums; ++i) {
const index = numFrustums - i - 1;
const frustumCommands = frustumCommandsList[index];
// ...
// 截取不透明物体的通道指令执行代码片段
us.updatePass(Pass.OPAQUE);
commands = frustumCommands.commands[Pass.OPAQUE];
length = frustumCommands.indices[Pass.OPAQUE];
for (j = 0; j < length; ++j) {
executeCommand(commands[j], scene, context, passState);
}
// ...
}
其中,下发给 executeCommand() 函数的 commands[j] 参数,就是具体的某个指令对象。
所以这两个过程之间,是怎么做指令对象传递的?
答案就在 View 原型链上的 createPotentiallyVisibleSet 方法中。
View 对象是 Scene、Camera 之间的纽带,负责“眼睛”与“世界”之间信息的处理,即视图。
View 原型链上的 createPotentiallyVisibleSet 方法的作用,就是把 Scene 上的计算指令、覆盖物指令,帧状态上的指令列表,根据 View 的可见范围做一次筛选,减少要执行指令个数提升性能。
具体来说,就是把分散在各处的指令,筛选后绑至 View 对象的 frustumCommandsList 列表中,借助 View.js 模块内的 insertIntoBin() 函数完成:
// View.js 模块内
function insertIntoBin(view, scene, command, commandNear, commandFar) {
// ...
const frustumCommandsList = view.frustumCommandsList;
const length = frustumCommandsList.length;
for (let i = 0; i < length; ++i) {
// ...
frustumCommands.commands[pass][index] = command;
// ...
}
// ...
}
这个函数内做了对指令的筛选判断。
本篇调查清楚了 Scene 对象上各种三维物体是如何绘制的,即借助 指令 对象保存待绘制的信息,最后交由 Context 对象完成 WebGL 代码的执行。
期间,发生了指令的分类和可见集的筛选;篇幅原因,CesiumJS 在这漫长的渲染过程中还做了很多细节的事情。
不过,Cesium 的三维物体的渲染架构就算讲完了。
接下来,则是另两个比较头痛的话题:
指令和通道的概念仍然会继续使用,敬请期待。
出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>
我对最新版本的Rails有疑问。我创建了一个新应用程序(railsnewMyProject),但我没有脚本/生成,只有脚本/rails,当我输入ruby./script/railsgeneratepluginmy_plugin"Couldnotfindgeneratorplugin.".你知道如何生成插件模板吗?没有这个命令可以创建插件吗?PS:我正在使用Rails3.2.1和ruby1.8.7[universal-darwin11.0] 最佳答案 随着Rails3.2.0的发布,插件生成器已经被移除。查看变更日志here.现在
如何使用RSpec::Core::RakeTask初始化RSpecRake任务?require'rspec/core/rake_task'RSpec::Core::RakeTask.newdo|t|#whatdoIputinhere?endInitialize函数记录在http://rubydoc.info/github/rspec/rspec-core/RSpec/Core/RakeTask#initialize-instance_method没有很好的记录;它只是说:-(RakeTask)initialize(*args,&task_block)AnewinstanceofRake
在我的Controller中,我通过以下方式在我的index方法中支持HTML和JSON:respond_todo|format|format.htmlformat.json{renderjson:@user}end在浏览器中拉起它时,它会自然地以HTML呈现。但是,当我对/user资源进行内容类型为application/json的curl调用时(因为它是索引方法),我仍然将HTML作为响应。如何获取JSON作为响应?我还需要说明什么? 最佳答案 您应该将.json附加到请求的url,提供的格式在routes.rb的路径中定义。这
我遵循了教程http://gettingstartedwithchef.com/,第1章。我的运行list是"run_list":["recipe[apt]","recipe[phpap]"]我的phpapRecipe默认Recipeinclude_recipe"apache2"include_recipe"build-essential"include_recipe"openssl"include_recipe"mysql::client"include_recipe"mysql::server"include_recipe"php"include_recipe"php::modul
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?