Tutorial #00: Bewegliche Kamera

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" />
tut0300_moving_camera.png
Moving Camera Szene

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:

R Reset
1/2/3 Mode 1/2/3
LEFT/RIGHT/UP/DOWN Bewegung nach links/rechts/vor/zurück
SHIFT/STRG Bewegung nach oben/unten
Q/A Rotation um X
W/S Rotation um Y
E/D Rotation um Z

    // 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.


Copyright © 2011-2018 Spraylight GmbH.