TheMrIron2 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

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

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! :D
https://github.com/pspdev/pspsdk/tree/master/src/samples/gu/shadowprojection

2 Comments

  • sblast3
  • TheMrIron2
You need to be logged in to comment