3D-Programmierung (Mathematik) Teil 06: Matrizen aus Sicht des Programmierers

Bei der täglichen Arbeit mit Vektoren und Matrizen können Sie als Spieleentwickler entweder auf eine vorgefertigte Mathe-Bibliothek vertrauen, die Sie bei Bedarf schrittweise durch zusätzliche Funktionen erweitern können oder Sie fangen bei null an und programmieren alles in Eigenregie. Für Anfänger ist die hauseigene D3DXMath-Bibliothek sicher die beste Wahl, zumal Sie auf unserer Seite eine quelloffene Implementierung sämtlicher Klassen und Funktionen als Teil aller Beispielprogramme und Frameworks finden können.


Kernstück für die Arbeit mit Matrizen ist in DirectX die D3DXMATRIX-Klasse, bzw. die moderne 16-byte-aligned D3DXMATRIXA16-Erweiterung, die auf allen Prozessoren ab dem Pentium 4 eine bessere Performance bietet. Dank Operatorüberladung lassen sich Matrizen ganz natürlich addieren, subtrahieren und miteinander multiplizieren. Demonstriert werden soll dies beim Aufstellen der Gesamttransformationsmatrix (Skalierung, Rotation und Translation) für ein Spieleobjekt.

Aufstellen von Welt- und Kameratransformationsmatrizen
Gesamttransformationsmatrizen lassen sich sowohl für die Spielewelt (world space) als auch für den Kameraraum (camera space) aufstellen. Im zweiten Fall wird zusätzlich noch die aktuelle Kameraposition in der Spielewelt berücksichtigt.

Hinweis:
Im Rahmen des nachfolgenden Beispiels gehen wir davon aus, dass unser 3D-Modell aus nur einem einzigen Teil(mesh) besteht. Komplexere Modelle bestehen jedoch aus mehreren Modellteilen (Bsp. ein Auto: 4 Räder + Karosserie). Für jedes Modellteil müsste man dann eine separate Gesamttransformationsmatrix erstellen!

D3DXMATRIXA16 TransformationMatrix;
D3DXMATRIXA16 TotalRotationMatrix;
D3DXMATRIXA16 WorldSpaceTransformationMatrix;
D3DXMATRIXA16 CameraSpaceTransformationMatrix;

. . .

D3DXMatrixIdentity(&TransformationMatrix);

TransformationMatrix._11 = ScaleX;
TransformationMatrix._22 = ScaleY;
TransformationMatrix._33 = ScaleZ;

TransformationMatrix *= TotalRotationMatrix;
// bzw.
TransformationMatrix = TransformationMatrix*TotalRotationMatrix;

WorldSpaceTransformationMatrix = TransformationMatrix;
CameraSpaceTransformationMatrix = TransformationMatrix;

// World Space Transformationsmatrix:
WorldSpaceTransformationMatrix._41 = WorldSpacePos.x;
WorldSpaceTransformationMatrix._42 = WorldSpacePos.y;
WorldSpaceTransformationMatrix._43 = WorldSpacePos.z;

// Camera Space Transformationsmatrix:
CameraSpaceTransformationMatrix._41 = WorldSpacePos.x – CameraPos.x;
CameraSpaceTransformationMatrix._42 = WorldSpacePos.y – CameraPos.y;
CameraSpaceTransformationMatrix._43 = WorldSpacePos.z – CameraPos.z;

Multiplikation eines Vektors mit einer Matrix
Für die Durchführung der Multiplikation eines Vektors mit einer Matrix bietet sich die Verwendung von zwei unterschiedlichen Funktionen an. Handelt es sich um Rotations- bzw. um Rotationsskalierungsmatrizen, dann können die Matrixelemente der vierten Zeile bei der Multiplikation vernachlässigt werden, was einen kleinen Geschwindigkeitsgewinn mit sich bringt.

INLINE void Multiply3DVectorWithRotationMatrix(D3DXVECTOR3* pVecOut,
                                               D3DXVECTOR3* pVecIn,
                                               D3DXMATRIXA16 *pMatrix)
{
    pVecOut->x = pVecIn->x*pMatrix->_11 + pVecIn->y*pMatrix->_21 +
                 pVecIn->z*pMatrix->_31;

    pVecOut->y = pVecIn->x*pMatrix->_12 + pVecIn->y*pMatrix->_22 +
                 pVecIn->z*pMatrix->_32;

    pVecOut->z = pVecIn->x*pMatrix->_13 + pVecIn->y*pMatrix->_23 +
                 pVecIn->z*pMatrix->_33;
}

Bei Matrizen, die darüber hinaus auch Translationen beschreiben, müssen die Matrixelemente der vierten Zeile zwingend berücksichtigt werden, da eben diese für die Verschiebung zuständig sind.

INLINE void Multiply3DVectorWithMatrix(D3DXVECTOR3* pVecOut,
                                       D3DXVECTOR3* pVecIn,
                                       D3DXMATRIXA16 *pMatrix)
{
    pVecOut->x = pVecIn->x*pMatrix->_11 + pVecIn->y*pMatrix->_21 +
                 pVecIn->z*pMatrix->_31 + pMatrix->_41;

    pVecOut->y = pVecIn->x*pMatrix->_12 + pVecIn->y*pMatrix->_22 +
                 pVecIn->z*pMatrix->_32 + pMatrix->_42;

    pVecOut->z = pVecIn->x*pMatrix->_13 + pVecIn->y*pMatrix->_23 +
                 pVecIn->z*pMatrix->_33 + pMatrix->_43;
}

Betrachten wir nun die benötigten DirectX-Funktionen zum Aufstellen der Transformationsmatrizen für Skalierung, Rotation und Translation:

Aufstellen der Einheitsmatrix

INLINE D3DXMATRIX* D3DXMatrixIdentity(D3DXMATRIX *pOut)
{
    pOut->m[0][1] = pOut->m[0][2] = pOut->m[0][3] =
    pOut->m[1][0] = pOut->m[1][2] = pOut->m[1][3] =
    pOut->m[2][0] = pOut->m[2][1] = pOut->m[2][3] =
    pOut->m[3][0] = pOut->m[3][1] = pOut->m[3][2] = 0.0f;

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

    return pOut;
}

Aufstellen einer Skalierungsmatrix

INLINE D3DXMATRIX* D3DXMatrixScaling(D3DXMATRIX *pOut,
                                     FLOAT sx,
                                     FLOAT sy,
                                     FLOAT sz)
{
    pOut->m[0][1] = pOut->m[0][2] = pOut->m[0][3] =
    pOut->m[1][0] = pOut->m[1][2] = pOut->m[1][3] =
    pOut->m[2][0] = pOut->m[2][1] = pOut->m[2][3] =
    pOut->m[3][0] = pOut->m[3][1] = pOut->m[3][2] = 0.0f;

    pOut->m[0][0] = sx;
    pOut->m[1][1] = sy;
    pOut->m[2][2] = sz;

    return pOut;
}


Aufstellen einer Translationsmatrix

INLINE D3DXMATRIX* D3DXMatrixTranslation(D3DXMATRIX *pOut,
                                         FLOAT x,
                                         FLOAT y,
                                         FLOAT z)
{
    D3DXMatrixIdentity(pOut);
    pOut->m[3][0] = x;
    pOut->m[3][1] = y;
    pOut->m[3][2] = z;
    return pOut;
}

Aufstellen einer x-Achsen-Rotationsmatrix

INLINE D3DXMATRIX* D3DXMatrixRotationX(D3DXMATRIX *pOut, FLOAT Angle)
{
    float tempCos = cosf(Angle);
    float tempSin = sinf(Angle);

    pOut->_11 = 1.0f;
    pOut->_12 = 0.0f;
    pOut->_13 = 0.0f;
    pOut->_14 = 0.0f;
    pOut->_21 = 0.0f;
    pOut->_22 = tempCos;
    pOut->_23 = tempSin;
    pOut->_24 = 0.0f;
    pOut->_31 = 0.0f;
    pOut->_32 = -tempSin;
    pOut->_33 = tempCos;
    pOut->_34 = 0.0f;
    pOut->_41 = 0.0f;
    pOut->_42 = 0.0f;
    pOut->_43 = 0.0f;
    pOut->_44 = 1.0f;

    return pOut;
}

Aufstellen einer y-Achsen-Rotationsmatrix

INLINE D3DXMATRIX* D3DXMatrixRotationY(D3DXMATRIX *pOut, FLOAT Angle)
{
    float tempCos = cosf(Angle);
    float tempSin = sinf(Angle);

    pOut->_11 = tempCos;
    pOut->_12 = 0.0f;
    pOut->_13 = -tempSin;
    pOut->_14 = 0.0f;
    pOut->_21 = 0.0f;
    pOut->_22 = 1.0f;
    pOut->_23 = 0.0f;
    pOut->_24 = 0.0f;
    pOut->_31 = tempSin;
    pOut->_32 = 0.0f;
    pOut->_33 = tempCos;
    pOut->_34 = 0.0f;
    pOut->_41 = 0.0f;
    pOut->_42 = 0.0f;
    pOut->_43 = 0.0f;
    pOut->_44 = 1.0f;

    return pOut;
}

Aufstellen einer z-Achsen-Rotationsmatrix

INLINE D3DXMATRIX* D3DXMatrixRotationZ(D3DXMATRIX *pOut, FLOAT Angle)
{
    float tempCos = cosf(Angle);
    float tempSin = sinf(Angle);

    pOut->_11 = tempCos;
    pOut->_12 = tempSin;
    pOut->_13 = 0.0f;
    pOut->_14 = 0.0f;
    pOut->_21 = -tempSin;
    pOut->_22 = tempCos;
    pOut->_23 = 0.0f;
    pOut->_24 = 0.0f;
    pOut->_31 = 0.0f;
    pOut->_32 = 0.0f;
    pOut->_33 = 1.0f;
    pOut->_34 = 0.0f;
    pOut->_41 = 0.0f;
    pOut->_42 = 0.0f;
    pOut->_43 = 0.0f;
    pOut->_44 = 1.0f;

    return pOut;
}

Aufstellen einer Rotationsmatrix für die Beschreibung der Rotation um eine beliebige Achse

INLINE D3DXMATRIX* D3DXMatrixRotationAxis(D3DXMATRIX *pOut,
                   CONST D3DXVECTOR3 *pV, FLOAT Angle)
{
    D3DXVECTOR3 AxisNormalized;
    D3DXVec3Normalize(&AxisNormalized, pV);

    float tempCos = cosf(Angle);
    float tempSin = sinf(Angle);

    float tempDiff = 1.0f-tempCos;

    float temp1Factor = AxisNormalized.x*AxisNormalized.y;
    float temp2Factor = AxisNormalized.x*AxisNormalized.z;
    float temp3Factor = AxisNormalized.y*AxisNormalized.z;

    pOut->_11 = AxisNormalized.x*AxisNormalized.x*tempDiff+tempCos;
    pOut->_12 = temp1Factor*tempDiff+AxisNormalized.z*tempSin;
    pOut->_13 = temp2Factor*tempDiff-AxisNormalized.y*tempSin;
    pOut->_14 = 0.0f;

    pOut->_21 = temp1Factor*tempDiff-AxisNormalized.z*tempSin;
    pOut->_22 = AxisNormalized.y*AxisNormalized.y*tempDiff+tempCos;
    pOut->_23 = temp3Factor*tempDiff+AxisNormalized.x*tempSin;
    pOut->_24 = 0.0f;

    pOut->_31 = temp2Factor*tempDiff+AxisNormalized.y*tempSin;
    pOut->_32 = temp3Factor*tempDiff-AxisNormalized.x*tempSin;
    pOut->_33 = AxisNormalized.z*AxisNormalized.z*tempDiff+tempCos;
    pOut->_34 = 0.0f;

    pOut->_41 = 0.0f;
    pOut->_42 = 0.0f;
    pOut->_43 = 0.0f;
    pOut->_44 = 1.0f;

    return pOut;
}

Optimierung zum Ersten – Rotationsmatrizen mit gleichbleibendem Rotationswinkel
Für den Fall, dass ständig Rotationsmatrizen mit gleichbleibendem Rotationswinkel benötigt werden, bietet es sich an, diese Matrizen nur einmal zu erzeugen und dann immer wieder zu verwenden – eine einfache Optimierungsmethode, die leider aber immer wieder vergessen wird.

Optimierung zum Zweiten – Rotationsmatrizen für kleine Winkel
Für gewöhnlich drehen sich Objekte in jedem Frame nur um einen kleinen Winkel. Was liegt also näher, als die zugehörigen Frame-Rotationsmatrizen mit Hilfe von Sinus- und Kosinusnäherungen für kleine Drehwinkel zu berechnen.
Wenn Sie die grafische Darstellung einer Sinusfunktion in der Nähe des Ursprungs (0°) betrachten (in einer Formelsammlung Ihrer Wahl), werden Sie feststellen, dass dort ein linearer Zusammenhang zwischen dem Winkel und dem zugehörigen Sinuswert besteht.
Etwa bis zu einem Winkel von 10° gilt die folgende Näherung:

sin(Drehwinkel) = Drehwinkel
(Achtung: Die Näherung bezieht sich auf Winkel im Bogenmaß!)

Prüfen Sie diese Beziehung nach, rechnen Sie den Winkel von 10° ins Bogenmaß um, berechnen Sie dann den zugehörigen Sinuswert und vergleichen Sie beide Werte miteinander. Sie stimmen annähernd überein. Die Übereinstimmung wird umso besser, je kleiner die Winkel sind. Ab welchem Winkel die Abweichungen nicht mehr tolerierbar sind, hängt von der von Ihnen gewünschten Genauigkeit ab.
Als Nächstes betrachten wir die Darstellung der Kosinusfunktion in der Nähe des Ursprungs. Erinnert Sie die Kurve nicht ein wenig an eine Parabel? Richtig, die Kosinuswerte können für kleine Winkel mittels einer Parabelfunktion näherungsweise berechnet werden. Hier ist der korrekte Zusammenhang:

cos(Drehwinkel) = 1 – 0.5*Drehwinkel*Drehwinkel
(Achtung: Die Näherung bezieht sich auf Winkel im Bogenmaß!)

Sind Sie daran interessiert, wie man diese Näherungen mathematisch herleiten kann? Wenn Sie gerade ein Mathematikbuch zur Hand haben, schlagen Sie doch einfach die Stichwörter Taylor- bzw. Maclaurin-Reihe nach oder lassen Sie eine Internetsuchmaschine Ihrer Wahl nach diesen Begriffen suchen.
Nur so viel sei gesagt: Mathematische Funktionen können unter bestimmten Bedingungen in eine sogenannte Reihe entwickelt werden (auch ein Taschenrechner nutzt solche Reihenentwicklungen). Denken Sie an eine Geradengleichung (y = mx+b). Kennt man die Steigung m und den y-Achsenabschnitt b, lassen sich alle Punkte der Geraden berechnen. Im Zuge der Differentialrechnung lernt man, dass die Steigung einer Funktion gleich ihrer ersten Ableitung ist. Kluge Mathematiker hatten nun die geniale Idee, dass man die Funktionswerte von nicht so geradlinig verlaufenden Funktionen dadurch berechnen kann, dass man einfach auch die höheren Ableitungen dieser Funktionen berücksichtigt.

INLINE void CalcRotXMatrixS(D3DXMATRIXA16 *pMatrix, float Angle)
{
    if(Angle > 0.17f || Angle < -0.17f) // Winkel zu groß
    {
        D3DXMatrixRotationX(pMatrix, Angle);
        return;
    }

    float tempCos = 1.0f-0.5f*Angle*Angle;

    pMatrix->_11 = 1.0f;
    pMatrix->_12 = 0.0f;
    pMatrix->_13 = 0.0f;
    pMatrix->_14 = 0.0f;
    pMatrix->_21 = 0.0f;
    pMatrix->_22 = tempCos;
    pMatrix->_23 = Angle;
    pMatrix->_24 = 0.0f;
    pMatrix->_31 = 0.0f;
    pMatrix->_32 = -Angle;
    pMatrix->_33 = tempCos;
    pMatrix->_34 = 0.0f;
    pMatrix->_41 = 0.0f;
    pMatrix->_42 = 0.0f;
    pMatrix->_43 = 0.0f;
    pMatrix->_44 = 1.0f;
}

Optimierung zum Dritten – Rotationsmatrizen mit Hilfe von vorberechneten Sinus- und Kosinuswerten erzeugen
Rotationsmatrizen lassen sich auch erzeugen, indem man einer entsprechenden Funktion statt des Winkels die vorberechneten Sinus- bzw. Kosinuswerte übergibt. Diese werden dann direkt in die Matrix eingesetzt. Eine kleine Schwierigkeit ergibt sich jedoch daraus, dass die Rotation in zwei Richtungen erfolgen kann.
Wenn man aber die Symmetrien der Sinus- und Kosinusfunktionen bezüglich des Ursprungs (0°) berücksichtigt, ist das Problem schnell aus der Welt geschafft:

Kosinus      symmetrische Funktion:    cos(10°) = cos(-10°)
Sinus          asymmetrische Funktion:  -sin(10°) = sin(-10°)

Als Beispiel betrachten wir jetzt eine Rotation um +10°/-10°. Zunächst einmal werden der Sinus- und Kosinuswert von 10° im Voraus berechnet. Für eine Drehung um +10° übergibt man einfach die zuvor berechneten Werte. Für eine Drehung um –10° übergibt man der Funktion statt des positiven Sinuswerts einfach den negativen Wert. Um den Kosinuswert braucht man sich keine Gedanken zu machen, da dieser symmetrisch zum Ursprung ist.

INLINE void CreateRotXMatrix(D3DXMATRIXA16 *pMatrix, float s, float c)
{
    pMatrix->_11 = 1.0f;
    pMatrix->_12 = 0.0f;
    pMatrix->_13 = 0.0f;
    pMatrix->_14 = 0.0f;
    pMatrix->_21 = 0.0f;
    pMatrix->_22 = c;
    pMatrix->_23 = s;
    pMatrix->_24 = 0.0f;
    pMatrix->_31 = 0.0f;
    pMatrix->_32 = -s;
    pMatrix->_33 = c;
    pMatrix->_34 = 0.0f;
    pMatrix->_41 = 0.0f;
    pMatrix->_42 = 0.0f;
    pMatrix->_43 = 0.0f;
    pMatrix->_44 = 1.0f;
}