Як перемістити камеру в інтервалі повного пікселя?


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

Мені також було б цікаво дізнатися про більш прості та / або кращі способи вирішити цю проблему.


"Чи є кращий термін для цього?" Збудження? У такому випадку багатоспробний антизіаз (MSAA) може бути альтернативним рішенням?
Юсеф Амар

@Paraknight Вибачте, я забув згадати, що я працюю над пікселізованою грою, де мені потрібна чітка графіка, щоб антизбудження не працювало (AFAIK). Я додав цю інформацію до питання. Спасибі, хоча.
Джошуа

Чи є причина працювати з більш високою роздільною здатністю, інакше працюйте з роздільною здатністю 1 пікселя і підвищуйте кінцевий результат?
Фельсір

@Felsir Так, я переміщуся в пікселях екрана, щоб гра була максимально гладкою. Переміщення пікселя з цілою текстурою виглядає дуже хитро.
Джошуа

Відповіді:


22

Ваша проблема не в переміщенні камери з кроком в повному пікселі. Це те, що коефіцієнт тексель до пікселя трохи не цілий . Я запозичу кілька прикладів із цього подібного питання, на який я відповів на StackExchange .

Ось дві копії Маріо - обидві рухаються по екрану з однаковою швидкістю (або спрайтами, що рухаються праворуч у світі, або камерою, яка рухається ліворуч - це в кінцевому рахунку еквівалентно), але лише у верхній зображено ці пульсаційні артефакти та нижній не:

Два справі Маріо

Причина в тому, що я трохи збільшив верхній Маріо на 1 коефіцієнт 1,01x - ця додаткова крихітна частка означає, що він більше не поєднується з піксельною сіткою екрану.

Невелика трансляційна нерівність не є проблемою - фільтрування текстур "Найближчий" все одно буде притискатися до найближчого текселя, не роблячи нічого особливого.

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

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

1: 1 Масштабування

Правильно масштабований спрайт рухається без пульсацій

Навіть незважаючи на те, що спрайт плавно переміщується до підпіксельних координат, його відтворене зображення все одно правильно знімається кожного разу, коли він подорожує повним пікселем. Нам не потрібно робити нічого особливого, щоб зробити цю роботу.

1: 1,0625 Масштабування

Спрайт 16х16 виводив на екран 17х17, показуючи артефакти

У цьому прикладі текстовий гриб з текстилем 16х16 масштабується до 17х17 пікселів екрана, і тому оснащення відбувається по-різному від однієї частини зображення до іншої, створюючи пульсацію або хвилю, яка розтягує та стискає її під час руху.

Отже, хитрість полягає в тому, щоб налаштувати розмір камери / поле зору так, щоб текстові текстилі вихідних ресурсів відображалися на цілу кількість екранних пікселів. Це не повинно бути 1: 1 - будь-яке ціле число чудово працює:

1: 3 Масштабування

Збільшення масштабу в потрійному масштабі

Це потрібно зробити трохи інакше, залежно від цільової роздільної здатності - просто масштабування під вікно майже завжди призведе до дробової шкали. Для певної роздільної здатності може знадобитися деяка кількість накладки по краях екрана.


1
Перш за все, велике спасибі! Це чудова візуалізація. Майте нагороду лише за це. На жаль, я не можу знайти що-небудь цілочисельне у наданому коді. Розмір mapSprite100x100, The playerSpriteвстановлюється на розмір 1x1, а область перегляду встановлюється на розмір 32x20. Навіть змінити все на кратні 16, схоже, не вирішує проблему. Будь-які ідеї?
Джошуа

2
Цілком можливо мати цілі числа скрізь і все одно отримати неціле число. Візьмемо приклад 16-ти текстових спрайтів, що відображаються на 17 пікселях екрана. Обидва значення є цілими числами, але їх співвідношення не є. Я не знаю багато про libgdx, але в Unity я би роздивився, скільки я показую текстулів на світову одиницю (наприклад, роздільна здатність спрайту, поділену на світовий розмір спрайту), скільки світових одиниць я відображаю вікно (з використанням розміру камери) та кількість пікселів екрана у моєму вікні. Зв'язавши ці 3 значення, ми можемо розробити коефіцієнт тексель-піксель для заданого розміру вікна.
DMGregory

"і вікно перегляду встановлено у розмірі 32x20" - якщо "розмір клієнта" вашого вікна (область, що передається) не є цілим кратним цього числа, що, мабуть, це не так, 1 одиниця у вашому вікні перегляду буде деяким цілим числом кількість пікселів.
MaulingMonkey

Спасибі. Я спробував ще раз. window sizeдорівнює 1000x1000 (пікселів), WORLD_WIDTHx WORLD_HEIGHT100x100, FillViewport10x10, playerSprite1x1. На жаль, проблема все ще зберігається.
Джошуа

Ці деталі будуть небагато спробувати розібратися в нитці коментарів. Хочете запустити кімнату чату для розробки ігор або надіслати пряме повідомлення мені у 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. не створюйте mipmaps, якщо ви не знаєте, чи варто.

  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, просто перемістіть ці камери вліво і вправо за допомогою камери.


Гей! Дякую за вашу відповідь. 1) Я вже зберігаю фактичне положення камери та округлене окремо. 2) Я використовую LibGDX, OrthographicCameraщоб сподіватися так. (Ви використовуєте Unity, правда?) 3-5) Я вже роблю це в своїй грі.
Джошуа

Гаразд - тоді вам потрібно дізнатися, скільки одиниць становить 1 піксель. це може залежати від роздільної здатності екрана. Я відредагував "5." в моєму відповіді. спробуйте це.
OC_RaizW

0

Я не знайомий з libgdx, але маю подібні проблеми в інших місцях. Я думаю, що проблема полягає в тому, що ви використовуєте lerp та потенційно помилку в значеннях з плаваючою комою. Я думаю, що можливим рішенням вашої проблеми є створення власного об’єкта розташування камери. Використовуйте цей об’єкт для коду руху та оновіть його відповідно, а потім встановіть положення камери на найближче шукане (ціле?) Значення об'єкта розташування.


Перш за все, дякую за ваше розуміння. Це дійсно може мати щось спільне з помилками з плаваючою комою. Однак проблема ( yаномально зростаюча) також була присутня до того, як я користувався lerp. Я фактично додав його вчора, щоб люди також могли побачити іншу проблему, коли розмір спрайтів не залишається постійним, коли камера наближається.
Джошуа
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.