Tutorial #13: Framebuffer

Usually the whole scene is rendered directly to the back buffer of the on-screen framebuffer. When all drawing operations are considered complete, the back buffer and the front buffer are swapped and the calculated image is displayed on the monitor (double buffering).

Some applications may want to render images without actually displaying them to the user. This "off-screen rendering" can be accomplished with Graph::FrameBuffer nodes. They will render parts of the scene graph to an "off-screen framebuffer". The off-screen framebuffer is not directly visible on the display but can be used e.g. as a dynamically created texture.

Framebuffer & FrameBufferTexture

A Graph::FrameBuffer node represents the base object for off-screen rendering (render target).

To be able to access the generated contents, a Graph::FrameBuffer object must refer to at least one proper Graph::ITexture object. The rendered data will then be stored in the referred Graph::FrameBufferTexture object (or in multiple texture objects).

The assignment is done by specifying the corresponding id:

  • colorTextureId (RGBA pixel color values)
  • depthTextureId (depth buffer values)
  • stencilTextureId (stencil buffer values)

Often, only color values are needed but the rendering process requires an active depth buffer for visible surface detection. In such a case, it is not necessary to create and attach a depth texture (with depthTextureId); instead, it is sufficient to explicitly set a depth buffer format (attribute depthBufferFormat) to create a depth buffer that is used internally only.

To use a FrameBuffer object for rendering, one or more Graph::View nodes must refer to this framebuffer via the frameBufferId attribute. All objects visible to the camera of this view are rendered to the FrameBuffer.

Note
Attention! When multiple textures are attached to a FrameBuffer object, all of these textures must have the same dimensions, or initialization will fail.

Overlay Example

As a simple example we create a FrameBufferTexture and display it as overlay above a background image. We use a PlaneGeometry object and a suitable texture to draw the background image.

<TextureState slot="0" textureId="/textures/image"/>
<PlaneGeometry textureSlots="0" materialSlot="3" scaleFactorX="800" scaleFactorY="600"/>

We create a FrameBufferTexture object and a FrameBuffer object for the off-screen rendering. The attribute colorTextureId is used to assign the color texture.

<FrameBufferTexture id="fb_tex_overlay" type="FLAT" sizeX="700" sizeY="500"/>
<FrameBuffer id="fb_overlay" colorTextureId="fb_tex_overlay"/>

Now we need to create a view and a camera object. By specifying the frameBufferId we define that the view should render into our framebuffer instead of the back buffer. All visible objects of the camera fb_camera will therefore be drawn to our FrameBufferTexture fb_tex_overlay.

<View id="view_overlay" frameBufferId="fb_overlay"/>
<Camera id="fb_camera"
        projectionType="ORTHOGRAPHIC"
        viewId="view_overlay" unitSizeX="1" unitSizeY="1" 
        nearPlane="0.1" farPlane="10" colorClearValue="c0000000h">

We use a orthographic camera with unitSize="1". By doing so, we will get a perfect 1:1 pixel match. The parameter colorClearValue="c0000000h" defines a semitransparent black as background color (ARGB).

To creata a visible content, we add some TextGeometry objects and one Button object as child nodes to the Camera node; a Aligner node is used to automatically arrange the objects.

<Transform posZ="-1">
    <Aligner id="aligner"
        axis="Y" order="DESCENDING"
        spacing="20" posX="0" posY="200" containerAlignmentY="TOP">
        <Instance graphResourceId="package_main:graph_textnode"
                    fontSize="30" textAlignmentX="CENTER"
                    text="Lightsword inspired 100% 2D nightmare."/>
        <Instance graphResourceId="package_main:graph_textnode"
                  fontSize="12" textAlignmentX="CENTER"
                  text="Charming by hate for endless runner Kill Screen!&#10;Superbot / John Doe / et al."/>
        <Instance graphResourceId="package_main:graph_textnode" 
                  text="Tactical because kids impressive experimentation but Software Development Kit in Windows VVVVVV trailer. Rip-off podcast Crayon Physics Deluxe Kill Screen Gnome’s Lair map editor Ludum Dare Indiegogo passion. The Banner Saga permadeath Molleindustria Microsoft god mode rapidshare explosions, Agency while self-publishing Minecraft Pac-Man Messhof retina otherwise indiegames.com otherwise internet amazing Twitter invite."/>
        <Instance graphResourceId="package_main:graph_textnode" 
                  text="Nanomachines and developer or sequel 16GB RAM before artsy although inventory inspiration famous. Infamous Anita Sarkeesian Gabe Newell Daedalic Geforce sale Third-Person-Shooter. McPixel if extra life Batman otherwise railgun although financial until Ron Gilbert and Sony new point and click. VVVVVV damsel in distress beat ‘em up PES Derek Yu 100% World of Warcraft internet gameplay hard to master."/>
        <Instance graphResourceId="package_main:graph_textnode" 
                  text="Speed was niche market when Bastion itch.io deep art tollerance video coding. Love when small rpgmaker in FTW for FTL and experimentation by Messhof while energy 2D. Analogue: A Hate Story 1-Bit since color palette IndieCade since Messhof split-screen. Feature Desktop Dungeons so history writer text adventure. Early access Daniel Benmergui and announcement because style parallax scrolling otherwise clones not-game The Banner Saga in peaceful self-publishing small team, IOS inclusive happy glitch if Analogue: A Hate Story provocative local multiplayer."/>
        <Instance graphResourceId="package_main:graph_textnode" 
                  text="Explore Christine Love or Phil Fish Assassin's Creed when Tecmo i7 while Flash Win8 future. Quirky Polytron Corporation gamestar.de PlayStation 3 in-game advertising action wrestling theme small-size bugged."/>
        <Instance graphResourceId="package_main:graph_textnode" 
                  text="Spiritual stealth 2016 portable lovely Street Fighter cactus Mountain Dew flight simulator shard. Map editor procedural content generation Vlambeer it's like experimentation studio i7. Doritos life-changing thoughtful gamescom IndieCade. Text trash non-commercial even if WASD Luftrausers cartridge life-changing. Limited 10/10 Tecmo television PC non-commercial bugged re-release FPS member Minecraft. "/>
        <Node>
            <Button id="button" sizeX="300" sizeY="50"/>
            <PlaneGeometry parametersSlot="0" materialSlot="2" scaleFactorX="300" scaleFactorY="50"/>
            <Instance graphResourceId="package_main:graph_textnode"
                      fontSize="20"
                      text="murlengine.com"
                      textAlignmentX="CENTER"/>
        </Node>
    </Aligner>
</Transform>
Remarks
The dummy text has been created with the text generator Vidya Gaemz Ipsum.

To display the rendered texture, we create a PlaneGeometry object which uses the FrameBufferTexture and which is drawn with the main camera.

<TextureState textureId="fb_tex_overlay" slot="1"/>
<PlaneGeometry id="planeDFB" 
                  materialSlot="4" textureSlots="1" scaleFactorX="700" scaleFactorY="500" 
                  depthOrder="2"/>

As a result we get two PlaneGeometry objects, where one object uses an image as texture and one object uses a FrameBufferTexture. Unfortunately the texture of the FrameBufferTexture is drawn upside down.

tut0113_mirrored.png
Framebuffer drawn upside down

To mirror the Y coordinates we change the attributes texCoordY1 and texCoordY2 of the PlaneGeometry from default 0/1 to 1/0.

<PlaneGeometry id="planeDFB" 
               materialSlot="4" textureSlots="1" scaleFactorX="700" scaleFactorY="500" 
               depthOrder="2" texCoordY1="1" texCoordY2="0"/>
tut0113_texcoord.png
Framebuffer corrected

Scrolling

By adding some lines of code we make the displayed text scrollable. To be able to do this, we define a second Button with id="buttonDFB" in accordance to the PlaneGeometry object.

    <Button id="buttonDFB" sizeX="700" sizeY="500"/>

We evaluate the button event values and directly change the position values of the Aligner node.

    // Scrolling
    if (deviceHandler->WasMouseButtonPressed(IEnums::MOUSE_BUTTON_LEFT) || deviceHandler->WasTouchPressed(0))
    {
        UInt32 id = mButtonDFB->GetActiveEventId(0);
        const Graph::Vector& vectorB = mButtonDFB->GetLocalEventPosition(id);
        mOldPosY = vectorB.y;
    }
    else if (deviceHandler->IsMouseButtonPressed(IEnums::MOUSE_BUTTON_LEFT) || deviceHandler->IsTouchPressed(0))
    {
        UInt32 id = mButtonDFB->GetActiveEventId(0);
        const Graph::Vector& vectorB = mButtonDFB->GetLocalEventPosition(id);

        Real transformY = mAligner->GetTransformInterface()->GetPositionY();
        Real newPos = transformY + (vectorB.y - mOldPosY);

        mOldPosY = vectorB.y;
        mAligner->GetTransformInterface()->SetPositionY(newPos);
    }

To get a looped scrolling, we need to additionally correct the position value, when the Aligner has been completely scrolled out of the visible area:

        Real fbSizeYHalf = mButtonDFB->GetScaleFactorY()/2;
        const Graph::IBoundingVolume* boundingVolume = mAligner->GetBoundingVolume();
        const Graph::Box& box = boundingVolume->GetInnerLocalBox();
        const Graph::Vector& min = box.GetMinimum();
        const Graph::Vector& max = box.GetMaximum();
        Real alignerSizeY = max.y - min.y;

        if (newPos < -fbSizeYHalf)
            newPos = alignerSizeY + fbSizeYHalf;
        else if (newPos > alignerSizeY + fbSizeYHalf)
            newPos = -fbSizeYHalf;

Button Events

In addition to the TextGeometry objects we also defined a Button as child node of the framebuffer camera. If we query the Button from within the program logic as usual, we see that the Button doesn't react to mouse and touch events. When we look closer, the reason soon becomes clear: The main camera does not know anything about the button; it only sees a texture which is drawn on a PlaneGeometry object.

To still be able to query the button, we use the Button buttonDFB and assign our FrameBuffer object via the attribute frameBufferId.

<Button id="buttonDFB" sizeX="700" sizeY="500"
        frameBufferId="fb_overlay" outCoordY1="-1" outCoordY2="1"/>

The button buttonDFB forwards all events to the frambuffer scene and we are able to query the button as usual. By specifying outCoordY1="-1" and outCoordY2="1" we are again correcting the mirrored Y coordinates.

Now we are able to react to button events in the program logic as usual. We indicate a button press by showing a debug message and by opening a website in the default browser.

// Button Press
if (mButton->WasReleasedInside())
{
    state->SetUserDebugMessage("Button pressed");
    if (deviceHandler->IsWebControlAvailable())
    {
        deviceHandler->OpenUrlInSystemBrowser("https://murlengine.com/?id=FrameBufferTutorial");
    }
}
tut0113_button_events.png
Framebuffer button events


Copyright © 2011-2024 Spraylight GmbH.