Perspektivisch korrekte Texturzuordnung

7

Ich arbeite an einer kleinen Rendering-Engine für ein persönliches Projekt und habe Probleme mit dem Textur-Mapping-Teil.

Es scheint in einigen Fällen zu funktionieren, in anderen jedoch nicht. Wenn sich beispielsweise einer der Scheitelpunkte hinter der Kamera befindet, wird die Textur gedehnt.

Scheinbar korrekter Fall Falscher FallScheinbar korrekter Fall Falscher Fall

Ich vermute, dass es etwas damit zu tun hat, dass das Textur-Mapping nicht perspektivisch korrekt ist. Ich habe verschiedene Änderungen versucht, die hauptsächlich den Z-Abstand zur Kamera betreffen, konnte jedoch keine schnelle Lösung für meinen Code finden.

Hier ist mein Code für die perspektivische Projektion:

public double[] project(double x, double y, double z) {
    double tx = x - camera.x;
    double ty = z - camera.z;
    double tz = y - camera.y;

    double cx = Math.cos(camera.pitch);
    double cy = Math.cos(camera.yaw);
    double cz = Math.cos(camera.roll);

    double sx = Math.sin(camera.pitch);
    double sy = Math.sin(camera.yaw);
    double sz = Math.sin(camera.roll);

    double dx = cy * (sz * ty + cz * tx) - sy * tz;
    double dy = sx * (cy * tz + sy * (sz * ty + cz * tx)) + cx * (cz * ty - sz * tx);
    double dz = cx * (cy * tz + sy * (sz * ty + cz * tx)) - sx * (cz * ty - sz * tx);

    double ez = 1.0 / Math.tan(FOV / 2.0);

    double bx = ez / dz * dx;
    double by = ez / dz * dy;

    if (dz < 0.0) {
        bx = -bx;
        by = -by;
    }

    int px = (int) (width + bx * height) / 2;
    int py = (int) (height + by * height) / 2;

    return new double[] { px, py, dz };
}

und hier mein Code für das Textur-Mapping:

public double[] map(double x, double y, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) {
    double A = (x0 - x) * (y0 - y2) - (y0 - y) * (x0 - x2);
    double B = ((x0 - x) * (y1 - y3) - (y0 - y) * (x1 - x3) + (x1 - x) * (y0 - y2) - (y1 - y) * (x0 - x2)) / 2.0;
    double C = (x1 - x) * (y1 - y3) - (y1 - y) * (x1 - x3);

    double det = A - 2.0 * B + C;

    double u;
    if (det == 0.0) {
        u = A / (A - C);
        if (Double.isNaN(u) || u < 0.0 || u > 1.0)
            return null;
    } else {
        double u1 = ((A - B) + Math.sqrt(B * B - A * C)) / det;
        boolean u1valid = !Double.isNaN(u1) && u1 >= 0.0 && 1.0 >= u1;

        double u2 = ((A - B) - Math.sqrt(B * B - A * C)) / det;
        boolean u2valid = !Double.isNaN(u2) && u2 >= 0.0 && 1.0 >= u2;

        if (u1valid && u2valid)
            u = u1 < u2 ? u2 : u1;
        else if (u1valid)
            u = u1;
        else if (u2valid)
            u = u2;
        else
            return null;
    }

    double v1 = ((1.0 - u) * (x0 - x) + u * (x1 - x)) / ((1.0 - u) * (x0 - x2) + u * (x1 - x3));
    boolean v1valid = !Double.isNaN(v1) && v1 >= 0.0 && 1.0 >= v1;

    double v2 = ((1.0 - u) * (y0 - y) + u * (y1 - y)) / ((1.0 - u) * (y0 - y2) + u * (y1 - y3));
    boolean v2valid = !Double.isNaN(v2) && v2 >= 0.0 && 1.0 >= v2;

    double v;
    if (v1valid && v2valid)
        v = v1 < v2 ? v2 : v1;
    else if (v1valid)
        v = v1;
    else if (v2valid)
        v = v2;
    else
        return null;

    return new double[] { u, v };
}

und hier ist mein Quad-Zeichnungscode:

public void renderFace(Screen screen, int x0, int y0, int z0, int x1, int y1, int z1, int x2, int y2, int z2, int x3, int y3, int z3) {
    boolean render = true;

    double[] p0 = screen.project(x0, y0, z0);
    int px0 = (int) p0[0], py0 = (int) p0[1];
    render |= p0[2] >= ZCLIP && px0 >= 0 && px0 < screen.width && py0 >= 0 && py0 < screen.height;

    double[] p1 = screen.project(x1, y1, z1);
    int px1 = (int) p1[0], py1 = (int) p1[1];
    render |= p1[2] >= ZCLIP && px1 >= 0 && px1 < screen.width && py1 >= 0 && py1 < screen.height;

    double[] p2 = screen.project(x2, y2, z2);
    int px2 = (int) p2[0], py2 = (int) p2[1];
    render |= p2[2] >= ZCLIP && px2 >= 0 && px2 < screen.width && py2 >= 0 && py2 < screen.height;

    double[] p3 = screen.project(x3, y3, z3);
    int px3 = (int) p3[0], py3 = (int) p3[1];
    render |= p3[2] >= ZCLIP && px3 >= 0 && px3 < screen.width && py3 >= 0 && py3 < screen.height;

    if (!render)
        return;

    int minX = Math.min(Math.min(px0, px1), Math.min(px2, px3));
    if (minX < 0)
        minX = 0;
    if (minX > screen.width)
        minX = screen.width;

    int minY = Math.min(Math.min(py0, py1), Math.min(py2, py3));
    if (minY < 0)
        minY = 0;
    if (minY > screen.height)
        minY = screen.height;

    int maxX = Math.max(Math.max(px0, px1), Math.max(px2, px3));
    if (maxX < 0)
        maxX = 0;
    if (maxX > screen.width)
        maxX = screen.width;

    int maxY = Math.max(Math.max(py0, py1), Math.max(py2, py3));
    if (maxY < 0)
        maxY = 0;
    if (maxY > screen.height)
        maxY = screen.height;

    if (minX == maxX || minY == maxY)
        return;

    for (int py = minY; py < maxY; ++py)
        for (int px = minX; px < maxX; ++px) {
            double[] uv = screen.map(px + 0.5, py + 0.5, px0, py0, px1, py1, px2, py2, px3, py3);
            if (uv == null)
                continue;
            double u = uv[0], v = uv[1];

            double pz = (1 - u) * ((1 - v) * p0[2] + v * p2[2]) + u * ((1 - v) * p1[2] + v * p3[2]);
            if (pz < ZCLIP)
                continue;

            int texX = 15 - Math.min(15, (int) (16 * u));
            int texY = 15 - Math.min(15, (int) (16 * v));
            screen.setPixel(px, py, pz, Art.WALLS.getPixel(texX, texY) * BRICKS);
        }
}

Kann jemand darauf hinweisen, was ich falsch mache? Ich bin nicht sehr erfahren, da dies mein erster Versuch ist, eine Spiel-Engine zu implementieren.

Vielen Dank für jeden Einblick.

ordentlich
quelle
Vielleicht haben Sie eine bessere Zeit, diese Frage auf computergraphics.stackexchange.com
Soapy
@Soapy Danke, ich werde dort nach der Abklingzeit posten.
ordentlich
@Syntac_ Ich habe diese Seite bereits gesehen, konnte aber keine Lösung für mein Problem finden.
ordentlich
Ihr Code benötigt dringend Kommentare.
Sam Hocevar

Antworten:

1

Ihr Code ist überhaupt nicht falsch. Sie gehen richtig mit Matrizen und Scheitelpunkten um (Ihre 3D-Simulation funktioniert tatsächlich), und selbst die Texturabbildung ist in Ordnung. Wichtig ist, dass Ihr Texturabbildungsalgorithmus nur eine lineare Interpolation verwendet, um einen Punkt im 3D-Raum auf einen Punkt im 2D-Plan einer bestimmten Textur abzubilden.

Texturabbildung
Beim Zuordnen von Texturen zu Netzpolygonen betrachten Sie normalerweise ein 3-Ple im dreidimensionalen Raum (drei 3D-Scheitelpunkte) und ordnen es einem anderen 3-Ple in einem 2D-Plan zu (drei 2D-Scheitelpunkte innerhalb einer Textur). Beim Zeichnen werden die drei 3D-Eckpunkte auf den Bildschirm projiziert ( [x, y, z] bis [x ', y'] ). Dann wird das auf der Textur betrachtete Dreieck (unsere drei 2D-Scheitelpunkte) auf dem Bildschirm gezeichnet, wobei die 2D-Scheitelpunkte schließlich so transformiert werden, dass sie zur vorherigen Projektion passen. Sie wissen das, weil Sie es tatsächlich getan haben.
Der von Ihnen implementierte Texturabbildungsalgorithmus ist jedoch eine affine Texturabbildung. Dies bedeutet, dass die Dreiecke nur mit ebenen affinen Transformationen gezeichnet werden(also Translation, Rotation, Skalierung, Spiegeln / Spiegeln). Wenn Sie also zeichnen, betrachten Sie die Koordinaten so, wie sie sind, ohne Informationen über ihre tatsächliche Tiefe in ihren dreidimensionalen Gegenstücken. Sie zeichnen nur Sprites.

Perspektivkorrektur Um Texturen in einer 3D-Raumprojektion korrekt zu zeichnen, müssen wir die Scheitelpunkte zusammen mit einigen Informationen über ihre Tiefe im Raum in Bezug auf die Position der 3D-Kamera berücksichtigen. Um eine perspektivische Korrektur für Texturen zu erreichen, betrachten wir die lineare Interpolationsformel:

Geben Sie hier die Bildbeschreibung ein, wo Geben Sie hier die Bildbeschreibung ein.

Die affine Texturabbildung interpoliert direkt zwischen zwei Werten, und durch zweimaliges Lerping kann die Engine jedes benötigte Texel zeichnen. Dies ist eine schnelle Berechnung, aber das Ergebnis ist nicht so realistisch.

Durch die Einführung von Informationen zur Tiefe teilen wir der Engine mit, dass unser Dreieck keine flache Form auf dem Bildschirm hat. Dies wird erreicht, indem die Interpolationsformel wie folgt bearbeitet wird:

Geben Sie hier die Bildbeschreibung ein

Dabei ist z i die Tiefe des Scheitelpunkts i bei der Interpolation. Auf diese Weise befindet sich das berechnete Texel beim Bewegen entlang der Textur in einer anderen Position als Lerp , was die reale perspektivische Projektion mit viel größerer Genauigkeit widerspiegelt. Normalerweise ist diese Berechnung schwieriger durchzuführen als Lerping , und aufgrund dieser Komplexität ist eine dedizierte GPU zur Berechnung der Texturkoordinaten vorzuziehen (und zu optimieren). Wenn Sie die CPU die ganze Arbeit erledigen lassen, erzwingen Sie die Vertex-Verarbeitung und dies ist nicht gut für Ihre Spieleleistung.

Hier ist ein Vergleich zwischen der ursprünglichen Textur (Schachbrett), ihrer affinen Texturabbildung und der perspektivisch korrekten Texturabbildung.

Geben Sie hier die Bildbeschreibung ein

Das Zeichnen von der Textur zum Bildschirm wird durch Bildschirmunterteilungstechniken erreicht. Dies sind Methoden, um den Bereich auf dem Bildschirm, der zum Zeichnen von Texturen vorgesehen ist, in Teile zu unterteilen, bevor das Texturzeichnen durchgeführt wird.

liggiorgio
quelle