Como movo a câmera em intervalos de pixels completos?

13

Basicamente, quero impedir que a câmera se mova em subpixels, pois acho que isso leva os sprites a mudar visivelmente suas dimensões, mesmo que ligeiramente. (Existe um termo melhor para isso?) Observe que este é um jogo de pixel-art onde eu quero ter gráficos pixelados nítidos. Aqui está um gif que mostra o problema:

insira a descrição da imagem aqui

Agora, o que eu tentei foi o seguinte: Mova a câmera, projete a posição atual (portanto, as coordenadas da tela) e, em seguida, arredonde ou faça a conversão para int. Depois disso, converta-o de volta às coordenadas do mundo e use-o como a nova posição da câmera. Tanto quanto eu sei, isso deve bloquear a câmera nas coordenadas reais da tela, não em frações.

Por alguma razão, no entanto, o yvalor da nova posição simplesmente explode. Em questão de segundos, aumenta para algo como 334756315000.

Aqui está um SSCCE (ou um MCVE) baseado no código no wiki do 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());
    }
}

e aqui está osc_map.jpg e odungeon_guy.png

Eu também estaria interessado em aprender sobre maneiras mais simples e / ou melhores de corrigir esse problema.

Joschua
fonte
"Existe um termo melhor para isso?" Aliasing? Nesse caso, o multisample anti-aliasing (MSAA) pode ser uma solução alternativa?
Yousef Amar
@Paraknight Desculpe, esqueci de mencionar que estou trabalhando em um jogo pixelizado, onde preciso de gráficos nítidos para que o anti-aliasing não funcione (AFAIK). Eu adicionei essa informação à pergunta. Obrigado embora.
Joschua
Existe um motivo para trabalhar com uma resolução mais alta, caso contrário, trabalhe com uma resolução de 1 pixel e aprimore o resultado final?
Felsir 01/09/16
@Felsir Sim, movo os pixels da tela para que o jogo pareça o mais suave possível. Mover um pixel inteiro de textura parece muito instável.
Joschua 02/02

Respostas:

22

Seu problema não está movendo a câmera em incrementos de pixel completo. É que a sua relação texel / pixel é um pouco não inteira . Emprestarei alguns exemplos dessa pergunta semelhante que respondi no StackExchange .

Aqui estão duas cópias de Mario - ambas estão se movendo pela tela na mesma proporção (pelos sprites se movendo para a direita no mundo ou pela câmera se movendo para a esquerda - acaba equivalente), mas apenas a primeira mostra esses artefatos ondulados e os o inferior não:

Dois sprites de Mario

A razão é que eu dimensionei ligeiramente o Mario superior em 1 fator de 1,01x - essa fração minúscula significa que ele não está mais alinhado com a grade de pixels da tela.

Um pequeno desalinhamento da tradução não é um problema - a filtragem de textura "mais próxima" se encaixa no texel mais próximo de qualquer maneira, sem que façamos algo especial.

Mas uma incompatibilidade de escala significa que esse encaixe nem sempre é na mesma direção. Em um ponto, um pixel olhando para o texel mais próximo escolherá um ligeiramente para a direita, enquanto outro pixel escolherá um para a esquerda - e agora uma coluna de texels foi omitida ou duplicada no meio, criando uma ondulação ou brilho que se move sobre o sprite enquanto viaja pela tela.

Aqui está um olhar mais atento. Animai um sprite translúcido de cogumelo movendo-se suavemente, como se pudéssemos renderizá-lo com resolução ilimitada de sub-pixels. Sobreponha uma grade de pixels usando a amostragem do vizinho mais próximo. O pixel inteiro muda de cor para corresponder à parte do sprite sob o ponto de amostragem (o ponto no centro).

Escala 1: 1

Um sprite devidamente dimensionado movendo-se sem ondulações

Mesmo que o sprite se mova suavemente para as coordenadas de sub-pixel, sua imagem renderizada ainda se encaixa corretamente toda vez que ele percorre um pixel inteiro. Não precisamos fazer nada de especial para fazer isso funcionar.

1: 1.0625 Scaling

Sprite 16x16 renderizado 17x17 na tela, mostrando artefatos

Neste exemplo, o sprite de cogumelo texel de 16x16 foi dimensionado para 17x17 pixels da tela e, portanto, o encaixe acontece de maneira diferente de uma parte da imagem para outra, criando a ondulação ou onda que a estica e a comprime enquanto se move.

Portanto, o truque é ajustar o tamanho / campo de visão da câmera para que os textos de origem dos seus ativos sejam mapeados para um número inteiro de pixels da tela. Não precisa ser 1: 1 - qualquer número inteiro funciona muito bem:

Escala 1: 3

Ampliar em escala tripla

Você precisará fazer isso de maneira um pouco diferente, dependendo da resolução de seu objetivo - simplesmente aumentar o tamanho para caber na janela quase sempre resultará em uma escala fracionária. Alguma quantidade de preenchimento nas bordas da tela pode ser necessária para determinadas resoluções.

DMGregory
fonte
1
Primeiro de tudo, muito obrigado! Esta é uma ótima visualização. Tenha um voto positivo apenas por isso. Infelizmente, não consigo encontrar nada que não seja Inteiro no código fornecido. O tamanho de mapSpriteé 100x100, O playerSpriteé definido como um tamanho de 1x1 e a janela de exibição é definida como um tamanho de 32x20. Mesmo mudar tudo para múltiplos de 16 não parece resolver o problema. Alguma ideia?
Joschua 3/09/16
2
É perfeitamente possível ter números inteiros em todos os lugares e ainda assim obter uma proporção não inteira. Tomemos o exemplo dos 16 texel sprites exibidos em 17 pixels da tela. Ambos os valores são inteiros, mas sua proporção não é. Eu não sei muito sobre libgdx, mas no Unity eu veria quantos texels estou exibindo por unidade mundial (por exemplo, resolução de sprite dividida pelo tamanho mundial de sprite), quantas unidades mundiais estou exibindo no meu janela (usando o tamanho da câmera) e quantos pixels de tela existem na minha janela. Encadeando esses 3 valores, podemos calcular a proporção texel / pixel para um determinado tamanho da janela de exibição.
DMGregory
"e a janela de visualização estiver configurada para um tamanho de 32x20" - a menos que o "tamanho do cliente" (área renderizável) da sua janela seja um número inteiro múltiplo disso, o que eu acho que não, uma unidade na sua janela de visualização será um número inteiro número de pixels.
MaulingMonkey 4/16
Obrigado. Eu tentei novamente. window sizeé 1000x1000 (pixels), WORLD_WIDTHx WORLD_HEIGHTé 100x100, FillViewporté 10x10, playerSpriteé 1x1. Infelizmente, o problema ainda persiste.
Joschua
Esses detalhes serão um pouco demais para tentar resolver em um tópico de comentários. Deseja iniciar uma sala de bate-papo sobre desenvolvimento de jogos ou me enviar uma mensagem direta no Twitter @D_M_Gregory?
DMGregory
1

aqui uma pequena lista de verificação:

  1. Separe "posição atual da câmera" com "mira posição da câmera". (como ndenarodev mencionado) Se, por exemplo, a "posição atual da câmera" for 1.1 - faça a "mira da posição da câmera" 1.0

E apenas mude a posição atual da câmera se a câmera se mover.
Se a posição da câmera de mira for algo diferente de transform.position - defina transform.position (da sua câmera) para "mire a posição da câmera".

aim_camera_position = Mathf.Round(current_camera_position)

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

isso deve resolver seu problema. Se não: me diga. No entanto, você deve considerar fazer essas coisas também:

  1. defina "projeção da câmera" como "ortográfica" (para o seu problema em Y que aumenta anormalmente) (a partir de agora, você só deve ampliar com o "Campo de visão")

  2. defina a rotação da câmera em passos de 90 ° (0 ° ou 90 ° ou 180 °)

  3. não gere mipmaps se você não souber se deve.

  4. use tamanho suficiente para seus sprites e não use filtro bilinear ou trilinear

  5. 5)

Se 1 unidade não é uma unidade de pixels, talvez sua resolução de tela seja dependente. Você tem acesso ao campo de visão? Dependendo do seu campo de visão: Descubra quanta resolução é 1 unidade.

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 agora se onePixel (largura) e onePixel (altura) não são 1.0000f, mova-se nessas unidades para a esquerda e a direita com sua câmera.

OC_RaizW
fonte
Ei! Obrigado pela sua resposta. 1) Eu já guardo a posição atual da câmera e a arredondada separadamente. 2) Eu uso LibGDX OrthographicCamerapara que esperemos que funcione assim. (Você está usando o Unity, certo?) 3-5) Eu já faço isso no meu jogo.
Joschua 02/09
Ok, então você precisa descobrir a quantidade de unidades de 1 pixel. pode ser dependente da resolução da tela. Eu editei "5." no meu post de resposta. tente isso.
OC_RaizW
0

Não estou familiarizado com a libgdx, mas tenho problemas semelhantes em outros lugares. Eu acho que o problema está no fato de você estar usando o lerp e potencialmente erro em valores de ponto flutuante. Acho que uma possível solução para o seu problema é criar seu próprio objeto de localização da câmera. Use esse objeto para o seu código de movimento, atualize-o de acordo e defina a posição da câmera para o valor desejado (número inteiro?) Mais próximo do seu objeto de localização.

ndenarodev
fonte
Primeiro de tudo, obrigado pela sua compreensão. Realmente pode ter algo a ver com erros de ponto flutuante. No entanto, o problema (de yaumentar anormalmente) também estava presente antes de eu estar usando lerp. Na verdade, eu o adicionei ontem para que as pessoas pudessem ver o outro problema, onde o tamanho dos sprites não permanece constante quando a câmera está se aproximando.
Joschua 01/09/16