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,並行的,通過可視化技術的數據處理能力,開啟了一個新的窗口,迎來的一個新世界。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM