¿Cómo muevo la cámara a intervalos de píxeles completos?

13

Básicamente, quiero evitar que la cámara se mueva en subpíxeles, ya que creo que esto hace que los sprites cambien visiblemente sus dimensiones, aunque sea ligeramente. (¿Existe un término mejor para eso?) Tenga en cuenta que este es un juego de pixel art donde quiero tener gráficos pixelados nítidos. Aquí hay un gif que muestra el problema:

ingrese la descripción de la imagen aquí

Ahora lo que intenté fue esto: mover la cámara, proyectar la posición actual (por lo que son las coordenadas de la pantalla) y luego redondear o lanzar a int. Después de eso, conviértalo de nuevo a coordenadas mundiales y úselo como la nueva posición de la cámara. Hasta donde yo sé, esto debería bloquear la cámara a las coordenadas reales de la pantalla, no a fracciones de la misma.

Sin embargo, por alguna razón, el yvalor de la nueva posición simplemente explota. En cuestión de segundos aumenta a algo así 334756315000.

Aquí hay un SSCCE (o un MCVE) basado en el código en el wiki de 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());
    }
}

y aquí está elsc_map.jpg y eldungeon_guy.png

También me interesaría conocer formas más simples y / o mejores de solucionar este problema.

Joschua
fuente
"¿Hay un término mejor para eso?" Aliasing? ¿En ese caso, el antisolapamiento de múltiples muestras (MSAA) podría ser una solución alternativa?
Yousef Amar
@Paraknight Lo siento, olvidé mencionar que estoy trabajando en un juego pixelado, donde necesito gráficos nítidos para que el suavizado no funcione (AFAIK). Agregué esa información a la pregunta. Gracias sin embargo.
Joschua el
¿Hay alguna razón para trabajar con una resolución más alta, de lo contrario trabajar con una resolución de 1 píxel y mejorar el resultado final?
Felsir
@Felsir Sí, me muevo en píxeles de la pantalla, para que el juego se sienta lo más fluido posible. Mover un píxel de textura completa se ve muy nervioso.
Joschua

Respuestas:

22

Su problema no es mover la cámara en incrementos de píxeles completos. Es que su relación texel-to-pixel es ligeramente no entera . Tomaré prestados algunos ejemplos de esta pregunta similar que respondí en StackExchange .

Aquí hay dos copias de Mario: ambas se mueven por la pantalla a la misma velocidad (ya sea por los sprites que se mueven hacia la derecha en el mundo o la cámara que se mueve hacia la izquierda, termina siendo equivalente), pero solo la parte superior muestra estos artefactos ondulantes y el el inferior no:

Dos sprites de Mario

La razón es que escalé ligeramente al Mario superior en 1 factor de 1.01x, esa fracción minúscula adicional significa que ya no se alinea con la cuadrícula de píxeles de la pantalla.

Una pequeña desalineación traslacional no es un problema: el filtrado de textura "más cercano" se ajustará al texel más cercano de todos modos sin que hagamos nada especial.

Pero un desajuste de escala significa que este ajuste no siempre está en la misma dirección. En un punto, un píxel que mira hacia arriba el texel más cercano elegirá uno ligeramente a la derecha, mientras que otro píxel elige uno ligeramente a la izquierda, y ahora se ha omitido o duplicado una columna de texels en el medio, creando una ondulación o brillo que se mueve sobre el sprite mientras viaja por la pantalla.

Aquí hay una mirada más cercana. He animado un sprite de hongo translúcido que se mueve suavemente, como si pudiéramos renderizarlo con una resolución ilimitada de subpíxeles. Luego superpuse una cuadrícula de píxeles usando el muestreo del vecino más cercano. Todo el píxel cambia de color para que coincida con la parte del sprite debajo del punto de muestreo (el punto en el centro).

Escala 1: 1

Un sprite adecuadamente escalado que se mueve sin ondulación

Aunque el sprite se mueve suavemente a las coordenadas de subpíxeles, su imagen renderizada se ajusta correctamente cada vez que viaja un píxel completo. No necesitamos hacer nada especial para que esto funcione.

1: Escala 1.0625

Sprite 16x16 renderizado 17x17 en pantalla, mostrando artefactos

En este ejemplo, el sprite de hongo texel de 16x16 se ha ampliado a píxeles de pantalla de 17x17, por lo que el ajuste ocurre de manera diferente de una parte de la imagen a otra, creando la ondulación u onda que se estira y aplasta a medida que se mueve.

Por lo tanto, el truco es ajustar el tamaño / campo de visión de su cámara para que los texels de origen de sus activos se asignen a un número entero de píxeles de pantalla. No tiene que ser 1: 1; cualquier número entero funciona muy bien:

Escala 1: 3

Acercar en escala triple

Tendrá que hacer esto de manera un poco diferente dependiendo de la resolución de su objetivo: simplemente escalar para ajustarse a la ventana casi siempre resultará en una escala fraccional. Es posible que se necesite cierta cantidad de relleno en los bordes de la pantalla para ciertas resoluciones.

DMGregory
fuente
1
Antes que nada, ¡muchas gracias! Esta es una gran visualización. Ten un voto positivo solo por eso. Lamentablemente, no puedo encontrar nada que no sea entero en el código proporcionado. El tamaño de la mapSpritees de 100x100, playerSpritese establece en un tamaño de 1x1 y la ventana gráfica se establece en un tamaño de 32x20. Incluso cambiar todo a múltiplos de 16 no parece resolver el problema. ¿Algunas ideas?
Joschua
2
Es completamente posible tener números enteros en todas partes y aún así obtener una relación no entera. Tome el ejemplo del sprite de 16 texel que se muestra en 17 píxeles de pantalla. Ambos valores son enteros, pero su relación no lo es. No sé mucho sobre libgdx, pero en Unity vería cuántos texels estoy mostrando por unidad mundial (por ejemplo, resolución de sprite dividida por el tamaño mundial de sprite), cuántas unidades mundiales estoy mostrando en mi ventana (usando el tamaño de la cámara) y cuántos píxeles de pantalla hay en mi ventana. Encadenando esos 3 valores, podemos calcular la relación de texel a pixel para un tamaño de ventana de visualización dado.
DMGregory
"y la ventana gráfica está configurada en un tamaño de 32x20", a menos que el "tamaño del cliente" (área representable) de su ventana sea un múltiplo entero de esto, lo que supongo que no es así, 1 unidad en su ventana gráfica no será entera Número de píxeles.
MaulingMonkey
Gracias. Lo he intentado de nuevo. window sizees 1000x1000 (píxeles), WORLD_WIDTHx WORLD_HEIGHTes 100x100, FillViewportes 10x10, playerSpritees 1x1. Lamentablemente, el problema aún persiste.
Joschua
Estos detalles serán un poco demasiado difíciles de tratar en un hilo de comentarios. ¿Desea iniciar una sala de chat de desarrollo de juegos o enviarme un mensaje directo en Twitter @D_M_Gregory?
DMGregory
1

Aquí una pequeña lista de verificación:

  1. Separe la "posición actual de la cámara" con "apuntar la posición de la cámara". (como mencionó ndenarodev) Si, por ejemplo, la "posición actual de la cámara" es 1.1, haga que la "posición de la cámara objetivo" sea 1.0

Y solo cambie la posición actual de la cámara si la cámara debe moverse.
Si la posición de la cámara de puntería es algo más que transform.position: configure transform.position (de su cámara) en "posición de la cámara de puntería".

aim_camera_position = Mathf.Round(current_camera_position)

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

Esto debería solucionar tu problema. Si no: dime. Sin embargo, deberías considerar hacer esas cosas también:

  1. establezca "proyección de cámara" en "ortográfico" (para su problema y anormalmente creciente) (de ahora en adelante, solo debe hacer zoom con el "Campo de visión")

  2. ajuste la rotación de la cámara en 90 ° - pasos (0 ° o 90 ° o 180 °)

  3. no genere mapas MIP si no sabe si debería hacerlo.

  4. use suficiente tamaño para sus sprites y no use filtros bilineales o trilineales

  5. 5)

Si 1 unidad no es una unidad de píxeles, tal vez dependa de su resolución de pantalla. ¿Tienes acceso al campo de visión? Dependiendo de su campo de visión: descubra cuánta resolución es 1 unidad.

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 

^ Y ahora, si onePixel (ancho) y onePixel (alto) no son 1.0000f, simplemente mueva esas unidades a izquierda y derecha con su cámara.

OC_RaizW
fuente
¡Oye! Gracias por tu respuesta. 1) Ya almaceno la posición real de la cámara y la redondeada por separado. 2) Utilizo LibGDX OrthographicCamerapara que, con suerte, funcione así. (Estás usando Unity, ¿verdad?) 3-5) Ya hago eso en mi juego.
Joschua
Bien, entonces necesitas saber cuántas unidades es 1 píxel. podría depender de la resolución de la pantalla. Edité "5". en mi post de respuesta. prueba esto.
OC_RaizW
0

No estoy familiarizado con libgdx, pero tengo problemas similares en otros lugares. Creo que el problema radica en el hecho de que está utilizando lerp y potencialmente error en los valores de coma flotante. Creo que una posible solución a su problema es crear su propio objeto de ubicación de cámara. Use este objeto para su código de movimiento y actualícelo en consecuencia y luego ajuste la posición de la cámara al valor deseado más cercano (¿entero?) De su objeto de ubicación.

ndenarodev
fuente
En primer lugar, gracias por su comprensión. Realmente puede tener algo que ver con errores de coma flotante. Sin embargo, el problema (de yaumento anormal) también estaba presente antes de usarlo lerp. De hecho, lo agregué ayer para que la gente también pudiera ver el otro problema, donde el tamaño de los sprites no se mantiene constante cuando se acerca la cámara.
Joschua