如何以全像素间隔移动相机?


13

基本上,我想阻止相机移动到亚像素,因为我认为这会导致精灵明显地更改其尺寸,即使只是稍微改变一下。(是否有更好的用语?)请注意,这是一款像素艺术游戏,我想拥有清晰的像素化图形。这是显示问题的gif:

在此处输入图片说明

现在我尝试的是:移动相机,投影当前位置(因此是屏幕坐标),然后将其舍入或投射为int。之后,将其转换回世界坐标,并将其用作新的相机位置。据我所知,这应该将相机锁定在实际的屏幕坐标上,而不是分数。

但是由于某种原因,y新头寸的价值才爆炸式增长。在短短的几秒钟内,它增加到了类似的程度334756315000

这是一个基于LibGDX Wiki中代码的SSCCE(或MCVE):

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

这里是sc_map.jpgdungeon_guy.png

我也有兴趣了解解决此问题的更简单和/或更好的方法。


“有更好的用语吗?” 混叠?在那种情况下,多样本抗锯齿(MSAA)可能是替代解决方案?
Yousef Amar

@Paraknight对不起,我忘了提及我在玩像素化游戏,在这里我需要清晰的图形,因此抗锯齿功能不起作用(AFAIK)。我将该信息添加到问题中。不过谢谢
约书亚

是否有理由以更高的分辨率工作,否则以1像素的分辨率工作并放大最终结果?
Felsir'9

@Felsir是的,我以屏幕像素为单位移动,以使游戏感觉尽可能流畅。移动整个纹理像素看起来非常不稳定。
约书亚

Answers:


22

您的问题不是以全像素增量移动相机。这是因为您的纹素与像素之比略微不整数我将从在StackExchange上回答的类似问题中借用一些示例

这是马里奥的两份副本-两者都以相同的速度在屏幕上移动(要么是精灵在世界上向右移动,要么是相机在向左移动-最终等同),但是只有最上面的一个显示这些涟漪的伪像,并且最底层的没有:

两个马里奥精灵

原因是我以1倍1.01倍的比例稍微缩放了顶部马里奥-极小的分数意味着他不再与屏幕的像素网格对齐。

较小的平移对齐不成问题-无需我们做任何特殊处理,“最近的”纹理过滤无论如何都会捕捉到最接近的纹理像素。

但是比例不匹配意味着这种捕捉并不总是朝着相同的方向。在一个点上,向上看最近的像素的像素将向右稍微拾取一个像素,而另一个像素的像素向左稍微拾取一个像素-现在一列像素被省略或在它们之间重复,从而产生波纹或微光在精灵穿过屏幕时在其上方移动。

仔细看看。我为半透明的蘑菇精灵动画制作了平滑的动画,好像我们可以用无限的亚像素分辨率渲染它一样。然后,我使用最近邻采样覆盖了像素网格。整个像素会改变颜色,以匹配子像素在采样点(中心点)下的位置。

1:1比例

缩放比例合适的精灵,没有波动

尽管子画面可以平滑地移动到子像素坐标,但每次经过一个完整像素时,其渲染的图像仍然可以正确捕捉。我们不需要做任何特殊的事情就可以完成这项工作。

1:1.0625缩放

在屏幕上以17x17渲染16x16精灵,显示伪像

在此示例中,将16x16 texel蘑菇精灵放大到了17x17的屏幕像素,因此从图像的一个部分到另一部分的捕捉发生的方式有所不同,从而形成了波纹或波动,从而在移动时对其进行拉伸和压缩。

因此,诀窍在于调整相机的大小/视野,以使资产的源纹理映射到整数个屏幕像素。不必是1:1-任何整数都可以工作:

1:3缩放

放大三倍

您需要根据目标分辨率进行一些不同的操作-仅仅放大以适合窗口大小几乎总是会产生分数比例。对于某些分辨率,可能需要在屏幕边缘进行一些填充。


1
首先,非常感谢!这是一个很棒的可视化。对此有一个支持。可悲的是,在提供的代码中我找不到任何非整数的内容。的大小mapSprite为100x100,将playerSprite设置为1x1,视口设置为32x20。即使将所有内容更改为16的倍数也似乎无法解决问题。有任何想法吗?
约书亚

2
到处都可能有整数,而仍然获得非整数比率。以在17个屏幕像素上显示16 texel精灵为例。这两个值都是整数,但它们的比率不是。我对libgdx不太了解,但是在Unity中,我将查看每个世界单位显示多少个纹理像素(例如,子画面的分辨率除以子画面的世界大小),我在我的世界中显示多少个世界单位窗口(使用相机的大小),以及窗口上有多少屏幕像素。链接这三个值,我们可以计算出给定显示窗口大小的纹素与像素比率。
DMGregory

“并且视口设置为32x20的大小”-除非窗口的“客户端大小”(可渲染区域)是此值的整数倍,我想可能不是,您视口中的1个单位将是非整数像素数。
MaulingMonkey '16

谢谢。我再试一次。window size是1000x1000(像素),WORLD_WIDTHx WORLD_HEIGHT是100x100,FillViewport是10x10,playerSprite是1x1。可悲的是,问题仍然存在。
约书亚记

要在注释线程中进行整理,这些细节将有些繁琐。您想启动游戏开发聊天室,还是在Twitter @D_M_Gregory上直接给我发消息?
DMGregory

1

这是一个小清单:

  1. 将“当前摄像机位置”与“目标摄像机位置”分开。(例如提到的ndenarodev),例如,如果“当前摄像机位置”为1.1,则将“目标摄像机位置”设为1.0

并且只有在摄像机移动时才更改当前摄像机位置。
如果目标摄像机的位置不是transform.position,而是将(摄像机的)transform.position设置为“目标摄像机位置”。

aim_camera_position = Mathf.Round(current_camera_position)

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

这应该可以解决您的问题。如果没有:告诉我。但是,您也应该考虑做这些事情:

  1. 将“摄像机投影”设置为“正投影”(针对您异常增加的y问题)(从现在开始,您仅应使用“视野”进行缩放)

  2. 将摄像机旋转角度设置为90°-步进(0°或90°或180°)

  3. 如果您不知道是否应该不生成Mipmap。

  4. 为您的精灵使用足够的大小,不要使用双线性或三线性滤波器

  5. 5,

如果1个单位不是像素单位,则可能取决于其屏幕分辨率。您可以进入视野吗?根据您的视野:找出1单位的分辨率。

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 

^现在,如果onePixel(width)和onePixel(height)不是1.0000f,只需用相机左右移动这些单位即可。


嘿! 感谢您的回答。1)我已经分别存储了相机的实际位置和四舍五入的位置。2)我使用LibGDX OrthographicCamera,希望可以那样工作。(您使用的是Unity,对吗?)3-5)我已经在游戏中做到了。
约书亚

好的-然后,您需要找出1像素的单位数。它可能取决于屏幕分辨率。我编辑了“ 5”。在我的答案中。尝试这个。
OC_RaizW

0

我不熟悉libgdx,但在其他地方也遇到过类似的问题。我认为问题在于您正在使用lerp并可能在浮点值中出错。我认为可能的解决方案是创建自己的相机位置对象。使用此对象作为您的移动代码并进行相应的更新,然后将相机的位置设置为您的位置对象的最近的期望值(整数?)。


首先,感谢您的见解。它确实可能与浮点错误有关。但是,y在使用之前,还存在(异常增加)的问题lerp。我实际上是在昨天添加的,因此人们还可以看到另一个问题,即在接近相机时,子画面的大小不会保持恒定。
约书亚记'16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.