Cesium原理篇:6 Render模块(4: FBO)

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的实现。

FBO贴屏

部分浏览器,可能因为显卡兼容性的问题,比如你用的是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实现范走样效果。

PickFramebuffer

有了上面的过程,大家应该对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,并行的,通过可视化技术的数据处理能力,开启了一个新的窗口,迎来的一个新世界。

时间: 2024-09-29 08:57:53

Cesium原理篇:6 Render模块(4: FBO)的相关文章

Cesium原理篇:7最长的一帧之Entity(下)

上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSourceDisplay.Visualizer中.本篇则从Visualizer开始,介绍数据的处理,并最终实现渲染的过程. CesiumWidget.prototype.render = function() { if (this._canRender) { this._scene.initializ

Cesium原理篇:3最长的一帧之地形(2:高度图)

       这一篇,接着上一篇,内容集中在高度图方式构建地球网格的细节方面.        此时,Globe对每一个切片(GlobeSurfaceTile)创建对应的TileTerrain类,用来维护地形切片的相关逻辑:接着,在requestTileGeometry中,TileTerrain会请求对应该切片的地形数据.如果读者对这部分有疑问的话,可以阅读<Cesium原理篇:1最长的一帧之渲染调度>:最后,如果你是采用的高度图的地形服务,地形数据对应的是HeightmapTerrainDat

Cesium原理篇:6 Render模块(3: Shader)

在介绍Renderer的第一篇,我就提到WebGL1.0对应的是OpenGL ES2.0,也就是可编程渲染管线.之所以单独强调这一点,算是为本篇埋下一个伏笔.通过前两篇,我们介绍了VBO和Texture两个比较核心的WebGL概念.假设生产一辆汽车,VBO就相当于这个车的骨架,纹理相当这个车漆,但有了骨架和车漆还不够,还需要一台机器人来加工,最终才能成产出这辆汽车.而Shader模块就是负责这个生产的过程,加工参数(VBO,Texture),执行渲染任务. 这里假设大家对Shader有一个基本的

Cesium原理篇:6 Render模块(5: VAO&amp;RenderState&amp;Command)

VAO VAO(Vertext Array Object),中文是顶点数组对象.之前在<Buffer>一文中,我们介绍了Cesium如何创建VBO的过程,而VAO可以简单的认为是基于VBO的一个封装,为顶点属性数组和VBO中的顶点数据之间建立了关联.我们来看一下使用示例: var indexBuffer = Buffer.createIndexBuffer({ context : context, typedArray : indices, usage : BufferUsage.STATIC

Cesium原理篇:6 Renderer模块(2: Texture)

Texture也是WebGL中重要的概念,使用起来也很简单.但有句话叫大道至简,如果真的想要用好纹理,里面的水其实也是很深的.下面我们来一探究竟. 下面是WebGL中创建一个纹理的最简过程: var canvas = document.getElementById("canvas"); var gl = canvas.getContext("webgl"); // 创建纹理句柄 var texture = gl.createTexture(); // 填充纹理内容

Cesium原理篇:3最长的一帧之地形(3:STK)

有了之前高度图的基础,再介绍STK的地形相对轻松一些.STK的地形是TIN三角网的,基于特征值,坦白说,相比STK而言,高度图属于淘汰技术,但高度图对数据的要求相对简单,而且支持实时构建网格,STK具有诸多好处,但确实有一个不足,计算量比较大,所以必须预先生成.当然,Cesium也提供了一个Online的免费服务,不过因为是国外服务器,所以性能和不稳定因素都不小.好的东西自然得来不易,所以不同的层次,根据具体的情况选择不同的方案,技术并不是唯一决定因素,甚至不是主要因素. CesiumTerra

Cesium原理篇:1最长的一帧之渲染调度

原计划开始着手地形系列,但发现如果想要从逻辑上彻底了解地形相关的细节,那还是需要了解Cesium的数据调度过程,这样才能更好的理解,因此,打算先整体介绍一下Cesium的渲染过程,然后在过渡到其中的两个主要模块:地形数据和影像数据. 简述 设想一下,印度洋的暖流,穿过喜马拉雅山,形成了滴一滴水,落在了青藏高原的唐古拉山,顺势而下,涌入太平洋,长江之水自此经久不息.而Cesium的一切的一切,也是从一个并不起眼的函数开始的: 通过requestAnimationFrame函数,每一帧的结束,就是下

cesium原理篇(二)--网格划分【转】

转自:http://www.cnblogs.com/fuckgiser/p/5772077.html 上一篇我们从宏观上介绍了Cesium的渲染过程,本章延续上一章的内容,详细介绍一下Cesium网格划分的一些细节,包括如下几个方面: 流程 Tile四叉树的构建 LOD 流程 首先,通过上篇的类关系描述,我们可以看到,整个调度主要是update和endFrame两个函数中,前者分工,后者干活. 另外,QuadtreePrimitive类只要来维护整个地球的四叉树,而每一个Tile对应一个Quad

Cesium原理篇:3最长的一帧之地形(4:重采样)

       地形部分的原理介绍的差不多了,但之前还有一个刻意忽略的地方,就是地形的重采样.通俗的讲,如果当前Tile没有地形数据的话,则会从他父类的地形数据中取它所对应的四分之一的地形数据.打个比方,当我们快速缩放影像的时候,下一级的影像还没来得及更新,所以会暂时把当前Level的影像数据放大显示, 一旦对应的影像数据下载到当前客户端后再更新成精细的数据.Cesium中对地形也采用了这样的思路.下面我们具体介绍其中的详细内容.        上图是一个大概流程,在创建Tile的时候(prepa