Come si sposta la fotocamera a intervalli di pixel completi?


13

Fondamentalmente voglio fermare il movimento della telecamera nei subpixel, poiché penso che questo porti a sprite che modificano visibilmente le loro dimensioni, se non addirittura in modo così lieve. (C'è un termine migliore per quello?) Nota che questo è un gioco di pixel art in cui voglio avere una nitida grafica pixelata. Ecco una gif che mostra il problema:

inserisci qui la descrizione dell'immagine

Ora quello che ho provato è stato questo: spostare la telecamera, proiettare la posizione corrente (quindi le coordinate dello schermo) e quindi arrotondare o trasmettere a int. Successivamente, riconvertitelo in coordinate mondiali e utilizzatelo come nuova posizione della telecamera. Per quanto ne so, questo dovrebbe bloccare la fotocamera su coordinate dello schermo effettive, non su loro frazioni.

Per qualche ragione, tuttavia, il yvalore della nuova posizione esplode. In pochi secondi aumenta a qualcosa di simile 334756315000.

Ecco un SSCCE (o un MCVE) basato sul codice nel wiki di LibGDX :

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());
    }
}

ed ecco ilsc_map.jpg e ildungeon_guy.png

Sarei anche interessato a conoscere modi più semplici e / o migliori per risolvere questo problema.


"C'è un termine migliore per quello?" Aliasing? In tal caso, l'antialiasing multisample (MSAA) potrebbe essere una soluzione alternativa?
Yousef Amar,

@Paraknight Siamo spiacenti, ho dimenticato di menzionare che sto lavorando a un gioco pixelato, in cui ho bisogno di una grafica nitida in modo che l'antialiasing non funzioni (AFAIK). Ho aggiunto queste informazioni alla domanda. Grazie comunque.
Joschua,

C'è un motivo per lavorare a una risoluzione più elevata, altrimenti lavorare con una risoluzione di 1 pixel e migliorare il risultato finale?
Felsir,

@Felsir Sì, mi muovo nei pixel dello schermo, in modo che il gioco sia il più fluido possibile. Lo spostamento di un intero pixel di trama sembra molto nervoso.
Joschua,

Risposte:


22

Il tuo problema non sta spostando la fotocamera con incrementi di pixel completi. È che il rapporto texel-pixel è leggermente non intero . Prenderò in prestito alcuni esempi da questa domanda simile a cui ho risposto su StackExchange .

Ecco due copie di Mario - entrambi si muovono sullo schermo alla stessa velocità (o gli sprite si muovono a destra nel mondo, o la telecamera si sposta a sinistra - finisce equivalente), ma solo quello in alto mostra questi artefatti increspati e il quello in basso non:

Due sprite di Mario

Il motivo è che ho leggermente ridimensionato il primo Mario di 1 fattore di 1,01x - quella frazione extra piccola significa che non si allinea più con la griglia di pixel dello schermo.

Un piccolo disallineamento traslazionale non è un problema: il filtro di trama "più vicino" si aggancerà comunque al texel più vicino senza che noi facciamo nulla di speciale.

Ma una discrepanza di scala significa che questo scatto non è sempre nella stessa direzione. In un punto un pixel che guarda il texel più vicino ne prenderà uno leggermente a destra, mentre un altro pixel ne prenderà uno leggermente a sinistra - e ora una colonna di texel è stata omessa o duplicata nel mezzo, creando un'increspatura o un luccichio che si sposta sullo sprite mentre attraversa lo schermo.

Ecco uno sguardo più da vicino. Ho animato uno sprite di funghi traslucido che si muoveva senza intoppi, come se potessimo renderlo con una risoluzione sub-pixel illimitata. Quindi ho sovrapposto una griglia di pixel usando il campionamento più vicino. L'intero pixel cambia colore in modo che corrisponda alla parte dello sprite sotto il punto di campionamento (il punto al centro).

Ridimensionamento 1: 1

Uno sprite correttamente ridimensionato che si muove senza increspature

Anche se lo sprite si sposta agevolmente su coordinate sub-pixel, la sua immagine renderizzata scatta ancora correttamente ogni volta che percorre un pixel intero. Non abbiamo bisogno di fare nulla di speciale per far funzionare questo.

1: 1.0625 Ridimensionamento

16x16 sprite renderizzati 17x17 sullo schermo, mostrando artefatti

In questo esempio, lo sprite di funghi texel 16x16 è stato ridimensionato fino a pixel dello schermo 17x17, quindi lo snap avviene in modo diverso da una parte dell'immagine all'altra, creando l'ondulazione o l'onda che si allunga e la schiaccia mentre si muove.

Quindi, il trucco è regolare le dimensioni della videocamera / il campo visivo in modo che i texel di origine delle risorse siano mappati su un numero intero di pixel dello schermo. Non deve essere 1: 1 - qualsiasi numero intero funziona alla grande:

Ridimensionamento 1: 3

Ingrandisci su tripla scala

Dovrai farlo in modo leggermente diverso a seconda della risoluzione del tuo target: semplicemente il ridimensionamento per adattarsi alla finestra si tradurrà quasi sempre in una scala frazionaria. Una certa quantità di imbottitura ai bordi dello schermo potrebbe essere necessaria per alcune risoluzioni.


1
Prima di tutto, grazie mille! Questa è un'ottima visualizzazione. Avere un voto solo per quello. Purtroppo però, non riesco a trovare nulla di non intero nel codice fornito. La dimensione di mapSpriteè 100x100, La dimensione playerSpriteè impostata su 1x1 e la finestra è impostata su una dimensione di 32x20. Anche cambiare tutto in multipli di 16 non sembra risolvere il problema. Qualche idea?
Joschua,

2
È del tutto possibile avere numeri interi ovunque e ottenere comunque un rapporto non intero. Prendi l'esempio del 16 texel sprite visualizzato su 17 pixel dello schermo. Entrambi i valori sono numeri interi, ma il loro rapporto non lo è. Non so molto su libgdx, ma in Unity darei un'occhiata a quanti texel sto visualizzando per unità mondiale (es. Risoluzione di sprite divisa per dimensione mondiale di sprite), quante unità mondiali sto visualizzando nel mio finestra (utilizzando le dimensioni della fotocamera) e quanti pixel dello schermo ci sono sulla mia finestra. Concatenando questi 3 valori, possiamo calcolare il rapporto texel-pixel per una data dimensione della finestra di visualizzazione.
DMGregory

"e la finestra è impostata su una dimensione di 32x20" - a meno che la "dimensione client" della tua finestra (area renderizzabile) sia un multiplo intero di questo, che immagino non lo sia, 1 unità nella tua finestra non sarà un numero intero numero di pixel.
MaulingMonkey il

Grazie. Ci ho riprovato window sizeè 1000x1000 (pixel), WORLD_WIDTHx WORLD_HEIGHTè 100x100, FillViewportè 10x10, playerSpriteè 1x1. Purtroppo, il problema persiste ancora.
Joschua,

Questi dettagli saranno un po 'troppo per cercare di sistemarli in un thread di commenti. Desideri avviare una chat room per lo sviluppo di giochi o inviarmi un messaggio diretto su Twitter @D_M_Gregory?
DMGregory

1

qui una piccola lista di controllo:

  1. "Posizione attuale della telecamera" separata con "posizione della telecamera di mira". (come menzionato da ndenarodev) Se per esempio la "posizione attuale della telecamera" è 1.1 - crea la "posizione della telecamera di mira" 1.0

E modificare la posizione corrente della telecamera solo se la telecamera deve spostarsi.
Se la posizione della telecamera di mira è diversa da transform.position - imposta transform.position (della tua fotocamera) su "posizione della telecamera di mira".

aim_camera_position = Mathf.Round(current_camera_position)

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

Questo dovrebbe risolvere il tuo problema. In caso contrario: dimmelo. Tuttavia dovresti considerare di fare anche queste cose:

  1. imposta "proiezione videocamera" su "ortografico" (per il tuo problema y in modo anormale crescente) (d'ora in poi, dovresti ingrandire solo con "Campo visivo")

  2. imposta la rotazione della telecamera a 90 ° - passi (0 ° o 90 ° o 180 °)

  3. non generare mipmap se non sai se dovresti.

  4. usa dimensioni sufficienti per i tuoi sprite e non usare filtri bilineari o trilineari

  5. 5.

Se 1 unità non è un'unità pixel, forse dipende dalla risoluzione dello schermo. Hai accesso al campo visivo? A seconda del campo visivo: scopri quanta risoluzione è 1 unità.

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 

^ E ora se onePixel (larghezza) e onePixel (altezza) non sono 1.0000f, spostati in quelle unità a sinistra ea destra con la fotocamera.


Hey! Grazie per la tua risposta. 1) Memorizzo già la posizione attuale della telecamera e quella arrotondata separatamente. 2) Uso LibGDX in OrthographicCameramodo che, si spera, funzioni così. (Stai usando Unity, giusto?) 3-5) Faccio già quelli nel mio gioco.
Joschua,

Bene, allora devi scoprire quante unità sono 1 pixel. potrebbe dipendere dalla risoluzione dello schermo. Ho modificato "5." nel mio post di risposta. prova questo.
OC_RaizW,

0

Non ho familiarità con libgdx ma ho riscontrato problemi simili altrove. Penso che il problema risieda nel fatto che stai usando lerp e potenzialmente un errore nei valori in virgola mobile. Penso che una possibile soluzione al tuo problema sia quella di creare il tuo oggetto di posizione della videocamera. Utilizzare questo oggetto per il codice di movimento e aggiornarlo di conseguenza, quindi impostare la posizione della telecamera sul valore desiderato (intero?) Più vicino all'oggetto posizione.


Prima di tutto, grazie per la tua comprensione. Potrebbe davvero avere a che fare con errori in virgola mobile. Tuttavia, il problema ( ydell'aumento anomalo) era presente anche prima dell'uso lerp. L'ho effettivamente aggiunto ieri in modo che le persone potessero anche vedere l'altro problema, in cui le dimensioni degli sprite non rimangono costanti quando la telecamera si avvicina.
Joschua,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.