Comment déplacer la caméra à des intervalles de pixels complets?

13

Fondamentalement, je veux empêcher la caméra de se déplacer en sous-pixels, car je pense que cela conduit à des sprites qui modifient visiblement leurs dimensions, même si c'est très légèrement. (Y a-t-il un meilleur terme pour cela?) Notez qu'il s'agit d'un jeu pixel-art où je veux avoir des graphismes pixelisés nets. Voici un gif qui montre le problème:

entrez la description de l'image ici

Maintenant, ce que j'ai essayé était le suivant: déplacez la caméra, projetez la position actuelle (il s'agit donc des coordonnées de l'écran), puis arrondissez ou transformez en int. Ensuite, convertissez-le en coordonnées mondiales et utilisez-le comme nouvelle position de la caméra. Autant que je sache, cela devrait verrouiller la caméra aux coordonnées réelles de l'écran, et non à des fractions de celles-ci.

Pour une raison quelconque, cependant, la yvaleur de la nouvelle position explose. En quelques secondes, il augmente à quelque chose comme 334756315000.

Voici un SSCCE (ou un MCVE) basé sur le code du wiki 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());
    }
}

et voici lesc_map.jpg et ledungeon_guy.png

Je serais également intéressé à en apprendre davantage sur des moyens plus simples et / ou meilleurs de résoudre ce problème.

Joschua
la source
"Y a-t-il un meilleur terme pour ça?" Aliasing? Dans ce cas, l'anti-aliasing multi-échantillon (MSAA) pourrait être une solution alternative?
Yousef Amar
@Paraknight Désolé, j'ai oublié de mentionner que je travaille sur un jeu pixelisé, où j'ai besoin de graphismes nets pour que l'anti-aliasing ne fonctionne pas (AFAIK). J'ai ajouté cette information à la question. Merci quand même.
Joschua
Y a-t-il une raison de travailler à une résolution plus élevée, sinon travailler à la résolution de 1 pixel et augmenter le résultat final?
Felsir
@Felsir Oui, je me déplace en pixels d'écran, pour que le jeu soit le plus fluide possible. Déplacer un pixel de texture entier semble très nerveux.
Joschua

Réponses:

22

Votre problème ne consiste pas à déplacer l'appareil photo par incréments de pixels. C'est que votre rapport texel / pixel est légèrement non entier . J'emprunterai quelques exemples de cette question similaire à laquelle j'ai répondu sur StackExchange .

Voici deux copies de Mario - les deux se déplacent sur l'écran au même rythme (soit par les sprites se déplaçant à droite dans le monde, soit par la caméra à gauche - cela finit par être équivalent), mais seule celle du haut montre ces artefacts ondulants et le celui du bas ne fait pas:

Deux sprites de Mario

La raison en est que j'ai légèrement mis à l'échelle le Mario supérieur par un facteur de 1,01x - cette fraction minuscule supplémentaire signifie qu'il ne s'aligne plus avec la grille de pixels de l'écran.

Un petit désalignement translationnel n'est pas un problème - le filtrage de texture "le plus proche" se fixera de toute façon sur le texel le plus proche sans que nous ne fassions rien de spécial.

Mais un décalage d' échelle signifie que cette capture n'est pas toujours dans la même direction. À un endroit, un pixel recherchant le texel le plus proche en choisira un légèrement à droite, tandis qu'un autre pixel en choisira un légèrement à gauche - et maintenant une colonne de texels a été omise ou dupliquée entre les deux, créant une ondulation ou un chatoiement qui se déplace sur l'image-objet lorsqu'elle se déplace sur l'écran.

Voici un examen plus approfondi. J'ai animé un sprite champignon translucide se déplaçant en douceur, comme si nous pouvions le rendre avec une résolution de sous-pixels illimitée. Ensuite, j'ai superposé une grille de pixels en utilisant l'échantillonnage du plus proche voisin. Le pixel entier change de couleur pour correspondre à la partie du sprite sous le point d'échantillonnage (le point au centre).

Mise à l'échelle 1: 1

Un sprite correctement dimensionné se déplaçant sans ondulation

Même si l'image-objet se déplace en douceur vers les coordonnées sous-pixel, son image rendue s'accroche toujours correctement à chaque fois qu'elle parcourt un pixel complet. Nous n'avons rien à faire de spécial pour que cela fonctionne.

1: 1.0625 Mise à l'échelle

Sprite 16x16 rendu 17x17 à l'écran, montrant des artefacts

Dans cet exemple, le sprite champignon 16x16 texel a été mis à l'échelle jusqu'à 17x17 pixels d'écran, et donc l'accrochage se produit différemment d'une partie de l'image à l'autre, créant l'ondulation ou la vague qui s'étire et l'écrase en se déplaçant.

Ainsi, l'astuce consiste à ajuster la taille / le champ de vision de votre caméra afin que les texels source de vos actifs correspondent à un nombre entier de pixels d'écran. Il n'est pas nécessaire que ce soit 1: 1 - tout nombre entier fonctionne très bien:

Mise à l'échelle 1: 3

Zoom avant sur une triple échelle

Vous devrez le faire un peu différemment en fonction de votre résolution cible - une simple mise à l'échelle pour s'adapter à la fenêtre entraînera presque toujours une échelle fractionnaire. Une certaine quantité de rembourrage sur les bords de l'écran peut être nécessaire pour certaines résolutions.

DMGregory
la source
1
Tout d'abord, merci beaucoup! Ceci est une excellente visualisation. Ayez un vote positif pour cela seul. Malheureusement, je ne trouve rien de non entier dans le code fourni. La taille du mapSpriteest de 100x100, le playerSpriteest défini sur une taille de 1x1 et la fenêtre d'affichage est définie sur une taille de 32x20. Même tout changer en multiples de 16 ne semble pas résoudre le problème. Des idées?
Joschua
2
Il est tout à fait possible d'avoir des entiers partout et d'obtenir toujours un rapport non entier. Prenons l'exemple du sprite 16 texels affiché sur 17 pixels d'écran. Les deux valeurs sont des entiers, mais leur ratio ne l'est pas. Je ne sais pas grand chose sur libgdx, mais dans Unity je regarderais combien de texels est-ce que j'affiche par unité mondiale (par exemple, la résolution du sprite divisée par la taille du monde du sprite), combien d'unités mondiales j'affiche dans mon fenêtre (en utilisant la taille de la caméra), et combien de pixels d'écran sont sur ma fenêtre. En enchaînant ces 3 valeurs, nous pouvons déterminer le rapport texel / pixel pour une taille de fenêtre d'affichage donnée.
DMGregory
"et la fenêtre d'affichage est définie sur une taille de 32 x 20" - à moins que la "taille du client" de votre fenêtre (zone de rendu) ne soit un multiple entier de cela, ce que je suppose que ce n'est pas le cas, 1 unité dans votre fenêtre sera une partie non entière nombre de pixels.
MaulingMonkey du
Merci. J'ai encore essayé. window sizeest 1000x1000 (pixels), WORLD_WIDTHx WORLD_HEIGHTest 100x100, FillViewportest 10x10, playerSpriteest 1x1. Malheureusement, le problème persiste.
Joschua
Ces détails vont être un peu trop difficiles à trier dans un fil de commentaires. Souhaitez-vous lancer une salle de discussion sur le développement de jeux ou m'envoyer un message sur Twitter @D_M_Gregory?
DMGregory
1

voici une petite liste de contrôle:

  1. Séparez la "position actuelle de la caméra" avec la "position de la caméra visée". (comme ndenarodev mentionné) Si, par exemple, la "position actuelle de la caméra" est 1.1 - faites la "position de la caméra visée" 1.0

Et ne modifiez la position actuelle de la caméra que si la caméra doit bouger.
Si la position de la caméra visée est autre chose que transform.position - définissez transform.position (de votre caméra) sur "viser la position de la caméra".

aim_camera_position = Mathf.Round(current_camera_position)

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

Cela devrait résoudre votre problème. Sinon: dites-moi. Cependant, vous devriez également envisager de faire ces choses:

  1. réglez «projection de la caméra» sur «ortographique» (pour votre problème y anormalement croissant) (à partir de maintenant, vous ne devez zoomer qu'avec le «champ de vision»)

  2. régler la rotation de la caméra sur 90 ° - pas (0 ° ou 90 ° ou 180 °)

  3. ne générez pas de mipmaps si vous ne savez pas si vous le devez.

  4. utilisez une taille suffisante pour vos sprites et n'utilisez pas de filtre bilinéaire ou trilinéaire

  5. 5.

Si 1 unité n'est pas une unité de pixels, sa résolution d'écran dépend peut-être. Avez-vous accès au champ de vision? En fonction de votre champ de vision: découvrez la résolution de 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 

^ Et maintenant, si onePixel (largeur) et onePixel (hauteur) ne sont pas 1.0000f, déplacez-vous simplement dans ces unités vers la gauche et la droite avec votre appareil photo.

OC_RaizW
la source
Hey! Merci pour votre réponse. 1) Je stocke déjà séparément la position réelle de la caméra et celle arrondie. 2) J'utilise LibGDX OrthographicCamerapour que j'espère que ça fonctionne comme ça. (Vous utilisez Unity, non?) 3-5) Je les utilise déjà dans mon jeu.
Joschua
D'accord, alors vous devez savoir combien d'unités 1 pixel est. cela peut dépendre de la résolution de l'écran. J'ai édité "5." dans mon message de réponse. essaye ça.
OC_RaizW
0

Je ne connais pas libgdx mais j'ai rencontré des problèmes similaires ailleurs. Je pense que le problème réside dans le fait que vous utilisez lerp et potentiellement une erreur dans les valeurs à virgule flottante. Je pense qu'une solution possible à votre problème est de créer votre propre objet de localisation de caméra. Utilisez cet objet pour votre code de mouvement et mettez-le à jour en conséquence, puis définissez la position de la caméra sur la valeur souhaitée (entière?) La plus proche de votre objet de localisation.

ndenarodev
la source
Tout d'abord, merci pour votre perspicacité. Cela peut vraiment avoir quelque chose à voir avec les erreurs en virgule flottante. Cependant, le problème (d' yaugmentation anormale) était également présent avant mon utilisation lerp. Je l'ai ajouté hier pour que les gens puissent également voir l'autre problème, où la taille des sprites ne reste pas constante lorsque la caméra approche.
Joschua