完全なピクセル間隔でカメラを移動するにはどうすればよいですか?


13

基本的には、カメラがサブピクセル単位で移動するのを止めたいと思います。これにより、スプライトの寸法がほんのわずかでも視覚的に変化することになります。(より良い用語はありますか?)これは、鮮明なピクセル化されたグラフィックスを持ちたいピクセルアートゲームです。問題を示すgifは次のとおりです。

ここに画像の説明を入力してください

今私が試したのはこれです:カメラを移動し、現在の位置を投影して(画面座標になるように)、丸めまたは整数にキャストします。その後、ワールド座標に変換し直し、それを新しいカメラ位置として使用します。私の知る限り、これはカメラを実際の画面座標にロックするものであり、その一部ではありません。

ただし、何らかの理由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)が代替ソリューションになる可能性がありますか?
ユーセフアマル

@Paraknight申し訳ありませんが、アンチエイリアシングが機能しないために鮮明なグラフィックスが必要なピクセル化されたゲームに取り組んでいることを忘れていました(AFAIK)。その情報を質問に追加しました。どうもありがとう。
ジョシュア

より高い解像度で動作する理由がありますか、それ以外の場合は1ピクセルの解像度で動作し、最終結果をアップスケールしますか?
フェルシール

@Felsirはい、ゲームができるだけスムーズに感じるように、画面のピクセル単位で移動します。テクスチャピクセル全体を移動すると、非常に不安定になります。
ジョシュア

回答:


22

問題は、カメラをフルピクセル単位で動かさないことです。テクセルとピクセルの比率がわずかに非整数であることです。StackExchangeで回答したこの同様の質問からいくつかの例を借ります

マリオの2つのコピーがあります-両方が同じ速度で画面を横切って移動しています(世界で右に移動するスプライト、または左に移動するカメラのいずれかにより)-最終的に同等のものになります。一番下はそうではありません:

2つのマリオスプライト

その理由は、トップマリオを1.01xの1倍にわずかにスケーリングしたためです。このわずかな余分な部分は、画面のピクセルグリッドと整列しなくなったことを意味します。

小さな並進ずれは問題ではありません-「最も近い」テクスチャフィルタリングは、特別なことをせずに、とにかく最も近いテクセルにスナップします。

しかし、スケールの不一致は、このスナップが常に同じ方向にあるとは限らないことを意味します。ある場所では、最も近いテクセルを検索するピクセルが少し右に1つを選択し、別のピクセルが少し左に1つを選択します。テクセルの列が省略されるか、その間に複製されて、波紋または揺らめきを作成します。スプライトが画面上を移動するときにスプライト上を移動します。

詳細をご覧ください。無制限のサブピクセル解像度でレンダリングできるかのように、スムーズに移動する半透明のキノコスプライトをアニメーション化しました。次に、最近傍サンプリングを使用してピクセルグリッドをオーバーレイしました。ピクセル全体の色が変わり、サンプリングポイントの下のスプライトの部分(中央のドット)に一致します。

1:1スケーリング

リップルなしで移動する適切にスケーリングされたスプライト

スプライトはサブピクセル座標にスムーズに移動しますが、レンダリングされたイメージは、フルピクセルを移動するたびに正しくスナップされます。この作業を行うために特別なことをする必要はありません。

1:1.0625スケーリング

画面に17x17でレンダリングされた16x16スプライト、アーティファクトを表示

この例では、16x16テクセルマッシュルームスプライトが17x17スクリーンピクセルまで拡大されているため、画像の一部から別の部分にスナップが異なって発生し、移動するにつれて伸縮する波紋または波を作成します。

そのため、アセットのソーステクセルが整数の画面ピクセルにマッピングされるように、カメラのサイズ/視野を調整するのがコツです。1:1である必要はありません-すべての整数がうまく機能します:

1:3スケーリング

トリプルスケールでズームイン

これは、ターゲットの解像度に応じて少し異なる方法で行う必要があります-ウィンドウに合わせて拡大するだけで、ほとんど常に小数スケールになります。特定の解像度では、画面の端にある程度のパディングが必要になる場合があります。


1
まず第一に、どうもありがとう!これは素晴らしい視覚化です。それだけで賛成です。残念ながら、提供されたコードには整数以外のものは見つかりません。のサイズmapSpriteは100x100、The playerSpriteは1x1のサイズに設定され、ビューポートは32x20のサイズに設定されます。すべてを16の倍数に変更しても、問題は解決しないようです。何か案は?
ジョシュア

2
どこにでも整数を持ち、それでも整数以外の比率を取得することは完全に可能です。17画面ピクセルに表示される16テクセルスプライトの例を見てみましょう。両方の値は整数ですが、その比率はそうではありません。libgdxについてはあまり知りませんが、Unityでは、ワールドユニットごとに表示するテクセルの数(たとえば、スプライトの解像度をスプライトのワールドサイズで除算したもの)、表示するワールドユニットの数を調べますウィンドウ(カメラのサイズを使用)、およびウィンドウ上の画面ピクセル数。これらの3つの値を連結して、指定された表示ウィンドウサイズのテクセルとピクセルの比率を計算できます。
DMGregory

「そして、ビューポートは32x20のサイズに設定されます」-ウィンドウの「クライアントサイズ」(レンダリング可能領域)がこれの整数倍でない限り、これはそうではないと思いますが、ビューポートの1単位は非整数になりますピクセル数。
MaulingMonkey

ありがとう。もう一度試してみました。window size1000x1000(ピクセル)であり、WORLD_WIDTHxはWORLD_HEIGHT100×100でありFillViewport、10×10であるplayerSprite1×1です。悲しいことに、問題はまだ残っています。
ジョシュア

これらの詳細は、コメントスレッドで整理しようとするために少し多くなります。ゲーム開発チャットルームを開始しますか、それとも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. 必要があるかどうかわからない場合は、ミップマップを生成しないでください。

  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。実際に昨日追加したのは、カメラが近づいたときにスプライトのサイズが一定にならないという他の問題も見られるようにするためです。
ジョシュア
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.