Kreisbewegung auf Hardware mit geringem Stromverbrauch

10

Ich dachte an Plattformen und Feinde, die sich in alten 2D-Spielen im Kreis bewegten, und fragte mich, wie das gemacht wurde. Ich verstehe parametrische Gleichungen und es ist trivial, sin und cos zu verwenden, aber könnte ein NES oder SNES Echtzeit-Trigger-Aufrufe ausführen? Ich gebe schwere Unwissenheit zu, aber ich dachte, das wären teure Operationen. Gibt es eine clevere Möglichkeit, diese Bewegung billiger zu berechnen?

Ich habe daran gearbeitet, einen Algorithmus aus Triggersummenidentitäten abzuleiten, der nur vorberechnete Trigger verwendet, der jedoch kompliziert zu sein scheint.

Akroy
quelle
1
Diese Frage wurde mir vor einigen Jahren während eines Vorstellungsgesprächs gestellt.
Crashworks

Antworten:

14

Auf Hardware, wie Sie sie beschreiben, besteht eine übliche Lösung für den allgemeinen Fall darin, einfach eine Nachschlagetabelle für die Trigonometriefunktionen zu erstellen, an denen Sie interessiert waren, manchmal in Verbindung mit Festpunktdarstellungen für Werte.

Das potenzielle Problem bei dieser Technik besteht darin, dass sie Speicherplatz beansprucht. Sie können dies jedoch herunterspielen, indem Sie sich für eine niedrigere Auflösung der Daten in Ihrer Tabelle entscheiden oder die periodische Natur einiger Funktionen nutzen, um weniger Daten zu speichern und zur Laufzeit zu spiegeln.

Für das spezifische Durchlaufen von Kreisen - entweder um sie zu rastern oder um etwas entlang eines zu bewegen - kann jedoch eine Variation des Bresenham-Linienalgorithmus verwendet werden . Der eigentliche Algorithmus von Bresenham ist natürlich auch nützlich, um Linien, die nicht in den acht "primären" Richtungen liegen, recht billig zu durchqueren.


quelle
2
Wahre Geschichte. LUT und ein Kreis sind definiert als 256 Grad ergeben billigen Trigger. Die Spiegelung wurde nur durchgeführt, wenn der Speicher knapp war und als letztes Mittel, um ein paar Bytes zu gewinnen. Die Bresenham-Referenz ist auch für verschiedene Bewegungen genau richtig.
Patrick Hughes
4
Selbst auf moderner Hardware ist ein Trigger-Aufruf immer noch eine Nachschlagetabelle. Es ist nur eine Nachschlagetabelle in Hardware, die durch eine Taylor-Erweiterung verfeinert wurde. (Tatsächlich ist die Implementierung einer SIMD sin () -Funktion durch einen großen Konsolenhersteller einfach eine fest codierte Taylor-Serie.)
Crashworks
3
@ Crashworks: Es gibt absolut keine Möglichkeit, dass es eine Taylor-Serie ist, es wäre wirklich dumm von ihnen. Es ist höchstwahrscheinlich ein Minimax-Polynom. Tatsächlich basieren alle modernen Implementierungen von sin (), die ich jemals gesehen habe, auf Minimax-Polynomen.
Sam Hocevar
@ SamHocevar Könnte sein. Ich habe gerade die Zusammenfassung von ax + bx ^ 3 + cx ^ 5 + ... gesehen und "Taylor-Serie" angenommen.
Crashworks
9

Es gibt eine Variation von Bresenhams Algorithmus von James Frith , die noch schneller sein sollte, da sie die Multiplikation vollständig eliminiert. Es braucht keine Nachschlagetabelle, um dies zu erreichen, obwohl man die Ergebnisse in einer Tabelle speichern könnte, wenn der Radius konstant bleibt. Da sowohl der Bresenham- als auch der Frith-Algorithmus eine 8-fache Symmetrie verwenden, wäre diese Nachschlagetabelle relativ kurz.

// FCircle.c - Draws a circle using Frith's algorithm.
// Copyright (c) 1996  James E. Frith - All Rights Reserved.
// Email:  [email protected]

typedef unsigned char   uchar;
typedef unsigned int    uint;

extern void SetPixel(uint x, uint y, uchar color);

// FCircle --------------------------------------------
// Draws a circle using Frith's Algorithm.

void FCircle(int x, int y, int radius, uchar color)
{
  int balance, xoff, yoff;

  xoff = 0;
  yoff = radius;
  balance = -radius;

  do {
    SetPixel(x+xoff, y+yoff, color);
    SetPixel(x-xoff, y+yoff, color);
    SetPixel(x-xoff, y-yoff, color);
    SetPixel(x+xoff, y-yoff, color);
    SetPixel(x+yoff, y+xoff, color);
    SetPixel(x-yoff, y+xoff, color);
    SetPixel(x-yoff, y-xoff, color);
    SetPixel(x+yoff, y-xoff, color);

    balance += xoff++;
    if ((balance += xoff) >= 0)
        balance -= --yoff * 2;

  } while (xoff <= yoff);
} // FCircle //
ProphetV
quelle
Wenn Sie seltsame Ergebnisse erzielen, liegt dies daran, dass Sie undefiniertes (oder zumindest nicht angegebenes) Verhalten aufrufen . C ++ gibt nicht an, welcher Aufruf zuerst ausgewertet wird, wenn "a () + b ()" ausgewertet wird, und ruft außerdem modifizierende Integrale auf. Um dies zu vermeiden, ändern Sie eine Variable nicht in demselben Ausdruck, in dem Sie sie gelesen haben, wie in xoff++ + xoffund --yoff + yoff. Ihre Änderungsliste wird dies beheben. Ziehen Sie in Betracht, sie an Ort und Stelle zu reparieren, anstatt sie als Hinweis zu verwenden. (Siehe Abschnitt 5 Absatz 4 des C ++ - Standards für Beispiele und den Standard, der dies ausdrücklich
ausruft
@MaulingMonkey: Sie haben Recht mit der problematischen Bewertungsreihenfolge von balance += xoff++ + xoffund balance -= --yoff + yoff. Ich habe dies unverändert gelassen, da Friths Algorithmus ursprünglich so geschrieben wurde und der Fix später von ihm selbst hinzugefügt wurde (siehe hier ). Jetzt behoben.
ProphetV
2

Sie können auch eine ungefähre Version der Triggerfunktionen mithilfe von Taylor Expansions http://en.wikipedia.org/wiki/Taylor_series verwenden

Zum Beispiel können Sie eine vernünftige Annäherung an den Sinus verwenden, indem Sie die ersten vier Begriffe der Taylorreihe verwenden

Sinus

Gemeinschaft
quelle
Dies ist im Allgemeinen richtig, bringt jedoch so viele Einschränkungen mit sich, dass ich sogar sagen würde, Sie sollten praktisch nie Ihren eigenen sin () - Code schreiben, es sei denn, Sie sind mit dem, was Sie tun, sehr vertraut. Insbesondere gibt es (geringfügig) bessere Polynome als die aufgelisteten, noch bessere rationale Näherungen, und Sie müssen verstehen, wo Sie die Formel anwenden und wie Sie die Periodizität von sin und cos verwenden, um Ihre Argumentation auf einen Bereich einzugrenzen, in dem die Serie gilt. Dies ist einer der Fälle, in denen der alte Aphorismus „ein bisschen Wissen ist eine gefährliche Sache“ wahr ist.
Steven Stadnicki
Können Sie einige Referenzen angeben, damit ich diese Polynome oder andere bessere Näherungen lernen kann? Das möchte ich wirklich lernen. Diese Serie war der umwerfendste Teil meines Kalkülkurses.
Der klassische Ausgangspunkt ist das Buch Numerical Recipes, das eine Reihe von Informationen zur Berechnung der wichtigsten numerischen Funktionen und der Mathematik hinter ihren Annäherungen enthält. Ein anderer Ort, an dem Sie nach einem etwas veralteten, aber dennoch wissenswerten Ansatz suchen könnten, ist das Nachschlagen des sogenannten CORDIC- Algorithmus.
Steven Stadnicki
@Vandell: Wenn Sie Minimax-Polynome erstellen möchten, würde ich mich über Ihre Gedanken zu LolRemez freuen .
Sam Hocevar
Die Taylor-Reihe approximiert das Verhalten einer Funktion um einen einzelnen Punkt, nicht um ein Intervall. Das Polynom eignet sich hervorragend zur Auswertung von sin (0) oder seiner siebten Ableitung um x = 0, aber der Fehler bei x = pi / 2, nach dem Sie einfach spiegeln und wiederholen können, ist ziemlich groß. Sie können ungefähr fünfzig Mal besser abschneiden, indem Sie stattdessen die Taylor-Reihe um x = pi / 4 auswerten. Was Sie jedoch wirklich wollen, ist ein Polynom, das den maximalen Fehler im Intervall auf Kosten der Genauigkeit in der Nähe eines einzelnen Punkts minimiert.
Marcks Thomas
2

Ein großartiger Algorithmus, um gleichmäßig über einen Kreis zu fahren, ist der Goertzel-Algorithmus . Es sind nur 2 Multiplikationen und 2 Additionen pro Schritt, keine Nachschlagetabelle und ein sehr minimaler Zustand (4 Zahlen) erforderlich.

Definieren Sie zunächst einige Konstanten, möglicherweise fest codiert, basierend auf der erforderlichen Schrittgröße (in diesem Fall 2π / 64):

float const step = 2.f * M_PI / 64;
float const s = sin(step);
float const c = cos(step);
float const m = 2.f * c;

Der Algorithmus verwendet 4 Zahlen als Status, die wie folgt initialisiert wurden:

float t[4] = { s, c, 2.f * s * c, 1.f - 2.f * s * s };

Und zum Schluss die Hauptschleife:

for (int i = 0; ; i++)
{
    float x = m * t[2] - t[0];
    float y = m * t[3] - t[1];
    t[0] = t[2]; t[1] = t[3]; t[2] = x; t[3] = y;
    printf("%f %f\n", x, y);
}

Es kann dann für immer gehen. Hier sind die ersten 50 Punkte:

Goertzel-Algorithmus

Der Algorithmus kann natürlich auf Festkomma-Hardware arbeiten. Der klare Sieg gegen Bresenham ist die konstante Geschwindigkeit über den Kreis.

Sam Hocevar
quelle