Das Sprite in Half-Life ist eine RGB Bitmap mit 8 Bit Farbpalette (256 Farben). Das technische Grundgerüst stammt direkt vom Quake Spriteformat ab, wobei es aber einige signifikante Unterschiede zwischen dem Quake Format und dem Half-Life Format gibt! Im Klartext: Quake Sprites und Half-Life Sprites sind technisch inkompatibel zueinander. Valve hatte seinerzeit einige wichtige und sinnvolle Erweiterungen vorgenommen, z.B. das jedes Sprite seine eigene Farbpalette besitzt.
Im Kern besteht das Sprite aus drei Komponenten:
Der Header besitzt genau wie die Palette eine feste Größe. Der einzig variable Teil sind die Frames.
Hinweis: Die benutzten Datentypen entsprechen der allgemeinen Beschreibung und sind nicht an eine bestimmte Programmiersprache gebunden. In diesem Artikel findet ihr eine kurze Definition der internationalisierten Bezeichnungen.
Gesamte Größe = 40 Bytes (0x00 bis 0x27)
| Byte offset | Typ | Inhalt | Beschreibung |
|---|---|---|---|
| 0x00-0x03 | BYTE[4] | „IDSP“ | enthält die Zeichenkombination „IDSP“ ( id Software Sprite ). |
| 0x04-0x07 | DWORD | Version | enthält die Versionsnummer des Spriteformats. 1 = Quake, 2 = Half-Life |
| 0x08-0x0B | DWORD | Orientierung | enthält die Orientierung (Fixierungstyp) des Sprites siehe » Orientierung |
| 0x0C-0x0E | DWORD | Renderformat | enthält das Renderformat des Sprites siehe » Renderformate |
| 0x10-0x13 | FLOAT | Radius | enthält den Radius des Sprites. Hiermit ist die sogenannte Bounding Box des Sprites vom grafischen Mittelpunkt aus gemeint, wie man sie im Editor sehen kann. Im Allgemeinen ist der Radius 1.0 und kann im Editor geändert werden. Man spricht daher auch vom Skalierungsfaktor. |
| 0x14-0x17 | DWORD | Breite | enthält die Breite des Sprites in Pixel. Die Angaben hier beziehen sich auf ein Radius von 1.0, also die Ursprungsmaße des Sprites. Technisch betrachtet entspricht die Breite dem Offset des Bitmapbereichs zwischen zwei Pixelreihen, also z.B. zwischen dem Beginn der ersten Reihe und dem Beginn der nachfolgenden Reihe. Weitere Anmerkungen dazu am Ende |
| 0x18-0x1B | DWORD | Höhe | enthält die Höhe des Sprites in Pixel. Die Angaben hier beziehen sich auf ein Radius von 1.0, also die Ursprungsmaße des Sprites. Technisch betrachtet entspricht die Höhe der Anzahl der Pixelreihen des Bitmapbereichs. Weitere Anmerkungen dazu am Ende |
| 0x1C-0x1E | DWORD | Anzahl der Frames | enthält die Anzahl der Frames, die das Sprite gespeichert hat. Valves Maximum liegt bei 1000 möglichen Frames. |
| 0x20-0x23 | FLOAT | Beamlänge | enthält die Beamlänge des Sprites. Die Beamlänge definiert eine Verschiebung der Ursprungskoordinaten des Sprites siehe » Beamlänge |
| 0x24-0x27 | DWORD | Synchronisierung | 0 = synchron, 1 = zufällig. Die Synchronisierung legt fest, ob ein Sprite synchron oder zufällig durch seine Framesgruppen geht siehe » Framesgruppen |
Hinweis Das Quake Spriteformat ist dagegen nur 36 Bytes groß, denn in Version 1 der Spezifikation (welches für Quake Sprites gilt) fehlt die Angabe zum Renderformat.
struct spriteheader { int8_t idsp[4]; int32_t version; int32_t type; int32_t renderformat; float radius; int32_t maxwidth; int32_t maxheight; int32_t frames; float beamlength; int32_t synctype; };
Hinweis: Der Header wurde ursprünglich von id-Software/Valve für 32 Bit Systeme entworfen, wo long auch 32 Bit breit ist. Aktuelle 64 Bit Systeme können für long allerdings 64 Bit bereitstellen, weshalb es sich empfiehlt fixed stdint Types zu verwenden (wie geschehen) die in <stdint.h> zu finden sind.
public class spriteheader { public byte idsp[] = new byte[4]; public int version; public int type; public int renderformat; public float radius; public int maxwidth; public int maxheight; public int frames; public float beamlength; public int synctype; }
Hinweis: Die Variable id muß später beim Anzeigen auf dem Bildschirm usw. vorher entweder in ein char Array oder gleich in einen String konvertiert werden. In Java ist der Typ int immer 4 Bytes groß.
Nachfolgend die Werte für einen Spriteheader mit seinen Standard-, Minimum- und Maximumwerten:
| Bezeichnung | Standard | Minimum | Maximum | Hinweise |
|---|---|---|---|---|
| IDSP | „IDSP“ | Muß immer „IDSP“ sein. | ||
| Version | 2 | 1 | 2 | Muß immer 2 sein. |
| Orientierung | 2 | 0 | 4 | |
| Renderformat | 0 | 0 | 3 | |
| Radius | 1.0 | > 0.0 | Das Maximum ist mir nicht bekannt, sollte aber realistisch bleiben. Ein Radius von 5000.0 würde ein Sprite theoretisch 5000 mal größer als das Original skalieren. Ebenso verhält es sich mit dem Minimum. Ein Radius von 0.001 würde ein Sprite theoretisch 1000 mal kleiner als das Original skalieren. | |
| Breite | 16 | 16 | 256 | Die Breite sollte gleich der Höhe sein, wobei nichtquadratische Sprites auch möglich sind. |
| Höhe | 16 | 16 | 256 | Dasselbe gilt auch für die Höhenangabe. |
| Anzahl der Frames | 1 | 1 | 1000 | Valve hat definiert, daß es maximal 1000 Frames geben darf. |
| Beamlänge | 0.0 | -1.0 | 1.0 | Eine Beamlänge von z.B. 0.5 verschiebt den Ursprung des Sprites um die Hälfte seiner Breite und Höhe. |
| Synchronisierung | 1 | 0 | 1 |
| Wert | Typ | Beschreibung |
|---|---|---|
| 0x00 | Normal | Die Pixel des Sprite werden ohne Farbberechnungen dargestellt |
| 0x01 | Additive | Alle Schwarzanteile einer Farbe werden mit den Pixeln des Hintergrund verrechnet |
| 0x02 | Indexalpha | Graustufen. Die Palettenposition 255 enthält die Farbe, in der das Sprite dann nachträglich getönt wird. |
| 0x03 | Alphatest | Die Palettenposition 255 enhält die Alphafarbe des Sprite. Alle Pixel, die diese Palettenposition benutzen, werden transparent dargestellt |
| Wert | Typ | Beschreibung |
|---|---|---|
| 0x00 | VP Parallel Upright | Z Achse ist fixiert |
| 0x01 | Facing Upright | Schiefe Ansicht, frei beweglich *) |
| 0x02 | VP Parallel | Frei beweglich (Standardwert) |
| 0x03 | Oriented | X, Y und Z Achsen sind fixiert |
| 0x04 | VP Parallel Oriented | Schiefe Ansicht, Z Achse ist fixiert *) |
*) = Diese Orientierungen sind im Spiel selber untauglich und sehen mehr als merkwürdig aus. Sie sollte daher nicht verwendet werden, außer man hat Augenkrebs… oder will ihn bekommen.
Die Beamlänge dient der Verschiebung der Ursprungskoordinaten eines Sprites. Diese Verschiebung wird erst nach vollständigen Ausrichtung der angegebenen Orientierung durchgeführt. Der Koordinatenursprung eines Sprites liegt standardmäßig genau in der Mitte des Sprites:
Breite = 32 Pixel, Höhe = 32 Pixel ⇒ (16, 16)
Die Beamlänge kann einen negativen als auch einen positiven Wert haben. Allerdings darf dabei das Minimum von -1.0 bzw. das Maximum von 1.0 nicht unter- bzw. überschritten werden (anscheinend ist die maximale Ausdehung des Sprites in Pixeln die natürliche Grenze). Die Formel dazu ist simpel:
(Breite / 2 * Beamlänge, Höhe / 2 * Beamlänge)
Folgende Beispieltabelle gibt Aufschluß bei einem Sprite mit der Größe 64×64 Pixel:
| Beamlänge | Ursprung | Neuer Ursprung |
|---|---|---|
| 0.0 | 32,32 | 32,32 |
| 0.5 | 32,32 | 16,16 |
| -0.2 | 32,32 | 6.4,6.4 → 6,6 |
So wie es aussieht kann die Ursprungsverschiebung nur synchron auf beide Dimensionen durchgeführt werden. Da ich mir aber da nicht so sicher bin, werde ich diesbezüglich nochmals Nachforschungen anstellen.
Direkt nach dem Header kommt die Palette des Sprites. Eingeleitet wird sie durch einen 2 Byte großen Farbzähler, welcher angibt, wieviele Farben die Palette besitzt. In der Regel sind das immer 256 Farben. Die Farben selbst sind als 3 Byte RGB-Wert nacheinander gespeichert, so daß man also im Endeffekt 256*3 = 768 Bytes an Farbinformationen hat. Ein einzelner RGB Eintrag ist definiert in RR:GG:BB, d.h. jeder einzelne Farbwert liegt im Bereich von 0 (0x00 → aus) bis 255 (0xFF → volle Stärke).
Je nach Prozessorbauart (Little Endian oder Big Endian) können die drei Werte als RR:GG:BB oder (was häufiger ist) als BB:GG:RR vorliegen. Dann muß man entsprechend die Werte umformen. Außerdem sollte man hier byteweise lesen, denn einen 3 Byte großen Datentypen gibt es nicht als nativen Datentypen. Wer ein 4 Bytes großes DWORD als Lesetyp benutzt, der bekommt zwangsläufig einen Byteanteil der nächsten Farbe mit. Also Vorsicht!
Gesamte Größe = 770 Bytes (0x0028 bis 0x0329)
| Absoluter Byte offset | Relativer Byte offset | Typ | Inhalt | Beschreibung |
|---|---|---|---|---|
| 0x0028-0x0029 | 0x0000-0x0002 | WORD | Farbzähler | Hat immer den Wert 256 (0x0100) |
| 0x002A-0x0329 | 0x0003-0x0302 | RGB | Farbdaten | Hier stehen die Farbdaten drin |
Hinweis: Die Angaben zum absoluten Byte Offset beziehen sich auf die gesamte Datei, während der relative Byte Offset sich auf den Anfang der Palettensignatur bezieht. Die absoluten Angaben kann man daher direkt als Einstiegspunkt in Byte gemessen für die Palettensignatur betrachten.
Die eigentlichen Bilder des Sprites sind direkt nach dem Header gespeichert und werden Frames genannt. Da es keine Offset-Zeiger im Header für die einzelnen Frames gibt, sollte man das Sprite also komplett laden, puffern und sich dann byteweise durch den Puffer vorarbeiten. Wo ein neuer Frame anfängt, kann man so pauschal nicht von vornherein sagen, denn die Half-Life Spezifikation unterscheidet zwischen SINGLE (einzel) und GROUP (gruppierte) Frames. Beide Sorten haben unterschiedliche Strukturgrößen.
Die Spezifikation unterscheidet zwischen zwei Sorten von Sprites, nämlich zum einen die Sorte, wo nur ein einzelnes Bild gespeichert ist (Singleframe) und zum anderen die Sorte, wo mehrere Bilder nacheinander in einem einzigen Frame (Groupframe) gespeichert sind.
Singleframes (Einzelbilder) haben folgende Erkennungssignatur:
| Relativer Byte offset | Typ | Inhalt | Beschreibung |
|---|---|---|---|
| 0x00-0x03 | DWORD | Typsignatur | 0 = Singleframe |
| 0x04-0x07 | DWORD | Ursprung X-Achse | Ein Integerwert mit der Koordinate des Ursprungs auf der X-Achse |
| 0x08-0x0B | DWORD | Ursprung Y-Achse | Ein Integerwert mit der Koordinate des Ursprungs auf der Y-Achse |
| 0x0C-0x0F | DWORD | Breite in Pixel | Hier wird nochmals die Breite des Singleframes in Pixel ausgelesen. Er sollte logischerweise mit der Breitenangabe des Headers übereinstimmen. |
| 0x10-0x13 | DWORD | Höhe in Pixel | Selbiges gilt für die Höhe des Singleframes. |
| 0x14-MAX | BYTE | Pixeldaten | Jedes einzelne Byte entspricht einem Pixel. Der Wert ist der Index einer Farbe in der Farbpalette. |
Hinweis: Die Angaben zum relativen Byte Offset beziehen sich auf den Anfang der Framesignatur. Einen absoluten Offset gibt es so nicht bzw. ab dem 810. Byte (0x032A) der Datei beginnen die Framesignaturen. Für weitere Informationen siehe weiter unten!
Der ominöse Bezeichner MAX steht für einen Grenzwert, der das Ende eines Singleframes definiert. Er errechnet sich aus dem Produkt von Framebreite und Framehöhe (Breite x Höhe). Bei einem Frame mit der Dimension 64×64 Pixel wäre MAX also 4096, d.h. es sind 4096 Bytes an Pixeldaten einzulesen.
Groupframes (Multibilder in einem Frame) haben folgende Erkennungssignatur:
| Relativer Byte offset | Typ | Inhalt | Beschreibung |
|---|---|---|---|
| 0x00-0x04 | DWORD | Typsignatur | 1 = Groupframe |
| 0x05-0x08 | DWORD | Anzahl der Groupframes | Ein Integerwert mit der Anzahl der auszulesenden Groupframes |
Jetzt folgen ab dem Byte 0x09 die sogenannten Intervalle. Das sind FLOAT Werte, welche die Anzeigesequenz der Groupframes bestimmen und die als FLOAT Array ausgelesen werden müssen. Man kann so bestimmen, welcher Groupframe wann in der Reihenfolge erscheinen soll.
Angefangen wird mit dem Wert 0.0 (als erstes anzeigen) und aufgehört wird mit 1.0 (als letztes anzeigen). Da die Intervalle auch unsortiert vorliegen dürfen, ist eine Mischsequenz möglich. Intervallwerte für eine lineare Sequenz erhält man, indem man 1.0 durch die Anzahl der Groupframes teilt (1.0 / n). Bei 5 Groupframes wäre das also der Wert 0.2. Der erste Groupframe bekommt die 0.0, der zweite die 0.2, der dritte die 0.4, der vierte die 0.6 und der letzte die 1.0.
Wenn ein Sprite beispielsweise aus 5 Groupframes besteht, dann beziehen die Indizes des Intervallarrays auf den jeweiligen Groupframe:
| Index | ist Groupframe |
|---|---|
| 0 | 1 |
| 1 | 2 |
| … | … |
| N | N+1 |
Aus unerfindlichen Gründen ist das Intervallarray bei jedem einzelnden Groupframe immer wieder mit dabei, obwohl man es nur einmal auslesen müsste. Wer also die Integrität der Intervalle testen möchte, der kann sollte die Intervallarrays aller Groupframes vergleichen. Sie müssen alle dieselben FLOAT Werte besitzen. Trifft dies nicht zu, dann wurde entweder ein Lesefehler gemacht oder das betreffende Intervallarray des Sprites wurde damals bereits mit anderen Werten belegt. Es empfiehlt sich daher, das Intervallarray nur beim ersten Groupframe auszulesen und nachfolgend bei allen anderen Groupframes des Sprites zu überspringen.
Ich muß allerdings zugeben, daß mir bisher kein Groupframe unter die Augen gekommen ist und ich so nicht sagen kann, ob das mit den Intervallen bzw. mit ihrer Bedeutung auch so stimmt. Die vorliegenden Daten habe ich aus diversen Quellen zusammengetragen und ich lehne jede Verantwortung für die Richtigkeit dieser Angaben ab!
Jedes FLOAT ist 4 Byte groß, multipliziert mit der Anzahl der Groupframes ergibt sich so die Größe der einzulesenden FLOAT Werte (z.B. 4 Byte * 5 Frames = 20 Byte). Daher ist der Anfang der nächsten Werte unbestimmt. Ich habe das so gemacht, daß OFFSET das Ende der Intervallwerte darstellt, also nicht verzweifeln:
| Relativer Byte offset | Typ | Inhalt | Beschreibung |
|---|---|---|---|
| OFFSET - OFFSET+0x04 | DWORD | Ursprung X-Achse | Ein Integerwert mit der Koordinate des Ursprungs auf der X-Achse |
| OFFSET+0x05 - OFFSET+0x08 | DWORD | Ursprung Y-Achse | Ein Integerwert mit der Koordinate des Ursprungs auf der Y-Achse |
| OFFSET+0x09 - OFFSET+0x0C | DWORD | Breite in Pixel | Hier wird nochmals die Breite des Groupframes in Pixel ausgelesen. Er sollte logischerweise mit der Breitenangabe des Headers übereinstimmen. |
| OFFSET+0x0D - OFFSET+0x10 | DWORD | Höhe in Pixel | Selbiges gilt für die Höhe des Groupframes. |
| OFFSET+0x11 - MAX | BYTE | Pixeldaten | Jedes einzelne Byte entspricht einem Pixel. Der Wert ist der Index einer Farbe in der Farbpalette. |
Der Bezeichner MAX steht hier ebenfalls für einen Grenzwert, der das Ende eines Groupframes definiert. Er errechnet sich aus dem Produkt von Framebreite und Framehöhe (Breite x Höhe). Bei einem Frame mit der Dimension 64×64 Pixel wäre MAX also 4096, d.h. es sind 4096 Bytes an Pixeldaten einzulesen.
Das Einlesen der Frames erfolgt in einer Schleifenform, d.h. wenn es 18 Frames gibt, dann muß diese Einleseschleife 18 mal durchlaufen werden. Das erste was man in der Schleife tun muß ist das Einlesen der Typsignatur. Daran entscheidet man, ob man einen Single- oder einen Groupframe einliest:
// C/C++/Java Schema int i; for(i = 0; i < frames; ++i) { // Typsignatur einlesen ... // Fallunterscheidung if(type == SINGLE) { // Singleframe einlesen ... } else if(type == GROUP) { // Groupframe einlesen ... } else { // Fehlerhafte Typsignatur ... } }
In Java ist ein Byte (Datentyp byte) immer signed, d.h. vorzeichenbehaftet. Der Wertebereich geht von -128 bis 127. Beim byteweisen Einlesen vom Eingabestream müssen alle Bytes zwingend als unsigned (vorzeichenlos, 0 bis 255) interpretiert werden, denn sonst ist Schluß mit Lustig! Daher müssen die Bytes in einen höherwertigen Datentyp gecastet werden, vorzugsweise nach int. Das erledigt folgender Code in Java:
int unsignedByte = (int)signedByte & 0xFF;
Es werden grundsätzlich alle Bytes einer Spritedatei eingelesen und alle Bytes haben auch einen Sinn! Es gibt keine leere oder unnütze Bytes! Wer also prüfen will, ob alles seine Richtigkeit hat, der muß fehlerfrei die gesamte Spritedatei in Bytes einlesen können. Fehlen Bytes am Ende oder bleiben welche übrig, dann hat man falsch eingelesen bzw. zuviele oder zuwenige Bytes beim Einleseprozeß verbraucht.