Wie verschiebe ich ein Sprite in Subpixel-Schritten?

11

Pixel sind entweder ein oder aus. Der Mindestbetrag, um den Sie ein Sprite verschieben können, ist ein einzelnes Pixel. Wie bewegt sich das Sprite langsamer als 1 Pixel pro Frame?

Ich habe die Geschwindigkeit zu einer Variablen hinzugefügt und getestet, ob sie 1 (oder -1) erreicht hat. Wenn ja, würde ich das Sprite verschieben und die Variable wie folgt auf 0 zurücksetzen:

update(dt):
    temp_dx += speed * dt
    temp_dy += speed * dt

    if (temp_dx > 1)
        move sprite
        reset temp_dx to 0
    if (tempy_dy > 1)
        move sprite
        reset temp_dy to 0

Ich mochte diesen Ansatz nicht, weil er sich albern anfühlt und die Bewegung des Sprites sehr ruckartig aussieht. Auf welche Weise würden Sie die Subpixel-Bewegung implementieren?

bzzr
quelle
Das einzige, was Sie tun können, ist die bilineare Filterung.
AturSams
Wenn Sie weniger als 1 Pixel pro Bild bewegen, wie kann es ruckartig aussehen?

Antworten:

17

Es gibt eine Reihe von Optionen:

  1. Mach was du tust. Sie haben bereits gesagt, dass es nicht glatt aussieht. Es gibt jedoch einige Mängel bei Ihrer aktuellen Methode. Für xkönnten Sie Folgendes verwenden:

    tempx += speed * dt
    
    while (tempx > 0.5)
      move sprite to x+1
      tempx -= 1
    while (tempx < -0.5)
      move sprite to x-1
      tempx += 1

    das sollte besser sein. Ich habe die if-Anweisungen auf 0,5 umgestellt. Wenn Sie 0,5 übergeben haben, sind Sie näher am nächsten Wert als am vorherigen. Ich habe while-Schleifen verwendet, um eine Bewegung von mehr als 1 Pixel pro Zeitschritt zu ermöglichen (dies ist nicht der beste Weg, aber es sorgt für kompakten und lesbaren Code). Bei sich langsam bewegenden Objekten ist es jedoch immer noch nervös, da wir uns nicht mit dem grundlegenden Problem der Pixelausrichtung befasst haben.

  2. Verwenden Sie mehrere Grafiken für Ihr Sprite und verwenden Sie je nach Subpixel-Offset eine andere. Um beispielsweise eine Subpixel-Glättung nur in x durchzuführen, können Sie eine Grafik für Ihr Sprite unter erstellen. x+0.5Wenn die Position zwischen x+0.25und liegt x+0.75, verwenden Sie dieses Sprite anstelle Ihres Originals. Wenn Sie eine feinere Positionierung wünschen, erstellen Sie einfach mehr Subpixel-Grafiken. Wenn Sie dies in tun xund yIhre Anzahl von Renderings schnell in die Höhe schnellen kann, da die Anzahl mit dem Quadrat der Anzahl der Intervalle skaliert: Ein Abstand von 0.5würde 4 Renderings 0.25erfordern , würde 16 erfordern.

  3. Supersample. Dies ist eine träge (und möglicherweise sehr teure) Methode zum Erstellen eines Bildes mit Subpixel-Auflösung. Verdoppeln Sie im Wesentlichen (oder mehr) die Auflösung, mit der Sie Ihre Szene rendern, und verkleinern Sie sie dann zur Laufzeit. Ich würde hier Vorsicht empfehlen, da die Leistung schnell sinken kann. Ein weniger aggressiver Weg, dies zu tun, besteht darin, Ihr Sprite zu überabtasten und es zur Laufzeit zu verkleinern.

  4. Wie von Zehelvion vorgeschlagen , kann es sein, dass die von Ihnen verwendete Plattform dies bereits unterstützt. Wenn Sie nicht ganzzahlige x- und y-Koordinaten angeben dürfen, gibt es möglicherweise Optionen zum Ändern der Texturfilterung. Der nächste Nachbar ist häufig die Standardeinstellung, und dies führt zu "ruckartigen" Bewegungen. Eine andere Filterung (linear / kubisch) würde zu einem viel gleichmäßigeren Effekt führen.

Die Wahl zwischen 2. 3. und 4. hängt davon ab, wie Ihre Grafiken implementiert sind. Ein Bitmap-Grafikstil eignet sich viel besser für das Pre-Rendering, während ein Vektorstil für Sprite-Supersampling geeignet ist. Wenn Ihr System dies unterstützt, ist 4. möglicherweise der richtige Weg.

Jez
quelle
Warum in opt 1 den Schwellenwert durch 0,5 ersetzen? Ich verstehe auch nicht, warum Sie die Anweisungen in eine while-Schleife verschoben haben. Die Update-Funktionen werden einmal pro Tick aufgerufen.
bzzr
1
Gute Frage - Ich habe die Antwort anhand Ihres Kommentars geklärt.
Jez
Supersampling mag "faul" sein, aber in vielen Fällen wären die Leistungskosten von 2x oder sogar 4x Oversampling kein besonderes Problem, insbesondere wenn andere Aspekte des Renderns vereinfacht werden könnten.
Supercat
In Spielen mit großem Budget sehe ich normalerweise 3 als Option, die in den Grafikeinstellungen als Anti-Aliasing aufgeführt ist.
Kevin
1
@ Kevin: Das tust du eigentlich wahrscheinlich nicht. Die meisten Spiele verwenden Multisampling , was etwas anders ist. Üblicherweise werden auch andere Varianten verwendet, z. B. mit unterschiedlicher Abdeckung sowie Tiefenproben. Supersampling wird aufgrund der Kosten für die Ausführung des Fragment-Shaders fast nie durchgeführt.
Imallett
14

Eine Möglichkeit, wie viele Old-Skool-Spiele dieses Problem lösten (oder versteckten), bestand darin, das Sprite zu animieren.

Das heißt, wenn sich Ihr Sprite weniger als ein Pixel pro Frame bewegen würde (oder insbesondere wenn das Pixel / Frame-Verhältnis etwas Seltsames wie 2 Pixel in 3 Frames sein würde), könnten Sie das Ruckeln verbergen, indem Sie ein n machen Frame-Animationsschleife, die über diese n Frames das Sprite um einige k < n Pixel verschob.

Der Punkt ist , dass, solange das Sprite immer bewegt sich in irgendeiner Weise auf jeden Frame, gibt es nie einen einzigen Frame , wo die ganze Sprite würde plötzlich „Ruck“ nach vorn zu gehen.


Ich konnte kein aktuelles Sprite aus einem alten Videospiel finden, um dies zu veranschaulichen (obwohl ich denke, dass z. B. einige der Grabanimationen von Lemmings so waren), aber es stellt sich heraus, dass das "Segelflug" -Muster aus Conways Spiel des Lebens eine sehr schöne Illustration:

Geben Sie hier die Bildbeschreibung ein
Animation von Kieff / Wikimedia Commons , verwendet unter der CC-By-SA 3.0- Lizenz.

Hier kriechen die kleinen schwarzen Pixelblöcke, die nach unten und rechts kriechen, die Segelflugzeuge. Wenn Sie genau hinschauen, werden Sie feststellen, dass sie vier Animationsframes benötigen, um ein Pixel diagonal zu crawlen. Da sie sich jedoch auf jedem dieser Frames auf irgendeine Weise bewegen , sieht die Bewegung nicht so ruckelig aus (zumindest nicht mehr) ruckelig als irgendetwas sieht sowieso diese Bildrate an).

Ilmari Karonen
quelle
2
Dies ist ein ausgezeichneter Ansatz, wenn die Geschwindigkeit fest ist oder wenn sie weniger als 1/2 Pixel pro Bild beträgt oder wenn die Animation "groß" und schnell genug ist, um die Bewegungsschwankungen zu verdecken.
Supercat
Dies ist ein guter Ansatz. Wenn eine Kamera dem Sprite folgen muss, treten dennoch Probleme auf, da sie sich nicht reibungslos bewegt.
Elden Abob
6

Die Position des Sprites sollte als Gleitkommagröße beibehalten und erst kurz vor der Anzeige auf eine Ganzzahl gerundet werden.

Sie können das Sprite auch auf einer hohen Auflösung halten und das Sprite vor der Anzeige herunterrechnen. Wenn Sie das Sprite auf der 3-fachen Anzeigeauflösung gehalten haben, haben Sie abhängig von der Subpixel-Position des Sprites 9 verschiedene tatsächliche Sprites.

ddyer
quelle
3

Die einzige wirkliche Lösung besteht darin, eine bilineare Filterung zu verwenden. Die Idee ist, die GPU den Wert jedes Pixels basierend auf den vier Sprite-Pixeln berechnen zu lassen, die sich damit überlappen. Es ist eine übliche und effektive Technik. Sie müssen das Sprite lediglich als Textur auf einer 2D-Ebene (einer Werbetafel) platzieren. Verwenden Sie dann die GPU, um diese Ebenen zu rendern. Dies funktioniert gut, aber erwarten Sie etwas verschwommene Ergebnisse und verlieren Sie das 8-Bit- oder 16-Bit-Aussehen, wenn Sie es anstreben.

Profis:

Bereits implementierte, sehr schnelle, hardwarebasierte Lösung.

Nachteile:

Verlust der 8-Bit / 16-Bit-Wiedergabetreue.

AturSams
quelle
1

Gleitkomma ist die normale Methode, insbesondere wenn Sie schließlich auf ein GL-Ziel rendern, bei dem die Hardware mit einem schattierten Polygon mit Gleitkoordinaten sehr zufrieden ist.

Es gibt jedoch eine andere Möglichkeit, das zu tun, was Sie gerade tun, jedoch etwas weniger ruckartig: den Fixpunkt. Ein 32-Bit-Positionswert kann Werte von 0 bis 4.294.967.295 darstellen, obwohl Ihr Bildschirm mit ziemlicher Sicherheit weniger als 4 KB Pixel entlang einer der Achsen aufweist. Wenn Sie also einen festen "Binärpunkt" (analog zum Dezimalpunkt) in die Mitte setzen, können Sie Pixelpositionen von 0 bis 65536 mit einer weiteren Subpixelauflösung von 16 Bit darstellen. Sie können dann wie gewohnt Zahlen hinzufügen. Sie müssen nur daran denken, durch Konvertieren nach rechts 16 Bit zu konvertieren, wenn Sie in den Bildschirmbereich konvertieren oder wenn Sie zwei Zahlen miteinander multipliziert haben.

(Ich habe damals eine 3D-Engine mit 16-Bit-Festkomma geschrieben, als ich einen Intel 386 ohne Gleitkommaeinheit hatte. Sie erreichte die Blendgeschwindigkeit von 200 Phong-schattierten Polygonen pro Frame.)

pjc50
quelle
1

Zusätzlich zu den anderen Antworten hier können Sie in gewissem Umfang Dithering verwenden. Beim Dithering ist die Kante der Pixel eines Objekts heller / dunkler, um dem Hintergrund zu entsprechen, wodurch eine weichere Kante entsteht. Angenommen, Sie hatten ein 4-Pixel-Quadrat, mit dem ich ungefähr rechnen werde:

OO
OO

Wenn Sie dieses halbe Pixel nach rechts verschieben würden, würde sich nichts wirklich bewegen. Aber mit etwas Zittern könnte man ein bisschen Illusion von Bewegung machen. Betrachten Sie O als schwarz und o als grau, also könnten Sie Folgendes tun:

oOo
oOo

In einem Frame und

 OO
 OO

Im nächsten. Das tatsächliche "Quadrat" mit den grauen Kanten würde tatsächlich 3 Pixel Durchmesser haben, aber da sie heller grau als das Schwarz sind, erscheinen sie kleiner. Dies kann jedoch schwierig zu codieren sein, und mir ist derzeit nichts bekannt, was dies bewirkt. Ungefähr zu der Zeit, als sie anfingen, Dithering für Dinge zu verwenden, wurden die Auflösungen schnell besser und es gab nicht so viel Bedarf, außer bei solchen Dingen wie der Bildmanipulation.

Serpardum
quelle