Tuesday, June 17, 2008

Skyboxes

The skybox (or dome, sphere, etc.) will play an important role in the game I'm building as it will serve to further immerse the player in the stylized world of the game. From what my limited knowledge can say there are a variety of ways to accomplish a "skybox" effect, from the simplicity of pasting textures onto a very large set of polygons at the borders of the level/world to pre-rendering miniature models and using cube maps to give a greater illusion of depth (wikipedia has a pretty good entry about this).

So how exactly did I create my skybox?
First of all, I'm using a skysphere (however, I'll keep referring to it as a skybox anyway) - it looks a lot nicer. Though, the sphere is a pretty jagged one, it has only 72 faces.

Here is picture of the texture I used for the skybox:



And here is the CgFx shader code for displaying it:
float4x4 ModelViewProjXf;
float4x4 WorldXf;
float3 SkyCamPos;

float3 MultiplyColour <
> = {1.0f,1.0f,1.0f};

texture skyEnvMap : Environment <
string ResourceType = "Cube";
>;

samplerCUBE SkyboxSampler = sampler_state {
Texture = < skyEnvMap >;
WrapS = ClampToEdge;
WrapT = ClampToEdge;
WrapR = ClampToEdge;
};

// application to vertex shader -----------------
struct appdata {
float3 Position : POSITION;
};

// vertex shader to pixel shader ----------------
struct vertexOutput {
float4 HPosition : POSITION;
float3 WorldView : TEXCOORD0;
};

// Vertex Shader --------------------------------
vertexOutput skybox_VS(appdata IN) {
vertexOutput OUT = (vertexOutput)0;
float4 Pos = float4(IN.Position.xyz,1);
float3 Pw = mul(WorldXf,Pos).xyz;
OUT.HPosition = mul(ModelViewProjXf,Pos);
OUT.WorldView = normalize(Pw -
float3(SkyCamPos.x,SkyCamPos.y,SkyCamPos.z));
return OUT;
}

// Fragement Shader (Floating Point) ------------
float4 skybox_PS(vertexOutput IN) : COLOR {
// All we need to do is a look up in the cube map
// using the vector from the eye to the fragment...
float3 SkyColour = texCUBE(SkyboxSampler, IN.WorldView).xyz;
return float4(SkyColour * MultiplyColour, 1.0f);
}

technique Skybox
{
pass pass0
{
CullFaceEnable = true;
CullFace = Back;
DepthTestEnable = true;
DepthFunc = LEqual;

VertexProgram = compile vp40 skybox_VS();
FragmentProgram = compile fp40 skybox_PS();
}
}
The skybox is a spherical polygon mesh that's loaded into the game engine, I then have a very specific effect (the above "Skybox.cgfx" shader), which I apply to that mesh. The shader does the texture mapping for the skybox by doing a look-up in a given cubemap. In the case of the "Deco" level of the game, the cube map has just one texture applied to all 6 faces of the cube. The "SkyCamPos" variable defines the center of the skybox - this is the location from which the 'rays' shoot out and sample the cubemap, giving the illusion that the skybox is very far away since it will always remain stationary to the viewer. I should note here that some skybox techniques like those used in Valve's Source engine allow for a very small amount of skybox movement as the viewer moves - I considered this but since the game is mostly stationary it's really not necessary.

The "MultiplyColour" variable is used to multiply the colour of the cubemap texel look-up with some specified colour in the game code. In the case of the "Deco" world, the colour is constantly shifting between a set of established colours. I do this with the following code:
// Figure out what the colour of the background should be...
double colourChangeInc = dT * COLOUR_CHANGE_SPEED;
Colour nextColour = COLOUR_CHANGE_LIST[
(this->currColourIndex + 1) % NUM_COLOUR_CHANGES];
bool transitionDone = true;

// Find out if there is a significant difference
// in each colour channel, if there is then move
// towards the next target colour in that channel
if (fabs(currColour.R() - nextColour.R()) >
COLOUR_CHANGE_SPEED) {
int changeSgn =
NumberFuncs::SignOf(nextColour.R() - currColour.R());
this->currColour[0] += changeSgn*colourChangeInc;
transitionDone = false;
}
if (fabs(currColour.G() - nextColour.G()) >
COLOUR_CHANGE_SPEED) {
int changeSgn =
NumberFuncs::SignOf(nextColour.G() - currColour.G());
this->currColour[1] += changeSgn*colourChangeInc;
transitionDone = false;
}
if (fabs(currColour.B() - nextColour.B()) >
COLOUR_CHANGE_SPEED) {
int changeSgn =
NumberFuncs::SignOf(nextColour.B() - currColour.B());
this->currColour[2] += changeSgn*colourChangeInc;
transitionDone = false;
}

// If we're close enough to the next colour
// then move on to the next colour
if (transitionDone) {
this->currColourIndex = (this->currColourIndex + 1) %
NUM_COLOUR_CHANGES;
}

cgGLSetParameter3f(this->colourMultParam,
this->currColour[0], this->currColour[1],
this->currColour[2]);
This code acts almost like a very simplified parametric spline - where each of the colours in the COLOUR_CHANGE_LIST array act as points in 3D space (RGB instead of XYZ). Each of the if-statements is responsible for shifting along the line that leads to the next colour point. If you look at the code really closely you'll notice that there are small jumps around the points since the COLOUR_CHANGE_SPEED is not a very small value (it's currently set to around 0.07), these jumps are impossible to see, however, since they too are still quite small.

Thursday, June 12, 2008

The Beginnings of a Game

I recently started to put some serious work into a game, which I'll be calling "Biff! Bam!! Blammo?!!". The game is a glorified version of "Arkanoid", which, I'm aware, has a billion+ copies out there, but I choose this type of game mainly because the game logic is straight forward, which paves the way for more time spent on graphics and exploring interesting game mechanics.

These posts concerning the game are intended to record things that have driven me insane or insights I've gained while working on it. But first here are some pretty pictures:

This first picture is of the first 'world' - the game will be split up into worlds, each with its own theme - the theme of this first world being "Deco".



Next is a picture of the same scene but without the game pieces or HUD.



Finally here's a picture of the deco-block mesh that makes up the solid blocks in the Deco world.



Materials

So far I've developed 2 CgFx materials: a basic celshading/toon material and a basic phong material. Both materials have black outlines to remain consistent and to bring out more of the features of the various meshes.

I've designed materials so that they share all uniform and varying parameters - typically these are just various projection, view and world transform matrices, a diffuse sampler, specular colour, diffuse colour, shininess, and light positions/colours. My reasoning behind this is so that I can move all the parameters (in code), up into a superclass for materials. This allows me to abstract any of the game materials, where I may change the parameters of a material without needing to know the material I'm dealing with. Of course, I lose this abstraction if I ever create really complicated materials with unusual parameters - but for the purposes of this game, I doubt I'll have any materials of that sort.

To deal with the CGcontext and other persistent Cg-type objects I created a singleton manager for CgFx - this makes creating, getting, and destroying those objects easy and centralizes that functionality. It also provides a central place to load effects and check for errors in the Cg runtime.

Instancing Issues

Probably the most time consuming and troubling issue I've had so far was dealing with instancing of the blocks/bricks that make up game levels. When I first drew a full level with all of its bricks, CgFx materials included, I had a frame rate of 10... which seemed absolutely ridiculous on a NVIDIA 8800 GT - my goal for this game is to ALWAYS keep the frame rate at 60 fps. So at that point there was much science to be done; I had never dealt with extreme instancing before and I had some ideas of what the problem was...
  • Naive hypothesis: high polygon block meshes

  • Less Naive hypothesis, but still naive: using only one display list many times

  • Partly good hypothesis: overly complicated CgFx shaders (move outline pass out of shader, simplify some of the math)

  • Correct hypothesis: ALL material parameters are being changed for EVERY BLOCK (yikes!)
Obviously the first two hypothesis had little/nothing to do with the problem, the last two, together, had just about everything to do with it - but mostly it was the last. The issue when it comes to instancing is that every instance will have its own transformation, material properties, etc. but the mesh is the same... so in a perfect world why not just draw one mesh many, many times with changed parameters feeding into each respective material? Because it kills your GPU... one of the slowest parts of drawing a material is setting the parameters, especially the transforms - not only is that data being sent to the GPU but its also affecting the entire shader.

I was able to remedy the problem by rendering the entire level as a set of individual display lists for all the blocks, each display list with that block's full world transform set as vertex positions - sure it takes up more memory, but the blocks are really small anyway. Then, as I draw each of the display lists I only set the transform parameters once and the only varying parameter for now is the diffuse colour. This remedy put the frame rate back at 60 fps.

This is obviously NOT a solution that uses "instancing" in the sense of modern GPU instancing (i.e., one that uses the latest and greatest OpenGL extensions for instancing or even using pseudo instancing), but it's certainly a solution that works.