Wie bewege ich die Kamera in vollen Pixelintervallen?

13

Grundsätzlich möchte ich verhindern, dass sich die Kamera in Subpixeln bewegt, da dies meiner Meinung nach dazu führt, dass Sprites ihre Abmessungen sichtbar ändern, wenn auch nur geringfügig. (Gibt es einen besseren Begriff dafür?) Beachten Sie, dass dies ein Pixel-Art-Spiel ist, bei dem ich gestochen scharfe Pixelgrafiken haben möchte. Hier ist ein GIF, das das Problem zeigt:

Bildbeschreibung hier eingeben

Ich habe Folgendes versucht: Bewege die Kamera, projiziere die aktuelle Position (also die Bildschirmkoordinaten) und runde sie dann oder wirke sie auf int. Danach wandle es wieder in Weltkoordinaten um und verwende diese als neue Kameraposition. Soweit ich weiß, sollte dies die Kamera an die tatsächlichen Bildschirmkoordinaten und nicht an Bruchteile davon binden.

Aus irgendeinem Grund yexplodiert jedoch der Wert der neuen Position. In wenigen Sekunden steigt es auf so etwas wie 334756315000.

Hier ist eine SSCCE (oder eine MCVE), die auf dem Code im LibGDX-Wiki basiert :

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.badlogic.gdx.utils.viewport.Viewport;


public class PixelMoveCameraTest implements ApplicationListener {

    static final int WORLD_WIDTH = 100;
    static final int WORLD_HEIGHT = 100;

    private OrthographicCamera cam;
    private SpriteBatch batch;

    private Sprite mapSprite;
    private float rotationSpeed;

    private Viewport viewport;
    private Sprite playerSprite;
    private Vector3 newCamPosition;

    @Override
    public void create() {
        rotationSpeed = 0.5f;

        playerSprite = new Sprite(new Texture("/path/to/dungeon_guy.png"));
        playerSprite.setSize(1f, 1f);

        mapSprite = new Sprite(new Texture("/path/to/sc_map.jpg"));
        mapSprite.setPosition(0, 0);
        mapSprite.setSize(WORLD_WIDTH, WORLD_HEIGHT);

        float w = Gdx.graphics.getWidth();
        float h = Gdx.graphics.getHeight();

        // Constructs a new OrthographicCamera, using the given viewport width and height
        // Height is multiplied by aspect ratio.
        cam = new OrthographicCamera();

        cam.position.set(0, 0, 0);
        cam.update();
        newCamPosition = cam.position.cpy();

        viewport = new ExtendViewport(32, 20, cam);
        batch = new SpriteBatch();
    }


    @Override
    public void render() {
        handleInput();
        cam.update();
        batch.setProjectionMatrix(cam.combined);

        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        batch.begin();
        mapSprite.draw(batch);
        playerSprite.draw(batch);
        batch.end();
    }

    private static float MOVEMENT_SPEED = 0.2f;

    private void handleInput() {
        if (Gdx.input.isKeyPressed(Input.Keys.A)) {
            cam.zoom += 0.02;
        }
        if (Gdx.input.isKeyPressed(Input.Keys.Q)) {
            cam.zoom -= 0.02;
        }
        if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) {
            newCamPosition.add(-MOVEMENT_SPEED, 0, 0);
        }
        if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) {
            newCamPosition.add(MOVEMENT_SPEED, 0, 0);
        }
        if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) {
            newCamPosition.add(0, -MOVEMENT_SPEED, 0);
        }
        if (Gdx.input.isKeyPressed(Input.Keys.UP)) {
            newCamPosition.add(0, MOVEMENT_SPEED, 0);
        }
        if (Gdx.input.isKeyPressed(Input.Keys.W)) {
            cam.rotate(-rotationSpeed, 0, 0, 1);
        }
        if (Gdx.input.isKeyPressed(Input.Keys.E)) {
            cam.rotate(rotationSpeed, 0, 0, 1);
        }

        cam.zoom = MathUtils.clamp(cam.zoom, 0.1f, 100 / cam.viewportWidth);

        float effectiveViewportWidth = cam.viewportWidth * cam.zoom;
        float effectiveViewportHeight = cam.viewportHeight * cam.zoom;

        cam.position.lerp(newCamPosition, 0.02f);
        cam.position.x = MathUtils.clamp(cam.position.x,
                effectiveViewportWidth / 2f, 100 - effectiveViewportWidth / 2f);
        cam.position.y = MathUtils.clamp(cam.position.y,
                effectiveViewportHeight / 2f, 100 - effectiveViewportHeight / 2f);


        // if this is false, the "bug" (y increasing a lot) doesn't appear
        if (true) {
            Vector3 v = viewport.project(cam.position.cpy());
            System.out.println(v);
            v = viewport.unproject(new Vector3((int) v.x, (int) v.y, v.z));
            cam.position.set(v);
        }
        playerSprite.setPosition(newCamPosition.x, newCamPosition.y);
    }

    @Override
    public void resize(int width, int height) {
        viewport.update(width, height);
    }

    @Override
    public void resume() {
    }

    @Override
    public void dispose() {
        mapSprite.getTexture().dispose();
        batch.dispose();
    }

    @Override
    public void pause() {
    }

    public static void main(String[] args) {
        new Lwjgl3Application(new PixelMoveCameraTest(), new Lwjgl3ApplicationConfiguration());
    }
}

und hier ist dassc_map.jpg und dasdungeon_guy.png

Ich würde auch gerne mehr über einfachere und / oder bessere Möglichkeiten zur Behebung dieses Problems erfahren.

Joschua
quelle
"Gibt es dafür einen besseren Begriff?" Aliasing? In diesem Fall könnte Multisample-Anti-Aliasing (MSAA) eine alternative Lösung sein?
Yousef Amar
@Paraknight Entschuldigung, ich habe vergessen zu erwähnen, dass ich an einem pixeligen Spiel arbeite, bei dem ich gestochen scharfe Grafiken benötige, damit Anti-Aliasing nicht funktioniert (AFAIK). Diese Information habe ich der Frage hinzugefügt. Trotzdem danke.
Joschua
Gibt es einen Grund, mit einer höheren Auflösung zu arbeiten, andernfalls mit der Auflösung von 1 Pixel zu arbeiten und das Endergebnis zu skalieren?
Felsir
@Felsir Ja, ich bewege mich in Bildschirmpixeln, damit sich das Spiel so flüssig wie möglich anfühlt. Das Bewegen eines ganzen Texturpixels sieht sehr nervös aus.
Joschua

Antworten:

21

Ihr Problem besteht darin, die Kamera nicht in Vollpixelschritten zu bewegen. Es ist so, dass Ihr Texel-zu-Pixel-Verhältnis nicht ganzzahlig ist . Ich werde einige Beispiele aus dieser ähnlichen Frage ausleihen, die ich auf StackExchange beantwortet habe .

Hier sind zwei Kopien von Mario - beide bewegen sich mit der gleichen Geschwindigkeit über den Bildschirm (entweder durch die Sprites, die sich nach rechts in der Welt bewegen, oder durch die Kamera, die sich nach links bewegt - es endet gleichwertig), aber nur die obere zeigt diese plätschernden Artefakte und unten tut man nicht:

Zwei Mario-Sprites

Der Grund dafür ist, dass ich den oberen Mario leicht um den Faktor 1,01 skaliert habe - dieser winzige Bruchteil bedeutet, dass er nicht mehr mit dem Pixelraster des Bildschirms übereinstimmt.

Eine kleine Übersetzung ist kein Problem - die "Nearest" -Texturfilterung wird ohnehin auf das nächstgelegene Texel ausgerichtet, ohne dass wir etwas Besonderes tun.

Aber eine Skaleninkongruenz bedeutet, dass dieses Einrasten nicht immer in die gleiche Richtung geht. An einer Stelle wählt ein Pixel, das auf das nächste Texel schaut, ein Pixel leicht nach rechts aus, während ein anderes Pixel ein Pixel leicht nach links auswählt - und jetzt wurde eine Texelspalte entweder weggelassen oder dazwischen dupliziert, wodurch eine Kräuselung oder ein Schimmer erzeugt wird Bewegt sich über das Sprite, während es sich über den Bildschirm bewegt.

Hier ist ein genauerer Blick. Ich habe ein durchscheinendes Pilz-Sprite animiert, das sich reibungslos bewegt, als könnten wir es mit unbegrenzter Subpixel-Auflösung rendern. Dann habe ich ein Pixelraster mit dem nächsten Nachbarn überlagert. Das gesamte Pixel ändert die Farbe, um dem Teil des Sprites unter dem Abtastpunkt (dem Punkt in der Mitte) zu entsprechen.

1: 1-Skalierung

Ein richtig skaliertes Sprite, das sich ohne Welligkeit bewegt

Auch wenn sich das Sprite reibungslos in Subpixel-Koordinaten bewegt, wird das gerenderte Bild bei jeder Bewegung um ein ganzes Pixel korrekt gefangen. Wir müssen nichts Besonderes tun, damit dies funktioniert.

1: 1.0625 Skalierung

16x16-Sprite, gerendert 17x17 auf dem Bildschirm, zeigt Artefakte

In diesem Beispiel wurde das 16 x 16-Texel-Pilz-Sprite auf 17 x 17 Bildschirmpixel skaliert, sodass das Einrasten von einem Teil des Bildes zum anderen unterschiedlich verläuft und die Welligkeit oder Welle erzeugt, die es bei seiner Bewegung dehnt und zusammenpresst.

Der Trick besteht also darin, die Größe / das Sichtfeld Ihrer Kamera so anzupassen, dass die Quelltexte Ihrer Assets einer ganzzahligen Anzahl von Bildschirmpixeln zugeordnet werden. Es muss nicht 1: 1 sein - jede ganze Zahl funktioniert hervorragend:

1: 3-Skalierung

Vergrößern Sie die dreifache Skala

Je nach Auflösung des Ziels müssen Sie dies etwas anders machen. Wenn Sie es einfach an das Fenster anpassen, erhalten Sie fast immer eine gebrochene Skalierung. Für bestimmte Auflösungen ist möglicherweise etwas Abstand an den Bildschirmrändern erforderlich.

DMGregory
quelle
1
Zunächst einmal vielen Dank! Dies ist eine großartige Visualisierung. Habe allein dafür eine Gegenstimme. Leider kann ich im bereitgestellten Code keine Nicht-Ganzzahl finden. Die Größe der mapSpriteist 100x100, Die playerSpriteist auf eine Größe von 1x1 eingestellt und das Ansichtsfenster ist auf eine Größe von 32x20 eingestellt. Selbst wenn alles auf ein Vielfaches von 16 geändert wird, scheint das Problem nicht gelöst zu sein. Irgendwelche Ideen?
Joschua
2
Es ist durchaus möglich, überall Ganzzahlen zu haben und trotzdem ein nicht ganzzahliges Verhältnis zu erhalten. Nehmen Sie das Beispiel des 16-Texel-Sprites, das auf 17 Bildschirmpixeln angezeigt wird. Beide Werte sind Ganzzahlen, ihr Verhältnis jedoch nicht. Ich weiß nicht viel über libgdx, aber in Unity würde ich mir ansehen, wie viele Texel ich pro Weltmaßeinheit anzeige (z. B. Auflösung des Sprites geteilt durch die Weltgröße des Sprites), wie viele Weltmaßeinheiten ich in meinem anzeige Fenster (unter Verwendung der Größe der Kamera) und wie viele Bildschirmpixel sich auf meinem Fenster befinden. Durch Verketten dieser drei Werte können wir das Verhältnis von Texel zu Pixel für eine bestimmte Anzeigefenstergröße berechnen.
DMGregory
msgstr "und das Ansichtsfenster ist auf eine Größe von 32x20 eingestellt" - es sei denn, die "Clientgröße" (darstellbarer Bereich) Ihres Fensters ist ein ganzzahliges Vielfaches davon, was vermutlich nicht der Fall ist. 1 Einheit in Ihrem Ansichtsfenster ist eine nicht ganzzahlige Einheit Anzahl der Pixel.
MaulingMonkey
Vielen Dank. Ich habe es noch einmal versucht. window sizeist 1000x1000 (Pixel), WORLD_WIDTHx WORLD_HEIGHTist 100x100, FillViewportist 10x10, playerSpriteist 1x1. Leider besteht das Problem immer noch.
Joschua
Diese Details werden ein bisschen viel sein, um zu versuchen, sie in einem Kommentarthread zu sortieren. Möchten Sie einen Chat- Raum für die Spieleentwicklung starten oder eine direkte Nachricht an mich auf Twitter @D_M_Gregory senden?
DMGregory
1

hier eine kleine Checkliste:

  1. Trennen Sie "aktuelle Kameraposition" mit "Kameraposition ausrichten". (wie ndenarodev erwähnt) Wenn zum Beispiel die "aktuelle Kameraposition" 1.1 ist - machen Sie die "Zielkameraposition" 1.0

Und ändern Sie die aktuelle Kameraposition nur, wenn sich die Kamera bewegen soll.
Wenn die Zielkameraposition etwas anderes als transform.position ist, stellen Sie transform.position (Ihrer Kamera) auf "Zielkameraposition".

aim_camera_position = Mathf.Round(current_camera_position)

if (transform.position != aim_camera_position)
{
  transform.position = aim_camera_position;
}

Dies sollte Ihr Problem lösen. Wenn nicht: sag es mir. Sie sollten jedoch in Betracht ziehen, auch diese Dinge zu tun:

  1. setze "camera projection" auf "ortographic" (für dein abnormal ansteigendes y-problem) (ab jetzt solltest du nur noch mit dem "field of view" zoomen)

  2. Kameradrehung in 90 ° - Schritten einstellen (0 ° oder 90 ° oder 180 °)

  3. Generieren Sie keine Mipmaps, wenn Sie nicht wissen, ob Sie sollten.

  4. Verwenden Sie genügend Größe für Ihre Sprites und verwenden Sie keine bilinearen oder trilinearen Filter

  5. 5.

Wenn 1 Einheit keine Pixeleinheit ist, hängt sie möglicherweise von der Bildschirmauflösung ab. Haben Sie Zugang zum Sichtfeld? Abhängig von Ihrem Sichtfeld: Finden Sie heraus, wie hoch die Auflösung 1 Einheit ist.

float tan2Fov = tan(radians( Field of view / 2)); 
float xScrWidth = _CamNear*tan2Fov*_CamAspect * 2; 
float xScrHeight = _CamNear*tan2Fov * 2; 
float onePixel(width) = xWidth / screenResolutionX 
float onePixel(height) = yHeight / screenResolutionY 

^ Wenn onePixel (width) und onePixel (height) nicht 1.0000f sind, bewegen Sie diese Einheiten mit Ihrer Kamera nach links und rechts.

OC_RaizW
quelle
Hallo! Danke für deine Antwort. 1) Ich speichere die aktuelle und die gerundete Kameraposition bereits separat. 2) Ich benutze LibGDXs, OrthographicCameradamit es hoffentlich so funktioniert. (Du benutzt Unity, richtig?) 3-5) Ich mache die bereits in meinem Spiel.
Joschua
Okay - dann müssen Sie herausfinden, wie viel Einheiten 1 Pixel sind. Dies kann abhängig von der Bildschirmauflösung sein. Ich habe "5." in meinem Antwortpost. Versuche dies.
OC_RaizW
0

Ich bin nicht mit libgdx vertraut, habe aber an anderer Stelle ähnliche Probleme. Ich denke, das Problem liegt in der Tatsache, dass Sie Lerp und möglicherweise Fehler in Gleitkommawerten verwenden. Ich denke, eine mögliche Lösung für Ihr Problem besteht darin, ein eigenes Kameraobjekt zu erstellen. Verwenden Sie dieses Objekt für Ihren Bewegungscode und aktualisieren Sie es entsprechend. Stellen Sie dann die Position der Kamera auf den nächsten gewünschten Wert (Ganzzahl?) Ihres Standortobjekts ein.

ndenarodev
quelle
Zunächst einmal vielen Dank für Ihren Einblick. Es kann wirklich etwas mit Gleitkommafehlern zu tun haben. Das Problem (der yabnormalen Zunahme) war jedoch auch vorhanden, bevor ich es verwendete lerp. Ich habe es gestern hinzugefügt, damit die Leute auch das andere Problem sehen können, bei dem die Größe der Sprites nicht konstant bleibt, wenn sich die Kamera nähert.
Joschua