3D-Programmierung (Mathematik) Teil 08: Kamera- und Projektionstransformation

In den vorangegangenen Teilen dieser Artikelserie sind wir bereits ausführlich darauf eingegangen, wie sich Spieleobjekte in der Spielewelt (world space) transformieren lassen. Bis zu ihrer Darstellung auf dem Bildschirm sind jedoch noch zwei weitere Transformationsschritte notwendig – die Kameratransformation sowie die Projektionstransformation.


Kameratransformation (Sichttransformation)
Alle Objekte einer 3D-Szene werden durch das Auge einer Kamera abgebildet. Entsprechend ihrer Position und Blickrichtung wird man die Objekte später unter einem ganz bestimmten Blickwinkel sehen. Den Vorgang, bei dem ein Objekt aus der 3D-Welt in den Kameraraum transformiert wird, bezeichnet man als Sicht- oder Kameratransformation.

In der nachfolgenden Abbildung sind die vier Vektoren definiert, die für die Beschreibung der Kameraausrichtung notwendig sind.












  • Look-Vektor (Blickrichtung der Kamera)
  • Eye-Vektor (Kameraposition)
  • Up-Vektor (senkrechte Kameraachse)
  • Right-Vektor (horizontale Kameraachse)

Mithilfe dieser Vektoren lässt sich die View-Matrix wie folgt erstellen:








Die D3DXMatrixLookAtLH()-Funktion erzeugt eine View-Matrix für ein linkshändiges Koordinatensystem:

INLINE D3DXMATRIX* D3DXMatrixLookAtLH(D3DXMATRIX *pOut,
                                      CONST D3DXVECTOR3 *pEye,
                                      CONST D3DXVECTOR3 *pAt,
                                      CONST D3DXVECTOR3 *pUp)
{
    D3DXVECTOR3 right, rightn, up, upn, vec, ViewDirection;

    D3DXVec3Subtract(&vec, pAt, pEye);
    D3DXVec3Normalize(&ViewDirection, &vec);

    D3DXVec3Cross(&right, pUp, &ViewDirection);
    D3DXVec3Cross(&up, &ViewDirection, &right);
    D3DXVec3Normalize(&rightn, &right);
    D3DXVec3Normalize(&upn, &up);

    pOut->m[0][0] = rightn.x;
    pOut->m[1][0] = rightn.y;
    pOut->m[2][0] = rightn.z;
    pOut->m[3][0] = -D3DXVec3Dot(&rightn,pEye);

    pOut->m[0][1] = upn.x;
    pOut->m[1][1] = upn.y;
    pOut->m[2][1] = upn.z;
    pOut->m[3][1] = -D3DXVec3Dot(&upn, pEye);

    pOut->m[0][2] = ViewDirection.x;
    pOut->m[1][2] = ViewDirection.y;
    pOut->m[2][2] = ViewDirection.z;
    pOut->m[3][2] = -D3DXVec3Dot(&ViewDirection, pEye);

    pOut->m[0][3] = 0.0f;
    pOut->m[1][3] = 0.0f;
    pOut->m[2][3] = 0.0f;
    pOut->m[3][3] = 1.0f;

    return pOut;
}

Beim Aufstellen der Sichtmatrix können zwei Strategien verfolgt werden. Übergibt man beim Aufruf der D3DXMatrixLookAtLH()-Funktion für den Parameter pEye die tatsächliche Position der Kamera in der Spielewelt, dann entspricht der dritte Parameter pAt nicht der eigentlichen Blickrichtung sondern einem Punkt in der Spielewelt, auf den die Kamera ausgerichtet ist. Soll diese Ausrichtung während einer Kamerabewegung aufrechterhalten werden, dann ändert sich hierbei fortwährend die Kamerablickrichtung:

D3DXVec3Subtract(&vec, pAt, pEye);
// bzw. vec = *pAt - *pEye;

D3DXVec3Normalize(&ViewDirection, &vec);

Bei Variante 2 wird die Kameraposition nicht im Zuge der Sichttransformation sondern bei der Welttransformation aller 3D-Objekte berücksichtigt. In diesem Fall entspricht der Parameter pAt der tatsächlichen Kamerablickrichtung. Anstelle der tatsächlichen Kameraposition muss man dem Parameter pEye der D3DXMatrixLookAtLH()-Funktion jedoch einen Nullvektor (0, 0, 0) übergeben.

// Skalieren, Rotation, Translation
[...]

// (Variante 1) World Space Transformationsmatrix:
WorldSpaceTransformationMatrix = TransformationMatrix;

WorldSpaceTransformationMatrix._41 = WorldSpacePos.x;
WorldSpaceTransformationMatrix._42 = WorldSpacePos.y;
WorldSpaceTransformationMatrix._43 = WorldSpacePos.z;

D3DXMatrixLookAtLH(&g_matView, &CameraPos,
                   &g_LookAtPos, &g_CameraVertical);


// Variante 2) Camera Space Transformationsmatrix:
CameraSpaceTransformationMatrix = TransformationMatrix;

CameraSpaceTransformationMatrix._41 = WorldSpacePos.x – CameraPos.x;
CameraSpaceTransformationMatrix._42 = WorldSpacePos.y – CameraPos.y;
CameraSpaceTransformationMatrix._43 = WorldSpacePos.z – CameraPos.z;

D3DXMatrixLookAtLH(&g_matView, &g_NullVector,
                   &g_CameraViewDirection, &g_CameraVertical);

Hinweis:
Bei Variante 2 entspricht die Sichtmatrix der invertierten Kamera-Transformationsmatrix. Bewegt sich der Spieler und mit ihm die Kamera durch die 3D-Welt, ändert sich je nach Position und Blickrichtung auch der Blickwinkel, unter dem die Objekte der 3D-Welt gesehen werden. Aber anstatt nun die Kamera zu bewegen oder zu drehen, fixiert man diese im Ursprung des Weltkoordinatensystems mit Blickrichtung entlang der positiven z-Richtung und transformiert stattdessen alle Objekte gemäß der invertierten Kamerabewegung. Soll sich die Kamera auf ein Objekt zu bewegen, wird stattdessen das Objekt auf die Kamera zu bewegt, soll sich die Kamera um 30° um die y-Achse drehen, wird stattdessen das Objekt um -30° um die y-Achse gedreht.

Projektionstransformation
Im Rahmen der Projektionstransformation wird die 3D-Szene auf den zweidimensionalen Bildschirm projiziert. Um die Arbeitsweise der zugehörigen Projektionsmatrix verstehen zu können, muss zunächst der Begriff des Viewing Volumes verstanden werden. Das Viewing Volume beschreibt den Ausschnitt einer 3D-Szene, den man von einer bestimmten Position und Blickrichtung aus einsehen kann. Die maximale Sichtweite wird durch die Far Clipping Plane, die minimale Sichtweite durch die Near Clipping Plane beschrieben. Das FOV (Field of View) entspricht dem Sichtwinkel (Blickfeld) des Betrachters.











Durch die Projektionsmatrix wird nun jedes Objekt der 3D-Szene auf der Projektionsfläche abgebildet.

 


Die D3DXMatrixPerspectiveFovLH()-Funktion erzeugt eine perspektivische Projektionsmatrix für ein linkshändiges Koordinatensystem:

INLINE D3DXMATRIX* D3DXMatrixPerspectiveFovLH(D3DXMATRIX *pOut,
                                              FLOAT fovy,
                                              FLOAT Aspect,
                                              FLOAT zn /* near plane */,
                                              FLOAT zf /* far plane */)
{
    pOut->m[0][1] = 0.0f;
    pOut->m[0][2] = 0.0f;
    pOut->m[0][3] = 0.0f;

    pOut->m[1][0] = 0.0f;
    pOut->m[1][2] = 0.0f;
    pOut->m[1][3] = 0.0f;

    pOut->m[2][0] = 0.0f;
    pOut->m[2][1] = 0.0f;

    pOut->m[3][0] = 0.0f;
    pOut->m[3][1] = 0.0f;
    pOut->m[3][3] = 0.0f;

    float tanhalffov = tan(fovy/2.0f);

    pOut->m[0][0] = 1.0f / (Aspect * tanhalffov);
    pOut->m[1][1] = 1.0f / tanhalffov;

    pOut->m[2][2] = zf / (zf - zn);
    pOut->m[2][3] = 1.0f;
    pOut->m[3][2] = (zf * zn) / (zn - zf);

    return pOut;
}

z-Fighting
Moderne Spiele zeichnen sich unter anderem durch ihre enorme Sichtweite aus. Diese lässt zwar die Spielewelt viel natürlicher und detaillierter erscheinen als in älteren Spielen, in denen die Umgebung schon nach wenigen Metern hinter einem dichten Nebelvorhang verschwindet, führt jedoch wegen der mangelnden Auflösung des z-Buffers (24 Bit sind Standard) zu Darstellungsfehlern, die unter dem Begriff z-Fighting bekannt sind. Aufgrund von Rundungsfehlern bei der Berechnung der Tiefenwerte sind Teilbereiche von eigentlich verdeckten Flächen fälschlicherweise sichtbar, während Teile von sichtbaren Flächen fälschlicherweise verdeckt sind.
Ein typisches Szenario, bei dem sich das z-Fighting besonders stark bemerkbar macht, betrifft die Darstellung von Straßen und Wegen. Zur Vermeidung dieses Problems bietet sich die Verwendung einer zweiten Projektionsmatrix an, bei welcher die maximale Sichtweite (Far Clipping Plane, FLOAT zf) mittels eines Bias-Wertes (g_ClippingPlaneFarZBias) leicht modifiziert wird. Während die erste Matrix beim Rendering des Terrains und der Gebäude zum Einsatz kommt, könnte man die zweite Matrix für die Darstellung der Straßen nutzen.

D3DXMatrixPerspectiveFovLH(&g_matProj, g_FOV_Angle,
                           g_Aspect, 0.1f, g_ViewDistance);

D3DXMatrixPerspectiveFovLH(&g_matProj_ZBiased, g_FOV_Angle,
                           g_Aspect, 0.1f,
                           g_ViewDistance+g_ClippingPlaneFarZBias);