Skyboxes

Skyboxes are conceptually a pretty simple idea: Attach a cube to the camera and draw a cubemap. This can certainly be approached like this but I'll be showing a couple tricks I use to make this easier and more bug free.

The Mesh

The mesh for a skybox I use is the default blender cube with the face normals flipped.

To be more specific: A flat shaded cube where it's vertex positions lie on the +-1.0 planes on each axis, and the face normals pointing to the origin.

(Flipping normals in blender reverse winding order)

When exporting this I only export the position data. We don't export any extra information, thus we can compact the mesh data to be smaller.

The vertex positions are the texture coordinates

To sample a cubemap you use a 3D vector from the origin pointing the the point you want to sample on the cubemap. Using this cube lets us directly pass the positions as texture coodinates with no additional processing in the shader code.

No world transform

I render my skyboxes without a world transform. This is inherently unnessesary. Why transform the cube somewhere just for the view matrix to transform it back. Simply doing this unnessesary math also helps us avoid floating point precision errors.

The one thing we still need to account for is the camera orientation. This is still pretty simply to accomplish.

Consider your standard vertex matrix multiplication:

gl_Position = projection * view * transform * vec4(position, 1.0);

This is pretty standard to place objects within your world. But this can be modified to easily account for how to want our skybox positioning to work.

gl_Position = projection * vec4(mat3(view) * position, 1.0);
  • Firstly we remove our world transform multiplication, this isn't needed.
  • Next we cast the view matrix to a mat3. The result of this is that the last row and column gets removed. This removes tralsation from the camera.
  • Group the now mat3 with the vec3 to satisfy matrix vector multiplication rules.

Depth testing

Without modifying how we render the object, it'll perform its depth testing just like any other object and not really look like a skybox. There's a couple ways to fix this. One is to simply disable depth testing and thus depth writing. As long as we draw the skybox first, it'll look correct.

Draw order

Generally we want to reduce overdraw as much as possible. The skybox can easily be drawn last in the opaque 3D pipeline.

Consider the case where it's drawn first, given its locked to the camera, an entire framebuffer's worth of pixels will be computed. Some values will eventually be discard when you draw something that covers any portion of the skybox. This is not ideal as we have just wasted performance calculating some of those fragments.

However to draw it last requires us to be able to depth test. Luckily there's some simple shader trickery to allos us to do this.

Shader trickery

In OpenGL the depth of each fragment gets computed for every fragment that gets rasterized. With a value of 1 meaning the depth is at the far clipping plane.

We can force the depth calculations for our skybox to evaluate to 1. Meaning even when drawn last, it'll still render behind everything and still restpect depth testing.

The rendering pipline has a fixed function step known as perspective division. This is done for perspective correct texture mapping, but that is another topic for later. This means at some point in the shader pipeline this gets computed: gl_Position.xyz /= gl_Position.w.

After we compute gl_Position we can simply set the z value to the w value. After perspective division this will force the depth value to be 1.

After apply this step you will notice some clipping at the far plane.

Depth function

With our depth values being 1 we will face Z clipping issues, since values >= 1.0 get clipped. This is given by the default state of glDepthFunc(GL_LESS). This affects how depth testing works. For the skybox we can make a simple adjustment: glDepthFunc(GL_LEQUAL).

Changing this state will fix the clipping issue, this effectivly means our clipping range now includes the 1.0 value. This can be done globally for your application as this doesn't negativly affect rendering other objects.

Conclusing

In graphics rendering everything is just approximations and illusions. By breaking our traditional rendering rules we simplified the process to draw a skybox and probably broken the skybox illusion.

Not all skyboxes are handled like this. This is just the method I like using as of writing.