Tutorial #13: Framebuffer

Normalerweise wird die gesamte Szene direkt in den Backbuffer des Onscreen-Framebuffers gerendert. Nachdem ein Bild fertig gerendert wurde, wird Back- und Frontbuffer getauscht und die GPU zeigt das berechnete Bild auf dem Bildschirm an (Doppelpufferung).

In manchen Anwendungen ist es wünschenswert ein Bild zu rendern, ohne es gleich direkt dem Anwender anzuzeigen. Dieses "Offscreen-Rendering" kann mit dem Graph::FrameBuffer Knoten bewerkstelligt werden. Dabei werden Teile des Szenengraphen in einen "Offscreen-Framebuffer" gerendern. Der Offscreen-Framebuffer ist nicht direkt am Bildschirm sichtbar, kann aber z.B. als dynamische Textur verwendet werden.

Framebuffer & FrameBufferTexture

Ein Graph::FrameBuffer Knoten stellt das Basisobjekt für Offscreen-Rendering zur Verfügung ("Render Target"). Um auf die berechneten Daten zugreifen zu können, muss das Graph::FrameBuffer-Objekt mit zumindest einem passenden Graph::ITexture-Objekt verknüpft werden. Das Render Ergebnis wird dann in dem verknüpften Graph::FrameBufferTexture-Objekt (oder in mehreren Textur-Objekten) gespeichert.

Die Zuordnung erfolgt über die entsprechende id:

  • colorTextureId (RGBA Pixel-Farbwerte)
  • depthTextureId (z-Buffer Werte)
  • stencilTextureId (Stencil Buffer Werte)

Oftmals werden lediglich die Farbwerte benötigt, der Renderingprozess benötigt aber für die korrekte Sichtbarkeitsbestimmung einen aktiven Z-Buffer ("Depth Buffer"). In solchen Fällen ist es nicht notwendig eine eigene Z-Buffer-Textur (über depthTextureId) anzugeben. Das Setzen eines expliziten Z-Buffer Formats (Attribut depthBufferFormat) ist ausreichend, damit ein interner Z-Buffer angelegt und verwendet wird.

Um in den FrameBuffer rendern zu können, muss dieser mit einem oder mehreren Graph::View Knoten verknüpft werden (Attribut frameBufferId). Alle Objekte die für die Kamera deses Views sichtbar sind, werden in den FrameBuffer gerendert.

Zu beachten
Achtung! Werden mehrere Texturen mit einem Framebuffer verbunden, müssen alle Texturen dieselbe Größe haben.

Overlay Example

Als einfaches Beispiel erzeugen wir eine FrameBufferTexture und zeigen diese als Overlay über einem Hintergrundbild an. Das Hintergrundbild zeichnen wir als PlaneGeometry-Objekt mit passender Textur.

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

Für das Offscreen-Rendering benötigen wir ein FrameBufferTexture-Objekt und ein FrameBuffer-Objekt. Mit dem Attribut colorTextureId wird dem FrameBuffer die id der Farbtextur zugewiesen.

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

Jetzt benötigen wir noch einen View und ein Kamera-Objekt. Mit dem Attribut frameBufferId legen wir fest, dass dieser View nicht direkt in den Backbuffer sondern in unser FrameBuffer-Objekt zeichnen soll. Alles was für die Kamera fb_camera sichtbar ist, wird daher direkt in die FrameBufferTexture fb_tex_overlay gezeichnet.

<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">

Wir verwenden eine orthografische Kamera mit einer unitSize von 1. Dadurch erhalten wir einen 1:1 Pixelmatch. Mit colorClearValue="c0000000h" definieren wir ein halbtransparentes Schwarz als Hintergrundfarbe (ARGB).

Als Anzeigeobjekte definieren wir einfach einige TextGeometry-Objekte und ein Button-Objekt als Kindknoten der Kamera und ordnen diese nacheinander mit einem Aligner-Objekt an.

<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>
Bemerkungen
Der Blindtext wurde mit dem Text-Generator Vidya Gaemz Ipsum erzeugt.

Um die gerenderte Textur auch anzuzeigen, verwenden wir ein PlaneGeometry-Objekt, welches die FrameBufferTexture verwendet und mit der "normalen" Kamera gezeichnet wird.

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

Als Ergebnis erhalten wir zwei PlaneGeometry-Objekte, wobei ein Objekt ein vorgegebenes Bild als Textur verwendet und das zweite Objekt die FrameBufferTexture verwendet. Allerdings ist das Bild der FrameBufferTexture auf den Kopf gestellt.

tut0113_mirrored.png
Framebuffer gespiegelt

Um die Y-Koordinaten zu spiegeln ändern wir die texCoordY1 und texCoordY2 Attribute der PlaneGeometry von default 0/1 auf 1/0.

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

Scrolling

Mit ein paar Zeilen Code können wir den angezeigten Text auch noch verschiebbar machen. Dafür definieren wir einen zweiten Button mit id="buttonDFB" passend zum PlaneGeometry-Objekt.

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

Abhängig von der Event-Position am Button ändern wir direkt die Position des Aligner Knotens.

    // 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);
    }

Soll der Text in einer Schleife laufen, müssen wir die Position noch passend korrigieren, wenn der Aligner vollständig aus dem sichtbaren Bereich geschoben wurde:

        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

Zusätzlich zu den TextGeometry-Objekten haben wir einen Button als Kindknoten der Framebuffer Kamera erstellt. Wenn wir diesen Button wie gewohnt aus der Programmlogik abfragen wollen, müssen wir feststellen, dass dieser Button nicht auf Maus- und Touch-Events reagiert. Bei genauerer Überlegung wird auch klar warum das so ist: Für die Bildschirmkamera existiert der Button ja nicht, sondern lediglich die Textur die auf ein PlaneGeometry Objekt gezeichnet wird.

Um trotzdem eine Abfrage durchführen zu können, verwenden wir den Button buttonDFB und verknüpfen diesen über das Attribut frameBufferId mit unserem FrameBuffer-Objekt:

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

Der Button buttonDFB reicht nun die Events an die Framebuffer-Szene weiter und eine Abfrage kann wie gewohnt erfolgen. Durch die Angabe von outCoordY1="-1" und outCoordY2="1" wird wiederum die Spiegelung an der Y-Achse berücksichtigt.

In der Programmlogik können wir nun wie gewohnt auf den Button-Event reagieren. Wir zeigen den Event als Debug Meldung an und öffnen eine Webseite im System-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-2018 Spraylight GmbH.