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:
OnPackageWillBeLoaded()
: Aufruf bevor der Lade-Prozess beginntOnPackageWasLoaded()
: Aufruf nachdem der Lade-Prozess abgeschlossen wurdeOnPackageWillBeUnloaded()
: Aufruf bevor der Entlade-Prozess beginntOnPackageWasUnloaded()
: Aufruf nachdem der Entlade-Prozess abgeschlossen wurde
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:
<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:
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:
- eine Lichtquelle (
Graph::Light
) - ein Transformationsknoten zum Positionieren des Lichts (
Graph::LightTransform
) - ein State-Knoten zum Aktivieren der Lichtquelle (
Graph::LightState
)
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:
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-Ticktrue
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.
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" />
Ü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?