Tutorial #02: Color Cube

In diesem Beispiel definieren wir ein neues farbiges Material für den Würfel, fügen der Szene eine Licht-quelle hinzu und reagieren auf das Drücken bestimmter Tasten. Als Basis dient der Code aus dem vori-gen Beispiel.

Die Projektdateien können im Ordner tutorials/chapter01/02_color_cube/project gefunden werden.

Version 1: Graphen-Dateien strukturieren

Der Übersichtlichkeit halber lagern wir die Definition des Materials in eine eigene Datei mit dem Namen graph_materials.xml aus. Die Aufteilung logischer Gruppen in eigene Dateien ist vor allem bei komplexeren Projekten sehr hilfreich und erlaubt das Wiederverwenden einzelner Dateien.

Namespaces

Um Namenskonflikte zu vermeiden, können mit Graph::Namespace-Knoten eigene Namensräume definiert werden. Diese können auch ineinander verschachtelt werden.

Ähnlich wie bei Pfadangaben, setzt sich die Namensreferenz dann aus den (verschachtelten) Namespace-IDs, gefolgt von der eigentlichen id, zusammen. Das Zeichen / dient, wie in einem UNIX-System, als Trennzeichen. Ein vorangestelltes / bezeichnet den Root-Namespace (analog zu absoluter Pfadangabe). Ohne vorangestelltes / wird das Element relativ zum aktuellen Namespace bezeichnet. Mit den Zeichen ../ kann eine Ebene höher gesprungen werden kann.

<MaterialState materialId="/material/mat_white">
<MaterialState materialId="../material/mat_white">
<MaterialState materialId="/material/uncolored/mat_white">

Um auf Ressourcen aus einem bestimmten Paket zugreifen zu können, muss die id des Paket-Dokuments vorangestellt werden. Das Zeichen : dient als Trennzeichen:

<Instance graphResourceId="package_main:graph_mat"/>

In unserem Beispiel ändert sich also der Name "mat_white" außerhalb des Namespaces "material" auf "/material/mat_white" und der Name "myCube01" außerhalb des Namespaces "myGraph" auf "/myGraph/myCube01" .

Mit der optionalen Angabe von activeAndVisible="no" wird noch angegeben, dass alle Unterknoten unsichtbar und deaktiviert sind. Wir werden später sehen, warum.

<?xml version="1.0"?>
<Graph>
    <Namespace id="material" activeAndVisible="no">
        <FixedProgram
            id="prg_white"
        />
        <Material
            id="mat_white"
            programId="prg_white"
        />
    </Namespace>
</Graph>

Die Datei graph_main.xml ändert sich auf:

<?xml version="1.0" ?>
<Graph>
    <Instance graphResourceId="package_main:graph_mat"/>
    <Namespace id="myGraph">
        <View id="view"/>
        <Camera id="camera"
            viewId="view"
            fieldOfViewX="400"
            nearPlane="400" farPlane="2500"
            clearColorBuffer="1"
        >
            <CameraTransform
                cameraId="camera"
                posX="0" posY="0" posZ="800"
            />
            <MaterialState
                materialId="/material/mat_white"
                slot="0"
            />
            <CubeGeometry id="myCube01"
                scaleFactor="200"
                posX="0" posY="0" posZ="0"
            />
        </Camera>
    </Namespace>
</Graph>

In der Datei package.xml müssen beide Dateien als Ressource angegeben werden:

<?xml version="1.0" ?>

<Package id="package_main">
  
  <!-- Graph resources -->
  <Resource id="graph_main" fileName="graph_main.xml"/>
  <Resource id="graph_mat" fileName="graph_materials.xml"/>

  <!-- Graph instances -->
  <Instance graphResourceId="graph_main"/>

</Package>

Mit der Methode Graph::IRoot::PrintTree() kann ausgehend von einem gegebenen Knoten der Graph auf der Konsole ausgegeben werden. Ein Vergleich mit dem vorherigen Cube-Graphen ohne Namespace-Knoten zeigt, dass das Element Namespace tatsächlich ein echtes Knotenelement im Graphen darstellt und dass der gesamte Graph in einen Root-Namespace eingebettet ist.

<Namespace flags=e000011e vnode="2">
  <Container flags=e000011d vnode="1">
    <Instance flags=8000011e vnode="5">
      <Container flags=8000011e vnode="4">
        <Namespace id="material" flags=80000186 vnode="3">
          <FixedProgram id="prg_white" flags=0000001e vnode="1"/>
          <Material id="mat_white" flags=8000001e vnode="1"/>
        <Namespace/> <!-- id="material" -->
      <Container/>
    <Instance/>
    <Namespace id="myGraph" flags=e000011e vnode="6">
      <View id="view" flags=e000001e vnode="1"/>
      <Camera id="camera" flags=e000011e vnode="4">
        <CameraTransform flags=4000001e vnode="1"/>
        <MaterialState flags=8000001e vnode="1"/>
        <CubeGeometry id="myCube01" flags=8000001e vnode="1"/>
      <Camera/> <!-- id="camera" -->
    <Namespace/> <!-- id="myGraph" -->
  <Container/>
<Namespace/>
<Namespace flags=e000011e vnode="2">
  <Container flags=e000011d vnode="1">
    <View id="view" flags=e000001e vnode="1"/>
    <Camera id="camera" flags=e000011e vnode="6">
      <CameraTransform flags=4000001e vnode="1"/>
      <FixedProgram id="prg_white" flags=0000001e vnode="1"/>
      <Material id="mat_white" flags=8000001e vnode="1"/>
      <MaterialState flags=8000001e vnode="1"/>
      <CubeGeometry id="myCube01" flags=8000001e vnode="1"/>
    <Camera/> <!-- id="camera" -->
  <Container/>
<Namespace/>

Bei einem Aufruf von root->PrintTree() in der Methode OnInit() können durchaus noch Knoten aus dem startup Paket im Graphen vorhanden sein, obwohl zuvor die Methode UnloadPackage("startup") aufgerufen wurde. Durch den Aufruf von UnloadPackage() werden die Knoten lediglich inaktiv gesetzt und für das Entladen gekennzeichnet. Um über das Ende des Unload-Prozesses informiert zu werden, muss die Callback-Methode OnPackageWasUnloaded() überschrieben werden. Insgesamt existieren vier solche Callback-Methoden, um den Lade-/Entladezustand von Paketen abzufragen:

Version 2: Farbmaterial definieren

Als nächstes definieren wir ein neues FixedProgram mit coloringEnabled="yes". Zusätzlich definieren wir ein Material mit Standardwerten und ein FixedParameters-Element mit dem Farbattribut diffuseColor.

Farbwerte können je nach Endung unterschiedlich angegeben werden:

Endung Wertebereich Datentyp Format
f 0.0 – 1.0 floating point e.g. 0f, 0.50f, 0.75f, 1f RGBA / RGB
i 0 – 255 integer e.g. 0i, 128i, 191i, 255i RGBA / RGB
h 00 – FF 8 bit hex int e.g. 0h, 80h, BFh, FFh RGBA / RGB
h 0 – FFFFFFFF 32 bit hex int e.g. FF0080BFh ARGB / RGB
        <FixedProgram
            id="prg_color"
            coloringEnabled="yes"
        />
        <Material
            id="mat_color"
            programId="prg_color"
        />
        <FixedParameters
            id="par_cube_color"
            diffuseColor="0f, 0.50f, 0.75f, 1f"
        />

In der Datei graph_main.xml müssen wir nur noch das neue Material und die neuen Parameter aktivieren. Die Zeile slot="0" könnte auch bedenkenlos weggelassen werden, da der Standardwert für das Attribut slot ohnehin immer 0 ist.

            <CameraTransform
                cameraId="camera"
                posX="0" posY="0" posZ="800"
            />            
            <MaterialState
                materialId="/material/mat_color"
                slot="0"
            />
            <ParametersState
                parametersId="/material/par_cube_color"
                slot="0"
            />
            <CubeGeometry id="myCube01"
                scaleFactor="200"
                posX="0" posY="0" posZ="0"
            />

Als Ergebnis wird der Würfel nun in der gewünschten Farbe gezeichnet:

tut0102_plain_color_cube.png
V2: Farbiger Würfel

Version 3: Lichtquelle definieren

Um eine einfache Beleuchtung für den Würfel zu erhalten, sind zwei weitere Schritte notwendig:

  • Beleuchtung für das FixedProgram aktivieren
  • dem Graphen eine Lichtquelle hinzufügen

Die Beleuchtung kann einfach mit lightingEnabled="yes" aktiviert werden:

        <FixedProgram
            id="prg_color"
            coloringEnabled="yes"
            lightingEnabled="yes"
        />

Bisher wurde der Würfel immer als Kinderknoten der Kamera hinzugefügt. Zur besseren Veranschaulichung verwenden wir diesmal einen einen CameraState-Knoten, mit welchem die Kamera und Geometrie-Definitionen voneinander getrennt werden können. Analog zu Material- und Parameter-States, aktiviert ein CameraState Knoten eine Kamera für alle nachfolgenden Objekte bis ein anderer CameraState Knoten ausgewählt wird.

<?xml version="1.0" ?>

<Graph>
    <Instance graphResourceId="package_main:graph_mat"/>
    
    <Namespace id="myGraph">
        <View id="view"/>
        <Camera
            id="camera"
            viewId="view"
            fieldOfViewX="400"
            nearPlane="400" farPlane="2500"
            clearColorBuffer="1"
        />
        <CameraTransform
            cameraId="camera"
            posX="0" posY="0" posZ="800"
        />
        <CameraState
            cameraId="camera"
        />

Um eine Lichtquelle zu definieren sind drei zusätzliche Graphenknoten notwendig:

Mit dem Light-Knoten wird die eigentliche Lichtquelle erzeugt. Der LightTransform-Knoten positioniert das Licht in der virtuellen Welt (in unserem Beispiel positionieren wir das Licht an derselben Position wie die Kamera). Der LightState-Knoten aktiviert die Lichtquelle für alle nachfolgenden Knoten:

        <Light
            id="light"
        />
        <LightTransform
            lightId="light"
            posX="0" posY="0" posZ="800"
        />
        <LightState
            lightId="light"
            unit="0"
        />

Nun kann der Würfel mit einem geeigneten Material gezeichnet werden:

        <MaterialState materialId="/material/mat_color"/>
        <ParametersState parametersId="/material/par_cube_color"/>
        <CubeGeometry
            id="myCube01"
            scaleFactor="200"
            posX="0" posY="0" posZ="0"
        />
    </Namespace>
</Graph>

Das Ergebnis ist ein sich drehender Würfel mit einfacher Beleuchtung:

tut0102_light_color_cube.png
V3: Beleuchteter Würfel

Version 4: Device Handler

Als letztes wollen wir noch das Programm beenden, sobald die ESC-Taste oder der Zurück-Button bei einem Android-Gerät gedrückt wird, und die Farbparameter für die Beleuchtung verbessern.

Um auf Geräteeingaben reagieren zu können, stellt die Murl Engine ein Device-Handler-Objekt zur Verfügung (Logic::IDeviceHandler). Mit Logic::IState::GetDeviceHandler() können wir uns einen Zeiger auf dieses Objekt holen. Um einen Tastendruck abzufragen, steht die Methode WasRawKeyPressed() zur Verfügung und für Android die Methode WasRawButtonPressed().

Für Keys gibt es neben der Methode WasRawKeyPressed() noch die Methoden IsRawKeyPressed() und WasRawKeyReleased(). Bei einem Tastendruck werden diese Methoden nacheinander wahr, wie in der folgenden Sequenz beschrieben:

  • Beim ersten Niederdrücken einer Taste gibt WasRawKeyPressed() true zurück für die Dauer von genau einem Logic-Tick
  • Solange diese Taste gedrückt bleibt, gibt IsRawKeyPressed() in jedem aufeinanderfolgenden Logic-Tick true zurück.
  • Wenn die Taste losgelassen wird, gibt WasRawKeyReleased() true zurück für die Dauer von genau einem Logic-Tick

Dieses Schema (was pressed, is pressed, was released) findet sich auch an anderen Stellen in der Murl Engine, z.B. bei

Input::IRawKeyboardDevice, Input::IRawButtonDevice, Input::IJoystickDevice, Input::IMouseButtons etc.

tut0102_pressed_released.png
Pressed/released Input-Zustände
void App::ColorCubeLogic::OnProcessTick(const Logic::IState* state)
{
    Double angle = state->GetCurrentTickTime();
    angle = Math::Fmod(angle, Math::TWO_PI);
    mCubeTransform->SetRotation(angle, angle, 0);
    state->SetUserDebugMessage(Util::DoubleToString(angle));
    
    Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler();
    if (deviceHandler->WasRawKeyPressed(RAWKEY_ESCAPE) ||
        deviceHandler->WasRawButtonPressed(RAWBUTTON_BACK))
    {
        deviceHandler->TerminateApp();
    }
}

Durch Veränderung der Farbparameter von Licht und Würfelmaterial, kann das Beleuchtungsergebnis noch variiert werden:

        <Light
            id="light"
            diffuseColor="0.5f,0.5f,0.5f,1f"
        />
        <FixedProgram
            id="prg_color"
            coloringEnabled="yes"
            lightingEnabled="yes"
        />
        <Material
            id="mat_color"
            programId="prg_color"
        />
        <FixedParameters
            id="par_cube_color"
            diffuseColor="0f, 0.50f, 0.75f, 1f"
            specularColor="1f, 1f, 1f, 1f"
            ambientColor="0f, 0f, 0f, 1f"
            shininess="32"
        />
tut0102_final_color_cube.png
V4: Farbiger Würfel mit erweiterten Beleuchtungsparametern

Übungen

  • Probiere verschiedene Farbparameter aus.
  • Wie muss das Programm verändert werden, damit der Würfel nicht automatisch, sondern mit den Tasten 1, 2 und 3 um die Achsen x, y und z gedreht werden kann?


Copyright © 2011-2024 Spraylight GmbH.