Im ersten 3D Tutorial zeigen wir wie eine einfache 3D-Szene mit Skybox erstellt werden kann und wie die Kamera in der Szene bewegt werden kann.
Skybox
Um das Tutorial einfach zu halten, soll die Szene lediglich aus einem auf einer Ebene stehenden Würfel bestehen. Zusätzlich umhüllen wir die Szene mit einer Skybox.
Die Verwendung einer Skybox ist eine einfache Möglichkeit um einen Hintergrund für eine 3D-Szene zu erstellen. Dabei wird die Szene von einem texturierten Quader umschlossen. Jede Fläche des Quaders hat eine unterschiedliche Textur ("cube mapping"). Die Skybox-Texturen können neben dem Himmel auch entfernte Gebirge oder Gebäude beinhalten und damit den optischen Eindruck eines viel größeren Levels erzeugen.
- Zu beachten
- Hinweis: Wir werden das "Cube Mapping" nicht nur bei der Skybox, sondern auch bei dem stehenden Würfel anwenden.
CubemapTexture
Mit dem Knoten Graph::CubemapTexture
können wir ein Textur-Objekt erzeugen, dass alle sechs Grafiken für den Quader kapselt.
Wir definieren zunächst die einzelnen Image Ressourcen in der Datei package.xml
.
<!-- Image resources --> <Resource id="gfx_plane" fileName="plane.png"/> <Resource id="gfx_cube_nx" fileName="cube_neg_x.png"/> <Resource id="gfx_cube_px" fileName="cube_pos_x.png"/> <Resource id="gfx_cube_ny" fileName="cube_neg_y.png"/> <Resource id="gfx_cube_py" fileName="cube_pos_y.png"/> <Resource id="gfx_cube_nz" fileName="cube_neg_z.png"/> <Resource id="gfx_cube_pz" fileName="cube_pos_z.png"/> <Resource id="gfx_sky_up" fileName="sky1_up.jpg"/> <Resource id="gfx_sky_down" fileName="sky1_down.jpg"/> <Resource id="gfx_sky_left" fileName="sky1_left.jpg"/> <Resource id="gfx_sky_right" fileName="sky1_right.jpg"/> <Resource id="gfx_sky_front" fileName="sky1_front.jpg"/> <Resource id="gfx_sky_back" fileName="sky1_back.jpg"/>
Danach können wir uns ein CubemapTexture
-Objekt erzeugen und mit TextureState
einen geeigneten textureSlot
auswählen.
<FlatTexture id="texture_plane" imageResourceId="gfx_plane"/> <CubemapTexture id="texture_cube" imageResourceIdPosX="gfx_cube_nx" imageResourceIdNegX="gfx_cube_px" imageResourceIdPosY="gfx_cube_ny" imageResourceIdNegY="gfx_cube_py" imageResourceIdPosZ="gfx_cube_nz" imageResourceIdNegZ="gfx_cube_pz" wrapModeX="CLAMP_TO_EDGE" wrapModeY="CLAMP_TO_EDGE" /> <CubemapTexture id="texture_sky" imageResourceIdPosX="gfx_sky_right" imageResourceIdNegX="gfx_sky_left" imageResourceIdPosY="gfx_sky_up" imageResourceIdNegY="gfx_sky_down" imageResourceIdPosZ="gfx_sky_front" imageResourceIdNegZ="gfx_sky_back" wrapModeX="CLAMP_TO_EDGE" wrapModeY="CLAMP_TO_EDGE" /> <TextureState slot="0" textureId="texture_plane"/> <TextureState slot="2" textureId="texture_cube"/> <TextureState slot="3" textureId="texture_sky"/>
Wir ändern die Attribute wrapModeX
und wrapModeY
von default REPEAT
auf CLAMP_TO_EDGE
, damit es zu keinen störenden Artefakten an den Rändern kommt.
Shader
Da es kein FixedProgram
für das Zeichnen von Cubemaps gibt, benötigen wir ein ShaderProgram
.
Dafür definieren wir zunächst den Shader-Code als XML-Shader Ressource in der Datei shader_skybox.xml
:
<?xml version="1.0" ?> <Shader languages="GLSL_ES_120,HLSL_30,HLSL_40_93"> <Attributes> <Attribute item="COORD" type="FLOAT_VECTOR_4"/> </Attributes> <ConstantBuffers> <ConstantBuffer item="MODEL"/> <ConstantBuffer item="CAMERA"/> </ConstantBuffers> <Varyings> <Varying name="vCubeCoord" type="FLOAT_VECTOR_3" precision="MEDIUM"/> </Varyings> <Textures> <Texture unit="0" type="CUBE" precision="LOW"/> </Textures> <VertexSource> <![CDATA[ void main() { gl_Position = uCameraViewProjectionMatrix * (uModelMatrix * aPosition); vCubeCoord = aPosition.xyz; } ]]> </VertexSource> <FragmentSource> <![CDATA[ void main() { gl_FragColor = textureCube(uTexture0, vCubeCoord); } ]]> </FragmentSource> </Shader>
Die neue Datei muss in package.xml
als Ressource gelistet sein, um sie verwenden zu können.
<!-- Shader resources --> <Resource id="shader_skybox_res" fileName="shader_skybox.xml"/>
In der Datei materials.xml
definieren wir einen Shader
-Knoten und ein ShaderProgram
.
<Shader id="shader_skybox" shaderResourceId="shader_skybox_res" /> <ShaderProgram id="prg_cube" vertexShaderId="shader_skybox" fragmentShaderId="shader_skybox" />
Das ShaderProgram
verwenden wir für die Erstellung unserer Material
-Knoten.
<Material id="mat_cube" visibleFaces="FRONT_AND_BACK" programId="prg_cube" /> <Material id="mat_sky" visibleFaces="BACK_ONLY" programId="prg_cube" />
Nun können wir die Knoten für Skybox, Würfel und die Ebene definieren.
<MaterialState slot="11" materialId="/material/mat_cube"/> <MaterialState slot="12" materialId="/material/mat_sky"/> <PlaneGeometry id="plane" materialSlot="3" scaleFactor="10000" posX="0" posY="-75" angleX="90deg" texCoordX2="8" texCoordY2="8"/> <CubeGeometry id="cube" materialSlot="11" textureSlot="2" scaleFactor="100" posY="-25"/> <CubeGeometry id="skybox" materialSlot="12" textureSlot="3" scaleFactor="10000" />
Bewegliche Kamera
Wir können die Position der Kamera (gleich wie bei jedem anderen Objekt) durch eine Reihe von Transformationen bestimmen. Die primäre Position wird durch den CameraTransform
-Knoten festgelegt. Weitere Transformationen können durch Verschachteln des CameraTransform
-Knotens mit zusätzlichen Transform
-Knoten definiert werden.
Eine Transformation wird in der Computer-Graphik üblicherweise durch Multiplikation der Vertex-Daten (Eckpunkte) mit einer vierdimensionalen Transformationsmatrix erreicht (siehe auch de.wikipedia.org/wiki/Homogene_Koordinaten). Jeder Transform
-Knoten beinhaltet eine solche 4x4 Transformationsmatrix.
Wir bewegen die Kamera, indem wir direkt die Werte der Transformationsmatrix des CameraTransform
-Knoten ändern. Dabei ist es wichtig, die Reihenfolge, in der die Operationen abgearbeitet werden, zu beachten.
Innerhalb eines Transform
-Knotens ist die Reihenfolge:
- Skalierung
- Rotation
- Translation
Die Rotationsreihenfolge für Eulerwinkel innerhalb eines Transform
-Knotens ist:
- Z
- Y
- X
Bei verschachtelten Transform
-Knoten werden die Operationen von Innen nach Außen abgearbeitet. Die Operationen des innersten Transform Knoten werden also als erstes angewendet.
Für die Bewegung der Kamera gibt es je nach Anwendung natürlich verschiedenste Möglichkeiten. Nachfolgend werden drei verschiedene Methoden gezeigt.
Initialisierung
Wir definieren in der Header-Datei eine Referenz auf den CameraTransform
-Knoten, zwei Membervariablen um die Rotation und Position zu speichern und eine weitere Membervariable um den aktuellen Modus zu speichern.
Murl::Logic::TransformNode mCameraTransform; Vector mRot; Vector mPos; UInt32 mMode;
Die Variablen werden im Konstruktor bzw. in der OnInit
Methode initialisiert:
App::MovingCameraLogic::MovingCameraLogic(Logic::IFactory* factory) : BaseProcessor(factory) , mRot(Vector::ZERO_POSITION) , mPos(Vector::ZERO_POSITION) , mMode(1) { }
Bool App::MovingCameraLogic::OnInit(const Logic::IState* state) { state->GetLoader()->UnloadPackage("startup"); Graph::IRoot* root = state->GetGraphRoot(); AddGraphNode(mCameraTransform.GetReference(root, "main_camera_transform")); if (!AreGraphNodesValid()) { return false; } mPos = mCameraTransform->GetPosition(); state->SetUserDebugMessage("Mode "+Util::UInt32ToString(mMode)); return true; }
Nun können wir die Position der Kamera in der OnProcessTick
Methode ändern, indem wir die mCameraTransform
Werte ändern. Zur Steuerung der Kamera verwenden wir die Tastatur:
// get translation key input moveLeft = deviceHandler->IsRawKeyPressed(RAWKEY_LEFT_ARROW); moveRight = deviceHandler->IsRawKeyPressed(RAWKEY_RIGHT_ARROW); moveForward = deviceHandler->IsRawKeyPressed(RAWKEY_UP_ARROW); moveBackward = deviceHandler->IsRawKeyPressed(RAWKEY_DOWN_ARROW); moveUp = deviceHandler->IsRawKeyPressed(RAWKEY_LEFT_SHIFT); moveDown = deviceHandler->IsRawKeyPressed(RAWKEY_LEFT_CONTROL); // get rotation key input if (deviceHandler->IsRawKeyPressed(RAWKEY_Q)) rotDiffX = 1; else if (deviceHandler->IsRawKeyPressed(RAWKEY_A)) rotDiffX = -1; if (deviceHandler->IsRawKeyPressed(RAWKEY_W)) rotDiffY = 1; else if (deviceHandler->IsRawKeyPressed(RAWKEY_S)) rotDiffY = -1; if (deviceHandler->IsRawKeyPressed(RAWKEY_E)) rotDiffZ = 1; else if (deviceHandler->IsRawKeyPressed(RAWKEY_D)) rotDiffZ = -1;
Reset
Um die Kameraposition wieder auf den Anfang zurückzusetzen, erzeugen wir eine neue Einheitsmatrix (Identitätsmatrix) und setzen die passenden Translationswerte für die Kameraposition (0/0/512). Mit SetTransform()
überschreiben wir die Transformationsmatrix des CameraTransform
-Knotens mit den neuen Werten.
// RESET if (deviceHandler->WasRawKeyPressed(RAWKEY_R)) { mPos.x = 0; mPos.y = 0; mPos.z = 512; mRot = Vector::ZERO_POSITION; Matrix transformMatrix(Matrix::IDENTITY); transformMatrix.SetTranslationComponent(mPos); mCameraTransform->SetTransform(transformMatrix); }
Mode 1
Bei dieser Methode setzen wir direkt die Translations- und Rotationswerte der Transformationsmatrix. Die Rotationswerte werden in Form von Eulerschen Winkel gesetzt (siehe auch de.wikipedia.org/wiki/Eulersche_Winkel).
Zunächst berechnen wir uns einen Bewegungsvektor direction
, abhängig von den gedrückten Tasten. Diesen Vektor können wir auch bei den anderen Methoden verwenden.
Die Membervariablen mPos
und mRot
werden verwendet, um die aktuellen Translationswerte und Rotationswerte zu speichern. Mit GetTransform()
holen wir uns die Transformationsmatrix und setzen mit SetRotationComponent
und SetTranslationComponent
die Rotations- und Translationswerte.
// calculate translation vector Vector direction(Vector::ZERO_DIRECTION); if (moveForward) direction.z = -walkingSpeed; else if (moveBackward) direction.z = walkingSpeed; if (moveLeft) direction.x = -walkingSpeed; else if (moveRight) direction.x = walkingSpeed; if (moveUp) direction.y = walkingSpeed; else if (moveDown) direction.y = -walkingSpeed; if (mMode == 1) { // directly set rotation parameter mRot.x += rotDiffX * rotSpeed; mRot.y += rotDiffY * rotSpeed; mRot.z += rotDiffZ * rotSpeed; mCameraTransform->GetTransform().SetRotationComponent(mRot.x, mRot.y, mRot.z); // move along the x/y/z axis mPos += direction; mCameraTransform->GetTransform().SetTranslationComponent(mPos); }
Diese Methode hat einige Nachteile:
- Die Kamera bewegt sich immer entlang der Hauptachsen (X/Y/Z) und nicht entlang der Blickrichtung.
- Aufgrund der Rotationsreihenfolge (Z, Y, X) ist es nicht möglich, die einzelnen Rotationen unabhängig voneinander zu setzen. Wenn z.B. die Blickrichtung entlang der X-Achse verläuft, ist es nicht mehr möglich den Querneigungswinkel (Rollwinkel) zu ändern, da Rotationen um X und um Z die gleiche Auswirkung haben (siehe auch de.wikipedia.org/wiki/Gimbal_Lock).
Einfach etwas experimentieren, um die Limitierungen besser zu verstehen.
- Zu beachten
- Tipp! Abgesehen von Eulerwinkeln kann die Rotation auch über
SetRotationComponentAxisAngle
oder über Quaternionen definiert werden.
Mode 2
Diese Methode löst die Probleme der vorherigen Methode. Anstatt die Rotations- und Translationskomponenten der Matrix zu speichern und zu setzen, wenden wir jede Änderung durch eine Matrixmultiplikation sofort an.
if (mMode == 2) { // multiply by rotation/translation matrix Matrix transformMatrix(Matrix::IDENTITY); transformMatrix.SetRotationComponent(rotDiffX*rotSpeed, rotDiffY*rotSpeed, rotDiffZ*rotSpeed); transformMatrix.SetTranslationComponent(direction); mCameraTransform->SetTransform(mCameraTransform->GetTransform() * transformMatrix); }
Die Kamera bewegt sich nun immer in Blickrichtung und die drei Rotationswinkel können unabhängig voneinander gesetzt werden. Man kann in der Szene herumfliegen.
Mode 3
Die dritte Methode zeigt einen Modus, wie er z.B. bei FPS Spielen zum Einsatz kommt. Dafür erlauben wir das Steuern der Rotationswerte mit der Maus, bewegen uns aber immer auf der Ebene.
if (mMode == 3) { //get rotation mouse input if (deviceHandler->IsRawMouseAvailable()) { deviceHandler->GetRawMouseDelta(rotDiffY, rotDiffX); Real dummy; deviceHandler->GetRawWheelDelta(dummy, rotDiffZ); rotDiffY *= -1.0; } // FPS mode mRot.x += rotDiffX * rotSpeed; mRot.y += rotDiffY * rotSpeed; mRot.z += rotDiffZ * rotSpeed; mCameraTransform->GetTransform().SetRotationComponent(mRot.x, mRot.y, 0); Matrix directionTransform(Matrix::IDENTITY); directionTransform.SetRotationComponent(0.0, mRot.y, 0.0); mPos += directionTransform.Multiply(direction); mPos.y = 0; mCameraTransform->GetTransform().SetTranslationComponent(mPos); }
Die Bewegung erfolgt immer in Richtung des Blickwinkels, wobei jedoch die Y-Position immer konstant bleibt (bzw. typischerweise durch das Terrain definiert wird). Man kann herumgehen und nach oben bzw. nach unten schauen.