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.