3D-Grafik auf dem STM32F103

Bild

Eine kurze Geschichte darüber, wie man die nicht bearbeitbaren und dreidimensionalen Echtzeitgrafiken mit einem Controller anzeigt, der weder Geschwindigkeit noch Speicher dafür hat.

Bereits 2017 (gemessen am Änderungsdatum der Datei) habe ich beschlossen, von AVR-Controllern auf leistungsstärkere STM32 umzusteigen. Der erste Controller war natürlich der weit verbreitete F103. Es ist nicht weniger selbstverständlich, dass die Verwendung von Standard-Debug-Boards zugunsten der Herstellung eines von Grund auf neu gemäß seinen Anforderungen abgelehnt wurde. Seltsamerweise gab es fast keine Pfosten (außer dass UART1 an einen normalen Stecker angeschlossen und nicht an der Verkabelung gekratzt werden sollte).

Im Vergleich zu AVR sind die Eigenschaften des Steins recht anständig: 72-MHz-Takt (in der Praxis können Sie auf 100 MHz oder sogar mehr übertakten, jedoch nur auf eigene Gefahr und Gefahr!), 20 kB RAM und 64 kB Flash. Eine Tonne Peripheriegeräte, bei denen das Hauptproblem darin besteht, keine Angst vor dieser Fülle zu haben und zu erkennen, dass Sie nicht alle zehn Register schaufeln müssen, um zu starten, reicht es aus, drei Bits in die richtigen zu setzen. Zumindest bis du etwas Seltsames willst.

Als die erste Euphorie aus dem Besitz einer solchen Macht vorüberging, entstand der Wunsch, ihre Grenzen zu erforschen. Als effektives Beispiel habe ich die Berechnung dreidimensionaler Grafiken mit all diesen Matrizen, Beleuchtung, polygonalen Modellen und einem Z-Puffer mit einem 320x240-Display auf dem ili9341-Controller gewählt. Die zwei offensichtlichsten zu lösenden Probleme sind Geschwindigkeit und Lautstärke. Eine Bildschirmgröße von 320 x 240 bei 16 Bit pro Farbe ergibt 150 kB pro Bild. Der gesamte Arbeitsspeicher beträgt jedoch nur 20 KB ... Und diese 150 KB müssen mindestens 10 Mal pro Sekunde auf das Display übertragen werden, dh der Wechselkurs sollte mindestens 1,5 MB / s oder 12 MB / s betragen, was bereits eine erhebliche Belastung des Kerns darstellt. Glücklicherweise gibt es in diesem Controller ein RAP-Modul (direkter Zugriff auf den Speicher, auch bekannt als Direct Memory Access, DMA), mit dem Sie den Kernel nicht mit Transfusionsvorgängen von leer nach leer laden können.Das heißt, Sie können einen Puffer vorbereiten, dem Modul mitteilen, dass Sie hier den Datenpuffer haben, arbeiten!, Und zu diesem Zeitpunkt die Daten für die nächste Übertragung vorbereiten. Unter Berücksichtigung der Fähigkeit der Anzeige, Daten in einem Stream zu empfangen, ergibt sich der folgende Algorithmus: Der vordere Puffer wird hervorgehoben, von dem der DMA Daten an die Anzeige überträgt, der hintere Puffer, in den das Rendern erfolgt, und der Z-Puffer, der zum Schneiden in die Tiefe verwendet wird. Puffer sind eine einzelne Zeile (oder Spalte, was auch immer) der Anzeige. Und statt 150 kB benötigen wir nur 1920 Bytes (320 Pixel pro Zeile * 3 Puffer * 2 Bytes pro Punkt), was perfekt in den Speicher passt. Der zweite Hack basiert auf der Tatsache, dass die Berechnung von Transformationsmatrizen und Scheitelpunktkoordinaten nicht für jede Zeile durchgeführt werden kann, da sonst das Bild auf bizarrste Weise verzerrt wird und die Geschwindigkeit nachteilig ist. Stattdessen "externe" Berechnungen,Das heißt, die Multiplikation von Transformationsmatrizen und ihre Anwendung auf die Scheitelpunkte werden in jedem Frame neu berechnet und dann in eine Zwischendarstellung konvertiert, die für das Rendern in einem 320x1-Bild optimiert ist.

Aus Hooligan-Gründen ähnelt die Bibliothek von außen OpenGL. Wie in der ursprünglichen OpenGL beginnt das Rendern mit der Bildung der Transformationsmatrix. Durch Löschen von glLoadIdentity () wird die aktuelle Matrixeinheit erstellt, dann wird eine Reihe von Transformationen glRotateXY (...), glTranslate (...) erstellt, die jeweils mit der aktuellen Matrix multipliziert werden. Da diese Berechnungen nur einmal pro Frame durchgeführt werden, gibt es keine besonderen Anforderungen an die Geschwindigkeit. Sie können mit einfachen Floats ohne Perversionen mit Festkommazahlen arbeiten. Die Matrix selbst ist ein Array von float [4] [4], das einem eindimensionalen Array von float [16] zugeordnet ist. Tatsächlich wird diese Methode normalerweise für dynamische Arrays verwendet, aber Sie können auch ein wenig von statischen Arrays profitieren. Ein weiterer Standard-Hack: Anstatt ständig Sinus und Cosinus zu berechnen, die in den Rotationsmatrizen häufig vorkommen,Zählen Sie sie im Voraus und schreiben Sie sie auf das Tablet. Teilen Sie dazu den vollen Kreis in 256 Teile, berechnen Sie den Sinuswert für jeden und geben Sie ihn in das Array sin_table [] ein. Nun, jeder aus der Schule kann den Kosinus vom Sinus bekommen. Es ist zu beachten, dass die Rotationsfunktionen nach Reduzierung auf den Bereich [0 ... 255] keinen Winkel im Bogenmaß, sondern in Bruchteilen einer vollen Umdrehung einnehmen. Es wurden jedoch "ehrliche" Funktionen implementiert, die die Umwandlung von Winkel zu Lappen unter der Haube durchführen.Durchführung der Umwandlung von Winkel zu Lappen unter der Haube.Durchführung der Umwandlung von Winkel zu Lappen unter der Haube.

Wenn die Matrix fertig ist, können Sie mit dem Zeichnen der Grundelemente beginnen. Im Allgemeinen gibt es in dreidimensionalen Grafiken drei Arten von Grundelementen - einen Punkt, eine Linie und ein Dreieck. Wenn wir uns jedoch für polygonale Modelle interessieren, sollte nur das Dreieck berücksichtigt werden. Sein "Rendern" erfolgt in der Funktion glDrawTriangle () oder glDrawTriangleV (). Das Wort "Rendern" wird in Anführungszeichen gesetzt, da zu diesem Zeitpunkt kein Rendern erfolgt. Wir multiplizieren einfach alle Punkte des Grundelements mit der Transformationsmatrix und extrahieren daraus die analytischen Formeln der Kanten y = ky * x + mit, die es uns ermöglichen, die Schnittpunkte aller drei Kanten des Dreiecks mit der aktuellen Ausgangslinie zu finden. Wir verwerfen einen von ihnen, da er nicht auf dem Intervall zwischen den Eckpunkten liegt, sondern auf seiner Fortsetzung.Das heißt, um einen Rahmen zu zeichnen, müssen Sie nur alle Linien durchgehen und für jede Farbe den Bereich zwischen den Schnittpunkten zeichnen. Wenn Sie diesen Algorithmus jedoch "frontal" anwenden, überlappt jedes Grundelement die zuvor gezeichneten. Wir müssen die Z-Koordinate (Tiefe) berücksichtigen, damit sich die Dreiecke schön schneiden. Anstatt einfach Punkt für Punkt zu drucken, betrachten wir die Z-Koordinate und geben sie im Vergleich zu der im Tiefenpuffer gespeicherten Z-Koordinate entweder aus (aktualisieren den Z-Puffer) oder ignorieren sie. Und um die Z-Koordinate jedes Punktes der für uns interessanten Linie zu berechnen, verwenden wir dieselbe Geradenformel z = kz * y + bz, die durch dieselben zwei Schnittpunkte mit Kanten berechnet wird. Infolgedessen besteht das Objekt der "halbfertigen" Dreiecksstruktur glTriangle aus drei X-Koordinaten der Eckpunkte (es macht keinen Sinn, die Y- und Z-Koordinaten zu speichern, sie werden berechnet) und k,b direkte Koeffizienten, na ja, Farbe zum Haufen. Hier ist im Gegensatz zur Berechnung von Transformationsmatrizen die Geschwindigkeit kritisch, so dass wir bereits Festkommazahlen verwenden. Wenn außerdem für den Term b die gleiche Genauigkeit wie für die Koordinaten (2 Bytes) ausreicht, ist die Genauigkeit des Faktors k umso größer, je besser, also nehmen wir 4 Bytes. Aber kein Float, da die Arbeit mit ganzen Zahlen auch bei gleicher Größe noch schneller ist.

Durch Aufrufen einer Reihe von glDrawTriangle () haben wir eine Reihe von halbfertigen Dreiecken vorbereitet. In meiner Implementierung werden Dreiecke einzeln durch explizite Funktionsaufrufe abgeleitet. In der Tat wäre es logisch, eine Reihe von Dreiecken mit den Adressen der Eckpunkte zu haben, aber hier habe ich beschlossen, nicht zu komplizieren. Wie auch immer, die Rendering-Funktion wird von Robotern geschrieben, und es spielt für sie keine Rolle, ob sie ein konstantes Array ausfüllen oder dreihundert identische Aufrufe schreiben. Es ist Zeit, die Halbzeuge der Dreiecke in ein schönes Bild auf dem Bildschirm zu übersetzen. Dazu wird die Funktion glSwapBuffers () aufgerufen. Wie oben beschrieben, geht es durch die Linien der Anzeige, sucht nach jedem Schnittpunkt mit allen Dreiecken und zeichnet Segmente gemäß der Filterung nach Tiefe. Nach dem Rendern jeder Zeile müssen Sie diese Zeile an die Anzeige senden. Zu diesem Zweck wird DMA gestartet, das die Adresse der Zeichenfolge und ihre Größe angibt.In der Zwischenzeit funktioniert DMA. Sie können zu einem anderen Puffer wechseln und die nächste Zeile rendern. Die Hauptsache ist, nicht zu vergessen, auf das Ende der Übertragung zu warten, wenn Sie das Rendern plötzlich früher beendet haben. Um das Verhältnis der Geschwindigkeiten zu visualisieren, habe ich nach dem Ende des Renderns eine rote LED hinzugefügt und nach Abschluss der DMA-Wartezeit ausgeschaltet. Es stellt sich so etwas wie PWM heraus, das die Helligkeit abhängig von der Latenz anpasst. Theoretisch könnten anstelle eines „dummen“ Wartens DMA-Interrupts verwendet werden, aber dann könnte ich sie nicht verwenden, und der Algorithmus wäre viel komplizierter geworden. Für ein Demo-Programm ist dies redundant.Um das Verhältnis der Geschwindigkeiten zu visualisieren, habe ich nach dem Ende des Renderns eine rote LED hinzugefügt und nach Abschluss der DMA-Wartezeit ausgeschaltet. Es stellt sich so etwas wie PWM heraus, das die Helligkeit abhängig von der Latenz anpasst. Theoretisch könnten anstelle eines "dummen" Wartens DMA-Interrupts verwendet werden, aber dann könnte ich sie nicht verwenden, und der Algorithmus wäre viel komplizierter geworden. Für ein Demo-Programm ist dies redundant.Um das Verhältnis der Geschwindigkeiten zu visualisieren, habe ich nach dem Ende des Renderns eine rote LED hinzugefügt und nach Abschluss der DMA-Wartezeit ausgeschaltet. Es stellt sich so etwas wie PWM heraus, das die Helligkeit abhängig von der Latenz anpasst. Theoretisch könnten anstelle eines „dummen“ Wartens DMA-Interrupts verwendet werden, aber dann könnte ich sie nicht verwenden, und der Algorithmus wäre viel komplizierter geworden. Für ein Demo-Programm ist dies redundant.

Das Ergebnis der obigen Verfahren war ein rotierendes Bild von drei sich kreuzenden Ebenen unterschiedlicher Farben und mit einer recht anständigen Geschwindigkeit: Die Helligkeit der roten LED ist ziemlich hoch, was auf einen großen Spielraum bei der Kernelleistung hinweist.

Wenn der Kern im Leerlauf ist, müssen Sie ihn laden. Und wir werden es mit besseren Modellen laden. Vergessen Sie jedoch nicht, dass der Speicher immer noch sehr begrenzt ist, damit der Controller nicht zu viele Polygone physisch zieht. Die einfachste Berechnung ergab, dass nach Subtrahieren des Speichers auf dem Zeilenpuffer und dergleichen ein Platz für 378 Dreiecke vorhanden war. Wie die Praxis gezeigt hat, sind Modelle aus dem alten, aber interessanten Gothic-Spiel perfekt für diese Größe. Tatsächlich wurden die Modelle einer Schlange und einer Blutfliege von dort herausgezogen (und bereits zum Zeitpunkt des Schreibens dieses Artikels und eines Glocoor, der auf KDPV zur Schau gestellt wurde), wonach dem Controller der Flash-Speicher ausgegangen war. Spielmodelle sind jedoch nicht für die Verwendung durch einen Mikrocontroller vorgesehen.

Nehmen wir an, sie enthalten Animationen, Texturen und dergleichen, was für uns nicht nützlich ist und nicht in den Speicher passt. Glücklicherweise können Sie mit Blender nicht nur in * .obj speichern, was für das Parsen besser geeignet ist, sondern bei Bedarf auch die Anzahl der Polygone reduzieren. Mit Hilfe eines einfachen selbstgeschriebenen Programms obj2arr * .obj werden die Dateien in Koordinaten sortiert, aus denen anschließend eine * .h-Datei zur direkten Aufnahme in die Firmware gebildet wird.

Aber im Moment sehen die Modelle genauso aus wie einfache lockige Flecken. Beim Testmodell hat uns das nicht gestört, da alle Gesichter in ihren eigenen Farben gemalt wurden, aber nicht jedem Polygon des Modells die gleichen Farben vorschreiben. Nein, Sie können natürlich eine Fliege in zufälligen Farben malen, aber sie wird aus heiterem Himmel hübsch aussehen, habe ich überprüft. Vor allem, wenn sich die Farben auch bei jedem Frame ändern ... Wenden Sie stattdessen einen weiteren Tropfen Vektormagie an und fügen Sie Beleuchtung hinzu.

Die Berechnung der Beleuchtung in ihrer primitiven Version besteht aus der Berechnung des Skalarprodukts der Normalen und der Richtung zur Lichtquelle, gefolgt von der Multiplikation mit der „nativen“ Gesichtsfarbe.
Wir haben jetzt drei Modelle - zwei aus dem Spiel und einen Test, von dem wir ausgegangen sind. Um sie zu wechseln, verwenden wir eine der beiden auf der Platine gelöteten Tasten. Gleichzeitig können Sie die Kontrolle über den Prozessor hinzufügen. Wir haben bereits eine Steuerung - eine rote LED, die mit der DMA-Latenz verbunden ist. Und die zweite grüne LED blinkt bei jedem Frame-Update - damit wir die Framerate abschätzen können. Für das bloße Auge waren es ungefähr 15 fps.


Im Allgemeinen bin ich mit dem Ergebnis zufrieden: Es ist schön, etwas zu implementieren, das grundsätzlich nicht direkt zu lösen ist. Natürlich gibt es noch viel zu optimieren und zu verbessern, aber das hat wenig Sinn. Objektiv gesehen ist der Controller für dreidimensionale Grafiken schwach und es geht nicht einmal um Geschwindigkeit, sondern um RAM. Wie jede Demoszenenprobe ist dieses Projekt jedoch nicht durch das Ergebnis, sondern durch den Prozess wertvoll.

Wenn jemand plötzlich interessiert, ist der Quellcode verfügbar hier .

All Articles