Sie befinden sich aktuell in den PSP Spieleentwicklung Blog-Archiven für den folgenden Tag 17.2.2010.
- Allgemein (6)
- Einfache 3D Welten (3)
- Erste Schritte (3)
- GU (4)
- Komplexe 3D Welten (1)
- 26.1.2011: Der Blog zieht um
- 23.3.2010: Der Anstrich - aber mit Plan
- 4.3.2010: 3D Landschaft - optimiert
- 17.2.2010: Landschaft 3D mit GU
- 28.1.2010: Ein Dreieck - GU Test
- 20.1.2010: Homebrew - 3D
- 12.1.2010: GU - und nu ?
- 11.1.2010: Neustart...
- 7.1.2010: Bildschirm aufräumen
- 5.1.2010: Schwarz/Weiß war gestern
PSP Allgemein
PSP Entwicklung
Archive für 17.2.2010
Landschaft 3D mit GU
17.2.2010 von AnMaBaGiMa.
Nun haben wir unser erstes Objekt im virtuellen Raum platziert. Nun können wir mit diesem Objekt einige Experimente machen um diverse Funktionen der GU an genau diesem auszuprobieren. Dies möchte ich hier aber etwas nach hinten schieben und allen Entdeckern überlassen. Denn unsere nächste Etappe soll sich mit der Landschaft die wir zunächst im Voxelspace betrachten konnten beschäftigen. Wie können wir diese über die GU im 3D Raum modellieren und betrachten ? Diese Frage wollen wir nun beantworten und an diesem reellen Beispiel nach und nach die einzelnen Möglichkeiten der GU ausprobieren und hoffentlich sinnvoll einzusetzen lernen.
Als Vorbereitung für diesen Teil legen wir zunächst ein neues Projekt mit dem Namen GuLandscape in Eclipse an. Die ersten Schritte sind nun ein Makefile in diesem Projekt zu erstellen, eine Applikationsklasse zu definieren und die main.cpp zu erstellen. Doch nochmal langsam der Reihe nach.
1. Das Makefile:
TARGET = GuLandscape OBJS = main.o GuLandscapeApp.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 = Gu Landscape PSP_LARGE_MEM=1 PSP_FW_VERSION=500 PSPSDK=$(shell psp-config --pspsdk-path) include $(PSPSDK)/lib/build.mak
2. Die neue Applikationsklasse ClGuLandscapeApp:
*
* GuLandscapeApp.h
* Author: andreborrmann
*/
#ifndef GULANDSCAPEAPP_H_
#define GULANDSCAPEAPP_H_
#include <3dHomebrew.h>
class ClGuLandscapeApp : public Cl3dHomebrew {
public:
/*
* die Methoden zum erzeigen und aufräumen der Singleton Instanz der
* Applikationsklasse
*/
static ClGuLandscapeApp* getInstance();
static void releaseInstance();
/*
* Überladen der Initi-Routine
*/
bool init();
protected:
ClGuLandscapeApp();
virtual ~ClGuLandscapeApp();
/*
* Überladen der Render Methode
*/
void render();
static ClGuLandscapeApp* _instance;
};
#endif /* GULANDSCAPEAPP_H_ */
/*
* GuLandscapeApp.cpp
*/
extern “C”{
#include <pspgu.h>
}
#include “GuLandscapeApp.h”
ClGuLandscapeApp* ClGuLandscapeApp::_instance = 0;
ClGuLandscapeApp* ClGuLandscapeApp::getInstance(){
if (!_instance){
_instance = new ClGuLandscapeApp();
}
return _instance;
}
void ClGuLandscapeApp::releaseInstance(){
if (_instance){
delete(_instance);
_instance = 0;
}
}
bool ClGuLandscapeApp::init(){
//zuerst die Super-Methode
if (!Cl3dHomebrew::init()) return false;
//hier nun die eigene Initialisierung…
return true;
}
void ClGuLandscapeApp::render() {
}
ClGuLandscapeApp::ClGuLandscapeApp() {
}
ClGuLandscapeApp::~ClGuLandscapeApp() {
}
3. Die main.cpp
/*
* main.cpp
*/
extern “C”{
#include <pspkernel.h>
}
#include “GuLandscapeApp.h”
PSP_MODULE_INFO(”GU Landscape”, 0, 1, 1);
PSP_MAIN_THREAD_ATTR(PSP_THREAD_ATTR_USER);
PSP_HEAP_SIZE_KB(-1024);
int main(int argc, char* argv[])
{
ClGuLandscapeApp* myHomebrew = ClGuLandscapeApp::getInstance();
if (myHomebrew->init()){
myHomebrew->run();
myHomebrew->exit();
}
ClGuLandscapeApp::releaseInstance();
sceKernelExitGame();
return 0;
}
Das war nun das Rahmenwerk für unsere neue Homebrew. Nun schauen wir uns den Teil an indem wir aus einer in einer Höhenkarte abgelegten Landschaft eine 3 dimensionale virtuelle Welt erschaffen. Ähnlich wie beim Ansatz im Voxelspace gehen wir dabei davon aus dass die X und Y Werte des Bildes die Ausdehnung der Landschaft in Breite und Tiefe und der Farbwert die Höhe beschreiben. Um unsere neue Homebrew möglichst übersichlich zu halten, werden wir die Umwandlung des 3D Terrains aus einer Höhenkarte und die Darstellung desselben in einer eigenen Klasse kapseln. Dadurch bleibt die Applikationsklasse klein und erweiterbar für weitere Funktionen die wir hier nach und nach - quasi Modular - Hinzufügen können.
Das Terrain
Für die Abbildung des Terrains legen wir eine neue Klasse mit dem Namen ClGuTerrain an. Diese Klasse wird nicht von einer anderen Abgeleitet. Die Klasse soll folgendermaßen aufgebaut sein:
Im Konstruktor übergeben wir den Dateinamen (.png) der Höhenkarte die maximal 512×512 Bildpunkte groß sein darf. Hier werden die einzelnen Bildpunkte aus der Datei extrahiert und in einem Array mit ihren Eigenschaften abgelegt.
In einem zweiten Schritt werden aus diesen Punkten die von der GU verarbeitbaren Vertices erstellt und zu einer Vertexliste zusammengesetzt. Dabei werden wir nicht wie in unserem ersten Beispiel ein enzelnes Dreieck erstellen, sondern die Landschaft in viele viele Dreiecke aufteilen. Dabei wird ein (quadratischer) Bereich der virtuellen Landschaft der aus 4 benachbarten Punkten besteht durch genau 2 Dreiecke dargestellt - im Detail zeigen das die Bilder die etwas weiter unten zu sehen sind.
Die Definition der neuen Klasse:
/*
* GuTerrain.h Die Klasse kapselt die Beschreibung und Darstellung eines 3D Terrain
* basierend auf einer Hightmap PNG Datei
*/
#ifndef GUTERRAIN_H_
#define GUTERRAIN_H_
/*
* Struktur zur Definition eines Punktes im Terrain
*/
typedef struct TerrainPoint{
int x, y; // Koordinaten
int height; // Höhe des Punktes
int color; // Farbe des Punktes
}TerrainPoint;
/*
* Struktur zur Definition eines GU Vertexes
*/
typedef struct Vertex {
int color;
float x, y, z;
}Vertex;
class ClGuTerrain {
public:
/*
* Der Konstruktor erhält den Filenamen der Höhenkarte
* aud dessen Basis das 3D Terrain erzeugt
*/
ClGuTerrain(const char* heightMapFile);
virtual ~ClGuTerrain();
/*
* Render Methode für das Terrain
*/
void render();
protected:
int width, height; //Größe des Terrains
TerrainPoint* terrainPoints;
Vertex* mesh; //Das Vertex-Netz des Terrains
int vertexCount;
/*
* Generierung der GU Vertices für das Terrain
*/
void generateMesh();
/*
* Laderoutine die ein PNG lädt und die Bilddaten zurückliefert
* der benötigte speicher wird in der Methode reserviert
*/
bool loadPNG(const char* fileName, void** data);
/*
* Diverse Daten sollten der Einfachheit halber immer in größen einer Potenz von 2 abgelegt
* sein. Diese Methode liefert von einer Zahl die nächstgrößere potenz von 2 - also 2 hoch n
*/
int getPowerOf2(int value);
};
#endif /* GUTERRAIN_H_ */
Die Implementierung dieser Klasse zeige ich nun Schritt für Schritt. Die Methode zum laden des PNG Files bedarf nun keiner großen Eklärungen mehr, da wir das bereits bei der Voxellandscape benutzt haben. Der Konstruktor ruft zunächst diese Methode auf um das Bild zu laden und extrahiert die Bilddaten dann in die Liste der Terrain Punkte.
/*
* Die notwendigen Header Files
*/
extern “C”{
#include <stdio.h>
#include <png.h>
#include <malloc.h>
#include <psptypes.h>
#include <pspgum.h>
#include <pspgu.h>
}
#include “GuTerrain.h”
ClGuTerrain::ClGuTerrain(const char *heightMapFile){
//Laden des Höhenkartenbildes und die Punkte extrahieren
int* altitude;
terrainPoints = 0; if (loadPNG(heightMapFile, (void**)&altitude)){
// Nun erzeugen wir die Punktinformationen welche die Landschaft aufspannen
// diese werden später in ein GU 3d “Drahtgitternetz” (mesh) umgewandelt.
this->terrainPoints = (TerrainPoint*)malloc(width*height*sizeof(TerrainPoint));
for (int x=0;x<width;x++){
for (int y=0;y<height;y++){
terrainPoints[x+width*y].x = -width/2 + x; //das Terrain soll im virtuellen Raum zentral Platziert sein
terrainPoints[x+width*y].y = -height/2 + y;
terrainPoints[x+width*y].height = 255 & altitude[x+width*y];
terrainPoints[x+width*y].color = altitude[x+width*y];
}
}
// den Speicher des Bildes freigeben, da er nun nicht mehr gebraucht wird.
free(altitude);
// nun die Punkte in GU Vertices konvertieren
generateMesh();
}
}
Der nächste Schritt wird nun die Generierung der sogenannten Mesh werden, also des “Drahtgitter” Modells wie man es vielleicht aus 3D Modellierungsprogrammen kennt. Im Grunde besteht dieses Drahtgitter wie schon erwähnt aus Dreiecken. Nun wird jedes Dreieck durch 3 Punkte definiert. Das bedeutet, dass wir bei einer Höhenkarte von 4×4 Punkten bereits 3×3 Quadrate von jeweils 2 Dreiecken mit jeweils 3 Punkten, also 18×3 - 54 Punkte für die Darstellung brauchen. Es gibt hier eine Optimierungsmöglichkeit die uns sowohl Speicherplatz beim Erzeugen der Mesh spart als auch Performancevorteile bringt. Es kommt ein sogenannter Dreiecksstreifen (TriangleStripe) zum Einsatz. Dabei werden die Punkte so angeordnet, dass die ersten 3 Punkte ein vollständiges Dreieck definieren. Für jedes weitere Dreieck wird dann nur noch ein neuer Punkt gebraucht der dann mit den beiden zuletzt aufgeführten Punkten wieder ein Dreieck bildet.
Eine kleine Illustration:
Es ist schön zu sehen dass für die 3 Dreiecke T1 bis T3 nun anstelle von 9 Punkten nur 5 benötigt werden. Um einen fortlaufenden Dreiecksstreifen zu erhalten müssen wir beim Wechsel in eine neue Zeile “unsichtbare” Dreiecke hinzufügen. Das ist in dem folgenden Bild mit den blauen Linien veranschaulicht. Die Zahlen repräsentieren die Vertexpunkte.

Dieses Verfahren werden wir nun bei der Erstellung der Vertexliste (Mesh) anwenden, auch wenn dieses offensichtlich noch einiges Optimierungspotential hat (da Punkte mehrfach/doppelt erzeugt werden):
void ClGuTerrain::generateMesh() {
// Erzeugen der Vertices für ein TriangleStripe
// Dies wird von links nach rechts aufgebaut Zeile für Zeile
// Das bedeutet, dass wir jeweils am Ende ein ganz ganz flaches (unsichtbares) Dreieck
// platzieren um den Wechsel in eine neue Zeile ohne Fehler zu ermöglichen.
// zuerst den Speicherbedarf für die Vertexe bestimmen und allokieren
mesh=0;
this->vertexCount = 2*(width+2)*height;
mesh = (Vertex*)malloc(this->vertexCount*sizeof(Vertex));
int vertex = 0;
float scale = 0.5f;
for (int y=0;y< height-1; y++){
for (int x=0;x < width; x++){
// Die Vertices werden in der Zeile immer Paarweise erzeugt.
mesh[vertex].x = terrainPoints[x+y*width].x*scale;
mesh[vertex].z = terrainPoints[x+y*width].y*scale;
mesh[vertex].y = (terrainPoints[x+y*width].height / 10.0f)- 20.0f;
mesh[vertex].color = terrainPoints[x+y*width].color;
vertex++;
mesh[vertex].x = terrainPoints[x+(y+1)*width].x*scale;
mesh[vertex].z = terrainPoints[x+(y+1)*width].y*scale;
mesh[vertex].y = (terrainPoints[x+(y+1)*width].height / 10.0f)- 20.0f;
mesh[vertex].color = terrainPoints[x+(y+1)*width].color;
vertex++;
// sobald wir am rechten Rand angekommen sind müssen wir das “unsichtbare”
// Dreieck erzeugt. Dazu wird der letzte Vertex und der 2. der ersten Zeile nochmal angehängt
// das ganze soll aber nicht in der letzten Zeile passieren…
if (x == (width-1) && y < (height-2)){
mesh[vertex].x = mesh[vertex-1].x;
mesh[vertex].y = mesh[vertex-1].y;
mesh[vertex].z = mesh[vertex-1].z;
mesh[vertex].color = mesh[vertex-1].color;
vertex++;
mesh[vertex].x = terrainPoints[(y+1)*width].x*scale;
mesh[vertex].z = terrainPoints[(y+1)*width].y*scale;
mesh[vertex].y = (terrainPoints[(y+1)*width].height / 10.0f)- 20.0f;
mesh[vertex].color = terrainPoints[(y+1)*width].color;
vertex++;
}
}
}
// Damit wir wissen wie viele Punkte unser Mesh wirklich hat merken wir uns hier die Anzahl
this->vertexCount = vertex;
}
Der nun noch offene Teil befasst sich mit der Darstellung des Terrain. Dazu wird die render-Methode ausgeprägt. Die Darstellung der Dreiecke erfolgt mit der nun schon bekannten Funktion sceGumDrawArray. Dabei ist zu beachten, dass diese nur maximal 65535 Vertices verarbeiten kann. Darum müssen wir unser gesammtes Drahtgitter - sollte es aus mehr Punkten bestehen - in mehreren Häppchen übergeben. Der Einfachheithalber wählen wir hier Pakete von maximal 65000 Punkten.
void ClGuTerrain::render(){
// Rendern des Terrains. Dabei nur bis zu 65k Vertices darstellen
// andernfalls kann es zu Abstürzen kommen, da die GU nur bis zu 65535 Vertices
// auf einmal rendern kann
int renderVertices;
sceGumMatrixMode(GU_VIEW);
sceGumLoadIdentity();
sceGumMatrixMode(GU_MODEL);
sceGumLoadIdentity();
sceGuFrontFace(GU_CCW);
renderVertices = vertexCount;
Vertex* drawMesh = mesh;
while (renderVertices>65000){
sceGumDrawArray(GU_TRIANGLE_STRIP, GU_COLOR_8888 | GU_VERTEX_32BITF | GU_TRANSFORM_3D, 65000, 0, drawMesh);
renderVertices-=65000;
drawMesh+=65000;
}
if (renderVertices > 0){
sceGumDrawArray(GU_TRIANGLE_STRIP, GU_COLOR_8888 | GU_VERTEX_32BITF | GU_TRANSFORM_3D, renderVertices, 0, drawMesh);
}
}
Abschließend müssen noch der Destruktor und die Hilfsmethode mit “Leben” gefüllt werden. Da diese keiner weiteren Erklärung bedürfen (glaube ich) hier das Coding:
int ClGuTerrain::getPowerOf2(int value){
short c;
int temp;
temp = value;
c=0;
while ((temp >>= 1) > 0) c++;
temp = 1 << c+1;
if (temp >> 1 == value){
temp = value;
}
return temp;
}
ClGuTerrain::~ClGuTerrain() {
if (terrainPoints)
free(terrainPoints);
if (mesh)
free(mesh);
}
Die nächsten Schritte werden nun das Terrain in unserer Landscape Homebrew zur Verfügung stellen, es erzeugen und rendern. Dazu sind nur noch kleine Anpassungen notwendig. Zuerst muss das Makefile erweitert werden, so dass die Terrain-Klassen-Objektdatei erzeugt wird:
OBJS = main.o GuLandscapeApp.o GuTerrain.o
In der LandscapeApp-Klasse fügen wir in der Header Datei die neue Klassenheaderdatei hinzu und definieren ein Attribut das eine Referenz auf die Terrainklasse enthalten kann:
#include “GuTerrain.h”
class ClGuLandscapeApp : public Cl3dHomebrew {
……
protected:
ClGuTerrain* terrain;
Die Implementierung der Applikationsklasse wird nun noch in der Init, der Render und Exit Methode angepasst:
bool ClGuLandscapeApp::init(){
if (!Cl3dHomebrew::init()) return false;
terrain = new ClGuTerrain(”altitude.png”);
if (!terrain) return false;
}
void ClGuLandscapeApp::render() {
if (terrain) terrain->render();
}
void ClGuLandscapeApp::exit(){
if (terrain)
delete(terrain);
}
Das Ergebnis
Wenn alles glatt geht, dann sollte das Ergebnis auf dem PSP Screen so aus sehen:

Geschrieben in Komplexe 3D Welten | Drucken | Keine Kommentare »