3D-Programmierung (Mathematik) Teil 09: Viewportkoordinaten, Bildschirmkoordinaten und Object Picking

Die Sicht- und Projektionstransformationen sind letzten Endes dafür verantwortlich, dass ein Objekt, das sich irgendwo im Blickfeld der Kamera befindet, auf dem Bildschirm dargestellt werden kann. Die Bildschirmposition eines Objekts kann nun entweder in Bildschirmkoordinaten (Screen Coordinates) oder Viewportkoordinaten angegeben werden.

Per Definition ordnet man der linken oberen Bildschirmecke die Bildschirmkoordinaten (0, 0) und der rechten unteren Bildschirmecke die Bildschirmkoordinaten (Bildauflösung Breite, Bildauflösung Höhe) zu (z.B. 1024, 768). Die Viewportkoordinaten sind unabhängig von der Bildschirmauflösung wie folgt definiert:

-1/1: linke obere Bildschirmecke
1/-1: rechte untere Bildschirmecke
0/0 : Bildschirmmitte

Mithilfe der beiden nachfolgenden Funktionen können Bildschirmkoordinaten und Viewportkoordinaten ineinander umgerechnet werden:

INLINE void Transform_Screen_To_Viewport_Coordinates(float* pViewportX,
                                                     float* pViewportY,
                                                     float* pScreenX,
                                                     float* pScreenY,
                                                     long &screenwidth,
                                                     long &screenheight)
{
    *pViewportX = 2.0f*(*pScreenX)/screenwidth - 1.0f;
    *pViewportY = 1.0f - 2.0f*(*pScreenY)/screenheight;
}


INLINE void Transform_Viewport_To_Screen_Coordinates(float* pViewportX,
                                                     float* pViewportY,
                                                     float* pScreenX,
                                                     float* pScreenY,
                                                     long &screenwidth,
                                                     long &screenheight)
{
    *pScreenX = 0.5f*((*pViewportX) + 1.0f)*screenwidth;
    *pScreenY = 0.5f*(1.0f - (*pViewportY))*screenheight;
}

Object Picking
In fast allen Spielen müssen die verschiedensten Spieleobjekte mithilfe des Mauszeigers selektiert werden. Für die Realisierung dieses als Objekt-Picking bekannten Verfahrens gibt es nun mehrere Möglichkeiten. Ein einfacher Weg besteht darin, die Bildschirmkoordinaten des Mauszeigers mit den Bildschirmkoordinaten des zu selektierenden Spieleobjekts zu vergleichen. Betrachten wir hierzu zwei Varianten:

Object Picking (einfach), bei dem der Kameraabstand des Objekts unberücksichtigt bleibt
Beim Objekt-Picking in seiner einfachsten Form bleibt der Kameraabstand des Objekts unberücksichtigt. Dies mag zwar zum Betätigen eines Buttons oder eines Schalters ausreichen, beim Selektieren eines 3D-Objekts – wie etwa in einem Strategiespiel – muss dessen Kameraabstand jedoch zwingend berücksichtigt werden. In diesem Zusammenhang muss man bedenken, dass die Größe eines Objekts auf dem Bildschirm vom Kameraabstand abhängig ist.

// Korrekturterme, damit die Genauigkeit beim Object Picking unabhängig
// von der gewählten Bildschirmauflösung ist:
g_WidthCorrection = g_screenwidth/1024.0f;
g_HeightCorrection = g_screenheight/768.0f;

. . .

if(abs(ObjectScreenPosX - g_CursorScreenX) < 15.0f*g_WidthCorrection)
{
    if(abs(ObjectScreenPosY - g_CursorScreenY) < 15.0f*g_HeightCorrection)
    {
        ObjectSelected = true;
    }
}

Object Picking mit Berücksichtigung des Kameraabstands
Die Berücksichtigung des Kameraabstands beim Objekt-Picking ist gleichbedeutend mit der Berücksichtigung der Größe des zu selektierenden Objekts auf dem Bildschirm. Hierfür benötigt man zusätzlich zu den Bildschirmkoordinaten des Objektmittelpunkts (ObjectCenterScreenPosX, ObjectCenterScreenPosY) einen zweiten Satz von Koordinaten, welche die Objektausdehnung auf dem Bildschirm beschreiben (ScreenPosBorderX, ScreenPosBorderY). Zusammengenommen ergeben beide Koordinatensätze den Picking-Bereich des Objekts. Dieser kann jetzt im zweiten Schritt mit dem Abstand des Mauszeigers vom Objektmittelpunkt verglichen werden.

Calculate_Screen_Coordinates(&ObjectCenterScreenPosX,
                             &ObjectCenterScreenPosY,
                             &ObjectCameraSpacePosition,
                             &ViewProjectionMatrix,
                             screenwidth, screenheight);

BorderPos = ObjectCameraSpacePosition +
            0.25f*ObjectScaleFactor*g_CameraHorizontal;

Calculate_Screen_Coordinates(&ScreenPosBorderX, &ScreenPosBorderY,
                             &BorderPos,
                             &ViewProjectionMatrix,
                             screenwidth, screenheight);

// Picking-Bereich (Screen range) des zu selektierenden Objekts bestimmen:
ObjectScreenRangeX = ScreenPosBorderX - ObjectCenterScreenPosX;
ObjectScreenRangeY = ScreenPosBorderY - ObjectCenterScreenPosY;

// Verhindern, dass der Picking-Bereich mit zunehmendem Kameraabstand
// zu klein wird:

if(ObjectScreenRangeX < 25.0f)
    ObjectScreenRangeX = 25.0f;

ObjectScreenRangeX *= g_WidthCorrection;

if(ObjectScreenRangeY < 25.0f)
    ObjectScreenRangeY = 25.0f;

ObjectScreenRangeY *= g_HeightCorrection;

// quadratischen Picking-Bereich berechnen:
ObjectScreenRangeXSquare = ObjectScreenRangeX*ObjectScreenRangeX;
ObjectScreenRangeYSquare = ObjectScreenRangeY*ObjectScreenRangeY;

// Abstand (Bildschirmkoordinaten) von Mauszeiger und Objektmittelpunkt:
CursorDistanceX = g_CursorScreenX - ObjectCenterScreenPosX;
CursorDistanceY = g_CursorScreenY - ObjectCenterScreenPosY;

// quadratischen Maus-Objekt-Abstand berechnen:
CursorDistanceXSquare = CursorDistanceX*CursorDistanceX;
CursorDistanceYSquare = CursorDistanceY*CursorDistanceY;

// Wenn sich der quadratische Maus-Objekt-Abstand innerhalb des
// quadratischen Picking-Bereichs befindet, dann gilt das Objekt als
//selektiert:

if(CursorDistanceXSquare + CursorDistanceYSquare <
   ObjectScreenRangeXSquare + ObjectScreenRangeYSquare)
{
    ObjectSelected = true;
}

Die Berechnung der Bildschirmkoordinaten mithilfe der Calculate_Screen_Coordinates()-Funktion erfolgt unter Verwendung der View- und Projektionsmatrix. Beide Matrizen werden jedoch nicht einzeln übergeben sondern in Form einer kombinierten View-Projection-Matrix:

ViewProjectionMatrix = matView*matProj;

Mittels einer einfachen Matrizenmultiplikation wird die Kameraposition in den Projektionsraum (projection space) transformiert und dann im zweiten Schritt in Bildschirmkoordinaten umgerechnet.

INLINE void Calculate_Screen_Coordinates(float* pScreenX, float* pScreenY,
                                         D3DXVECTOR3* pVec,
                                         D3DXMATRIXA16* pViewProjectionMatrix,
                                         long &screenwidth,
                                         long &screenheight)
{
    float tempX = pVec->x;
    float tempY = pVec->y;
    float tempZ = pVec->z;

    float tempX2 = pViewProjectionMatrix->_11*tempX +
                   pViewProjectionMatrix->_21*tempY +
                   pViewProjectionMatrix->_31*tempZ +
                   pViewProjectionMatrix->_41;

    float tempY2 = pViewProjectionMatrix->_12*tempX +
                   pViewProjectionMatrix->_22*tempY +
                   pViewProjectionMatrix->_32*tempZ +
                   pViewProjectionMatrix->_42;

    float tempW2 = pViewProjectionMatrix->_14*tempX +
                   pViewProjectionMatrix->_24*tempY +
                   pViewProjectionMatrix->_34*tempZ +
                   pViewProjectionMatrix->_44;

    float tempInvW2 = 1.0f/tempW2;

    *pScreenX = (1.0f + (tempX2*tempInvW2))*0.5f*screenwidth;
    *pScreenY = (1.0f - (tempY2*tempInvW2))*0.5f*screenheight;
}

Zu guter letzt möchte ich Ihnen noch eine weitere Funktion vorgestellen. Mithilfe der Calculate_ProjectedCameraSpacePosition()-Funktion kann die Kameraposition eines Objekts in den Projektionsraum (projection space) transformiert werden. Gleiches geschieht übrigens im Vertex Shader, nur dass dort die Vertices eines 3D-Modells transformiert werden.
Mithilfe der projizierten Kameraposition lassen sich im weiteren Verlauf durch Tiefenvergleiche (Abstand zur Kamera in Blickrichtung) Sichtbarkeitstests im Pixel (Fragment) Shader durchführen. Mögliche Fragestellungen sind: Verdeckt das Raumschiff oder der Planet die Sonne? Sind die Lens Flares sichtbar?

INLINE void Calculate_ProjectedCameraSpacePosition(D3DXVECTOR3* pVecOut,
                                                   D3DXVECTOR3* pVecIn,
                                                   D3DXMATRIXA16* pViewProjectionMatrix)
{
    float tempX = pVecIn->x;
    float tempY = pVecIn->y;
    float tempZ = pVecIn->z;

    float tempX2 = pViewProjectionMatrix->_11*tempX +
                   pViewProjectionMatrix->_21*tempY +
                   pViewProjectionMatrix->_31*tempZ +
                   pViewProjectionMatrix->_41;

    float tempY2 = pViewProjectionMatrix->_12*tempX +
                   pViewProjectionMatrix->_22*tempY +
                   pViewProjectionMatrix->_32*tempZ +
                   pViewProjectionMatrix->_42;

    float tempZ2 = pViewProjectionMatrix->_13*tempX +
                   pViewProjectionMatrix->_23*tempY +
                   pViewProjectionMatrix->_33*tempZ +
                   pViewProjectionMatrix->_43;

    float tempFloat = 1.0f/tempZ2;

    pVecOut->x = 0.5*tempX2*tempFloat + 0.5;
    pVecOut->y = 0.5*tempY2*tempFloat + 0.5;
    pVecOut->z = tempZ2;
}