Archive für März 2010

Der Anstrich - aber mit Plan

Die Drahtgitterlandschaften sind fertig gestellt - egal mit welcher Methode - und nun geht es daran auch dieser wieder etwas Farbe einzuhauchen. Dazu bedient man sich sogenannter Texturen. Das sind Bilder einer beliebigen Oberfläche die auf das virtuelle 3D Objekt gelegt werden. Dadurch erscheinen sie realer und plastischer, als wenn Sie einfach nur mit einer einfachen Farbe angestrichen wurden.

Texturen verwalten

Immer wieder , wenn wir 3D szenen auf der PSP darstellen wollen, werden wir die verschiedensten Texturen zum Einsatz bringen. Da macht es Sinn sich darüber Gedanken zu machen, wie wir diese am besten in unserer Homebrew verwalten. Die GU der PSP bringt hier “leider” keine Verwaltungsfunktionen mit. Darum werden wir hier eine sehr einfache Variante in Form einer Texterverwaltungsklasse selber erstellen. Ein paar Grundüberlegungen:
1. die Texturen sollen zunächst nur von PNG Bilddateien gelesen werden
2. ein und das selbe Bild soll als Textur nur einmal geladen werden
3. geladene Texturen sollen individuell wieder freigegeben werden können um Speicher zu sparen
4. die Texturen sollen ge-swizzled werden
5. die Klasse soll einfach erweiterbar sein, zBsp. um Laderoutinen für JPG oder andere Bildformate

Dies sind ein paar gute Rahmenbedingungen. Machen wir uns nun ans Werk. Wir erstellen eine neue Klasse mit dem Namen ClTextureMgr. Im Header definieren wir zunächst ein paar Beschränkungen, wie die maximale Größe einer Textur, die sich auf 512×512 Bildpunkte beschränkt (mehr kann die GU nicht verarbeiten). Zusätzlich eine Struktur, in der alle wichtigen Daten einer Textur die wir verwalten wollen abgelegt sind.

/*
 * TextureMgr.h
 */      

#ifndef TEXTUREMGR_H_
#define TEXTUREMGR_H_      

// Maximale größe einer Textur und anzahl Texturen die verwaltet werden sollen
#define TEX_MAX_WIDTH  (512)
#define TEX_MAX_HEIGHT (512)
#define MAX_TEX_COUNT  100      

/*
 * Definition einer Struktur die alle daten einer Textur enthält
 * die hier verwaltet werden soll
 */
typedef struct Texture{
	unsigned int id; //ID der Textur
	char*        name; //Name der Textur/Bilddatei
	int          type; //Typ der Textur - siehe GU_PSM* Enums
	int          width, height; //Breite und Höhe der Textur als Potenz von 2
	int          stride; //Pufferbreite für Zugriff auf Texturdaten als Potenz von 2
	bool         swizzled; //WAHR wenn Texturdaten ge-swizzled sind
	void*        data; //Zeiger auf die Bilddaten der Textur
}Texture;

Nun folgt die eigentliche Definition der Klasse. Sie wird als Singleton ausgeprägt und kann somit nicht direkt instanziiert werden. Dies vereinfacht die Verwaltungsaufgaben der Klasse. Es müssen also 2 statische Methoden zum holen und zum abräumen der Singleton-Instanz erzeugt werden. Alle weiteren Methoden werden dann Instanz basiert ausgeprägt. Die Definition sieht dann folgendermaßen aus.

class ClTextureMgr {
public:
	/*
	 * Statische Methoden für den Singleton Zugriff
	 */
	static ClTextureMgr* getInstance();
	static void freeInstance();     

	/*
	 * Laden einer Textur aus einer PNG Datei
	 */
	Texture* loadFromPNG(const char* filename);     

	/*
	 * Liefert die verwaltete Textur anhand der ID
	 */
	Texture* getTexture(unsigned int id);     

	/*
	 * Freigeben der Ressorcen für eine Textur
	 */
	void freeTexture(unsigned int id);     

protected:
	/*
	 * Texturdaten "swizzlen"
	 */
	void swizzle(unsigned char* out, unsigned char* in, unsigned int width, unsigned int height);
	/*
	 * Konstruktor ist protected um direkte Instanziierung zu verhindern
	 */
	ClTextureMgr();
	virtual ~ClTextureMgr();     

	/*
	 * es folgen die Member-Variablen zur Klasse
	 */
private:
	static ClTextureMgr* _instance; //die singleton Instanz
	int managedTextures; //Anzahl verwalteter Texturen
	Texture* textures;   //Liste aller verwalteten Texturen
};     

#endif /* TEXTUREMGR_H_ */

Kommen wir nun zur Implementierung der Klasse. Den Start bildet hier eine kleine - von der Klasse losgelöste - Hilfsroutine um von einer gegebenen Zahl die nächst höhere Zahl als Potenz von 2 zu bestimmen (das haben wir schonmal gesehen ;) ). Daneben sind die statischen Methoden für die Verwendung der Klasse als Singleton implementiert.

/*
 * TextureMgr.cpp
 */    

//die notwendigen Includes
extern "C"{
#include <malloc.h>
#include <stdlib.h>
#include <png.h>
#include <psptypes.h>
#include <pspgu.h>
}    

#include "TextureMgr.h"    

/*
 * eine einfache Hilfsfunktion. Lokal und statisch...
 */
static int getNextPower2(int width)
{
	int b = width;
	int n;
	for (n = 0; b != 0; n++) b >>= 1;
	b = 1 << n;
	if (b == 2 * width) b >>= 1;
	return b;
}    

//Am Anfang ist die Singleton instanz nicht erzeugt
ClTextureMgr* ClTextureMgr::_instance = 0;    

ClTextureMgr::ClTextureMgr() {
	//der Konstruktor initialisiert die Datan
	this->managedTextures = 0;
	textures = (Texture*)malloc(MAX_TEX_COUNT*sizeof(Texture));    

}    

ClTextureMgr *ClTextureMgr::getInstance()
{
	if (!_instance) {
		_instance = new ClTextureMgr();
	}    

	return _instance;
}    

void ClTextureMgr::freeInstance(){
	if (_instance){
		delete(_instance);
	}
}

Der nächste Schritt ist die Methode zum laden eines PNG Files als verwaltete Textur. Da bereits an vielen anderen Stellen das laden der PNG Datei beschrieben und auch als Code dargestellt wurde, und weil der original Code Teil des PSPSDK ist, werde ich diesen hier aussparen, wohl aber den für den Texturverwaltungsteil wichtigen Part zeigen.

Texture *ClTextureMgr::loadFromPNG(const char *filename){
	//Befor wir das File laden, prüfen ob es schon eine Textur
	//mit dem selben Namen gibt, wenn ja eben diese Textur zurück liefern
	for (short t=0;t<managedTextures;t++){
		if (textures[t].name){
			if (strncmp(filename, textures[t].name, strlen(filename))==0){
				return &textures[t];
			}
		}
	}
	//es ist wirklich eine neue Textur, also diese Laden und die Daten füllen
	managedTextures++;
	if (managedTextures >= MAX_TEX_COUNT) return NULL;   

	// get the pointer of the new texture data structure
	Texture* newTex = &textures[managedTextures-1];
	newTex->name = (char*)malloc(strlen(filename));
	strcpy(newTex->name, filename);   

/*********************************************************
 * hier erfolgt nun das Laden des PNG Files und das Ablegen der Daten im
 * Speicher für diese Textur, sollte hier ein Fehler passieren wird NULL zurück-
 * gegeben.
 *********************************************************/   

	//vorbereiten für die Rückgabe der Textur
	newTex->id = managedTextures;   

	newTex->swizzled = false;
	//veruche die Texturdaten zu swizzlen
	//Speicher für die neuen Daten besorgen
	void* swizzleData = malloc(sizeof(u32)*newTex->stride*newTex->height);
	if(swizzleData){
		swizzle((unsigned char*)swizzleData, (unsigned char*)newTex->data, newTex->stride*sizeof(u32), newTex->height);
		newTex->swizzled = true;
		//Speicher der originalen Daten freigeben
		free(newTex->data);
		newTex->data = swizzleData;
	} else
		newTex->swizzled = false;   

	return newTex;
}

Swizzle

Das nun schon oft erwähnte Wort bedeutet direkt übersetzt: Bauernfängerei :D bzw. mit dem Zusatz “Stick” bedeutet es Rührstab. Letzteres trifft hier eher zu. Die Daten der Textur sind im Normalfall ja in einem Datenpuffer ordentlich in Reihenfolge der Bildpunkte abgelegt. Des Swizzle-Verfahren rührt diese Bildpunkte so “durcheinander” dass die Daten nicht mehr so abgelegt sind wie sie auf dem Bildschirm zu sehen sind, sondern optimal von der GU zugegriffen werden können. Das ganze bringt einen großen Geschwindigkeitsgewinn beim Arbeiten mit Texturen.
Eine leicht lesbare - nicht auf Performance getrimmte - Swizzle-Routine wird in unserer Textur-Verwaltungsklasse verwendet:

void ClTextureMgr::swizzle(unsigned char* out, unsigned char* in, unsigned int width, unsigned int height)
{
   unsigned int i,j;
   unsigned int rowblocks = (width / 16);  

   for (j = 0; j < height; ++j)
   {
      for (i = 0; i < width; ++i)
      {
         unsigned int blockx = i / 16;
         unsigned int blocky = j / 8;  

         unsigned int x = (i - blockx*16);
         unsigned int y = (j - blocky*8);
         unsigned int block_index = blockx + ((blocky) * rowblocks);
         unsigned int block_address = block_index * 16 * 8;  

         out[block_address + x + y * 16] = in[i+j*width];
      }
   }
}

Abschließend und der Vollständigkeit halber die Methode um eine verwaltete Textur einer bestimmten ID zurückzuliefern, eine Textur freizugeben und den Klassendestruktor.

Texture *ClTextureMgr::getTexture(unsigned int id){
	//Prüfen das die ID auch valide ist
	if (id > managedTextures){
		return NULL;
	}  

	return &textures[id-1];
}  

void ClTextureMgr::freeTexture(unsigned int id){
	Texture* text = &textures[id-1];
	if (text){
		free(text->data);
		free(text->name);
		text->data = 0;
		text->name = 0;
	}
}  

ClTextureMgr::~ClTextureMgr() {
	//Allen reservierten Speicher freigeben
	for (int t=0;t<managedTextures;t++){
		if (textures[t].data) free(textures[t].data);
		if (textures[t].name) free(textures[t].name);
	}
	if (managedTextures)
		free(textures);
}

3D Landschaft - optimiert

Unsere erste 3D Landschaft bestehend aus vielen kleinen Dreiecken ist nun fertig. Bei genauerer Betrachtung der Daten die wir für die Landschaft benötigen - also der Anzahl der Dreiecke die entstehen werden, wir schnell an Grenzen stoßen wenn wir größere Landschaftsflächen abbilden wollen. Eine dieser Grenzen ist die Anzahl der Dreiecke die im Hauptspeicher der PSP abgelegt werden können und zum anderen natürtliche die Anzahl der Dreiecke die in akzeptabler Geschwindigkeit von der GU prozessiert und gezeichnet werden können. Bevor wir unserer Landschaft “Leben” in Form von Bewegung einhauchen werden wir einen kurzen Exkurs in die Optimierung der Landschaft unternehmen.

Schlagworte die hier oft in diesem Zusammenhang aufgeführt werden sind SOAR oder LOD. Was dies ist und welche Methodik zur Optimierung ich gefunden habe (im Internet) stelle ich in diesem Bereich vor. Neben der theoretischen Abhandlung werde ich dort auch meine Ideen für die PSP Implementierung bereitstellen (zur Zeit noch in Arbeit).

Unabhängig davon können aber die sich an diese Überlegungen anschließenden nächsten Schritte in unserer Landschaft (Textur, Bewegung etc.) umgesetzt werden.

|