World/Camera Space Normal Mapping

Wie bereits im letzten Artikel angekündigt, werden wir uns heute mit dem sogenannten World Space Normal Mapping befassen. Gegenüber dem altbekannten Tangent (Texture) Space Normal Mapping bietet diese Technik einige entscheidene Vorteile:







  • Die Transformation der Lichtvektoren in den Tangent Space entfällt, was insbesondere bei der Verwendung von mehreren Lichtquellen von Vorteil ist.
  • Die Beleuchtungsberechnungen finden im World Space statt, nachdem die in der Normal Map gespeicherten Normalen in diesen transformiert worden sind. Dies ist unabhängig von der Anzahl der Lichtquellen nur ein einziges Mal notwendig.
  • Die Beleuchtungsberechnungen lassen sich ohne Probleme auch im Rahmen des Post Processings durchführen (deferred lighting). Die in den World Space transformierten Normalen müssen zu diesem Zweck lediglich in eine entsprechende Textur gerendert werden.


Im Artikel Ambiente, diffuse und spiegelnde Reflexion (Beleuchtungsmodelle) haben wir uns bereits mit den Grundlagen der Beleuchtungsberechnungen auseinandergesetzt. Im heutigen Artikel werden wir die Frage klären, wie wir an die für die Berechnungen notwendigen Normalen gelangen.

Zur Erklärung, bei der Generierung einer Normal Map werden die Normalenvektoren zunächst im sogenannten Texturkoordinatensystem (Texture Space: tu, tv, tw) berechnet. Texturiert man nun ein in der xy-Ebene liegendes Dreieck, so ist die x-Richtung des Modellkoordinatensystems parallel zur tu-Richtung des Texturkoordinatensystems orientiert. Die y- und z-Richtungen sind hingegen antiparallel zu den tv- und tw-Texturkoordinaten ausgerichtet (x = tu, y = -tv, z = -tw). Bevor sich nun korrekte Beleuchtungsberechnungen für in der xy-Ebene liegende Flächen (bzw. Dreiecke) durchführen lassen, müssen wir daher zunächst das Vorzeichen der y- und z-Komponente der Textur-Normalen ändern.
In einem 3D-Modell können die Dreiecksflächen jedoch beliebig orientiert sein. Vor der Durchführung der Beleuchtungsberechnungen müssen die Textur-Normalen daher im ersten Schritt aus dem Texturkoordinatensystem in dieselben Ebenen transformiert werden, in denen auch die zu texturierenden Dreiecksflächen liegen (Texture Space -> Model Space). Im zweiten Schritt erfolgt schließlich die Transformation der Normalen in die Spielewelt (World/Camera Space) mithilfe der Orientierungsmatrix (Rotationsmatrix) des betreffenden 3D-Modells. Zu diesem Zweck müssen wir die Textur-Normalen mit einer geeigneten Rotationsmatrix multiplizieren. Die Transformationsmatrix für Schritt 1 gilt es nun zu bestimmen und für jeden Vertex im jeweils verwendeten Vertexformat abzuspeichern:

struct CTexturedVertexWithNormal_NM
{
    float tu, tv, tw;

    // die nachfolgenden 9 Variablen sind die Elemente der
    // Transformationsmatrix (Texture Space -> Model Space):
    float Perpendicular1X, Perpendicular1Y, Perpendicular1Z;
    float Perpendicular2X, Perpendicular2Y, Perpendicular2Z;
    float NormalX, NormalY, NormalZ;

    // Hinweis:
    // perpendicular1,2 (perpendicular:=senkrecht) sind senkrecht
    // zur Normale orientierte Vektoren.
    // Die drei Orientierungsvektoren (perpendicular1, perpendicular2
    // und Normal) sind allesamt senkrecht zueinander orientiert und
    // lassen sich zu der gesuchten Transformationsmatrix
    // zusammenfassen!


    float PosX, PosY, PosZ; 
};

struct CTexturedAnimatedVertexWithNormal_NM
{
    float tu, tv, tw, ta;

    // die nachfolgenden 9 Variablen sind die Elemente der
    // Transformationsmatrix (Texture Space -> Model Space):
    float Perpendicular1X, Perpendicular1Y, Perpendicular1Z;
    float Perpendicular2X, Perpendicular2Y, Perpendicular2Z;
    float NormalX, NormalY, NormalZ;

    float PosX, PosY, PosZ;
};

Sind für jeden Vertex besagte Transformationsmatrizen bzw. die zugrunde liegenden Orientierungsvektoren (Perpendicular1, Perpendicular2 sowie Normal) erst einmal bekannt, sind die für das World/Camera Space Normal Mapping notwendigen Shader-Berechnungen das reinste Kinderspiel.
Um die Textur-Normalen in den World/Camera Space transformieren zu können, muss die Transformationsmatrix im Vertex Shader zunächst mit der Rotationsmatrix des 3D-Models multipliziert werden (Hinweis: Im Model Space wird die augenblickliche Orientierung des 3D-Models in der Spielewelt nicht berücksichtigt!):

// Rotation des Modells berücksichtigen:
gs_TexCoord[1].xyz = matRotation * gs_Normal;

// Perpendicular2:
gs_TexCoord[2].xyz = matRotation * gs_MultiTexCoord2.xyz;

// Perpendicular1:
gs_TexCoord[3].xyz = matRotation * gs_MultiTexCoord1.xyz;

Im Fragment Shader wird schließlich die Matrix zusammengesetzt und für die Transformation der Textur-Normalen verwendet:

mat3 matTexturespaceToWorldspace;

// Perpendicular2:
matTexturespaceToWorldspace[0] = gs_TexCoord[2].xyz;

// Perpendicular1:
matTexturespaceToWorldspace[1] = gs_TexCoord[3].xyz;

// Normal:
matTexturespaceToWorldspace[2] = gs_TexCoord[1].xyz;

vec3 NormalPerTexel = matTexturespaceToWorldspace*
     (2.0*texture(AsteroidNormalTexture, gs_TexCoord[0].st).rgb - 1.0);


Die so transformierten Textur-Normalen können nun, wie in den beiden vorherigen Kapiteln gezeigt, für die anstehenden Beleuchtungsberechnungen im Welt- bzw. Kameraraum verwendet werden.
Die nachfolgende Abbildung gibt uns einen ersten Hinweis, wie man die gesuchten Orientierungsvektoren (Perpendicular1 sowie Perpendicular2) für Vertex 1 (v1) berechnen kann:


















Zunächst bildet man den Differenzvektor s1 zwischen den Vertexpositionen von v2 und v1. Dreht man diesen Vektor nun um den Winkel Alpha im Uhrzeigersinn (mithilfe der Rotationsmatrix RotMatrix), so zeigt der Vektor nach der Drehung in die positive x-Richtung und – was noch wichtiger ist – in die positive tu-Richtung. Mit anderen Worten, der gedrehte Vektor s1(gedreht) beschreibt jetzt für Vertex v1 den Zusammenhang zwischen den Modellkoordinaten und den tu-Texturkoordinaten:

// Hinweis: den Vektor s1 bezeichnen wir im Source Code als
// perpendicular1, den Vektor s1(gedreht) als perpendicular1_transformed.
// Hiermit drücken wir aus, dass diese Vektoren Senkrecht (perpendicular)
// zur Normale orientiert sind!
Multiply3DVectorWithRotationMatrix(&perpendicular1_transformed,
                                   &perpendicular1,
                                   &
RotMatrix);


Nun benötigen wir noch einen zweiten Vektor, der den Zusammenhang zwischen den Modellkoordinaten und den tv-Texturkoordinaten beschreibt. Zu diesem Zweck berechnen wir jetzt einfach das Vektorprodukt aus der Vertexnormalen von Vertex 1 und dem Vektor s1(gedreht). Der Produktvektor s2(gedreht) zeigt nun in die negative y- und in die positive tv-Richtung und beschreibt damit den Zusammenhang zwischen den Modellkoordinaten und den tv-Texturkoordinaten.

// Hinweis: den Vektor s2 bezeichnen wir im Source Code als
// perpendicular2, den Vektor s2(gedreht) als perpendicular2_transformed.

D3DXVec3Cross(&perpendicular2_transformed, &Normal,
              &perpendicular1_transformed);


Für die Komponenten der gesuchten Transformationsmatrix ergeben sich dementsprechend die folgenden Werte:

Perpendicular1X=1; Perpendicular1Y=0; Perpendicular1Z=0;
Perpendicular2X=0; Perpendicular2Y=-1; Perpendicular2Z=0;
NormalX=0; NormalY=0; NormalZ=-1


Ganz konkret bedeutet dieses Resultat, dass bei der Berechnung des Normalenvektors im Fragment Shader – genau wie eingangs beschrieben! – das Vorzeichen der y- und z-Komponente der Textur-Normalen geändert wird.
Eine Kleinigkeit haben wir bei unseren bisherigen Betrachtungen jedoch noch unterschlagen, denn die Berechnung der Rotationsmatrix für die Drehung von s1 wird dadurch erschwert, dass je nach Orientierung von s1 vier Fälle unterschieden werden müssen, damit der Vektor s1(gedreht) nach der Drehung von s1 auch wirklich in die positive tu-Richtung zeigt. Alle möglichen Fälle sind zum besseren Verständnis in der nachfolgenden Abbildung dargestellt:


















Der nachfolgende Code-Abschnitt zeigt, wie besagte Rotationsmatrix für alle vier Fälle berechnet werden kann:

rotationAngle = (float)fabs(atanf(deltaTV1/deltaTU1));

if
(deltaTU1 >= 0.0f && deltaTV1 < 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, rotationAngle);
else if(deltaTU1 >= 0.0f && deltaTV1 >= 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, -rotationAngle);
else if(deltaTU1 < 0.0f && deltaTV1 < 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, (D3DX_PI-rotationAngle));
else if(deltaTU1 < 0.0f && deltaTV1 >= 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, -(D3DX_PI-rotationAngle));


Nun gilt es noch ein letztes Problem zu lösen – was passiert, wenn die Normal Map in tu- oder tv-Richtung gespiegelt auf das Dreieck gemappt wird? Die Antwort liegt auf der Hand, die Vektoren s1(gedreht) und s2(gedreht) zeigen nunmehr in die falschen Richtungen. Um diesen Fehler zu beheben, müssen die vorangegangenen Berechnungen mit der negativen Vertexnormale durchzuführt werden, wodurch sich der Drehsinn der Rotationsmatrix ändert. Soweit so gut, doch wie können wir in Erfahrung bringen, ob das Vorzeichen des Normalenvektors geändert werden muss oder nicht?

In der nachfolgenden Abbildung wird gezeigt, wie mithilfe der Texturkoordinatenänderungen in tu- und tv-Richtung ein von diesen Änderungen abhängiger Normalenvektor (normal_TexCoord) berechnet werden kann. Je nachdem, in welcher Richtung die tu- bzw. die tv-Koordinaten größer oder kleiner werden, ist der Winkel zwischen dem so berechneten Normalenvektor (normal_TexCoord) und der Vertexnormale (normal) kleiner oder größer als 90°. Ist der Winkel größer als 90°, muss für die Berechnung der Vektoren s1(gedreht) und s2(gedreht) die negative Vertexnormale verwendet werden:






















Hier nun der zugehörige Code-Ausschnitt:

Direction_deltaTU = deltaTU1*perpendicular1 +
                    deltaTU2*perpendicular2;

Direction_deltaTV = deltaTV1*perpendicular1 +
                    deltaTV2*perpendicular2;

D3DXVec3Cross(&NormalTexCoord, &Direction_deltaTU,
              &Direction_deltaTV);

if(D3DXVec3Dot(&Normal, &NormalTexCoord) < 0.0f)
   Normal *= -1.0f;


Damit haben wir alles beisammen, um die für das World/Camera Space Normal Mapping zusätzlich zur Normale benötigten Orientierungsvektoren (Perpendicular1 sowie Perpendicular2) berechnen zu können:

for(i = 0; i < NumMeshVertices; i++)
{
    VertexArray[i].Perpendicular1X = 0.0f;
    VertexArray[i].Perpendicular1Y = 0.0f;
    VertexArray[i].Perpendicular1Z = 0.0f;

    VertexArray[i].Perpendicular2X = 0.0f;
    VertexArray[i].Perpendicular2Y = 0.0f;
    VertexArray[i].Perpendicular2Z = 0.0f;
}


float deltaTU1, deltaTV1, deltaTU2, deltaTV2, rotationAngle;

D3DXVECTOR3 perpendicular1;
D3DXVECTOR3 perpendicular2;

D3DXVECTOR3 perpendicular1_transformed;
D3DXVECTOR3 perpendicular2_transformed;

D3DXVECTOR3 Direction_deltaTU;
D3DXVECTOR3 Direction_deltaTV;

D3DXVECTOR3 NormalTexCoord;

D3DXMATRIXA16
RotMatrix;

for(i = 0; i < MeshIndexData[0].NumIndices; i+=3)
{
    // Dreiecksvertex Nr. 1:

    Normal.x = VertexArray[IndexArray[i]].NormalX;
    Normal.y = VertexArray[IndexArray[i]].NormalY;
    Normal.z = VertexArray[IndexArray[i]].NormalZ;

    perpendicular1.x = VertexArray[IndexArray[i+1]].PosX -
                       VertexArray[IndexArray[i]].PosX;
    perpendicular1.y = VertexArray[IndexArray[i+1]].PosY -
                       VertexArray[IndexArray[i]].PosY;
    perpendicular1.z = VertexArray[IndexArray[i+1]].PosZ -
                       VertexArray[IndexArray[i]].PosZ;

    perpendicular2.x = VertexArray[IndexArray[i+2]].PosX -
                       VertexArray[IndexArray[i]].PosX;
    perpendicular2.y = VertexArray[IndexArray[i+2]].PosY -
                       VertexArray[IndexArray[i]].PosY;
    perpendicular2.z = VertexArray[IndexArray[i+2]].PosZ -
                       VertexArray[IndexArray[i]].PosZ;

    deltaTU1 = VertexArray[IndexArray[i+1]].tu -
               VertexArray[IndexArray[i]].tu;
    deltaTV1 = VertexArray[IndexArray[i+1]].tv -
               VertexArray[IndexArray[i]].tv;

    deltaTU2 = VertexArray[IndexArray[i+2]].tu -
               VertexArray[IndexArray[i]].tu;
    deltaTV2 = VertexArray[IndexArray[i+2]].tv -
               VertexArray[IndexArray[i]].tv;

    Direction_deltaTU = deltaTU1*perpendicular1 +
                        deltaTU2*perpendicular2;

    Direction_deltaTV = deltaTV1*perpendicular1 +
                        deltaTV2*perpendicular2;

    D3DXVec3Cross(&NormalTexCoord, &Direction_deltaTU,
                  &Direction_deltaTV);

    if(D3DXVec3Dot(&Normal, &NormalTexCoord) < 0.0f)
        Normal *= -1.0f;

    rotationAngle = (float)fabs(atanf(deltaTV1/deltaTU1));

    if(deltaTU1 >= 0.0f && deltaTV1 < 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, rotationAngle);
    else if(deltaTU1 >= 0.0f && deltaTV1 >= 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, -rotationAngle);
    else if(deltaTU1 < 0.0f && deltaTV1 < 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, (D3DX_PI-rotationAngle));
    else if(deltaTU1 < 0.0f && deltaTV1 >= 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, -(D3DX_PI-rotationAngle));

    Multiply3DVectorWithRotationMatrix(&perpendicular1_transformed,
                                       &perpendicular1,
                                       &
RotMatrix);

    D3DXVec3Cross(&perpendicular2_transformed, &Normal,
                  &perpendicular1_transformed);

    VertexArray[IndexArray[i]].Perpendicular1X +=
        perpendicular1_transformed.x;
    VertexArray[IndexArray[i]].Perpendicular1Y +=
        perpendicular1_transformed.y;
    VertexArray[IndexArray[i]].Perpendicular1Z +=
        perpendicular1_transformed.z;

    VertexArray[IndexArray[i]].Perpendicular2X +=
        perpendicular2_transformed.x;
    VertexArray[IndexArray[i]].Perpendicular2Y +=
        perpendicular2_transformed.y;
    VertexArray[IndexArray[i]].Perpendicular2Z +=
        perpendicular2_transformed.z;

    // Dreiecksvertex Nr. 2:

    Normal.x = VertexArray[IndexArray[i+1]].NormalX;
    Normal.y = VertexArray[IndexArray[i+1]].NormalY;
    Normal.z = VertexArray[IndexArray[i+1]].NormalZ;

    perpendicular1.x = VertexArray[IndexArray[i+2]].PosX -
                       VertexArray[IndexArray[i+1]].PosX;
    perpendicular1.y = VertexArray[IndexArray[i+2]].PosY -
                       VertexArray[IndexArray[i+1]].PosY;
    perpendicular1.z = VertexArray[IndexArray[i+2]].PosZ -
                       VertexArray[IndexArray[i+1]].PosZ;

    perpendicular2.x = VertexArray[IndexArray[i]].PosX -
                       VertexArray[IndexArray[i+1]].PosX;
    perpendicular2.y = VertexArray[IndexArray[i]].PosY -
                       VertexArray[IndexArray[i+1]].PosY;
    perpendicular2.z = VertexArray[IndexArray[i]].PosZ -
                       VertexArray[IndexArray[i+1]].PosZ;

    deltaTU1 = VertexArray[IndexArray[i+2]].tu -
               VertexArray[IndexArray[i+1]].tu;
    deltaTV1 = VertexArray[IndexArray[i+2]].tv -
               VertexArray[IndexArray[i+1]].tv;

    deltaTU2 = VertexArray[IndexArray[i]].tu -
               VertexArray[IndexArray[i+1]].tu;
    deltaTV2 = VertexArray[IndexArray[i]].tv -
               VertexArray[IndexArray[i+1]].tv;

    Direction_deltaTU = deltaTU1*perpendicular1 +
                        deltaTU2*perpendicular2;

    Direction_deltaTV = deltaTV1*perpendicular1 +
                        deltaTV2*perpendicular2;

    D3DXVec3Cross(&NormalTexCoord, &Direction_deltaTU,
                  &Direction_deltaTV);

    if(D3DXVec3Dot(&Normal, &NormalTexCoord) < 0.0f)
        Normal *= -1.0f;

    rotationAngle = (float)fabs(atanf(deltaTV1/deltaTU1));

    if(deltaTU1 >= 0.0f && deltaTV1 < 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, rotationAngle);
    else if(deltaTU1 >= 0.0f && deltaTV1 >= 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, -rotationAngle);
    else if(deltaTU1 < 0.0f && deltaTV1 < 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, (D3DX_PI-rotationAngle));
    else if(deltaTU1 < 0.0f && deltaTV1 >= 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, -(D3DX_PI-rotationAngle));

    Multiply3DVectorWithRotationMatrix(&perpendicular1_transformed,
                                       &perpendicular1,
                                       &
RotMatrix);

    D3DXVec3Cross(&perpendicular2_transformed, &Normal,
                  &perpendicular1_transformed);

    VertexArray[IndexArray[i+1]].Perpendicular1X +=
        perpendicular1_transformed.x;
    VertexArray[IndexArray[i+1]].Perpendicular1Y +=
        perpendicular1_transformed.y;
    VertexArray[IndexArray[i+1]].Perpendicular1Z +=
        perpendicular1_transformed.z;

    VertexArray[IndexArray[i+1]].Perpendicular2X +=
        perpendicular2_transformed.x;
    VertexArray[IndexArray[i+1]].Perpendicular2Y +=
        perpendicular2_transformed.y;
    VertexArray[IndexArray[i+1]].Perpendicular2Z +=
        perpendicular2_transformed.z;

    // Dreiecksvertex Nr. 3:

    Normal.x = VertexArray[IndexArray[i+2]].NormalX;
    Normal.y = VertexArray[IndexArray[i+2]].NormalY;
    Normal.z = VertexArray[IndexArray[i+2]].NormalZ;

    perpendicular1.x = VertexArray[IndexArray[i]].PosX -
                       VertexArray[IndexArray[i+2]].PosX;
    perpendicular1.y = VertexArray[IndexArray[i]].PosY -
                       VertexArray[IndexArray[i+2]].PosY;
    perpendicular1.z = VertexArray[IndexArray[i]].PosZ -
                       VertexArray[IndexArray[i+2]].PosZ;

    perpendicular2.x = VertexArray[IndexArray[i+1]].PosX -
                       VertexArray[IndexArray[i+2]].PosX;
    perpendicular2.y = VertexArray[IndexArray[i+1]].PosY -
                       VertexArray[IndexArray[i+2]].PosY;
    perpendicular2.z = VertexArray[IndexArray[i+1]].PosZ -
                       VertexArray[IndexArray[i+2]].PosZ;

    deltaTU1 = VertexArray[IndexArray[i]].tu -
               VertexArray[IndexArray[i+2]].tu;
    deltaTV1 = VertexArray[IndexArray[i]].tv -
               VertexArray[IndexArray[i+2]].tv;

    deltaTU2 = VertexArray[IndexArray[i+1]].tu -
               VertexArray[IndexArray[i+2]].tu;
    deltaTV2 = VertexArray[IndexArray[i+1]].tv -
               VertexArray[IndexArray[i+2]].tv;

    Direction_deltaTU = deltaTU1*perpendicular1 +
                        deltaTU2*perpendicular2;

    Direction_deltaTV = deltaTV1*perpendicular1 +
                        deltaTV2*perpendicular2;

    D3DXVec3Cross(&NormalTexCoord, &Direction_deltaTU,
                  &Direction_deltaTV);

    if(D3DXVec3Dot(&Normal, &NormalTexCoord) < 0.0f)
        Normal *= -1.0f;

    rotationAngle = (float)fabs(atanf(deltaTV1/deltaTU1));

    if(deltaTU1 >= 0.0f && deltaTV1 < 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, rotationAngle);
    else if(deltaTU1 >= 0.0f && deltaTV1 >= 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, -rotationAngle);
    else if(deltaTU1 < 0.0f && deltaTV1 < 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, (D3DX_PI-rotationAngle));
    else if(deltaTU1 < 0.0f && deltaTV1 >= 0.0f)
    D3DXMatrixRotationAxis(&
RotMatrix, &Normal, -(D3DX_PI-rotationAngle));

    Multiply3DVectorWithRotationMatrix(&perpendicular1_transformed,
                                       &perpendicular1,
                                       &
RotMatrix);

    D3DXVec3Cross(&perpendicular2_transformed, &Normal,
                  &perpendicular1_transformed);

    VertexArray[IndexArray[i+2]].Perpendicular1X +=
        perpendicular1_transformed.x;
    VertexArray[IndexArray[i+2]].Perpendicular1Y +=
        perpendicular1_transformed.y;
    VertexArray[IndexArray[i+2]].Perpendicular1Z +=
        perpendicular1_transformed.z;

    VertexArray[IndexArray[i+2]].Perpendicular2X +=
        perpendicular2_transformed.x;
    VertexArray[IndexArray[i+2]].Perpendicular2Y +=
        perpendicular2_transformed.y;
    VertexArray[IndexArray[i+2]].Perpendicular2Z +=
        perpendicular2_transformed.z;
}

// Nun müssen alle Senkrechten noch normiert werden:

for(i = 0; i < NumMeshVertices; i++)
{
    tempVektor3.x = VertexArray[i].Perpendicular1X;
    tempVektor3.y = VertexArray[i].Perpendicular1Y;
    tempVektor3.z = VertexArray[i].Perpendicular1Z;

    Normalize3DVector(&tempVektor3, &tempVektor3);

    VertexArray[i].Perpendicular1X = tempVektor3.x;
    VertexArray[i].Perpendicular1Y = tempVektor3.y;
    VertexArray[i].Perpendicular1Z = tempVektor3.z;

    tempVektor3.x = VertexArray[i].Perpendicular2X;
    tempVektor3.y = VertexArray[i].Perpendicular2Y;
    tempVektor3.z = VertexArray[i].Perpendicular2Z;

    Normalize3DVector(&tempVektor3, &tempVektor3);

    VertexArray[i].Perpendicular2X = tempVektor3.x;
    VertexArray[i].Perpendicular2Y = tempVektor3.y;
    VertexArray[i].Perpendicular2Z = tempVektor3.z;
}


Interessante Artikel