Bagaimana cara memindahkan kamera dalam interval piksel penuh?

13

Pada dasarnya saya ingin menghentikan kamera dari bergerak dalam subpiksel, karena saya pikir ini mengarah ke sprite yang secara nyata mengubah dimensi mereka jika hanya sedikit. (Apakah ada istilah yang lebih baik untuk itu?) Perhatikan bahwa ini adalah game pixel-art di mana saya ingin memiliki grafik pixelated yang tajam. Inilah gif yang menunjukkan masalah:

masukkan deskripsi gambar di sini

Sekarang yang saya coba adalah ini: Pindahkan kamera, proyeksikan posisi saat ini (jadi itu koordinat layar) dan kemudian bulatkan atau putar ke int. Setelah itu konversikan kembali ke koordinat dunia dan gunakan itu sebagai posisi kamera baru. Sejauh yang saya tahu, ini harus mengunci kamera ke koordinat layar yang sebenarnya, bukan pecahannya.

Namun, karena suatu alasan, ynilai posisi baru itu baru saja meledak. Dalam hitungan detik itu meningkat menjadi seperti 334756315000.

Berikut ini adalah SSCCE (atau MCVE) berdasarkan kode di wiki 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());
    }
}

dan inilah yangsc_map.jpg dan yangdungeon_guy.png

Saya juga tertarik untuk mempelajari tentang cara yang lebih sederhana dan / atau lebih baik untuk memperbaiki masalah ini.

Joschua
sumber
"Apakah ada istilah yang lebih baik untuk itu?" Mengasingkan? Dalam hal itu Multisample anti-aliasing (MSAA) mungkin menjadi solusi alternatif?
Yousef Amar
@Paraknight Maaf, saya lupa menyebutkan bahwa saya sedang mengerjakan game pixelated, di mana saya membutuhkan grafik yang tajam sehingga anti-aliasing tidak akan berfungsi (AFAIK). Saya menambahkan informasi itu ke pertanyaan. Terimakasih Meskipun.
Joschua
Apakah ada alasan untuk bekerja pada resolusi yang lebih tinggi, jika tidak bekerja pada resolusi 1 pixel dan meningkatkan hasil akhir?
Felsir
@Felsir Ya, saya bergerak dalam piksel layar, sehingga permainan terasa semulus mungkin. Memindahkan seluruh piksel tekstur terlihat sangat gelisah.
Joschua

Jawaban:

22

Masalah Anda tidak menggerakkan kamera dalam peningkatan piksel penuh. Rasio texel-ke-pixel Anda sedikit non-integer . Saya akan meminjam beberapa contoh dari pertanyaan serupa yang saya jawab di StackExchange .

Berikut adalah dua salinan Mario - keduanya bergerak melintasi layar dengan kecepatan yang sama (baik oleh sprite bergerak tepat di dunia, atau kamera bergerak ke kiri - berakhir setara), tetapi hanya yang teratas menunjukkan artefak beriak ini dan yang bawah tidak:

Dua sprite Mario

Alasannya adalah bahwa saya sedikit meningkatkan Mario atas dengan 1 faktor 1,01x - fraksi ekstra kecil itu berarti dia tidak lagi sejalan dengan kisi-kisi piksel layar.

Misalignment translasi kecil bukan masalah - pemfilteran tekstur "terdekat" akan tetap melekat pada texel terdekat tanpa kita melakukan sesuatu yang istimewa.

Tapi skala ketidakcocokan berarti gertakan ini tidak selalu dalam arah yang sama. Di satu tempat piksel mencari texel terdekat akan mengambil satu sedikit ke kanan, sementara piksel lain mengambil satu sedikit ke kiri - dan sekarang kolom texel telah dihilangkan atau digandakan di antara keduanya, menciptakan riak atau kilau yang bergerak di atas sprite saat bergerak melintasi layar.

Inilah yang lebih dekat. Saya telah menghidupkan sprite jamur tembus pandang yang bergerak dengan lancar, seolah-olah kita dapat membuatnya dengan resolusi sub-pixel yang tidak terbatas. Lalu saya overlay grid pixel menggunakan sampling tetangga terdekat. Seluruh piksel berubah warna agar sesuai dengan bagian sprite di bawah titik pengambilan sampel (titik di tengah).

Penskalaan 1: 1

Sprite dengan skala yang tepat bergerak tanpa riak

Meskipun sprite bergerak dengan lancar ke koordinat sub-pixel, gambar yang dihasilkannya tetap terkunci dengan benar setiap kali ia berjalan piksel penuh. Kita tidak perlu melakukan sesuatu yang istimewa untuk membuat ini bekerja.

1: 1.0625 Penskalaan

16x16 sprite ditampilkan 17x17 di layar, menunjukkan artefak

Dalam contoh ini, sprite jamur texel 16x16 telah diskalakan hingga 17x17 piksel layar, sehingga pemotretan terjadi secara berbeda dari satu bagian gambar ke yang lain, menciptakan riak atau gelombang yang membentang & meremasnya saat bergerak.

Jadi, triknya adalah menyesuaikan ukuran kamera / bidang pandang Anda sehingga sumber aset Anda akan memetakan peta ke sejumlah integer piksel layar. Tidak harus 1: 1 - angka berapa pun bekerja dengan baik:

1: 3 Skala

Memperbesar skala tiga

Anda harus melakukan ini sedikit berbeda tergantung pada resolusi target Anda - hanya meningkatkan skala agar sesuai dengan jendela akan selalu menghasilkan skala fraksional. Beberapa jumlah padding di tepi layar mungkin diperlukan untuk resolusi tertentu.

DMGregory
sumber
1
Pertama-tama, terima kasih banyak! Ini adalah visualisasi yang hebat. Dapatkan dukungan untuk itu saja. Sayangnya, saya tidak dapat menemukan sesuatu yang bukan bilangan bulat dalam kode yang disediakan. Ukuran mapSprite100x100, playerSpritediatur ke ukuran 1x1 dan viewport diatur ke ukuran 32x20. Bahkan mengubah segalanya menjadi kelipatan 16 tampaknya tidak menyelesaikan masalah. Ada ide?
Joschua
2
Sangat mungkin untuk memiliki bilangan bulat di mana-mana dan masih mendapatkan rasio non-integer. Ambil contoh sprite 16 texel yang ditampilkan pada 17 piksel layar. Kedua nilai tersebut adalah bilangan bulat, tetapi nilainya tidak. Saya tidak tahu banyak tentang libgdx, tetapi di Unity saya akan melihat berapa banyak texels yang saya tampilkan per unit dunia (mis. Resolusi sprite dibagi dengan ukuran sprite dunia), berapa banyak unit dunia yang saya tampilkan di saya jendela (menggunakan ukuran kamera), dan berapa banyak piksel layar di jendela saya. Dengan menggunakan 3 nilai tersebut, kita dapat menghitung rasio texel-ke-pixel untuk ukuran jendela tampilan yang diberikan.
DMGregory
"dan viewport diatur ke ukuran 32x20" - kecuali jika "ukuran klien" jendela Anda (area renderable) adalah kelipatan bilangan bulat dari ini, yang saya kira tidak, 1 unit dalam viewport Anda akan menjadi non-integer jumlah piksel.
MaulingMonkey
Terima kasih. Saya sudah mencoba lagi. window sizeadalah 1000x1000 (piksel), WORLD_WIDTHx WORLD_HEIGHTadalah 100x100, FillViewportadalah 10x10, playerSpriteadalah 1x1. Sayangnya, masalahnya masih berlanjut.
Joschua
Detail-detail ini akan sedikit banyak untuk mencoba memilah-milah di utas komentar. Apakah Anda ingin memulai ruang Obrolan Pengembangan Game , atau mengirimi saya pesan langsung di Twitter @D_M_Gregory?
DMGregory
1

di sini daftar kecil:

  1. Pisahkan "posisi kamera saat ini" dengan "arahkan posisi kamera". (seperti ndenarodev disebutkan) Jika misalnya "posisi kamera saat ini" adalah 1,1 - buat "posisi kamera tujuan" 1,0

Dan hanya mengubah posisi kamera saat ini jika kamera harus bergerak.
Jika posisi kamera tujuan adalah sesuatu yang lain dari transform.position - atur transform.position (dari kamera Anda) ke "arahkan posisi kamera".

aim_camera_position = Mathf.Round(current_camera_position)

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

ini harus menyelesaikan masalah Anda. Jika tidak: beri tahu saya. Namun Anda harus mempertimbangkan untuk melakukan hal-hal itu juga:

  1. atur "proyeksi kamera" menjadi "ortografis" (untuk masalah y Anda yang meningkat secara abnormal) (mulai sekarang, Anda hanya perlu memperbesar dengan "Bidang Tampilan")

  2. atur rotasi kamera dalam 90 ° - langkah (0 ° atau 90 ° atau 180 °)

  3. jangan menghasilkan peta jalan jika Anda tidak tahu jika Anda harus.

  4. gunakan ukuran yang cukup untuk sprite Anda dan jangan gunakan filter bilinear atau trilinear

  5. 5.

Jika 1 unit bukan unit pixel mungkin tergantung pada resolusi layarnya. Apakah Anda memiliki akses ke bidang tampilan? Tergantung pada bidang pandang Anda: Cari tahu berapa resolusi 1 unit.

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 

^ Dan sekarang jika onePixel (lebar) dan onePixel (tinggi) bukan 1,0000f, cukup pindahkan unit-unit itu ke kiri dan kanan dengan kamera Anda.

OC_RaizW
sumber
Hei! Terima kasih atas jawaban anda. 1) Saya sudah menyimpan posisi kamera yang sebenarnya dan yang dibulatkan secara terpisah. 2) Saya menggunakan LibGDX OrthographicCamerasehingga semoga bekerja seperti itu. (Anda menggunakan Unity, kan?) 3-5) Saya sudah melakukan itu di permainan saya.
Joschua
Oke -kemudian Anda perlu mencari tahu berapa unit 1 pixel. bisa jadi itu tergantung pada resolusi layar. Saya mengedit "5." di posting jawaban saya. coba ini.
OC_RaizW
0

Saya tidak terbiasa dengan libgdx tetapi mengalami masalah serupa di tempat lain. Saya pikir masalahnya terletak pada kenyataan bahwa Anda menggunakan lerp dan berpotensi kesalahan dalam nilai floating point. Saya pikir solusi yang mungkin untuk masalah Anda adalah membuat objek lokasi kamera Anda sendiri. Gunakan objek ini untuk kode gerakan Anda dan perbarui sesuai dan kemudian atur posisi kamera ke nilai yang diinginkan (integer?) Terdekat dari objek lokasi Anda.

ndenarodev
sumber
Pertama-tama, terima kasih atas wawasan Anda. Ini mungkin ada hubungannya dengan kesalahan floating point. Namun masalah ( ypeningkatan yang tidak normal) juga ada sebelum saya gunakan lerp. Saya benar-benar menambahkannya kemarin sehingga orang-orang juga bisa melihat masalah lain, di mana ukuran sprite tidak konstan ketika kamera mendekat.
Joschua