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:
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 y
valor 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.
Respuestas:
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:
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
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
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
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.
fuente
mapSprite
es de 100x100,playerSprite
se 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?window size
es 1000x1000 (píxeles),WORLD_WIDTH
xWORLD_HEIGHT
es 100x100,FillViewport
es 10x10,playerSprite
es 1x1. Lamentablemente, el problema aún persiste.Aquí una pequeña lista de verificación:
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".
Esto debería solucionar tu problema. Si no: dime. Sin embargo, deberías considerar hacer esas cosas también:
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")
ajuste la rotación de la cámara en 90 ° - pasos (0 ° o 90 ° o 180 °)
no genere mapas MIP si no sabe si debería hacerlo.
use suficiente tamaño para sus sprites y no use filtros bilineales o trilineales
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.
^ Y ahora, si onePixel (ancho) y onePixel (alto) no son 1.0000f, simplemente mueva esas unidades a izquierda y derecha con su cámara.
fuente
OrthographicCamera
para que, con suerte, funcione así. (Estás usando Unity, ¿verdad?) 3-5) Ya hago eso en mi juego.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.
fuente
y
aumento anormal) también estaba presente antes de usarlolerp
. 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.