Как мне перемещать камеру с полными пиксельными интервалами?

13

По сути, я хочу, чтобы камера не двигалась в субпикселях, так как я думаю, что это приводит к тому, что спрайты заметно меняют свои размеры, хотя бы немного. (Есть ли лучший термин для этого?) Обратите внимание, что это пиксельная игра, в которой я хочу иметь четкую пиксельную графику. Вот рисунок, который показывает проблему:

введите описание изображения здесь

Теперь я попытался сделать следующее: переместить камеру, спроецировать текущую позицию (так, чтобы это были координаты экрана), а затем округлить или привести к int. После этого конвертируйте его обратно в мировые координаты и используйте его в качестве новой позиции камеры. Насколько я знаю, это должно привязывать камеру к фактическим координатам экрана, а не к их долям.

Однако по какой-то причине yценность новой позиции просто взрывается. В считанные секунды он увеличивается до чего-то вроде 334756315000.

Вот SSCCE (или MCVE), основанный на коде вики 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());
    }
}

и вот иsc_map.jpgdungeon_guy.png

Мне также было бы интересно узнать о более простых и / или лучших способах решения этой проблемы.

Joschua
источник
"Есть ли лучший термин для этого?" Aliasing? В этом случае Multisample сглаживание (MSAA) может быть альтернативным решением?
Юсеф Амар
@Paraknight К сожалению, я забыл упомянуть, что я работаю над пиксельной игрой, где мне нужна четкая графика, чтобы сглаживание не работало (AFAIK). Я добавил эту информацию к вопросу. Спасибо хоть.
Джошуа
Есть ли причина работать с более высоким разрешением, иначе работать с разрешением 1 пиксель и повысить конечный результат?
Felsir
@Felsir Да, я перемещаюсь в пикселях экрана, чтобы игра выглядела максимально плавно. Перемещение целого пикселя текстуры выглядит очень нервно.
Иошуа

Ответы:

22

Ваша проблема не в перемещении камеры с шагом в полпикселя. Дело в том, что ваше отношение текселей к пикселям немного нецелое . Я возьму несколько примеров из этого похожего вопроса, на который я ответил на StackExchange .

Вот две копии Марио - обе движутся по экрану с одинаковой скоростью (либо спрайтами, движущимися вправо в мире, либо камерой, движущейся влево - это заканчивается эквивалентно), но только верхняя показывает эти рябь артефакты и нижний не:

Два Марио Спрайты

Причина в том, что я немного увеличил верхнюю часть Mario в 1 раз в 1,01 раза - эта лишняя доля означает, что он больше не совпадает с пиксельной сеткой экрана.

Небольшой переводческий смещение не является проблемой - «Ближайшая» фильтрация текстур все равно будет привязана к ближайшему текселю, и мы ничего особенного не сделаем.

Но масштаб несоответствие означает, что этот щелчок не всегда в одном направлении. В одном месте пиксель, ищущий ближайший тексель, выберет один немного вправо, в то время как другой пиксель выберет один немного влево - и теперь столбец текселей либо пропущен, либо дублирован между ними, создавая рябь или мерцание, которые перемещается по спрайту по мере продвижения по экрану.

Вот поближе. Я анимировал полупрозрачный грибной спрайт, движущийся плавно, как если бы мы могли рендерить его с неограниченным субпиксельным разрешением. Затем я наложил пиксельную сетку, используя выборку ближайшего соседа. Весь пиксель меняет цвет, чтобы соответствовать части спрайта под точкой выборки (точка в центре).

Масштабирование 1: 1

Правильно масштабированный спрайт, движущийся без пульсации

Несмотря на то, что спрайт плавно перемещается к субпиксельным координатам, его визуализированное изображение по-прежнему корректно фиксируется при каждом перемещении на полный пиксель. Нам не нужно делать ничего особенного, чтобы сделать эту работу.

1: 1,0625 Масштабирование

16x16 спрайт отображается 17x17 на экране, показывая артефакты

В этом примере текстовый грибной спрайт 16x16 был масштабирован до 17x17 пикселей экрана, поэтому привязка происходит по-разному от одной части изображения к другой, создавая рябь или волну, которая растягивает и сдавливает его при движении.

Итак, хитрость заключается в том, чтобы настроить размер камеры / поле обзора таким образом, чтобы исходные тексели ваших ресурсов отображались в целое число пикселей экрана. Это не должно быть 1: 1 - любое целое число прекрасно работает:

Масштабирование 1: 3

Увеличить по тройной шкале

Вам нужно будет сделать это немного по-разному, в зависимости от вашего целевого разрешения - простое увеличение по размеру окна почти всегда приведет к дробному масштабированию. Некоторое количество отступов по краям экрана может потребоваться для определенных разрешений.

ДМГригорий
источник
1
Прежде всего, большое спасибо! Это отличная визуализация. Имей голос за это в одиночку. К сожалению, я не могу найти ничего нецелого в предоставленном коде. Размер mapSprite100x100. playerSpriteРазмер 1x1, размер окна просмотра 32x20. Кажется, даже изменение всего числа, кратного 16, не решает проблему. Есть идеи?
Иошуа
2
Вполне возможно иметь целые числа везде и при этом получить нецелое соотношение. Возьмите пример 16-тексельного спрайта, отображаемого на 17 пикселях экрана. Оба значения являются целыми числами, но их соотношение не является. Я не очень разбираюсь в libgdx, но в Unity я бы посмотрел, сколько текселей я показываю на единицу мира (например, разрешение спрайта, деленное на размер спрайта в мире), сколько единиц мира я показываю в своем окно (используя размер камеры), и сколько пикселей экрана на моем окне. Связав эти 3 значения, мы можем определить соотношение текселей к пикселям для данного размера окна дисплея.
DMGregory
«и область просмотра установлена ​​на размер 32x20» - если «размер клиента» (область визуализации) вашего окна не является целым числом, кратным этому, что, я думаю, не так, 1 единица в вашем окне просмотра будет не целочисленной количество пикселей.
MaulingMonkey
Благодарю. Я попробовал еще раз. window size1000x1000 (пикселей), WORLD_WIDTHx WORLD_HEIGHT100x100, FillViewport10x10, playerSprite1x1. К сожалению, проблема все еще сохраняется.
Иошуа
Эти детали будут немного полезны, чтобы попытаться разобраться в ветке комментариев. Хотели бы вы открыть чат для разработчиков игр или написать мне на Twitter @D_M_Gregory?
DMGregory
1

вот небольшой контрольный список:

  1. Разделяйте «текущее положение камеры» с «целевым положением камеры». (как упомянуто ndenarodev) Если, например, «текущее положение камеры» равно 1.1 - установите «прицельное положение камеры» 1.0

И только изменить текущее положение камеры, если камера должна двигаться.
Если положение камеры прицеливания отличается от transform.position - установите для transform.position (вашей камеры) значение «target camera 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. не создавайте мипмапы, если не знаете, следует ли

  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 (ширина) и onePixel (высота) не 1.0000f, просто переместите эти единицы влево и вправо с вашей камерой.

OC_RaizW
источник
Привет! Спасибо за Ваш ответ. 1) Я уже храню фактическое положение камеры и округленное отдельно. 2) Я использую LibGDX, OrthographicCameraтак что, надеюсь, так и будет работать. (Вы используете Unity, верно?) 3-5) Я уже делаю это в своей игре.
Иошуа
Хорошо, тогда вам нужно выяснить, сколько единиц составляет 1 пиксель. это может зависеть от разрешения экрана. Я отредактировал "5" в моем ответном посте. попробуй это.
OC_RaizW
0

Я не знаком с libgdx, но у меня были подобные проблемы в других местах. Я думаю, что проблема заключается в том, что вы используете lerp и, возможно, ошибку в значениях с плавающей запятой. Я думаю, что возможным решением вашей проблемы является создание собственного объекта определения местоположения камеры. Используйте этот объект для вашего кода движения и обновите его соответствующим образом, а затем установите положение камеры на ближайшее значение (целое число?) Вашего объекта местоположения.

ndenarodev
источник
Прежде всего, спасибо за понимание. Это действительно может иметь отношение к ошибкам с плавающей запятой. Однако проблема ( yувеличение ненормально) также присутствовала, прежде чем я использовал lerp. Я фактически добавил это вчера, чтобы люди могли также видеть другую проблему, где размер спрайтов не остается постоянным, когда камера приближается.
Иошуа