Output-States, Slots und Units

Diese Seite beschreibt, wie einzelne Knoten (z.B. Materialien, Texturen und Geometrie) während der Ausgabe-Verarbeitung des Szenengraphen zusammenspielen. Du solltest dir bereits ein Basiswissen über diese verschiedenen Knotenarten angeeignet haben. Falls nicht, empfehlen wir, zuerst das vorherige Kapitel (Ausgabe Verarbeitung) durchzulesen, bevor du hier weitermachst.

Output-States

Während das Kapitel über die Ausgabe Verarbeitung sowohl Audio- als auch Videorendering abdeckt, liegt hier der Fokus bei der Anzeige von visuellen Objekten. Die Audioausgabe ist eher unkompliziert und wird z.B. im Audio Tutorial genau beschrieben.

Die folgenden Abschnitte zeigen verschiedene Möglichkeiten Output-States einzusetzen, um ein effizientes Rendering zu erreichen.

Gekapselte States

Wenn du bereits das Cube Tutorial durchgearbeitet hast, kommt dir der folgende XML-Szenengraph wahrscheinlich bekannt vor. Er zeigt ein sehr "abgespecktes" Beispiel davon, wie Geometrie am Bildschirm mit allen benötigten Knoten gerendert wird.

<?xml version="1.0" ?>

<Graph>
    <View id="view"/>
    <Camera viewId="view" fieldOfViewX="2">
        <FixedProgram id="prog"/>
        <Material programId="prog">
            <PlaneGeometry posZ="-1"/>
        </Material>
    </Camera>
</Graph>

Die Camera hat zwei Kinderknoten: Einen FixedProgram Knoten, der offensichtlich Standardparameter benutzt, und einen über das programId Attribut verbundenen Material Knoten. Der Material Knoten selbst besitzt einen PlaneGeometry Knoten als Kindknoten.

Was können wir hier nun genau erkennen?

  • Erstens: Wir können eine Kamera aktivieren, indem wir einfach zeichenbare Geometrie als Subgraph-Knoten der Kamera definieren (siehe auch Tutorial). Ein Beispiel ist der PlaneGeometry-Knoten, der ein Kindeskind der Kamera ist.
  • Zweitens: Es ist möglich, ein bestimmtes Material einem Geometrieobjekt zuzuweisen, indem man wieder die Geometrie irgendwo innerhalb des Subgraphen des Materials definiert.

Wir können sehen, dass die Murl Engine irgendwie wissen muss, welche Kamera und welches Material zu einem bestimmten Zeitpunkt aktiv ist, wenn sie während der Abarbeitung des Szenengraphen (Traversierung) auf Geometrie zum Rendern trifft. Tatsächlich speichert die Murl Engine einen Zeiger auf unseren Kameraknoten in einem internen Stateobjekt bevor sie ihre Kinder verarbeitet, wobei immer ein Kind nach dem anderen verarbeitet wird. Sobald dieser Vorgang abgeschlossen ist, wird der vorherige Zustand wieder hergestellt, d.h. keine Kamera ist mehr aktiv. Dasselbe gilt hier für den Materialknoten, der auch in einem separaten internen Stateobjekt gespeichert wird.

Für diese Methode, wenn Knoten durch Kapselung einander zugewiesen werden, verwenden wir weiterhin den Begriff State-Kapselung.

Die folgenden Knoten können auf IDrawable Knoten wirken:

Auf Audioknoten, welche das IPlayable Interface implementiert haben, wirkt nur folgender Knoten:

Nachfolgend findet sich ein anderes Codebeispiel, das zeigt, wie ein flaches Objekt mittels Kamera, Material, Parameter und Textur gerendert wird. Die hervorgehobenen Zeilen markieren den Bereich, wo genau das Material zum Rendern aktiv ist, d.h. wo der interne Material-State auf das Material zeigt.

Zu beachten
In den folgenden Beispielen werden einige Angaben bewusst weggelassen und nur die relevanten Teile gezeigt. Die gezeigten Beispiele funktionieren daher großteils nicht für sich allein. Beispielsweise wird im folgendem Code-Schnipsel auf eine Grafikressource "main:image1" zugegriffen, die vorher definiert werden müsste.
<View id="view"/>
<Camera viewId="view" fieldOfViewX="2">
    <FixedParameters diffuseColor="FF0080BFh">
        <FixedProgram id="prog" coloringEnabled="yes"/>
        <Material programId="prog">
            <FlatTexture imageResourceId="main:image1">
                <PlaneGeometry posZ="-1"/>
            </FlatTexture>
        </Material>
    </FixedParameters>
</Camera>

Beachte, dass die Reihenfolge, in der Material, Textur etc. definiert werden, nicht von Bedeutung ist, solange alle gewünschten States für die Geometrie gesetzt werden. Das nächste Codefragment zeigt eine geänderte Hierarchie bei gleichbleibender Funktionalität. Die hervorgehobenen Zeilen markieren wieder den Bereich, wo das Material aktiv ist:

<View id="view"/>
<FixedProgram id="prog" coloringEnabled="yes"/>
<Material programId="prog">
    <FlatTexture imageResourceId="main:image1">
        <FixedParameters diffuseColor="FF0080BFh">
            <Camera viewId="view" fieldOfViewX="2">
                <PlaneGeometry posZ="-1"/>
            </Camera>
        </FixedParameters>
    </FlatTexture>
</Material>

Versuchen wir noch ein Beispiel zu konstruieren. Angenommen wir haben eine bestimmte Anzahl von kleinen Tieren in einem 2D-Spiel mit den folgenden Eigenschaften:

  • Jedes Tier soll aus einer texturierten Flächengeometrie bestehen, die das tatsächliche Aussehen repräsentiert, und einer anderen Flächengeometrie, die für das Rendern eines einfachen, halbtransparenten Schlagschattens verwendet wird.
  • Der Schlagschatten soll leicht nach rechts und nach unten versetzt sein.
  • Jedes Tier soll eine eigene Textur für ein individuelles Aussehen verwenden.
  • Zusätzlich soll jedem Tier ein eindeutiger Farbwert unabhängig von der Texturgrafik zugewiesen werden.
  • Alle Tiere teilen sich dieselbe Schlagschattentextur.
  • Die Hauptgrafik und die Schattengrafik werden mit unterschiedlichen Materialien gerendert.
  • Die Tiere sollen über dem Spielfeld verteilt sein und unabhängig voneinander bewegt werden.

Die folgende Grafik ist dem Spiel "Crazy Rings" (mit freundlicher Genehmigung von Cervo Media GmbH zur Verfügung gestellt) entnommen und zeigt eine Abbildung davon, was wir zu erreichen versuchen.

crazyrings.png
Tiere aus dem Spiel, wie wir sie gerendert haben möchten.

Als Negativ-Beispiel werden wir zuerst versuchen, den Szenengraphen nur mittels gekapselter States zu erstellen. Der Übersichtlichkeit halber begrenzen wir die Anzahl von Tieren auf drei und lassen einige XML-Attribute aus, wie z.B. den Kamera View etc.

Hier ist unser erster Ansatz:

<FixedProgram id="animal_prg" coloringEnabled="yes"/>
<FixedProgram id="shadow_prg"/>

<Camera>

    <!-- Main transformation for animal #1 -->
    <Transform id="animal_pos_1">
        <Material programId="animal_prg">
            <Texture imageResourceId="main:image1">
                <FixedParameters diffuseColor="FF8080h">
                    <PlaneGeometry depthOrder="1"/>
                </FixedParameters>
            </Texture>
        </Material>
        <Material programId="shadow_prg">
            <Texture imageResourceId="main:shadow">
                <PlaneGeometry depthOrder="0" x="10" y="-10"/>
            </Texture>
        </Material>
    </Transform>
    
    <!-- Main transformation for animal #2 -->
    <Transform id="animal_pos_2">
        <Material programId="animal_prg">
            <Texture imageResourceId="main:image2">
                <FixedParameters diffuseColor="80FF80h">
                    <PlaneGeometry depthOrder="1"/>
                </FixedParameters>
            </Texture>
        </Material>
        <Material programId="shadow_prg">
            <Texture imageResourceId="main:shadow">
                <PlaneGeometry depthOrder="0" x="10" y="-10"/>
            </Texture>
        </Material>
    </Transform>

    <!-- Main transformation for animal #2 -->
    <Transform id="animal_pos_3">
        <Material programId="animal_prg">
            <Texture imageResourceId="main:image3">
                <FixedParameters diffuseColor="8080FFh">
                    <PlaneGeometry depthOrder="1"/>
                </FixedParameters>
            </Texture>
        </Material>
        <Material programId="shadow_prg">
            <Texture imageResourceId="main:shadow">
                <PlaneGeometry depthOrder="0" x="10" y="-10"/>
            </Texture>
        </Material>
    </Transform>

</Camera>

Es ist offensichtlich, dass wir eine Vielzahl von doppelten Materialien und Texturen haben. Das ist grundsätzlich eine schlechte Idee, da z.B. jeder Texturknoten einen eigenen Speicherbereich für seine Grafikdaten hält und daher unnötig viel Speicher verbraucht wird. Wir benötigen also eine bessere Alternative.

Um auch im zweiten Ansatz lediglich gekapselte States zu verwenden, versuchen wir die Knoten im Graphen anders zu gruppieren, sodass wir zumindest keine Texturen duplizieren. Solch ein Ansatz könnte etwa so aussehen:

<FixedProgram id="animal_prg" coloringEnabled="yes"/>
<FixedProgram id="shadow_prg"/>

<Camera>

    <Material programId="animal_prg">
        <Texture imageResourceId="main:image1">
            <!-- Main transformation for animal #1 -->
            <Transform id="animal_pos_1">
                <FixedParameters diffuseColor="FF8080h">
                    <PlaneGeometry depthOrder="1"/>
                </FixedParameters>
            </Transform>
        </Texture>
        <Texture imageResourceId="main:image3">
            <!-- Main transformation for animal #2 -->
            <Transform id="animal_pos_2">
                <FixedParameters diffuseColor="80FF80h">
                    <PlaneGeometry depthOrder="1"/>
                </FixedParameters>
            </Transform>
        </Texture>
        <Texture imageResourceId="main:image2">
            <!-- Main transformation for animal #3 -->
            <Transform id="animal_pos_3">
                <FixedParameters diffuseColor="8080FFh">
                    <PlaneGeometry depthOrder="1"/>
                </FixedParameters>
            </Transform>
        </Texture>
    </Material>

    <Material programId="shadow_prg">
        <Texture imageResourceId="main:shadow">
            <!-- Shadow transformation for animal #1 -->
            <Transform id="shadow_pos_1">
                <PlaneGeometry depthOrder="0" x="10" y="-10"/>
            </Transform>
            <!-- Shadow transformation for animal #2 -->
            <Transform id="shadow_pos_1">
                <PlaneGeometry depthOrder="0" x="10" y="-10"/>
            </Transform>
            <!-- Shadow transformation for animal #3 -->
            <Transform id="shadow_pos_1">
                <PlaneGeometry depthOrder="0" x="10" y="-10"/>
            </Transform>
        </Texture>
    </Material>

</Camera>

Wir haben es geschafft, die doppelten Materialien und sogar die doppelten Texturen zu vermeiden, jedoch nur, weil wir einen separaten Transformknoten für jede Schattenfläche eingefügt haben. Das Ergebnis ist trotzdem nicht zufriedenstellend, da jedes Mal wenn die einzelnen Tiere bewegt werden, müssen auch die Schattenpositionen explizit neu gesetzt werden. Es muss also noch eine bessere Möglichkeit geben!

Explizite State Knoten

Die oben gezeigten Beispiele zeigen deutlich, dass die Verwendung von gekapselten States manchmal relativ unhandlich werden kann. Dies trifft besonders auf große Szenegraphen zu, in denen viele Geometrieobjekte gerendert und z.B. viele verschiedene Materialien und/oder Texturen zugewiesen werden müssen.

Außerdem haben die Beispiele gezeigt, dass es mit diesem Schema unmöglich ist, z.B. einen bestimmten Materialknoten wiederzuverwenden, wenn dieser nicht im selben Subgraphen liegt. Daher müssen wir entweder massenhaft Knoten duplizieren (was nicht ressourcen-freundlich ist) oder einen unhandlichen und nicht mehr intuitiven Szenengraphen erstellen.

Glücklicherweise gibt es dafür eine Lösung: die Verwendung von expliziten State-Knoten. Alle Knoten, die auf der vorherigen Seite (Ausgabe Verarbeitung) beschrieben wurden, haben einen dazugehörigen State-Knoten. Dieser erlaubt es, die Elemente erst später im Szenengraph zu aktivieren. Das Grundkonzept wird im folgenden Codefragment dargestellt, wobei derselbe Task wie oben durchgeführt wird: das Rendern einer Fläche mittels Kamera, Material, Parameter und Texturen. Zum besseren Verständnis ist wieder der aktive Bereich des Materials hervorgehoben:

<!-- Create the actual nodes -->
<View id="view"/>
<Camera id="camera" viewId="view" fieldOfViewX="2"/>
<FixedParameters id="params" diffuseColor="FF0080BFh"/>
<FixedProgram id="prog" coloringEnabled="yes"/>
<Material id="mat" programId="prog"/>
<FlatTexture id="tex" imageResourceId="main:image1"/>

<!-- Set the desired states -->
<CameraState cameraId="camera"/>
<ParametersState parametersId="params"/>
<MaterialState materialId="mat"/>
<TextureState textureId="tex"/>

<!-- Render geometry -->

<PlaneGeometry posZ="-1"/>

Nun können wir (neben der komplett veränderten Struktur) einen deutlichen Unterschied sehen: Die Gesamtknotenanzahl hat sich nahezu verdoppelt. In der Praxis ist das normalerweise kein großes Problem, da die neu eingefügten State-Knoten sehr leichtgewichtig sind (im Vergleich zu den Knoten, auf die sie verweisen). Sie benötigen nur einen geringen Speicherplatz und nur wenig Zeit für die Verarbeitung.

Ein anderer Unterschied ist, dass den Knoten eine eigene ID zugewiesen werden muss, um diese dann mit einem zugehörigen State-Knoten referenzieren zu können. Auch hier wiegen normalerweise die Vorteile den höheren Schreibaufwand bei weitem auf, da der jeweilige Knoten beliebig oft referenziert (und somit wiederverwendet) werden kann.

Starten wir nun einen dritten Ansatz für unser 2D-Spiel unter Verwendung von expliziten State-Knoten. Der resultierende XML-Code sieht dem vorherigen Beispiel recht ähnlich. Im untenstehenden Codefragment sind jene Zeilen hervorgehoben, in denen der erste FixedParameters Knoten ("par_1") aktiv ist, um die tatsächliche Lebenszeit eines Knotens zu zeigen:

<!-- Create the actual nodes -->
<Camera id="camera"/>

<FixedProgram id="figure_prg" coloringEnabled="yes"/>
<FixedProgram id="shadow_prg"/>

<Material id="figure_mat" programId="figure_prg"/>
<Material id="shadow_mat" programId="shadow_prg"/>

<Texture id="tex_1" imageResourceId="main:image1"/>
<Texture id="tex_2" imageResourceId="main:image2"/>
<Texture id="tex_3" imageResourceId="main:image3"/>
<Texture id="tex_shadow" imageResourceId="main:shadow"/>

<FixedParameters id="par_1" diffuseColor="FF8080h"/>
<FixedParameters id="par_2" diffuseColor="80FF80h"/>
<FixedParameters id="par_3" diffuseColor="8080FFh"/>

<!-- Set common states -->
<CameraState cameraId="camera"/>

<!-- Main transformation for animal #1 -->
<Transform id="animal_pos_1">
    <MaterialState materialId="figure_mat"/>
    <TextureState textureId="tex_1"/>
    <ParametersState parametersId="par_1"/>
    <PlaneGeometry depthOrder="1"/>

    <MaterialState materialId="shadow_mat"/>
    <TextureState textureId="tex_shadow"/>
    <PlaneGeometry depthOrder="0" x="10" y="-10"/>
</Transform>

<!-- Main transformation for animal #2 -->
<Transform id="animal_pos_2">
    <MaterialState materialId="figure_mat"/>
    <TextureState textureId="tex_2"/>
    <ParametersState parametersId="par_2"/>
    <PlaneGeometry depthOrder="1"/>

    <MaterialState materialId="shadow_mat"/>
    <TextureState textureId="tex_shadow"/>
    <PlaneGeometry depthOrder="0" x="10" y="-10"/>
</Transform>

<!-- Main transformation for animal #3 -->
<Transform id="animal_pos_3">
    <MaterialState materialId="figure_mat"/>
    <TextureState textureId="tex_3"/>
    <ParametersState parametersId="par_3"/>
    <PlaneGeometry depthOrder="1"/>

    <MaterialState materialId="shadow_mat"/>
    <TextureState textureId="tex_shadow"/>
    <PlaneGeometry depthOrder="0" x="10" y="-10"/>
</Transform>

In diesem Beispiel haben wir es geschafft, ohne Material- und Texturduplikate auszukommen, indem wir einfach die jeweiligen States vor dem Rendern der Flächen passend gesetzt haben. Zusätzlich, kommen wir nun mit einem einzigen Transformknoten pro Tier aus, welcher auf die Tiergrafik und auf den Schatten wirkt.

Wir haben jetzt zwar insgesamt mehr Knoten, aber alle neu hinzugefügten Knoten sind einfache, leichtgewichtige State-Knoten.

Wie man an den markierten Zeilen sehen kann, reicht die Lebenszeit des Parameters-States über den Closing-Tag seines Eltern Transform Knotens hinaus. Solange man auf keinen anderen Knoten trifft, der den aktuellen State verändert, bleibt der zuletzt gesetzte State bestehen. Dies ist vielleicht später von Bedeutung, da Probleme auftreten können, wenn man vergisst einen bestimmten State zurückzusetzen. Auf mögliche Lösungen für diese Situation, werden wir im Abschnitt Sub-States noch genauer eingehen.

Zu beachten
Randnotiz: Vielleicht hast du dich schon über die einzelnen ParametersState Knoten gewundert, welche den Parameter für die jeweilige Grafikgeometrie setzen: Der aktuelle Parameters-State ist immer noch aktiv, wenn die folgende Schattengeometrie gerendert wird! In unserem Fall spielt das keine Rolle, da wir für das "shadow_mat" Material keine Farbe aktiviert haben. Somit hat der Farbparameter keinen Effekt beim Rendern der Schattenfläche.

Kombination von expliziten State-Knoten und gekapselten States

Natürlich schließen sich die beiden Methoden, explizite State-Knoten und gekapselte States, nicht gegenseitig aus. Abhängig von der Aufgabenstellung, kann die eine oder die andere Methode die bessere Alternative sein. Natürlich können auch beide Methoden in einem Szenengraphen kombiniert werden.

Üblicherweise sind gekapselte States die bessere Wahl, wenn bestimmte Knoten nicht wiederverwendet werden müssen. Dies ist z.B. bei FixedParameters Knoten häufig der Fall, wenn diese lediglich eine bestimmte Farbe für eine bestimmte Geometrie definieren (wie beispielweise in unserem 2D-Spiel, wo wie erwähnt jedem Tier eine eindeutige Farbe zugewiesen werden soll). Da wir in unserem einfachen Beispiel auch lediglich eine Kamera für die gesamte Geometrie verwenden, ist dieser Knoten der nächste Kandidat für gekapselte States.

Wir schreiben daher den Szenengraphen erneut um (dieses mal ist es aber eher Geschmackssache).

<!-- Create the nodes we want to reuse -->
<FixedProgram id="figure_prg" coloringEnabled="yes"/>
<FixedProgram id="shadow_prg"/>

<Material id="figure_mat" programId="figure_prg"/>
<Material id="shadow_mat" programId="shadow_prg"/>

<Texture id="tex_1" imageResourceId="main:image1"/>
<Texture id="tex_2" imageResourceId="main:image2"/>
<Texture id="tex_3" imageResourceId="main:image3"/>
<Texture id="tex_shadow" imageResourceId="main:shadow"/>

<!-- Activate single camera -->
<Camera>

    <!-- Main transformation for animal #1 -->
    <Transform id="animal_pos_1">
        <MaterialState materialId="figure_mat"/>
        <TextureState textureId="tex_1"/>
        <FixedParameters diffuseColor="FF8080h">
            <PlaneGeometry depthOrder="1"/>
        </FixedParameters>

        <MaterialState materialId="shadow_mat"/>
        <TextureState textureId="tex_shadow"/>
        <PlaneGeometry depthOrder="0" x="10" y="-10"/>
    </Transform>

    <!-- Main transformation for animal #2 -->
    <Transform id="animal_pos_2">
        <MaterialState materialId="figure_mat"/>
        <TextureState textureId="tex_2"/>
        <FixedParameters diffuseColor="80FF80h">
            <PlaneGeometry depthOrder="1"/>
        </FixedParameters>

        <MaterialState materialId="shadow_mat"/>
        <TextureState textureId="tex_shadow"/>
        <PlaneGeometry depthOrder="0" x="10" y="-10"/>
    </Transform>

    <!-- Main transformation for animal #3 -->
    <Transform id="animal_pos_3">
        <MaterialState materialId="figure_mat"/>
        <TextureState textureId="tex_3"/>
        <FixedParameters diffuseColor="8080FFh">
            <PlaneGeometry depthOrder="1"/>
        </FixedParameters>

        <MaterialState materialId="shadow_mat"/>
        <TextureState textureId="tex_shadow"/>
        <PlaneGeometry depthOrder="0" x="10" y="-10"/>
    </Transform>

</Camera>

Nun sieht der Szenengraph schon recht ordentlich aus. Trotzdem geht es noch besser, wie der nächste Abschnitt zeigt.

State Slots

Bisher wurde gezeigt, wie die Ausgabe-Verarbeitung des Szenengraphen in der Murl Engine funktioniert und wie einzelne Eigenschaften Geometrieobjekten zugewiesen werden.

Wenn man sich die letzten Beispiele zum 2D-Spiel ansieht, fühlt es sich trotzdem noch relativ umständlich an. Für jedes einzelne Tier schalten wir States zurück und nach vor und der daraus resultierende Szenengraph sieht etwas überladen aus.

Um den Szenengraphen diesbezüglich noch weiter zu verbessern, verwenden wir einen weiteren Mechanismus der Murl Engine: State Slots. Einfach ausgedrückt, die Engine kann sich mehr als nur einen State für die folgenden Elemente merken:

All diese Elemente sind vom IStateSlot Interface abgeleitet und bieten Methoden um einen Slot-Index zu setzten und zu lesen. Mit dem slot Attribut kann dieser Slot-Index in einer XML-Datei des Szenengraphen definiert werden. Insgesamt gibt es 128 parallele State Slots für Materialien, 128 für Parameter und ebenfalls 128 für Texturen.

Was heißt das also? In allen vorherigen Beispielen wurde kein slot Attribut innerhalb unseres Szenengraphen angegeben, weshalb dafür immer der Default-Wert 0 verwendet wurde. Nun schreiben wir den Szenengraph unter Verwendung des slot Attributs nochmals um. Der Klarheit halber geben wir das slot Attribut auch an, wenn der Parameter ohnehin dem Standardwert 0 entspricht. Die geänderten Zeilen sind hervorgehoben:

    <!-- Main transformation for animal #1 -->
    <Transform id="animal_pos_1">
        <MaterialState materialId="figure_mat" slot="0"/>
        <TextureState textureId="tex_1" slot="0"/>
        <FixedParameters diffuseColor="FF8080h" slot="0">
            <PlaneGeometry depthOrder="1"/>
        </FixedParameters>

        <MaterialState materialId="shadow_mat" slot="1"/>
        <TextureState textureId="tex_shadow" slot="4"/>
        <PlaneGeometry depthOrder="0" x="10" y="-10"/>
    </Transform>

Wenn wir den Szenengraphen so rendern würden, müssten wir feststellen, dass der Tierschatten völlig falsch gezeichnet würde. Wir haben das "shadow_mat" Material auf Slot 1 und die "tex_shadow" Textur auf Slot 4 gesetzt, der PlaneGeometry-Knoten verwendet aber immer noch das aktive Material und die aktive Textur von Slot 0 (also das Material "figure_mat" und die Textur "tex_1").

Um dieses Problem zu lösen, müssen wir die materialSlot, parametersSlot und textureSlots Attribute des PlaneGeometry Knotens verwenden. Diese Attribute erlauben uns den Slot-Index zu definieren, den der PlaneGeometry Knoten zum Rendern verwendet. Achtung: Es heißt textureSlots und nicht textureSlot. Den Grund dafür lernen wir später noch kennen! Wieder werden die eigentlich überflüssigen 0-Index-Attribute der Klarheit halber angegeben. Die Änderungen sind hervorgehoben:

    <!-- Main transformation for animal #1 -->
    <Transform id="animal_pos_1">
        <MaterialState materialId="figure_mat" slot="0"/>
        <TextureState textureId="tex_1" slot="0"/>
        <FixedParameters diffuseColor="FF8080h" slot="0">
            <PlaneGeometry materialSlot="0" textureSlots="0" parametersSlot="0" depthOrder="1"/>
        </FixedParameters>

        <MaterialState materialId="shadow_mat" slot="1"/>
        <TextureState textureId="tex_shadow" slot="4"/>
        <PlaneGeometry materialSlot="1" textureSlots="4" parametersSlot="0" depthOrder="0" x="10" y="-10"/>
    </Transform>

Nun stimmt der gezeichnete Tierschatten wieder. Wir wenden diese Änderungen auch auf die anderen Tiere an. Zusätzlich weisen wir jeder Haupttextur "tex_1" bis "tex_3" einen individuellen Slot-Index von 0 bis 2 zu (es gibt mit 128 Slots ohnehin genug). Die Zeilen, in denen eine Materialzuweisung erfolgt, sind wiederum markiert:

<!-- Activate single camera -->
<Camera>

    <!-- Main transformation for animal #1 -->
    <Transform id="animal_pos_1">
        <MaterialState materialId="figure_mat" slot="0"/>
        <TextureState textureId="tex_1" slot="0"/>
        <FixedParameters diffuseColor="FF8080h" slot="0">
            <PlaneGeometry materialSlot="0" textureSlots="0" parametersSlot="0" depthOrder="1"/>
        </FixedParameters>

        <MaterialState materialId="shadow_mat" slot="1"/>
        <TextureState textureId="tex_shadow" slot="4"/>
        <PlaneGeometry materialSlot="1" textureSlots="4" parametersSlot="0" depthOrder="0" x="10" y="-10"/>
    </Transform>

    <!-- Main transformation for animal #2 -->
    <Transform id="animal_pos_2">
        <MaterialState materialId="figure_mat" slot="0"/>
        <TextureState textureId="tex_3" slot="2"/>
        <FixedParameters diffuseColor="80FF80h" slot="0">
            <PlaneGeometry materialSlot="0" textureSlots="2" parametersSlot="0" depthOrder="1"/>
        </FixedParameters>

        <MaterialState materialId="shadow_mat" slot="1"/>
        <TextureState textureId="tex_shadow" slot="4"/>
        <PlaneGeometry materialSlot="1" textureSlots="4" parametersSlot="0" depthOrder="0" x="10" y="-10"/>
    </Transform>

    <!-- Main transformation for animal #3 -->
    <Transform id="animal_pos_3">
        <MaterialState materialId="figure_mat" slot="0"/>
        <TextureState textureId="tex_2" slot="1"/>
        <FixedParameters diffuseColor="8080FFh" slot="0">
            <PlaneGeometry materialSlot="0" textureSlots="1" parametersSlot="0" depthOrder="1"/>
        </FixedParameters>

        <MaterialState materialId="shadow_mat" slot="1"/>
        <TextureState textureId="tex_shadow" slot="4"/>
        <PlaneGeometry materialSlot="1" textureSlots="4" parametersSlot="0" depthOrder="0" x="10" y="-10"/>
    </Transform>

</Camera>

Wenn wir uns nun die einzelnen MaterialState Knoten in den markierten Zeilen genauer ansehen, sehen wir, dass sich die Zuweisung für die einzelnen Slots in unserem Graphen nicht mehr ändert. Beispielsweise wird dem Material Slot 0 in Zeile 6 das Material "figure_mat" zugewiesen. Diese Zuweisung wiederholt sich in den Zeilen 19 und 32, ohne dass der Slot 0 dazwischen für ein anderes Material verwendet wurde. Dasselbe gilt für das Material "shadow_mat" und dem Material Slot 1 in den Zeilen 12, 25 und 38.

Diese wiederholten Zuweisungen mit demselben Material sind natürlich völlig unnötig. Dasselbe gilt für die State-Zuweisung der "tex_shadow" Textur. Wir entfernen nun alle überflüssigen State-Knoten und Ändern die Reihenfolge für eine bessere Lesbarkeit. Außerdem entfernen wir die überflüssigen Slot-Attribute für die Farbparameter, da diese sowieso standardmäßig auf 0 gesetzt sind.

<!-- Create the nodes we want to reuse -->
<FixedProgram id="figure_prg" coloringEnabled="yes"/>
<FixedProgram id="shadow_prg"/>

<Material id="figure_mat" programId="figure_prg"/>
<Material id="shadow_mat" programId="shadow_prg"/>

<Texture id="tex_1" imageResourceId="main:image1"/>
<Texture id="tex_2" imageResourceId="main:image2"/>
<Texture id="tex_3" imageResourceId="main:image3"/>
<Texture id="tex_shadow" imageResourceId="main:shadow"/>

<!-- Set common states -->
<MaterialState materialId="figure_mat" slot="0"/>
<MaterialState materialId="shadow_mat" slot="1"/>

<TextureState textureId="tex_1" slot="0"/>
<TextureState textureId="tex_2" slot="1"/>
<TextureState textureId="tex_3" slot="2"/>
<TextureState textureId="tex_shadow" slot="4"/>

<!-- Activate single camera -->
<Camera>

    <!-- Main transformation for animal #1 -->
    <Transform id="animal_pos_1">
        <FixedParameters diffuseColor="FF8080h">
            <PlaneGeometry materialSlot="0" textureSlots="0" depthOrder="1"/>
        </FixedParameters>
        <PlaneGeometry materialSlot="1" textureSlots="4" depthOrder="0" x="10" y="-10"/>
    </Transform>

    <!-- Main transformation for animal #2 -->
    <Transform id="animal_pos_2">
        <FixedParameters diffuseColor="80FF80h">
            <PlaneGeometry materialSlot="0" textureSlots="2" depthOrder="1"/>
        </FixedParameters>
        <PlaneGeometry materialSlot="1" textureSlots="4" depthOrder="0" x="10" y="-10"/>
    </Transform>

    <!-- Main transformation for animal #3 -->
    <Transform id="animal_pos_3">
        <FixedParameters diffuseColor="8080FFh">
            <PlaneGeometry materialSlot="0" textureSlots="1" depthOrder="1"/>
        </FixedParameters>
        <PlaneGeometry materialSlot="1" textureSlots="4" depthOrder="0" x="10" y="-10"/>
    </Transform>

</Camera>

Nun sieht der Graph schon deutlich besser aus! Es gibt keine doppelten Knoten und keine überflüssigen State-Zuweisungen mehr. Die einzelnen Subgraphen für die Tiere sind deutlich kleiner geworden (jedes Tier hat nur mehr vier Knoten). Das ist ziemlich gut für das, was wir erreichen möchten.

Genau genommen haben wir fast keine doppelten Knoten, da die Geometrien der Schattenflächen alle gleich sind. Außerdem sind die drei Subgraphen der Tiere sehr ähnlich zueinander. Für solche Fälle gibt es weitere nützliche Methoden, um den Szenengraphen noch effizienter zu gestalten, welche auf der Seite Instanzen und Referenzen genauer beschrieben werden.

Die Verwendung von State Slots hat noch einen weiteren Vorteil: Wenn z.B. eine Grafik eines Tieres im Code schnell geändert werden soll, kannst du einfach auf den PlaneGeometry-Knoten im jeweiligen Subgraphen über das IDrawable Interface zugreifen und mit der SetTextureSlot() Methode den Slot-Index auf einen anderen Wert setzen. Dies geht sehr einfach und ist zudem eine sehr effiziente Möglichkeit.

Eine Sache sollte bei der Verwendung von State Slots jedoch immer beachtet werden: Kenne deine States! Sobald der Szenengraph etwas komplexer wird, kann es schwierig werden, einen Überblick über alle State-Zuweisungen zu bewahren, vor allem wenn du dir dein Layout nicht sorgfältig durchgeplant hast. Tipps diesbezüglich werden auch im Abschnitt Sub-States behandelt.

State Units

Erinnerst du dich noch an die Namenskonfusion im oberen Abschnitt mit textureSlots bzw. textureSlot? Es gibt noch eine Sache, die man beim Arbeiten mit multiplen States beachten muss.

Bisher haben wir immer nur eine einzelne Textur auf ein Geometrieobjekt angewandt. Moderne Grafikprozessoren verfügen jedoch über ein sehr hilfreiches Feature, bekannt als Multitexturing; siehe auch de.wikipedia.org/wiki/Texture_Mapping. Mit der Murl Engine können (derzeit) bis zu acht einzelne Texturen gleichzeitig für das Rendern eines Geometrieobjekts verwendet werden (üblicherweise mit einem GPU-Shaderprogramm).

Wie können nun mehrere Texturen gleichzeitig angegeben werden? Die Antwort lautet State-Units.

Wir wissen bereits, wie man bis zu 128 Texturen über die Slots einzelnen Geometrien zuweist. Dabei wurde bisher immer die Default-Textur-Unit mit dem Standardindex 0 verwendet.

Dieses Verhalten können wir durch die Angabe des unit Attributs ändern. Angenommen wir wollen einige beleuchtete (light mapped) Geometrieflächen rendern; siehe auch en.wikipedia.org/wiki/Lightmap. Dafür verwenden wir verschiedene Grundtexturen, die der Textur-Unit 0 zugewiesen werden, sowie verschiedene Lightmaps, die der Textur-Unit 1 zugweisen werden. Wir setzen voraus, dass das Shaderprogramm des jeweiligen Materials diese zwei Texturen geeignet kombiniert, um die Ausgabegrafik zu berechnen. Auf der Seite Shader-basiertes Rendering befinden sich weitere Details dazu.

<!-- Set material state -->
<MaterialState materialId="my_lightmap_mat_1" slot="0"/>
<MaterialState materialId="my_lightmap_mat_2" slot="1"/>

<!-- Set base texture states -->
<TextureState textureId="base_tex_1" slot="0" unit=0"/>
<TextureState textureId="base_tex_2" slot="1" unit=0"/>
<TextureState textureId="base_tex_3" slot="2" unit=0"/>

<!-- Set lightmap texture states -->
<TextureState textureId="light_tex_1" slot="0" unit=1"/>
<TextureState textureId="light_tex_2" slot="1" unit=1"/>
<TextureState textureId="light_tex_3" slot="2" unit=1"/>
<TextureState textureId="light_tex_4" slot="3" unit=1"/>
<TextureState textureId="light_tex_5" slot="4" unit=1"/>

<!-- Render planes -->
<PlaneGeometry materialSlot="0" textureSlots="0,2" x="0"/>
<PlaneGeometry materialSlot="0" textureSlots="1,1" x="10"/>
<PlaneGeometry materialSlot="0" textureSlots="2,0" x="20"/>
<PlaneGeometry materialSlot="0" textureSlots="0,1" x="30"/>
<PlaneGeometry materialSlot="0" textureSlots="1,4" x="40"/>
<PlaneGeometry materialSlot="1" textureSlots="2,3" x="50"/>
<PlaneGeometry materialSlot="1" textureSlots="0,4" x="60"/>
<PlaneGeometry materialSlot="1" textureSlots="1,2" x="70"/>

Eine Zuweisung einer Textur, an eine andere Unit als 0, kann also einfach durch das unit Attribut eines TextureState Knoten erfolgen. Dabei muss der angegebene Index im Bereich 0 bis 7 liegen.

Außerdem sehen wir, dass es offensichtlich ein weiteres Slot-Set für die Textur-Unit 1 geben muss, da ansonsten die Slot-Zuweisungen der Zeilen 6 bis 8 in den Zeilen 11 bis 13 wieder überschrieben würden. Tatsächlich gibt es für jede Unit 128 Texturslots. Das heißt, dass es insgesamt 128x8=1024 verschiedene TextureState-Zuweisungen geben kann.

Für das Rendern können wir nun irgendeine Kombination zweier Texturen verwenden. Es muss nur darauf geachtet werden, dass eine Textur aus den Slots in Unit 0 entnommen wird und die andere aus den Slots in Unit 1. Wir definieren für jeden PlaneGeometry Knoten zwei Texturen.

Nun wird es offensichtlich, warum das Attribut textureSlots und nicht textureSlot heißt: Wir können eine durch Kommas getrennte Liste von Slot-Indexen festlegen, welche für die Auswahl der passenden Texturen verwendet wird. Der erste festgelegte Wert steht für den Index im Slot-Array für Unit 0, der nächste für Unit 1 usw. Diese durch Kommas getrennte Liste muss mindestens einen Wert enthalten (für Unit 0) und kann maximal acht verschiedene Werte haben, wobei jeder für eine verfügbare Textur in aufsteigender Reihenfolge steht.

Zum Beispiel verwendet der Geometrieknoten in Zeile 22 das Material von Slot 0 ("my_lightmap_mat_1"), die Textur von Slot 1 für die Textur-Unit 0 ("base_tex_2") und die Textur von Slot 4 für die Textur-Unit 1 ("light_tex_5") zum Rendern.

Es ist durchaus möglich, dass ein Shaderprogramm nur Texturen in den beiden Textur-Units 0 und 7 erwartet. In diesem Fall ist es eher störend, alle nicht relevanten Slot-Indexe (1 bis 6) setzen zu müssen (z.B. textureSlots="17,0,0,0,0,0,0,39"). Aus diesem Grund akzeptieren Geometrieknoten auch ein textureSlot Attribut (Achtung Singularform!) als Eingabe, aber nur in folgender Form:

<PlaneGeometry 
    materialSlot="42"
    textureSlot.0="17" 
    textureSlot.7="39" 
    x="0"
/>

Mehrere Units gibt es nicht nur bei Texturen, sondern auch bei anderen Elementen. Die folgende Liste zeigt alle Elemente, die vom IStateUnit Interface abgeleitet wurden und mehrere Units unterstützen:

Sub-States

Wie bereits im Abschnitt Explizite State Knoten oberhalb erwähnt, bleibt eine State-Zuweisung solange bestehen, bis ein anderer Stateknoten diese Zuweisung überschreibt. Das folgende XML-Fragment setzt z.B. einen Material-State zum Rendern von mehreren PlaneGeometry-Knoten.

<MaterialState materialId="mat1"/>

<Transform posX="5">
    <PlaneGeometry id="plane1" posY="2">
        <PlaneGeometry id="plane2" posZ="3"/>
    </PlaneGeometry>
    <PlaneGeometry id="plane3" posY="4"/>
</Transform>

Wir erweitern das Fragment um einen weiteren PlaneGeometry-Knoten. Der neue Knoten plane4 soll als Kind von plane1 eingefügt werden und ein anderes Material zum Rendern verwenden. Daher platzieren wir einen neuen MaterialState Knoten direkt davor:

<MaterialState materialId="mat1"/>

<Transform posX="5">
    <PlaneGeometry id="plane1" posY="2">
        <PlaneGeometry id="plane2" posZ="3"/>

        <MaterialState materialId="mat2"/>
        <PlaneGeometry id="plane4" posZ="-3"/>

    </PlaneGeometry>
    <PlaneGeometry id="plane3" posY="4"/>
</Transform>

Da der neu hinzugefügte MaterialState Knoten das Material mat2 ebenfalls Slot 0 zuweist, wird nun plane3 mit dem falschen Material gerendert. Wir beheben diesen Fehler, indem wir die State-Zuweisung rückgängig machen, nachdem plane4 gerendert wurde.

<MaterialState materialId="mat1"/>

<Transform posX="5">
    <PlaneGeometry id="plane1" posY="2">
        <PlaneGeometry id="plane2" posZ="3"/>

        <MaterialState materialId="mat2"/>
        <PlaneGeometry id="plane4" posZ="-3"/>
        <MaterialState materialId="mat1"/>

    </PlaneGeometry>
    <PlaneGeometry id="plane3" posY="4"/>
</Transform>

Das Ergebnis stimmt nun, trotzdem ist eine Schwachstelle erkennbar: Der MaterialState Knoten, der für das Rückgängigmachen des originalen Material-State zuständig ist, muss wissen, welches Material ursprünglich festgelegt war. Für unser einfaches Beispiel ist das kein Problem. In größeren Szenengraphen kann dies jedoch zu einem Problem werden. Besonders dann, wenn Subgraphen mittels Instance oder Reference Knotens hinzugefügt werden und diese Subgraphen eigene State-Zuweisungen vornehmen (siehe auch Instanzen und Referenzen). Die Wartung des Szenengraphen kann dann sehr mühsam werden.

<MaterialState materialId="mat1"/>

<Transform posX="5">
    <PlaneGeometry id="plane1" posY="2">
        <PlaneGeometry id="plane2" posZ="3"/>

        <!-- Black magic ahead -->
        <Instance graphResourceId="main:my_largest_sub_graph_ever"/>

    </PlaneGeometry>
    <PlaneGeometry id="plane3" posY="4"/>
</Transform>

Zur Lösung dieses Problems kann ein SubState Knoten verwendet werden, der alle State-Änderungen seiner Kind-Knoten wieder rückgängig macht.

<MaterialState materialId="mat1"/>

<Transform posX="5">
    <PlaneGeometry id="plane1" posY="2">
        <PlaneGeometry id="plane2" posZ="3"/>

        <!-- Black magic successfully banned -->
        <SubState>
            <Instance graphResourceId="main:my_largest_sub_graph_ever"/>
            <MaterialState materialId="mat2"/>
            <PlaneGeometry id="plane4" posZ="-3"/>
        </SubState>

    </PlaneGeometry>
    <PlaneGeometry id="plane3" posY="4"/>
</Transform>
Zu beachten
Abhängig vom Zweck eines über instanzierbaren bzw. referenzierbaren Subgraphen, ist es oft besser, den SubState Knoten innerhalb der jeweiligen Grafikressource zu verwenden, sodass der "Benutzer" dieses Subgraphen sich nicht darum kümmern muss.


Copyright © 2011-2024 Spraylight GmbH.