Cesium 不仅仅提供了 FBO,也就是 Framebuffer 类,而且整个渲染过程都是在 FBO 中进行的。FBO,中文就是帧缓冲区,通常都属于高级用法,但其实,如果你了解了它的基本原理后,用起来还是很简单的,关键在于理解。比如你盖楼,地基没打好,盖第一层楼,还可以,盖第二层楼,有点挫了,盖第三层楼,塌了。你会认为第三层楼(FBO)太难了,其实根本原因还是出在地基上。
窗口系统所管理的帧缓存有自己的缓存对象(颜色,深度和模板),它们诞生于窗口创建前,而我们自己创建的帧缓冲,这些缓存对象则需要自己来手动创建。还是默认大家了解 FBO 的概念和 webGL 中使用的方式,在这个基础上我们来看一下 Cesium 中对 FBO 的封装。首先看一下 FBO 中的主要属性:
- function Framebuffer(options) {
- var gl = options.context._gl;
- var maximumColorAttachments = ContextLimits.maximumColorAttachments;
- this._gl = gl;
- this._framebuffer = gl.createFramebuffer();
- this._colorTextures = [];
- this._colorRenderbuffers = [];
- this._activeColorAttachments = [];
- this._depthTexture = undefined;
- this._depthRenderbuffer = undefined;
- this._stencilRenderbuffer = undefined;
- this._depthStencilTexture = undefined;
- this._depthStencilRenderbuffer = undefined;
- }
通过属性可以看到,Cesium 的 FBO 主要支持两种方式渲染到 Texture(RTT)和渲染到渲染缓冲区(RBO)两种方式,而且两种方式在使用上都基本相同,二选一,当然可以有多个颜色纹理(缓存),只要不超过 maximumColorAttachments 限制。当然也提供了帧缓存附件来保存渲染结果,这提供了同时写入多个缓存的能力(MRT),可以实现一些多屏和分屏效果。个人认为 RenderBuffer 性能上更好一些,尽可能减少数据消耗的消耗,在支持的能力上两者都差不多,都属于离屏渲染。但纹理可以拿出来独立用,而 RBO 的数据必须要关联到一个帧缓存对象后才有意义。我们来看一下创建方式:
- globeDepth.framebuffer = new Framebuffer({
- context: context,
- colorTextures: [globeDepth._colorTexture],
- depthStencilTexture: globeDepth._depthStencilTexture,
- destroyAttachments: false
- });
- this._fb = new Framebuffer({
- context: context,
- colorTextures: [new Texture({
- context: context,
- width: width,
- height: height
- })],
- depthStencilRenderbuffer: new Renderbuffer({
- context: context,
- format: RenderbufferFormat.DEPTH_STENCIL
- })
- });
个人认为,RenderBuffer 相比 RenderTexture 的方式要好一些,但前者在使用上有诸多限制,使用起来也不方便,关键是有一些接口是 WebGL2.0 的标准,兼容性很差,比如 glBlitFramebuffer,所以很多情况下,如果我们想要读该缓存对象时,一般都采用 Texture 方式。下面我们看看,当我们 new 一个新的 Framebuffer 时,内部的构造过程:
- // 绑定FBO
- Framebuffer.prototype._bind = function() {
- var gl = this._gl;
- gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebuffer);
- };
- // 释放FBO
- Framebuffer.prototype._unBind = function() {
- var gl = this._gl;
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
- };
- // 绑定颜色纹理,指定帧缓存附件attachment
- function attachTexture(framebuffer, attachment, texture) {
- var gl = framebuffer._gl;
- gl.framebufferTexture2D(gl.FRAMEBUFFER, attachment, texture._target, texture._texture, 0);
- }
- // 绑定渲染缓存对象,指定帧缓存附件attachment
- function attachRenderbuffer(framebuffer, attachment, renderbuffer) {
- var gl = framebuffer._gl;
- gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, renderbuffer._getRenderbuffer());
- }
- function Framebuffer(options) {
- this._bind();
- if (defined(options.colorTextures)) {
- // 查看颜色纹理的个数是否超过上限
- length = this._colorTextures.length = this._activeColorAttachments.length = textures.length;
- if (length > maximumColorAttachments) {
- throw new DeveloperError('The number of color attachments exceeds the number supported.');
- }
- // 依次绑定颜色纹理
- for (i = 0; i < length; ++i) {
- texture = textures[i];
- attachmentEnum = this._gl.COLOR_ATTACHMENT0 + i;
- attachTexture(this, attachmentEnum, texture);
- this._activeColorAttachments[i] = attachmentEnum;
- this._colorTextures[i] = texture;
- }
- }
- // 同理依次绑定渲染缓存,深度,模板等
- this._unBind();
- }
封装了整个 FBO 创建的过程,用户只需要简单几句话,Cesium 就很好的完成了封装的过程。Over,FBO 的用法结束了,就是这么简单。下面讲两个 Cesium 中使用 FBO 的地方,一个是 Cesium 最终将 FBO 贴到屏的过程,一个是 Pick 的实现。
部分浏览器,可能因为显卡兼容性的问题,比如你用的是 A 卡,会不支持深度纹理,Cesium 对此做了一些特殊考虑。下面的逻辑是在支持深度纹理的情况下的一个大概流程。首先,在初始化时会在 GlobeDepth 中创建 FBO:
- function createFramebuffers(globeDepth, context, width, height) {
- // GlobeDepth中创建一个和当前窗口大小一样的颜色纹理
- globeDepth.framebuffer = new Framebuffer({
- context: context,
- colorTextures: [globeDepth._colorTexture],
- depthStencilTexture: globeDepth._depthStencilTexture,
- destroyAttachments: false
- });
- }
而我们的渲染过程大致如下:
- function render(scene, time) {
- // 清空FBO
- var passState = scene._passState;
- passState.framebuffer = undefined;
- // 更新passState.framebuffer,并对该FBO渲染
- updateAndExecuteCommands(scene, passState, defaultValue(scene.backgroundColor, Color.BLACK));
- // 处理FBO,并渲染到屏幕中
- resolveFramebuffers(scene, passState);
- }
首先我们先了解一下如何渲染到 FBO 的过程(updateAndExecuteCommands),大概的逻辑是选择合适的 FrameBuffer,然后将 DrawCommand 渲染到该 FBO 上,关键代码如下:
- // updateAndExecuteCommands中调用,更新passState.framebuffer
- function updateAndClearFramebuffers(scene, passState, clearColor, picking) {
- if (environmentState.isSunVisible && scene.sunBloom && !useWebVR) {
- passState.framebuffer = scene._sunPostProcess.update(passState);
- } else if (useGlobeDepthFramebuffer) {
- passState.framebuffer = scene._globeDepth.framebuffer;
- } else if (useFXAA) {
- passState.framebuffer = scene._fxaa.getColorFramebuffer();
- }
- }
- // updateAndExecuteCommands中调用,开始渲染所有的DrawCommand
- function executeCommandsInViewport(firstViewport, scene, passState, backgroundColor, picking) {
- executeCommands(scene, passState);
- }
- // DrawCommand实际上会调用Context.draw方法,下一篇会详细介绍DrawCommand
- DrawCommand.prototype.execute = function(context, passState) {
- context.draw(this, passState);
- };
- Context.prototype.draw = function(drawCommand, passState) {
- passState = defaultValue(passState, this._defaultPassState);
- // 获取对应的FBO,优先离屏渲染
- var framebuffer = defaultValue(drawCommand._framebuffer, passState.framebuffer);
- beginDraw(this, framebuffer, drawCommand, passState);
- continueDraw(this, drawCommand);
- };
可见,framebuffer 的优先选择_globeDepth,其次是_fxaa,而且此时 Context.prototype.draw 中,必然是离屏渲染,也就是渲染到 FBO 上。另外,我们渲染的对象都封装到一个 DrawCommand 类中,比如之前地形切片,模型数据还是 Geometry 数据,最终都会创建一个 DrawCommand 来完成最终的渲染。
然后就是最终一步,所以养兵千日用兵一时,我们费了这么一番周折,最终来到了最后的一步。还记得我们在初始化的时候创建的 globeDepth._colorTexture,绑定在 GlobeDepth 中,而之前的 FBO 的过程正是对这张纹理的渲染,现在,我们要做的事情就是讲该纹理渲染到 FXAA 中的 FBO 中,然后 FXAA 将其渲染到屏幕中:
- function resolveFramebuffers(scene, passState) {
- if (useFXAA) {
- if (!useOIT && useGlobeDepthFramebuffer) {
- // 绑定到FXAA中的FBO中
- passState.framebuffer = scene._fxaa.getColorFramebuffer();
- // 将globeDepth的_colorTexture渲染到fxaa中
- scene._globeDepth.executeCopyColor(context, passState);
- }
- // framebuffer置空,即渲染到屏幕
- passState.framebuffer = environmentState.originalFramebuffer;
- // 将fxaa._texture渲染到屏幕
- scene._fxaa.execute(context, passState);
- }
- }
在渲染到屏幕中,FXAA(Fast Approximate Anti-Aliasing)的 Shader 中实现了抗锯齿的效果,相当于对地球做了一次美颜效果,最终完成该帧的渲染。对应的是 vec3 FxaaPixelShader(vec2 pos, sampler2D tex, vec2 rcpFrame) 方法,通过 GPU 实现范走样效果。
有了上面的过程,大家应该对 FBO 的使用方式有一个清楚的了解,下面我们来看看如果通过 FBO 实现拾取功能,对应的是 PickFramebuffer 类。其实拾取的思路很简单,就是来一张 "ID" 纹理,对每一个渲染的 Object 赋予一个唯一的 ID 并将 ID 转为 RGBA,在渲染到 "ID 纹理" 时,渲染的是 ID 颜色。这时用户点击想要拾取每一个地物,则查找对应 ID 纹理中的颜色值并转为 ID,根据 ID 找到对应的地物。在这个过程中,我们可以通过 FBO 和 Shader 实现 ID 纹理的绘制,并读取 FBO 的颜色纹理值两个技术点。首先先看看 ID 纹理的实现方式:
- // 构建一个PickID对象,包括该Object以及Key和Color
- function PickId(pickObjects, key, color) {
- this._pickObjects = pickObjects;
- this.key = key;
- this.color = color;
- }
- // 提供构建PickID方法
- // 保证每一个Ojbect的ID唯一
- // 通过Color.fromRgba(key)方法将ID转为对应的Color
- Context.prototype.createPickId = function(object) {++this._nextPickColor[0];
- var key = this._nextPickColor[0];
- this._pickObjects[key] = object;
- return new PickId(this._pickObjects, key, Color.fromRgba(key));
- };
这样,更新该地物在渲染是的颜色(Color->PickColor),这在 GLSL 代码中很简单就可以做到。而点击事件会触发场景的 Pick 事件:
- Scene.prototype.pick = function(windowPosition) {
- var passState = this._pickFramebuffer.begin(scratchRectangle);
- updateAndExecuteCommands(this, passState, scratchColorZero, true);
- resolveFramebuffers(this, passState);
- var object = this._pickFramebuffer.end(scratchRectangle);
- }
这时,会更新 screenSpaceRectangle,只对点击的相关区域进行渲染, 也就是只会更新局部区域,并返回 PickFramebuffer 中的 FBO,因此渲染结果都是保存在 PickFramebuffer 的帧缓冲中,完成 ID 纹理。最后 ,在 PickFramebuffer.prototype.end 中读取对应纹理的颜色值,找到对应的 object,完成整个拾取的过程。下面是获取颜色 ID 对应 Object 的过程:
- PickFramebuffer.prototype.end = function(screenSpaceRectangle) {
- var width = defaultValue(screenSpaceRectangle.width, 1.0);
- var height = defaultValue(screenSpaceRectangle.height, 1.0);
- var context = this._context;
- // 获取点击区域的颜色值,RGBA类型
- var pixels = context.readPixels({
- x: screenSpaceRectangle.x,
- y: screenSpaceRectangle.y,
- width: width,
- height: height,
- framebuffer: this._fb
- });
- var colorScratch = new Color();
- // RGBA转为4个byte数组,分别对应0~1之间的一个float颜色分量
- colorScratch.red = Color.byteToFloat(pixels[0]);
- colorScratch.green = Color.byteToFloat(pixels[1]);
- colorScratch.blue = Color.byteToFloat(pixels[2]);
- colorScratch.alpha = Color.byteToFloat(pixels[3]);
- // 通过颜色值获取对应的Object并返回
- var object = context.getObjectByPickColor(colorScratch);
- if (defined(object)) {
- return object;
- }
- // 没有选中任何Object
- return undefined;
- };
- Context.prototype.getObjectByPickColor = function(pickColor) {
- // 颜色值转为4个byte,在换算成一个int
- // 感觉这里绕了一个圈子
- return this._pickObjects[pickColor.toRgba()];
- };
FBO 使用简单,功能强大,之所以不容易理解,也在于实际应用中的灵活运用,很多实际问题的解决思路都可以通过 FBO 的技术,实现理屏处理(在看不见的情况下,通过 Shader 的可编程管线,通过编码实现高效灵活的解决),比如 FXAA 的范走样,或者 ID 纹理,这也正是 FBO 的强大之处。可以说,有了 FBO,我们可以将任何属性信息,以我们自定义的格式渲染到渲染缓冲对象或纹理中,并按照这个规范来解读这些属性,从而可以扩展出很多高级应用。并且 FBO 支持 MRT 的能力,实现了硬件上基于 GPU,并行的,通过可视化技术的数据处理能力,开启了一个新的窗口,迎来的一个新世界。
来源: http://www.cnblogs.com/fuckgiser/p/5991174.html