Archive für Januar 2010

Ein Dreieck - GU Test

Die GU Bibliothek zur einfachen Erstellung unser ersten 3D Homebrew ist fertig und nun wollen wir natürlich einen ersten kleinen Test durchführen um - zunächsteinmal nur ein Dreieck - im virtuellen Raum auf den realen PSP Bildschirm zu zaubern.

Die in der PspHbc Bibliothek enthaltenen Header Files entpackt ihr am besten in euer PSPSDK Verzeichnis C:\pspsdk\psp\sdk\include das Lib-File kommt dann in das Verzeichnis C:\pspsdk\psp\sdk\lib dadurch wird die Nutzung in eigenen Projekten stark vereinfacht.

Nun aber los. Wie schon bei unserer ersten “normalen” Homebrew beginnt alles mit dem erstellen einer neuen Klasse. Diese wird nun von der 3DHomebrew-Basisklasse abgeleitet. Unsere Beispiel Homebrew soll den Namen HbcGuSample tragen. Die Klasse heißt dann treffender Weise: ClHbcGuSampleApp

/*
 * HbcGuSampleApp.h
 * Beispiel Implementierung der GU Homebrew Basisklasse
 */
#ifndef HBCGUSAMPLEAPP_H_
#define HBCGUSAMPLEAPP_H_
#include <3dHomebrew.h>
class ClHbcGuSampleApp : public Cl3dHomebrew {
public:
static ClHbcGuSampleApp* getInstance();
static void releaseInstance();
// Redefiniren der Render Methode um sie mit eigender Logik zu füllen
void render();protected: ClHbcGuSampleApp();
virtual ~ClHbcGuSampleApp();
static ClHbcGuSampleApp* _instance;
};
#endif /* HBCGUSAMPLEAPP_H_ */

Nachdem die neue Klasse nun definiert ist, können wir diese implementieren. Dabei ist der erste Teil der Implementierung recht einfach - glaube ich - da wir nur eine singleton Instanz erzeugen und diese zurück liefern. Die GU initializeirung bleibt unverändert.

/*
* HbcGuSampleApp.cpp
*
*/
extern “C”{
#include<pspgu.h>
#include<pspgum.h>
}
#include “HbcGuSampleApp.h”
ClHbcGuSampleApp* ClHbcGuSampleApp::_instance = 0;
// liefert singleton Instaz
ClHbcGuSampleApp *ClHbcGuSampleApp::getInstance(){
if(!_instance){
_instance = new ClHbcGuSampleApp();
}
return _instance;
}
//baut die singleton Instanz wieder ab und gibt Resourcen frei
void ClHbcGuSampleApp::releaseInstance(){
if (_instance) {
delete(_instance);
_instance = 0;
}
}

Der für die Klasse generierte Constructor und Destructor bleiben unberührt bzw. leer. Der nun “spannende” Teil befindet sich in der render Methode. Hier wollen wir unser erstes Objekt im virtuellen Raum definieren.

Wichtig:Definitionen von Objekten, Bewegungen und Objektrelationen im 3 dimensionalen Raum nutzen Vektoren und Matrizen. Wer also keinerlei Grundkenntnisse auf diesem Gebiet mitbringt sollte sich diese aneignen befor er hier weiter macht. Wikipedia ist eine gute Addresse um sich diesbezüglich aufzuschlauen. Oftmals helfen einem bei der Veranschaulichung auch ein paar Bleistifte (als Vektoren) und ein Tisch aus. :D

Das Dreieck

Das Dreick wird durch 3 Punkte im virtuellen Raum beschrieben. Diese Punkte erhalten zusätzlich zu ihrer Position noch einen Farbwert, so dass die Fläche die von dem Dreieck umspannt wird entsprechend ausgefüllt wird. Dabei verwenden wir hier für jeden Punkt einen anderen Farbwert um einen netten Farbverlauf zu erzeugen.

void ClHbcGuSampleApp::render(){
// Definition einer Struktur die einen 3D-Punkt beschreibt
typedef struct Vertex {
int color;
float x, y, z;
}Vertex;
//Speicher für 3 Punkte reservieren
//dieser Speicher ist dabei nur temporär und nur innerhalb der aktuellen GU Liste gültig
Vertex* triangle = (Vertex*)sceGuGetMemory(sizeof(Vertex)*3);
//Daten für die Punkte
triangle[0].x = -4.0f;
triangle[0].y = -2.0f;
triangle[0].z = -10.0f;
triangle[0].color = 0xff0000ff;triangle[1].x = 0.0f;
triangle[1].y = 2.0f;
triangle[1].z = -10.0f;
triangle[1].color = 0xffff0000;triangle[2].x = 4.0f;
triangle[2].y = -2.0f;
triangle[2].z = -10.0f;
triangle[2].color = 0xff00ff00;

Als nächsten Schritt wollen wir dieses Dreieck auch zeichnen. Zuerst leeren wir den Bildschirm mit einer von schwarz verschiedenen Farbe (schwarz macht’s die Basisklasse). Dadurch kann mann schneller erkennen ob hier was “gezeichnet” wird oder nicht, da ggfl. schwarz auf schwarz gezeichnete Objekte nicht sichtbar wären…
Nachdem wir den Bildschirm nochmals geleert haben müssen wir die Projektion der GU festlegen. Dafür legen wir für den aktuellen View und für das Objektmodell eine Einheitsmatrix fest. Dies bedeutet, dass weder unsere Kamera, noch unser Objekt im Raum manipuliert werden. Sie werden so dargestellt wie wir sie im Raum platziert haben. Mit der Model-Matrix wären wir z.Bsp. in der Lage das Objekt im Raum zu bewegen oder zu rotieren, ohne die ursprüngliche Definition der Punkte zu verändern. Doch das beleuchten wir zu einem späteren Zeitpunkt.

Doch nun zum Code:

//den Bildschirm mit einer eigenen Farbe leeren, anders als in 3DHomebrew
sceGuClearColor(0xff442222);
sceGuClear(GU_COLOR_BUFFER_BIT);// Festlegen der View und Model Matrix als Einheitsmatrix
sceGumMatrixMode(GU_VIEW);
sceGumLoadIdentity();
sceGumMatrixMode(GU_MODEL);
sceGumLoadIdentity();//damit die Farbwerte beim Füllen des Dreiecks verlaufen “smooth” (weiche/fortlaufende) Schattierung aktivieren
sceGuShadeModel(GU_SMOOTH);
//Das Dreieck kommt ganz ohne Texturen aus…also deaktivieren
sceGuDisable(GU_TEXTURE_2D);
// hier wird das Dreieck dann tatsächlich auf den Bildschirm gebracht, oder
// besser in den Zeichenpuffer der beim nächsten wechsel zwischen Display- und Zeichenpuffer
// sichtbar wird
sceGumDrawArray(GU_TRIANGLES, GU_TRANSFORM_3D | GU_VERTEX_32BITF | GU_COLOR_8888, 3, 0, triangle);
}

Ganz zum Schluss - bevor wir dieses Beispiel nun erstellen können müssen wir in unserem Makefile noch einige Bibliotheken aufnehmen, damit wir die GU Funktionen auch nutzen können. Im Detail sieht dieses dann folgendermaßen aus:

TARGET = hbcGuSample
OBJS = main.o HbcGuSampleApp.o
INCDIR =
CFLAGS = -G0 -Wall -g
CXXFLAGS = $(CFLAGS) -fno-exceptions -fno-rtti
ASFLAGS = $(CFLAGS)
BUILD_PRX = 1
LIBDIR =
LDFLAGS =
LIBS = -lstdc++ -lpsphbc -lpspgum -lpspgu -lpng -lz -lm
EXTRA_TARGETS = EBOOT.PBP
PSP_EBOOT_TITLE = HBC Gu Sample
PSP_LARGE_MEM=1
PSP_FW_VERSION=500
PSPSDK=$(shell psp-config --pspsdk-path)
include $(PSPSDK)/lib/build.mak

Wenn nun die Erstellung dieses Beispiels glatt gelaufen ist, dann sollte das ganze auf der PSP so aussehen:
GU Dreick

Homebrew - 3D

Die ersten Grundlagen für die GU habe ich zusammengestellt und die Initialisierung ist klar. Ähnlich wie am Anfang werde ich nun eine Homebrew-Basisklasse bauen die die Initialisierung der GU durchführt und von der die eigentliche Applikations-/Spielklasse abgeleitet werden kann.

Da die Grundlegende Homebrew Einrichtung (mit den Callbacks etc) bereits in einer Klasse zusammengefasst ist, werden wir diese einfach ableiten und eine neue Klasse Cl3dHomebrew erstellen:

#include “Homebrew.h”
class Cl3dHomebrew : public ClHomebrew
{
protected:
Cl3dHomebrew();
virtual ~Cl3dHomebrew();
};

Die Initialiserung wird ebenfalls in der init-Methode stattfinden die wir nun neu ausprägen.

class Cl3dHomwbrew : public ClHomebrew
{
public:
/*
 * Die initialisierung der GU Homebrew.
 */
bool init();
/*
 * Hier werden alle Aufräumarbeiten vor’m verlassen der
 * Homebrew platziert.
 */
void exit();

Die Implementierung der Klasse und der Initialisierung greift dabei die Überlegungen auf dieser Seite auf. Darum werde ich den gesammten Quellcode nicht nochmals hier rein stellen, sondern nur Ausschnittsweise andeuten. Zusätzlich notwendige Includes sind die pspgu.h und die pspgum.h:

extern “C” {
#include <pspgu.h>
#include <pspgum.h>
}
#include “3dHomebrew.h”
static unsigned int __attribute__((aligned(16))) list[262144];
bool Cl3dHomebrew::init(){
//zuerst die Superklassen Methode rufen
if (!ClHomebrew::init()) return false;
//nun die eigene Initialisierung der GU
sceGuInit();
…….
//abschließen der GU Liste führt zur sofortigen Abarbeitung
//in diesem Falle, da wir die Liste mit GU_DIRECT gestartet haben
sceGuFinish();
sceGuSync(0,0);
//Aktiviere die Anzeige der GU auf dem realen Bildschirm
sceGuDisplay(GU_TRUE);
return true;
}
void Cl3dHomebrew::exit(){
//GU beenden
sceGuTerm();
//Superklassen Methode am Schluss
ClHomebrew::exit();
}

Nun ist der Punkt erreicht, an dem wir unsere erste GU Homebrew beginnen können. Um den Einstieg nun möglichst einfach zu gestalten, habe ich die bis hier beschriebenen Basisklassen und Vorbereitungen mal in einer Bibliothek zusammen gefasst. Zusammen mit den Header-Files könnt Ihr diese in Euer PSPSDK einbinden und schon loslegen, ohne die ganzen Quellcodes hier abschreiben zu müssen.

Die Bibliothek PspHbc bietet dabei die Basis-Homebrewklasse, eine 2D Grafik-Basisklasse, so wie sie bei der Voxellandschaft zum Einsatz kam, und die nun neue 3D-GU Basisklasse.

Wie Ihr diese Bibliothek für Eure eigenen Projekte einsetzt um eigene Homebrews zu schreiben zeige ich am Beispiel einer kleinen 3D Homebrew in Kürze…
Die Dokumentation der Header-Files in dem Paket sind auf English - aber da hier alles auf deutsch erklärt wurde, sollte das kein Problem darstellen.

GU - und nu ?

Die GU - das ist die “Graphic Unit” der PSP. Ich habe keine wirkliche Auflösung dieses Kürzels gefunden, aber im Grunde kann man sich die GU wie einen Teil der Grafikkarte eines PC’s vorstellen. Dieser Teil ist in der Lage 3D Grafiken zu berechnen und darzustellen.

Der Vorteil der GU ist, dass diese eine sehr leistungsstarke Einheit ist, die quasi parallel zur CPU arbeitet und einen optimierten Datenbus zur Grafikausgabe und zu bestimmten Hauptspeicherbereichen hat. Wer sich schon mit verschieden 3D Spielen auf dem PC auseinander gesetzt hat weiß vielleicht, dass Hardwarebeschleunigung in einer Grafikkarte bei der Berechnung der Grafik gegenüber der reinen CPU enorme Vorteile und Performancegewinn bietet und uns dadurch auch auf der PSP vermutlich einige Türen öffnen wird.

Der “Haken” an der ganzen Sache ? Nun - wir müssen uns wieder durch ein wenig staubige Theorie quälen um zu verstehen, was wir machen müssen um mit der GU zu arbeiten und erste Resultate zu erzielen….

Damit dieser Teil der ersten Betrachtung der GU und weitere Erkenntnisse nicht in der Blog-Geschichte untergehen, platziere ich das ganze auf einer separaten Seite…

Neustart…

Nun bin ich schon so weit gekommen. Die Ergebnisse sind auch schon ganz passabel, aber können doch mit dem aktuellen Anspruch an 3D Spiele nicht mithalten. Die Berechnung der Voxellandschaft ist sehr CPU lastig und kann die vielen Vorteile der in der PSP verbauten Grafikhardware nicht nutzen.
Ernüchternd macht sich etwas Enttäuschung breit - habe ich mir doch so viel vorgenommen…

Die Recherche in einigen Foren bringt mich dann zu dem Entschluss die Voxel in ihrem Space liegen zu lassen und sich mal ein wenig mit den 3D Grafikmöglichkeiten der PSP zu beschäftigen, denn diese Hardware ist ja - wie wir von vielen Spielen wissen - in der Lage sehr schöne Welten zum Vorschein zu bringen. Da wollen wir doch mal schauen, ob da auch was für unser Projekt dabei ist.

Es macht sich ein wenig vorsichtiger Enthusiasmus breit :D

Bildschirm aufräumen

Bisher haben wir immer wieder unsere Zeichenfläche mit der Landschaft voll”gemalt”. Das sah schonmal ganz gut aus, jedoch fällt uns auf, dass in dem Falle das am Horizont hohe Berge zu sehen waren und danach wieder flache die hohen einfach “stehen” bleiben. Das liegt daran, dass wir den Bildschirm nicht aufräumen - also leeren - bevor wir neu zeichnen.
Wenn wir dies nun einfach so einbauen, werden wir schnell feststellen, dass dies zum Teil zum Flackern kommt. Das sieht sehr unschön aus. Um das zu verhindern gibt es einen recht einfachen Trick:
Backbuffer
Oder wie auch immer man es nennen wil. Man verwendet quasi zunächst eine unsichtbare Zeichenfläche.Diese wird geleert und das neue Bild dargestellt/berechnet. Anschließend wird der fertige Bildschirm sichtbar gemacht. Nun wird wieder im “verborgenen” gezeichnet. Das führt dazu dass wir nun 2 Datenbereiche haben in die wir unser Spiel zeichnen und sagen der PSP am Ende nur noch wo der fertige Bildschirm liegt.
Zuerst definieren wir einen “flip” parameter vom Typ bool der immer anzeigt welcher Puffer denn nun der ist, in den wir zeichnen. Dann brauchen wir noch eine Methode die uns immer die Adresse auf den aktuellen Zeichenpuffer liefert, diese Adresse werden wir dann auch an die Render-Methode übergeben. Zum Schluss muss nur noch der Mainthread angepasst werden, so dass die Darstellung zwischen den 2 Puffern gewechselt wird.
Doch nun erstmal zu den neuen Definitionen in der Homebrew Basisklasse:

class ClHomebrew {
….
protected:
/*
* in welchen Puffer soll gezeichnet werden ?
*/
bool pg_drawframe;
/*
* ermitteln des adresse des zeichen Puffers
*/
unsigned char* pgGetVramAddr();

Die Implementierung für diese Methode sieht dann so aus:

unsigned char *ClHomebrew::pgGetVramAddr(){
if (pg_drawframe)
return VRAM_TOP + FRAMESIZE;
else
return VRAM_TOP;
}

Zusätzlich müssen wir den neuen Zeichenpuffer auch beim setzen der Bildpunkte berücksichtigen. Darum sieht die Methode setPixel nun wie folgt aus:

void ClHomebrew::setPixel(int x, int y, int color){
//Zeiger auf das zu zeichnende Pixel berechnen
unsigned char* screen = pgGetVramAddr()+x*PIXEL_SIZE+y*LINE_SIZE*PIXEL_SIZE;
//Farbe setzen
u32* s;
s = (u32*)screen;
*s = color;
}

Im nächsten Schritt überarbeiten wir nun den Mainthread unserer Voxelklasse. Dort holen wir uns erstmal die Adresse des Zeichenpuffers und setzen dort alle Daten auf 0 (also schwarz). Danach wird die Voxellandschaft gezeichnet und dann wird der Puffer dargestellt. Am Schluss folgt das setzen des Flags, dass wir nun in den 2. Puffer zeichnen wollen. Ggfl. müssen wir folgende Includes in unsere Voxelklasse mit aufnehmen:<stdlib.h> und <pspdisplay.h>

void ClPspVoxel::mainthread(){
unsigned char* drawBuffer;
drawBuffer = pgGetVramAddr();
//leer räumen des Bildschirms
memset(drawBuffer, 0, FRAMESIZE);
//Zeichnen
render();
//nun teilen wir der PSP mit, welches unser Zeichenpuffer ist der nun gezeigt werden soll
sceDisplaySetFrameBuf(drawBuffer, LINE_SIZE, PSP_DISPLAY_PIXEL_FORMAT_8888, PSP_DISPLAY_SETBUF_IMMEDIATE );
//Flip des schalters welcher Puffer gefüllt wird
pg_drawframe = pg_drawframe?false:true;
//nach jedem Zeichnungsvorgang darauf warten dass die PSP das Bild
//komplett ausgegeben hat
sceDisplayWaitVblankStart();
}

Schwarz/Weiß war gestern


Heute ist Farbe angesagt. Darum werden wir nun ein paar Farbtöpfe auf unserer Landschaft “vergießen”. Zusätzlich wollen wir die Bewegungen über der Landschaft durch den PSP Analogstick beinflussen, verhindern, dass wir uns in der Landschaft durchgraben müssen und dann noch ein paar kleine Erweiterungen einbauen, die den Gesammteindruck verbessern…
Doch immer eins nach dem anderen.

Erstmal die Farbe

Für die Farbe auf der Landschaft brauchen wir eine Textur die die Landschaft farblich darstellt, da die Höhenkarte ja nur Grau-Werte - eben für die Höhe enthält. Ich verwende für die Höhenkarte eine passende Textur die von den Dimensionen die selben Maße hat (512×512) und wie folgt aussieht:

Die Textur müssen wir natürlich auch laden um diese benutzen zu können. Dazu werden wir die Methode, welche uns eine PNG Datei bereits in den Höhenkartenspeicher lädt erweitern und den Speicherplatz dynamisch zuweisen anstelle ein festes array zu definieren.

Die erweiterungen in der Klassendefinition:

class ClPspVoxel : public Cl2dHomebrew {
….
protected:
….
//neuer Parameter zur Aufnahme der gelesenen Daten
bool loadMap(const char* filename, unsigned int* data);
….
//Höhenkarte nun als Zeiger für dynamische Speicher Reservierung
//unsigned int mapData[512][512]; //Höhenwerte der Karte
unsigned int* mapData;
unsigned int* texture;

Die Anpassungen in der Implentierung der Lademethode sehen dann wie folgt aus:

/*
* Laden der Bilddaten aus PNG-File in Datenpuffer
* dazu wird die Vorlage aus der SDK graphics.c genutzt
* der Einfachheit halber gehen wir von Daten 512×512 punkten aus
*/
bool ClPspVoxel::loadMap(const char* filename, unsigned int* data) {
….
//Zeile nun Punkt für Punkt durchgehen und die Farbe in den Puffer übertragen
for (x = 0; x < width; x++) {
u32 color = line[x];
//neuer Zugriff auf den Datenpuffer in linearer Form ->zeile*maxBreite + spalte
//mapData[y][x] = color;
data[y*512 + x] = color;
}

Für die Anpassung der Initialisierungsmethode bei der wir nun den Speicher für die Höhenkarte und die Texture reservieren müssen wir noch das zusätzliche Include malloc.h einbinden.Der Code hierfür sieht dann so aus:

bool ClPspVoxel::init(){
…..
//Speicher für die Höhenkarte
mapData = (unsigned int*)malloc(sizeof(unsigned int)*512*512);
//Höhenkarte laden
loadMap(”altitude.png”, mapData);
//speicher für die Textur
texture = (unsigned int*)malloc(sizeof(unsigned int)*512*512);
loadMap(”landscape.png”, texture);

Um den dynamisch reservierten Speicher wieder freizugeben, wenn er nicht mehr gebraucht wird - das ist ganz wichtig! - platzieren wir folgende zeilen in den Klassendestruktor:

free(mapData);
free(texture);

Um die Textur nun bei der Darstellung der Landschaft zu berücksichtigen müssen wir nur geringe Anpassungen in der render Methode vornehmen um auch die neue Art auf die Daten zuzugreifen zu berücksichtigen:

void ClPspVoxel::render() {
….
//Höhe des Voxel
altitude = mapData[voxelY*512 + voxelX]; //mapData[voxelY][voxelX];
//die Farbe dieses Voxel aus der textur lesen
unsigned int color = texture[voxelY*512 + voxelX];
……
// Punkt im Bildschirm
//setPixel(pixelX, pixelY, altitude);
setPixel(pixelX, pixelY, color);

Das Ergebnis sieht schon ganz ordentlich aus:

Volle Kontrolle?
Nachdem wir die Landschaft mit ewas Farbe aufgepeppt haben, wollen wir die Bewegungen auf der PSP mit dem Analogstick kontrollieren. Um das zu erreichen brauchen wir in der Implementierungsklasse ersteinmal ein neues Include dass uns die Notwendigen Funktionen bereitstellt: pspctrl.h.
Nun können wir an das Ende der Bildbereichnung die Abfrage des PSP Pads legen:

void ClPspVoxel::render() {
….
//nach dem rendern fragen wir das pad ab.
//wurde der Analog-Stick bewegt ?
SceCtrlData pad;
sceCtrlPeekBufferPositive(&pad, 1);
//die ausrichtung des analog sticks ist
//0 ganz links, 255 ganz rechts
//0 ganz oben und 255 ganz unten.
//für eine Bewegung brauchen wir -x bis +x für links rechts
//und -y bis + y
short moveX, moveY;
moveX = pad.Lx - 127;
moveY = 127 - pad.Ly;
nbsp;
//da wir uns nicht im vollen “Auschlag” des Stix bewegen wollen, sondern
//maximal um 4 Pixel pro durchlauf verringern wir die Werte entsprechend
moveX >>= 5;
moveY >>= 5;
posY+=moveY;
posX+=moveX;
}

Schweben
Nun wollen wir noch über unsere Landschaft “schweben”. Dazu müssen wir die render Methode nur geringfügig anpassen. Bis jetzt ist die Höhe des Betrachters immer fest vorgegeben gewesen. Nun wird diese aber aus der Höhe des Punktes berechnet über dem sich der Betrachter gerade befindet.Zusätzlich bauen wir nach dem zeichnen der Bildpunkte in einer Spalte noch eine Sicherheitsabfrage ein, die verhindert, dass der Strahl weiter verfolgt wird, wenn der Bildschirm schon bis oben hin voll-”gemalt” wurde…

void ClPspVoxel::render() {
…..
rayZ = 81 + (mapData[(posY&511)*512 + (posX&511)] & 255); //384;
….
}while (rayZ < currentAltitude);
}
// wenn der Bildschirm am oberen Rand erreicht ist, brauchen
//wir den strahl nicht weiter verfolgen…
if (pixelY <= 1) break;

Viel Spaß bei den ersten “Flugversuchen” ;)

|