In the previous blog post, I covered some simple "Hello World" and button input programs to help us test out our new PSP set up. This time, by popular demand, we're diving into 3D rendering! I will be using the ShadowProjection.c demo from the PSP SDK. I am going to avoid the typical cube example, because the manually defined vertices are overwhelming for some.
Shadows
Here, we set up our libraries for our code. You may recognise some of them, but some may be new. The libraries beginning with "std" are related to C/C++ code and will help us to use more advanced code than you may have previously encountered. math.h is self explanatory and string.h will be too. The final 3 are all related to managing the PSP GPU. Exciting! We're finally harnessing the PSP GPU and its hardware 3D rendering.
Okay! That's a lot of new stuff! Here we're naming our module as usual and defining printf, but we're also setting up an attribute. The use of this will become clear later. We also set up our very first struct, so we can create vertexes at will and define the X, Y and Z of them.
Another struct. Now we're defining textures. So when we create a new texture with this struct, we set its format, its mipmap level and its size.
Here we set up a 32x32 grid for our 3D rendering to take place.
Here's where our Vertex comes in. We set up our vertexes around the grid.
Now we're setting up a Torus!
Since we're dealing with shadows, we need to set up our lighting. We also set up our callbacks and generate our grid and torus.
Now it gets fun! We define our buffer as 512 to be standardised. Next, we set up our screen resolution. Unlike the GBA, we can set this to be practically whatever we want! For the sake of this article, I have set it to be 480x272 - the normal PSP screen resolution. The PSP does, however, support up to 720x576 and this can be displayed at full resolution via component cables. We set the pixel size as 4, and this is simply defining the standard screen mode. Framesize is calculated based on this and you don't need to know too much about that. The Z-buffer is a depth buffer, and it is 16-bit here.
Now we're defining our geometry. We set up our indices and vertices and create one or two unsigned ints. We also draw our geometry in this part; we use sceGuSetMatrix to set up this model, and then we draw this collection of triangles as our geometric world. It should be fairly self explanatory, and if you don't understand parts don't worry; these are publicly documented commands from the PSP libraries.
And now we get our shadows going as promised! ShadowCaster sets up what geometry will cast a shadow, and shadowReceiver is how the shadow is drawn based on geometry.
Now, our all-important main loop where all of our code comes together! We set up our usual callbacks, then we generate our grid and torus. Then, we do something important; we flush the kernel cache to make sure that leftover memory is cleaned up. We also do some standard setting-up for the VRAM buffers.
The Gu setup part is great, because you're simply enabling whatever you'd like and tuning the GPU as you want. We initialise and start the GPU first, naturally. Then we tell it to draw our buffer "frameBuffer", with the PSM_4444 format. Then it displays our buffer at 480x272, at PSP size, and sets up double buffering. This helps us to synchronise V-Blank, similar to double-buffered V-sync. We then set up our depth buffer (Z-buffer) and an offset or two, as well as the viewport. You don't need to know the following few, but you can enable depth testing, shading, dithering and more as should be self explanatory. Nice and easy, and makes you feel like a developer!
The next bit is messy, as we're setting up some manual matrices about light projection. It's probably not necessary to understand all of this.
And a nice easy part again. We're defining the resolution of our shadows, and changing the resolution and format of shadowmaps can be a nice optimisation to any budding game devs!
A little more defining for our geometry and grid.
And now we're rendering our world with the torus. A bit messy for sure, with all of the numbers involved, but if you want to find out more about how these specific commands work you can find online resources such as the archived pspdev.org and ps2dev.org.
Over halfway there! Here's our light source for our shadow casting. The light orbits so as to cast a different and dynamic shadow.
This is where things get fun, this is the shadow rendering. We start the GPU up, then we set an off-screen texture as a render target. We set up the viewport and then clear the contents of the buffered screen. The rest should be fairly self explanatory; while we draw shadows, we disable lighting and 2D textures so that the rendering can focus on the shadows. Then we finish and sync!
Wow.. that's a lot to go through. Okay, so basically, now we're rendering our hard work into our frame buffer so it can be displayed on our PSP screen. A lot of this should actually be self explanatory, but a few things I want to explain: for the sceGuTexFilter command, you can use nearest or linear filtering depending on preference. However, you cannot use anisotropic filtering like modern graphics programs in the same way. Additionally, the reason why we swap framebuffers at the end is a continuation of our double buffering plan so we can avoid screen tearing and make sure the display is consistent.
Callbacks! Now we can set up a few of these for useful, convenient code.
Here's a nice long function to generate our grid geometry. Good thing we have it as a callback!
And the final piece to this code, our torus callback. Now our code is complete; a nice function to draw our torus, our grid geometry, and we also have an orbiting light source and dynamic shadows! All in about 500 lines of code! This is what the final code should look like, courtesy of the PSP SDK:
Wow, that's almost exhausting just to look at. But we got through it - our code is finished up and ready! Now the last thing to do is to set up a makefile so we can run our code. Let's save this code as shadowprojection.c and use this makefile:
So we haven't set many flags in particular, especially no risky compiler flags like -O2 because we don't need the speedup - our PSP can power through this by itself! This will compile it into a standard EBOOT.PBP with the internal title "Shadow Projection Sample". Once this is done, you can connect your PSP to your computer with the USB cable that came with your PSP (Wow, nice move Sony..) and copy it across so you can run it! This will also run on PPSSPP or any PSP emulator that can render 3D graphics well.
Congratulations! If you made it this far, you've done a great job. The code above was more advanced than most people would expect from a second lesson, however I really wanted to show you how a real 3D renderer is made. Thankfully, you often won't have to do this sort of programming, because game engines exist and you can simply build your games around those. However, it's a fascinating insight into what goes on behind the scenes. Remember to like and tell me your thoughts below if you enjoyed this, or if you didn't and want to tell me how to improve it. You can also follow me to be notified of future posts like this. Until next time, though, thanks for reading!
Footnote: The source to this code can be found on the PSP SDK github, in the samples section, under the "gu" directory. Someone rightly pointed out that I should credit the original writings/code, so here it is! I'd also like to apologise, since in the previous articles - especially towards the end - I got tired and lazy and I started copying more and more liberally from pre-existing articles and resources instead of writing things in my own words. To rectify this, I have written this entire article without any assistance except the PSP SDK sample code!![big grin :D :D](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
https://github.com/pspdev/pspsdk/tree/master/src/samples/gu/shadowprojection
Shadows
Code:
#include <pspkernel.h>
#include <pspdisplay.h>
#include <pspdebug.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <pspge.h>
#include <pspgu.h>
#include <pspgum.h>
Here, we set up our libraries for our code. You may recognise some of them, but some may be new. The libraries beginning with "std" are related to C/C++ code and will help us to use more advanced code than you may have previously encountered. math.h is self explanatory and string.h will be too. The final 3 are all related to managing the PSP GPU. Exciting! We're finally harnessing the PSP GPU and its hardware 3D rendering.
Code:
PSP_MODULE_INFO("Shadow Projection Sample", 0, 1, 1);
PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER);
#define printf pspDebugScreenPrintf
static unsigned int __attribute__((aligned(16))) list[262144];
typedef struct Vertex_Normal
{
float nx,ny,nz;
float x,y,z;
} Vertex_Normal;
Okay! That's a lot of new stuff! Here we're naming our module as usual and defining printf, but we're also setting up an attribute. The use of this will become clear later. We also set up our very first struct, so we can create vertexes at will and define the X, Y and Z of them.
Code:
typedef struct Texture
{
int format;
int mipmap;
int width, height, stride;
const void* data;
} Texture;
Another struct. Now we're defining textures. So when we create a new texture with this struct, we set its format, its mipmap level and its size.
Code:
/* grid */
#define GRID_COLUMNS 32
#define GRID_ROWS 32
#define GRID_SIZE 10.0f
Here we set up a 32x32 grid for our 3D rendering to take place.
Code:
Vertex_Normal __attribute__((aligned(16))) grid_vertices[GRID_COLUMNS*GRID_ROWS];
unsigned short __attribute__((aligned(16))) grid_indices[(GRID_COLUMNS-1)*(GRID_ROWS-1)*6];
Here's where our Vertex comes in. We set up our vertexes around the grid.
Code:
#define TORUS_SLICES 48 // numc
#define TORUS_ROWS 48 // numt
#define TORUS_RADIUS 1.0f
#define TORUS_THICKNESS 0.5f
Vertex_Normal __attribute__((aligned(16))) torus_vertices[TORUS_SLICES*TORUS_ROWS];
unsigned short __attribute__((aligned(16))) torus_indices[TORUS_SLICES*TORUS_ROWS*6];
Now we're setting up a Torus!
Code:
#define LIGHT_DISTANCE 3.0f
int SetupCallbacks();
void genGrid( unsigned rows, unsigned columns, float size,
Vertex_Normal* dstVertices, unsigned short* dstIndices );
void genTorus( unsigned slices, unsigned rows, float radius, float thickness,
Vertex_Normal* dstVertices, unsigned short* dstIndices );
Since we're dealing with shadows, we need to set up our lighting. We also set up our callbacks and generate our grid and torus.
Code:
#define BUF_WIDTH (512)
#define SCR_WIDTH (480)
#define SCR_HEIGHT (272)
#define PIXEL_SIZE (4) /* change this if you change to another screenmode */
#define FRAME_SIZE (BUF_WIDTH * SCR_HEIGHT * PIXEL_SIZE)
#define ZBUF_SIZE (BUF_WIDTH SCR_HEIGHT * 2) /* zbuffer seems to be 16-bit? */
Now it gets fun! We define our buffer as 512 to be standardised. Next, we set up our screen resolution. Unlike the GBA, we can set this to be practically whatever we want! For the sake of this article, I have set it to be 480x272 - the normal PSP screen resolution. The PSP does, however, support up to 720x576 and this can be displayed at full resolution via component cables. We set the pixel size as 4, and this is simply defining the standard screen mode. Framesize is calculated based on this and you don't need to know too much about that. The Z-buffer is a depth buffer, and it is 16-bit here.
Code:
typedef struct Geometry
{
ScePspFMatrix4 world;
unsigned int count;
unsigned short* indices;
Vertex_Normal* vertices;
unsigned int color;
} Geometry;
void drawGeometry( Geometry* geom )
{
sceGuSetMatrix(GU_MODEL,&geom->world);
sceGuColor(geom->color);
sceGuDrawArray(GU_TRIANGLES,GU_NORMAL_32BITF|GU_VERTEX_32BITF|GU_INDEX_16BIT|GU_TRANSFORM_3D,geom->count,geom->indices,geom->vertices);
}
Now we're defining our geometry. We set up our indices and vertices and create one or two unsigned ints. We also draw our geometry in this part; we use sceGuSetMatrix to set up this model, and then we draw this collection of triangles as our geometric world. It should be fairly self explanatory, and if you don't understand parts don't worry; these are publicly documented commands from the PSP libraries.
Code:
void drawShadowCaster( Geometry* geom )
{
sceGuSetMatrix(GU_MODEL,&geom->world);
sceGuColor(0x00000000);
sceGuDrawArray(GU_TRIANGLES,GU_NORMAL_32BITF|GU_VERTEX_32BITF|GU_INDEX_16BIT|GU_TRANSFORM_3D,geom->count,geom->indices,geom->vertices);
}
void drawShadowReceiver( Geometry* geom, ScePspFMatrix4 shadowProjMatrix )
{
sceGuSetMatrix(GU_MODEL,&geom->world);
// multiply shadowmap projection texture by geometry world matrix
// since geometry coords are in object space
gumMultMatrix(&shadowProjMatrix, &shadowProjMatrix, &geom->world );
sceGuSetMatrix(GU_TEXTURE,&shadowProjMatrix);
sceGuColor(geom->color);
sceGuDrawArray(GU_TRIANGLES,GU_NORMAL_32BITF|GU_VERTEX_32BITF|GU_INDEX_16BIT|GU_TRANSFORM_3D,geom->count,geom->indices,geom->vertices);
}
And now we get our shadows going as promised! ShadowCaster sets up what geometry will cast a shadow, and shadowReceiver is how the shadow is drawn based on geometry.
Code:
int main(int argc, char* argv[])
{
SetupCallbacks();
// generate geometry
genGrid( GRID_ROWS, GRID_COLUMNS, GRID_SIZE, grid_vertices, grid_indices );
genTorus( TORUS_ROWS, TORUS_SLICES, TORUS_RADIUS, TORUS_THICKNESS, torus_vertices, torus_indices );
// flush cache so that no stray data remains
sceKernelDcacheWritebackAll();
// setup VRAM buffers
void* frameBuffer = (void*)0;
const void* doubleBuffer = (void*)0x44000;
const void* renderTarget = (void*)0x88000;
const void* depthBuffer = (void*)0x110000;
Now, our all-important main loop where all of our code comes together! We set up our usual callbacks, then we generate our grid and torus. Then, we do something important; we flush the kernel cache to make sure that leftover memory is cleaned up. We also do some standard setting-up for the VRAM buffers.
Code:
// setup GU
sceGuInit();
sceGuStart(GU_DIRECT,list);
sceGuDrawBuffer(GU_PSM_4444,frameBuffer,BUF_WIDTH);
sceGuDispBuffer(SCR_WIDTH,SCR_HEIGHT,(void*)doubleBuffer,BUF_WIDTH);
sceGuDepthBuffer((void*)depthBuffer,BUF_WIDTH);
sceGuOffset(2048 - (SCR_WIDTH/2),2048 - (SCR_HEIGHT/2));
sceGuViewport(2048,2048,SCR_WIDTH,SCR_HEIGHT);
sceGuDepthRange(0xc350,0x2710);
sceGuScissor(0,0,SCR_WIDTH,SCR_HEIGHT);
sceGuEnable(GU_SCISSOR_TEST);
sceGuDepthFunc(GU_GEQUAL);
sceGuEnable(GU_DEPTH_TEST);
sceGuFrontFace(GU_CW);
sceGuShadeModel(GU_SMOOTH);
sceGuEnable(GU_CULL_FACE);
sceGuEnable(GU_TEXTURE_2D);
sceGuEnable(GU_DITHER);
sceGuFinish();
sceGuSync(0,0);
sceDisplayWaitVblankStart();
sceGuDisplay(GU_TRUE);
The Gu setup part is great, because you're simply enabling whatever you'd like and tuning the GPU as you want. We initialise and start the GPU first, naturally. Then we tell it to draw our buffer "frameBuffer", with the PSM_4444 format. Then it displays our buffer at 480x272, at PSP size, and sets up double buffering. This helps us to synchronise V-Blank, similar to double-buffered V-sync. We then set up our depth buffer (Z-buffer) and an offset or two, as well as the viewport. You don't need to know the following few, but you can enable depth testing, shading, dithering and more as should be self explanatory. Nice and easy, and makes you feel like a developer!
Code:
// setup matrices
ScePspFMatrix4 identity;
ScePspFMatrix4 projection;
ScePspFMatrix4 view;
gumLoadIdentity(&identity);
gumLoadIdentity(&projection);
gumPerspective(&projection,75.0f,16.0f/9.0f,0.5f,1000.0f);
{
ScePspFVector3 pos = {0,0,-5.0f};
gumLoadIdentity(&view);
gumTranslate(&view,&pos);
}
ScePspFMatrix4 textureProjScaleTrans;
gumLoadIdentity(&textureProjScaleTrans);
textureProjScaleTrans.x.x = 0.5;
textureProjScaleTrans.y.y = -0.5;
textureProjScaleTrans.w.x = 0.5;
textureProjScaleTrans.w.y = 0.5;
ScePspFMatrix4 lightProjection;
ScePspFMatrix4 lightProjectionInf;
ScePspFMatrix4 lightView;
ScePspFMatrix4 lightMatrix;
gumLoadIdentity(&lightProjection);
gumPerspective(&lightProjection,75.0f,1.0f,0.1f,1000.0f);
gumLoadIdentity(&lightProjectionInf);
gumPerspective(&lightProjectionInf,75.0f,1.0f,0.0f,1000.0f);
gumLoadIdentity(&lightView);
gumLoadIdentity(&lightMatrix);
The next bit is messy, as we're setting up some manual matrices about light projection. It's probably not necessary to understand all of this.
Code:
// define shadowmap
Texture shadowmap = {
GU_PSM_4444,
0, 128, 128, 128,
sceGeEdramGetAddr() + (int)renderTarget
};
And a nice easy part again. We're defining the resolution of our shadows, and changing the resolution and format of shadowmaps can be a nice optimisation to any budding game devs!
Code:
// define geometry
Geometry torus = {
identity,
sizeof(torus_indices)/sizeof(unsigned short),
torus_indices,
torus_vertices,
0xffffff
};
Geometry grid = {
identity,
sizeof(grid_indices)/sizeof(unsigned short),
grid_indices,
grid_vertices,
0xff7777
};
A little more defining for our geometry and grid.
Code:
// run sample
int val = 0;
for(;;)
{
// update matrices
// grid
{
ScePspFVector3 pos = {0,-1.5f,0};
gumLoadIdentity(&grid.world);
gumTranslate(&grid.world,&pos);
}
// torus
{
ScePspFVector3 pos = {0,0.5f,0.0f};
ScePspFVector3 rot = {val * 0.79f * (GU_PI/180.0f), val * 0.98f * (GU_PI/180.0f), val * 1.32f * (GU_PI/180.0f)};
gumLoadIdentity(&torus.world);
gumTranslate(&torus.world,&pos);
gumRotateXYZ(&torus.world,&rot);
}
And now we're rendering our world with the torus. A bit messy for sure, with all of the numbers involved, but if you want to find out more about how these specific commands work you can find online resources such as the archived pspdev.org and ps2dev.org.
Code:
// orbiting light
{
ScePspFVector3 lightLookAt = { torus.world.w.x, torus.world.w.y, torus.world.w.z };
ScePspFVector3 rot1 = {0,val * 0.79f * (GU_PI/180.0f),0};
ScePspFVector3 rot2 = {-(GU_PI/180.0f)*60.0f,0,0};
ScePspFVector3 pos = {0,0,LIGHT_DISTANCE};
gumLoadIdentity(&lightMatrix);
gumTranslate(&lightMatrix,&lightLookAt);
gumRotateXYZ(&lightMatrix,&rot1);
gumRotateXYZ(&lightMatrix,&rot2);
gumTranslate(&lightMatrix,&pos);
}
gumFastInverse(&lightView,&lightMatrix);
Over halfway there! Here's our light source for our shadow casting. The light orbits so as to cast a different and dynamic shadow.
Code:
// render to shadow map
{
sceGuStart(GU_DIRECT,list);
// set offscreen texture as a render target
sceGuDrawBufferList(GU_PSM_4444,(void*)renderTarget,shadowmap.stride);
// setup viewport
sceGuOffset(2048 - (shadowmap.width/2),2048 - (shadowmap.height/2));
sceGuViewport(2048,2048,shadowmap.width,shadowmap.height);
// clear screen
sceGuClearColor(0xffffffff); // 0xFFFFFFFF = blank, dummy or empty data
sceGuClearDepth(0);
sceGuClear(GU_COLOR_BUFFER_BIT|GU_DEPTH_BUFFER_BIT);
// setup view/projection from light
sceGuSetMatrix(GU_PROJECTION,&lightProjection);
sceGuSetMatrix(GU_VIEW,&lightView);
// shadow casters are drawn in black
// disable lighting and texturing
sceGuDisable(GU_LIGHTING);
sceGuDisable(GU_TEXTURE_2D);
// draw torus to shadow map
drawShadowCaster( &torus );
sceGuFinish();
sceGuSync(0,0);
}
This is where things get fun, this is the shadow rendering. We start the GPU up, then we set an off-screen texture as a render target. We set up the viewport and then clear the contents of the buffered screen. The rest should be fairly self explanatory; while we draw shadows, we disable lighting and 2D textures so that the rendering can focus on the shadows. Then we finish and sync!
Code:
// render to frame buffer
{
sceGuStart(GU_DIRECT,list);
// set frame buffer
sceGuDrawBufferList(GU_PSM_4444,(void*)frameBuffer,BUF_WIDTH);
// setup viewport
sceGuOffset(2048 - (SCR_WIDTH/2),2048 - (SCR_HEIGHT/2));
sceGuViewport(2048,2048,SCR_WIDTH,SCR_HEIGHT);
// clear screen
sceGuClearColor(0xff554433);
sceGuClearDepth(0);
sceGuClear(GU_COLOR_BUFFER_BIT|GU_DEPTH_BUFFER_BIT);
// setup view/projection from camera
sceGuSetMatrix(GU_PROJECTION,&projection);
sceGuSetMatrix(GU_VIEW,&view);
sceGuSetMatrix(GU_MODEL,&identity);
// setup a light
ScePspFVector3 lightPos = { lightMatrix.w.x, lightMatrix.w.y, lightMatrix.w.z };
ScePspFVector3 lightDir = { lightMatrix.z.x, lightMatrix.z.y, lightMatrix.z.z };
sceGuLight(0,GU_SPOTLIGHT,GU_DIFFUSE,&lightPos);
sceGuLightSpot(0,&lightDir, 5.0, 0.6);
sceGuLightColor(0,GU_DIFFUSE,0x00ff4040);
sceGuLightAtt(0,1.0f,0.0f,0.0f);
sceGuAmbient(0x00202020);
sceGuEnable(GU_LIGHTING);
sceGuEnable(GU_LIGHT0);
// draw torus
drawGeometry( &torus );
// setup texture projection
sceGuTexMapMode( GU_TEXTURE_MATRIX, 0, 0 );
sceGuTexProjMapMode( GU_POSITION );
// set shadowmap as a texture
sceGuTexMode(shadowmap.format,0,0,0);
sceGuTexImage(shadowmap.mipmap,shadowmap.width,shadowmap.height,shadowmap.stride,shadowmap.data);
sceGuTexFunc(GU_TFX_MODULATE,GU_TCC_RGB);
sceGuTexFilter(GU_LINEAR,GU_LINEAR);
sceGuTexWrap(GU_CLAMP,GU_CLAMP);
sceGuEnable(GU_TEXTURE_2D);
// calculate texture projection matrix for shadowmap
ScePspFMatrix4 shadowProj;
gumMultMatrix(&shadowProj, &lightProjectionInf, &lightView);
gumMultMatrix(&shadowProj, &textureProjScaleTrans, &shadowProj);
// draw grid receiving shadow
drawShadowReceiver( &grid, shadowProj );
sceGuFinish();
sceGuSync(0,0);
}
sceDisplayWaitVblankStart();
frameBuffer = sceGuSwapBuffers();
val++;
}
sceGuTerm();
sceKernelExitGame();
return 0;
}
Wow.. that's a lot to go through. Okay, so basically, now we're rendering our hard work into our frame buffer so it can be displayed on our PSP screen. A lot of this should actually be self explanatory, but a few things I want to explain: for the sceGuTexFilter command, you can use nearest or linear filtering depending on preference. However, you cannot use anisotropic filtering like modern graphics programs in the same way. Additionally, the reason why we swap framebuffers at the end is a continuation of our double buffering plan so we can avoid screen tearing and make sure the display is consistent.
Code:
/* Exit callback */
int exit_callback(int arg1, int arg2, void *common)
{
sceKernelExitGame();
return 0;
}
/* Callback thread */
int CallbackThread(SceSize args, void *argp)
{
int cbid;
cbid = sceKernelCreateCallback("Exit Callback", exit_callback, NULL);
sceKernelRegisterExitCallback(cbid);
sceKernelSleepThreadCB();
return 0;
}
/* Sets up the callback thread and returns its thread id */
int SetupCallbacks(void)
{
int thid = 0;
thid = sceKernelCreateThread("update_thread", CallbackThread, 0x11, 0xFA0, 0, 0);
if(thid >= 0)
{
sceKernelStartThread(thid, 0, 0);
}
return thid;
}
Callbacks! Now we can set up a few of these for useful, convenient code.
Code:
/* usefull geometry functions */
void genGrid( unsigned rows, unsigned columns, float size, Vertex_Normal* dstVertices, unsigned short* dstIndices )
{
unsigned int i,j;
// generate grid (TODO: tri-strips)
for (j = 0; j < rows; ++j)
{
for (i = 0; i < columns; ++i)
{
Vertex_Normal* curr = &dstVertices[i+j*columns];
curr->nx = 0.0f;
curr->ny = 1.0f;
curr->nz = 0.0f;
curr->x = ((i * (1.0f/((float)columns)))-0.5f) * size;
curr->y = 0;
curr->z = ((j * (1.0f/((float)columns)))-0.5f) * size;
}
}
for (j = 0; j < rows-1; ++j)
{
for (i = 0; i < columns-1; ++i)
{
unsigned short* curr = &dstIndices[(i+(j*(columns-1)))*6];
*curr++ = i + j * columns;
*curr++ = (i+1) + j * columns;
*curr++ = i + (j+1) * columns;
*curr++ = (i+1) + j * columns;
*curr++ = (i+1) + (j+1) * columns;
*curr++ = i + (j + 1) * columns;
}
}
}
Here's a nice long function to generate our grid geometry. Good thing we have it as a callback!
Code:
void genTorus( unsigned slices, unsigned rows, float radius, float thickness, Vertex_Normal* dstVertices, unsigned short* dstIndices )
{
unsigned int i,j;
// generate torus (TODO: tri-strips)
for (j = 0; j < slices; ++j)
{
for (i = 0; i < rows; ++i)
{
struct Vertex_Normal* curr = &dstVertices[i+j*rows];
float s = i + 0.5f;
float t = j;
float cs,ct,ss,st;
cs = cosf(s * (2*GU_PI)/slices);
ct = cosf(t * (2*GU_PI)/rows);
ss = sinf(s * (2*GU_PI)/slices);
st = sinf(t * (2*GU_PI)/rows);
curr->nx = cs * ct;
curr->ny = cs * st;
curr->nz = ss;
curr->x = (radius + thickness * cs) * ct;
curr->y = (radius + thickness * cs) * st;
curr->z = thickness * ss;
}
}
for (j = 0; j < slices; ++j)
{
for (i = 0; i < rows; ++i)
{
unsigned short* curr = &dstIndices[(i+(j*rows))*6];
unsigned int i1 = (i+1)%rows, j1 = (j+1)%slices;
*curr++ = i + j * rows;
*curr++ = i1 + j * rows;
*curr++ = i + j1 * rows;
*curr++ = i1 + j * rows;
*curr++ = i1 + j1 * rows;
*curr++ = i + j1 * rows;
}
}
}
And the final piece to this code, our torus callback. Now our code is complete; a nice function to draw our torus, our grid geometry, and we also have an orbiting light source and dynamic shadows! All in about 500 lines of code! This is what the final code should look like, courtesy of the PSP SDK:
Code:
#include <pspkernel.h>
#include <pspdisplay.h>
#include <pspdebug.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <pspge.h>
#include <pspgu.h>
#include <pspgum.h>
PSP_MODULE_INFO("Shadow Projection Sample", 0, 1, 1);
PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER);
#define printf pspDebugScreenPrintf
static unsigned int __attribute__((aligned(16))) list[262144];
typedef struct Vertex_Normal
{
float nx,ny,nz;
float x,y,z;
} Vertex_Normal;
typedef struct Texture
{
int format;
int mipmap;
int width, height, stride;
const void* data;
} Texture;
/* grid */
#define GRID_COLUMNS 32
#define GRID_ROWS 32
#define GRID_SIZE 10.0f
Vertex_Normal __attribute__((aligned(16))) grid_vertices[GRID_COLUMNS*GRID_ROWS];
unsigned short __attribute__((aligned(16))) grid_indices[(GRID_COLUMNS-1)*(GRID_ROWS-1)*6];
/* torus */
#define TORUS_SLICES 48 // numc
#define TORUS_ROWS 48 // numt
#define TORUS_RADIUS 1.0f
#define TORUS_THICKNESS 0.5f
Vertex_Normal __attribute__((aligned(16))) torus_vertices[TORUS_SLICES*TORUS_ROWS];
unsigned short __attribute__((aligned(16))) torus_indices[TORUS_SLICES*TORUS_ROWS*6];
#define LIGHT_DISTANCE 3.0f
int SetupCallbacks();
void genGrid( unsigned rows, unsigned columns, float size,
Vertex_Normal* dstVertices, unsigned short* dstIndices );
void genTorus( unsigned slices, unsigned rows, float radius, float thickness,
Vertex_Normal* dstVertices, unsigned short* dstIndices );
#define BUF_WIDTH (512)
#define SCR_WIDTH (480)
#define SCR_HEIGHT (272)
#define PIXEL_SIZE (4) /* change this if you change to another screenmode */
#define FRAME_SIZE (BUF_WIDTH * SCR_HEIGHT * PIXEL_SIZE)
#define ZBUF_SIZE (BUF_WIDTH SCR_HEIGHT * 2) /* zbuffer seems to be 16-bit? */
typedef struct Geometry
{
ScePspFMatrix4 world;
unsigned int count;
unsigned short* indices;
Vertex_Normal* vertices;
unsigned int color;
} Geometry;
void drawGeometry( Geometry* geom )
{
sceGuSetMatrix(GU_MODEL,&geom->world);
sceGuColor(geom->color);
sceGuDrawArray(GU_TRIANGLES,GU_NORMAL_32BITF|GU_VERTEX_32BITF|GU_INDEX_16BIT|GU_TRANSFORM_3D,geom->count,geom->indices,geom->vertices);
}
void drawShadowCaster( Geometry* geom )
{
sceGuSetMatrix(GU_MODEL,&geom->world);
sceGuColor(0x00000000);
sceGuDrawArray(GU_TRIANGLES,GU_NORMAL_32BITF|GU_VERTEX_32BITF|GU_INDEX_16BIT|GU_TRANSFORM_3D,geom->count,geom->indices,geom->vertices);
}
void drawShadowReceiver( Geometry* geom, ScePspFMatrix4 shadowProjMatrix )
{
sceGuSetMatrix(GU_MODEL,&geom->world);
// multiply shadowmap projection texture by geometry world matrix
// since geometry coords are in object space
gumMultMatrix(&shadowProjMatrix, &shadowProjMatrix, &geom->world );
sceGuSetMatrix(GU_TEXTURE,&shadowProjMatrix);
sceGuColor(geom->color);
sceGuDrawArray(GU_TRIANGLES,GU_NORMAL_32BITF|GU_VERTEX_32BITF|GU_INDEX_16BIT|GU_TRANSFORM_3D,geom->count,geom->indices,geom->vertices);
}
int main(int argc, char* argv[])
{
SetupCallbacks();
// generate geometry
genGrid( GRID_ROWS, GRID_COLUMNS, GRID_SIZE, grid_vertices, grid_indices );
genTorus( TORUS_ROWS, TORUS_SLICES, TORUS_RADIUS, TORUS_THICKNESS, torus_vertices, torus_indices );
// flush cache so that no stray data remains
sceKernelDcacheWritebackAll();
// setup VRAM buffers
void* frameBuffer = (void*)0;
const void* doubleBuffer = (void*)0x44000;
const void* renderTarget = (void*)0x88000;
const void* depthBuffer = (void*)0x110000;
// setup GU
sceGuInit();
sceGuStart(GU_DIRECT,list);
sceGuDrawBuffer(GU_PSM_4444,frameBuffer,BUF_WIDTH);
sceGuDispBuffer(SCR_WIDTH,SCR_HEIGHT,(void*)doubleBuffer,BUF_WIDTH);
sceGuDepthBuffer((void*)depthBuffer,BUF_WIDTH);
sceGuOffset(2048 - (SCR_WIDTH/2),2048 - (SCR_HEIGHT/2));
sceGuViewport(2048,2048,SCR_WIDTH,SCR_HEIGHT);
sceGuDepthRange(0xc350,0x2710);
sceGuScissor(0,0,SCR_WIDTH,SCR_HEIGHT);
sceGuEnable(GU_SCISSOR_TEST);
sceGuDepthFunc(GU_GEQUAL);
sceGuEnable(GU_DEPTH_TEST);
sceGuFrontFace(GU_CW);
sceGuShadeModel(GU_SMOOTH);
sceGuEnable(GU_CULL_FACE);
sceGuEnable(GU_TEXTURE_2D);
sceGuEnable(GU_DITHER);
sceGuFinish();
sceGuSync(0,0);
sceDisplayWaitVblankStart();
sceGuDisplay(GU_TRUE);
// setup matrices
ScePspFMatrix4 identity;
ScePspFMatrix4 projection;
ScePspFMatrix4 view;
gumLoadIdentity(&identity);
gumLoadIdentity(&projection);
gumPerspective(&projection,75.0f,16.0f/9.0f,0.5f,1000.0f);
{
ScePspFVector3 pos = {0,0,-5.0f};
gumLoadIdentity(&view);
gumTranslate(&view,&pos);
}
ScePspFMatrix4 textureProjScaleTrans;
gumLoadIdentity(&textureProjScaleTrans);
textureProjScaleTrans.x.x = 0.5;
textureProjScaleTrans.y.y = -0.5;
textureProjScaleTrans.w.x = 0.5;
textureProjScaleTrans.w.y = 0.5;
ScePspFMatrix4 lightProjection;
ScePspFMatrix4 lightProjectionInf;
ScePspFMatrix4 lightView;
ScePspFMatrix4 lightMatrix;
gumLoadIdentity(&lightProjection);
gumPerspective(&lightProjection,75.0f,1.0f,0.1f,1000.0f);
gumLoadIdentity(&lightProjectionInf);
gumPerspective(&lightProjectionInf,75.0f,1.0f,0.0f,1000.0f);
gumLoadIdentity(&lightView);
gumLoadIdentity(&lightMatrix);
// define shadowmap
Texture shadowmap = {
GU_PSM_4444,
0, 128, 128, 128,
sceGeEdramGetAddr() + (int)renderTarget
};
// define geometry
Geometry torus = {
identity,
sizeof(torus_indices)/sizeof(unsigned short),
torus_indices,
torus_vertices,
0xffffff
};
Geometry grid = {
identity,
sizeof(grid_indices)/sizeof(unsigned short),
grid_indices,
grid_vertices,
0xff7777
};
// run sample
int val = 0;
for(;;)
{
// update matrices
// grid
{
ScePspFVector3 pos = {0,-1.5f,0};
gumLoadIdentity(&grid.world);
gumTranslate(&grid.world,&pos);
}
// torus
{
ScePspFVector3 pos = {0,0.5f,0.0f};
ScePspFVector3 rot = {val * 0.79f * (GU_PI/180.0f), val * 0.98f * (GU_PI/180.0f), val * 1.32f * (GU_PI/180.0f)};
gumLoadIdentity(&torus.world);
gumTranslate(&torus.world,&pos);
gumRotateXYZ(&torus.world,&rot);
}
// orbiting light
{
ScePspFVector3 lightLookAt = { torus.world.w.x, torus.world.w.y, torus.world.w.z };
ScePspFVector3 rot1 = {0,val * 0.79f * (GU_PI/180.0f),0};
ScePspFVector3 rot2 = {-(GU_PI/180.0f)*60.0f,0,0};
ScePspFVector3 pos = {0,0,LIGHT_DISTANCE};
gumLoadIdentity(&lightMatrix);
gumTranslate(&lightMatrix,&lightLookAt);
gumRotateXYZ(&lightMatrix,&rot1);
gumRotateXYZ(&lightMatrix,&rot2);
gumTranslate(&lightMatrix,&pos);
}
gumFastInverse(&lightView,&lightMatrix);
// render to shadow map
{
sceGuStart(GU_DIRECT,list);
// set offscreen texture as a render target
sceGuDrawBufferList(GU_PSM_4444,(void*)renderTarget,shadowmap.stride);
// setup viewport
sceGuOffset(2048 - (shadowmap.width/2),2048 - (shadowmap.height/2));
sceGuViewport(2048,2048,shadowmap.width,shadowmap.height);
// clear screen
sceGuClearColor(0xffffffff);
sceGuClearDepth(0);
sceGuClear(GU_COLOR_BUFFER_BIT|GU_DEPTH_BUFFER_BIT);
// setup view/projection from light
sceGuSetMatrix(GU_PROJECTION,&lightProjection);
sceGuSetMatrix(GU_VIEW,&lightView);
// shadow casters are drawn in black
// disable lighting and texturing
sceGuDisable(GU_LIGHTING);
sceGuDisable(GU_TEXTURE_2D);
// draw torus to shadow map
drawShadowCaster( &torus );
sceGuFinish();
sceGuSync(0,0);
}
// render to frame buffer
{
sceGuStart(GU_DIRECT,list);
// set frame buffer
sceGuDrawBufferList(GU_PSM_4444,(void*)frameBuffer,BUF_WIDTH);
// setup viewport
sceGuOffset(2048 - (SCR_WIDTH/2),2048 - (SCR_HEIGHT/2));
sceGuViewport(2048,2048,SCR_WIDTH,SCR_HEIGHT);
// clear screen
sceGuClearColor(0xff554433);
sceGuClearDepth(0);
sceGuClear(GU_COLOR_BUFFER_BIT|GU_DEPTH_BUFFER_BIT);
// setup view/projection from camera
sceGuSetMatrix(GU_PROJECTION,&projection);
sceGuSetMatrix(GU_VIEW,&view);
sceGuSetMatrix(GU_MODEL,&identity);
// setup a light
ScePspFVector3 lightPos = { lightMatrix.w.x, lightMatrix.w.y, lightMatrix.w.z };
ScePspFVector3 lightDir = { lightMatrix.z.x, lightMatrix.z.y, lightMatrix.z.z };
sceGuLight(0,GU_SPOTLIGHT,GU_DIFFUSE,&lightPos);
sceGuLightSpot(0,&lightDir, 5.0, 0.6);
sceGuLightColor(0,GU_DIFFUSE,0x00ff4040);
sceGuLightAtt(0,1.0f,0.0f,0.0f);
sceGuAmbient(0x00202020);
sceGuEnable(GU_LIGHTING);
sceGuEnable(GU_LIGHT0);
// draw torus
drawGeometry( &torus );
// setup texture projection
sceGuTexMapMode( GU_TEXTURE_MATRIX, 0, 0 );
sceGuTexProjMapMode( GU_POSITION );
// set shadowmap as a texture
sceGuTexMode(shadowmap.format,0,0,0);
sceGuTexImage(shadowmap.mipmap,shadowmap.width,shadowmap.height,shadowmap.stride,shadowmap.data);
sceGuTexFunc(GU_TFX_MODULATE,GU_TCC_RGB);
sceGuTexFilter(GU_LINEAR,GU_LINEAR);
sceGuTexWrap(GU_CLAMP,GU_CLAMP);
sceGuEnable(GU_TEXTURE_2D);
// calculate texture projection matrix for shadowmap
ScePspFMatrix4 shadowProj;
gumMultMatrix(&shadowProj, &lightProjectionInf, &lightView);
gumMultMatrix(&shadowProj, &textureProjScaleTrans, &shadowProj);
// draw grid receiving shadow
drawShadowReceiver( &grid, shadowProj );
sceGuFinish();
sceGuSync(0,0);
}
sceDisplayWaitVblankStart();
frameBuffer = sceGuSwapBuffers();
val++;
}
sceGuTerm();
sceKernelExitGame();
return 0;
}
/* Exit callback */
int exit_callback(int arg1, int arg2, void *common)
{
sceKernelExitGame();
return 0;
}
/* Callback thread */
int CallbackThread(SceSize args, void *argp)
{
int cbid;
cbid = sceKernelCreateCallback("Exit Callback", exit_callback, NULL);
sceKernelRegisterExitCallback(cbid);
sceKernelSleepThreadCB();
return 0;
}
/* Sets up the callback thread and returns its thread id */
int SetupCallbacks(void)
{
int thid = 0;
thid = sceKernelCreateThread("update_thread", CallbackThread, 0x11, 0xFA0, 0, 0);
if(thid >= 0)
{
sceKernelStartThread(thid, 0, 0);
}
return thid;
}
/* usefull geometry functions */
void genGrid( unsigned rows, unsigned columns, float size, Vertex_Normal* dstVertices, unsigned short* dstIndices )
{
unsigned int i,j;
// generate grid (TODO: tri-strips)
for (j = 0; j < rows; ++j)
{
for (i = 0; i < columns; ++i)
{
Vertex_Normal* curr = &dstVertices[i+j*columns];
curr->nx = 0.0f;
curr->ny = 1.0f;
curr->nz = 0.0f;
curr->x = ((i * (1.0f/((float)columns)))-0.5f) * size;
curr->y = 0;
curr->z = ((j * (1.0f/((float)columns)))-0.5f) * size;
}
}
for (j = 0; j < rows-1; ++j)
{
for (i = 0; i < columns-1; ++i)
{
unsigned short* curr = &dstIndices[(i+(j*(columns-1)))*6];
*curr++ = i + j * columns;
*curr++ = (i+1) + j * columns;
*curr++ = i + (j+1) * columns;
*curr++ = (i+1) + j * columns;
*curr++ = (i+1) + (j+1) * columns;
*curr++ = i + (j + 1) * columns;
}
}
}
void genTorus( unsigned slices, unsigned rows, float radius, float thickness, Vertex_Normal* dstVertices, unsigned short* dstIndices )
{
unsigned int i,j;
// generate torus (TODO: tri-strips)
for (j = 0; j < slices; ++j)
{
for (i = 0; i < rows; ++i)
{
struct Vertex_Normal* curr = &dstVertices[i+j*rows];
float s = i + 0.5f;
float t = j;
float cs,ct,ss,st;
cs = cosf(s * (2*GU_PI)/slices);
ct = cosf(t * (2*GU_PI)/rows);
ss = sinf(s * (2*GU_PI)/slices);
st = sinf(t * (2*GU_PI)/rows);
curr->nx = cs * ct;
curr->ny = cs * st;
curr->nz = ss;
curr->x = (radius + thickness * cs) * ct;
curr->y = (radius + thickness * cs) * st;
curr->z = thickness * ss;
}
}
for (j = 0; j < slices; ++j)
{
for (i = 0; i < rows; ++i)
{
unsigned short* curr = &dstIndices[(i+(j*rows))*6];
unsigned int i1 = (i+1)%rows, j1 = (j+1)%slices;
*curr++ = i + j * rows;
*curr++ = i1 + j * rows;
*curr++ = i + j1 * rows;
*curr++ = i1 + j * rows;
*curr++ = i1 + j1 * rows;
*curr++ = i + j1 * rows;
}
}
}
Wow, that's almost exhausting just to look at. But we got through it - our code is finished up and ready! Now the last thing to do is to set up a makefile so we can run our code. Let's save this code as shadowprojection.c and use this makefile:
Code:
TARGET = shadowprojection
OBJS = shadowprojection.o
INCDIR =
CFLAGS = -G0 -Wall
CXXFLAGS = $(CFLAGS) -fno-exceptions -fno-rtti
ASFLAGS = $(CFLAGS)
LIBDIR =
LDFLAGS =
LIBS = -lpspgum -lpspgu -lm
EXTRA_TARGETS = EBOOT.PBP
PSP_EBOOT_TITLE = Shadow Projection Sample
PSPSDK=$(shell psp-config --pspsdk-path)
include $(PSPSDK)/lib/build.mak
So we haven't set many flags in particular, especially no risky compiler flags like -O2 because we don't need the speedup - our PSP can power through this by itself! This will compile it into a standard EBOOT.PBP with the internal title "Shadow Projection Sample". Once this is done, you can connect your PSP to your computer with the USB cable that came with your PSP (Wow, nice move Sony..) and copy it across so you can run it! This will also run on PPSSPP or any PSP emulator that can render 3D graphics well.
Congratulations! If you made it this far, you've done a great job. The code above was more advanced than most people would expect from a second lesson, however I really wanted to show you how a real 3D renderer is made. Thankfully, you often won't have to do this sort of programming, because game engines exist and you can simply build your games around those. However, it's a fascinating insight into what goes on behind the scenes. Remember to like and tell me your thoughts below if you enjoyed this, or if you didn't and want to tell me how to improve it. You can also follow me to be notified of future posts like this. Until next time, though, thanks for reading!
Footnote: The source to this code can be found on the PSP SDK github, in the samples section, under the "gu" directory. Someone rightly pointed out that I should credit the original writings/code, so here it is! I'd also like to apologise, since in the previous articles - especially towards the end - I got tired and lazy and I started copying more and more liberally from pre-existing articles and resources instead of writing things in my own words. To rectify this, I have written this entire article without any assistance except the PSP SDK sample code!
https://github.com/pspdev/pspsdk/tree/master/src/samples/gu/shadowprojection