Introduction
A skybox is a panoramic view representing a sky or any other scenery. It is a simple way to add realism to a game with minimal performance cost.
A skybox is generated from a cube. Each face of the cube contains a texture representing a visible view (up, down, front, back, left, right) of the scenery.
To implement a skybox is quite simple. We simply unwrap a cube into its UV Map. Apply a texture to each face of the cube and render the cube in the middle of the scene.
Figure 1. Example of a Skybox
Objective
In this tutorial you will learn how to implement a skybox in a mobile device using OpenGL ES as shown in figure 2. This is a hands on tutorial. Feel free to download this template Xcode project and code along.
Figure 2. A Skybox in an iOS device
Things to know
In order to get the most out of this tutorial, I suggest to read these tutorials before hand:
Implementing a skybox
In order to render a skybox we need to do the following:
- Load the vertices of a cube into OpenGL buffers
- Load six textures into texture objects
- Render the cube in a scene.
If you have not done so, please read Loading data into OpenGL Buffers and Applying textures to a 3D model . You will need to know these concepts before moving on.
Our project contains a C++ class called SkyBox. Open up the project and locate the file SkyBox.h and SkyBox.mm. In this C++ class, you will implement the methods necessary to render a skybox in the scene.
Loading the vertices of the cube
As mentioned earlier, a skybox is simply a cube rendered on a scene. The cube’s vertices are loaded into OpenGL buffers and passed down to shaders.
The cube’s vertices have been declared in the array skyvertices[] in the Sky.h file.
Open up the SkyBox.mm file and locate the method setupOpenGL(). Lines 1-13 perform the necessary operations to load the cube vertices into the buffer. If these operations are no familiar to you, please read Loading data into OpenGL Buffers.
Loading the textures into a texture object
Lines 14-18 in the setupOpenGL() method is the section which you will implement. It is the section responsible for activating a texture unit, creating a texture object and loading the textures for the skybox.
Go to the setupOpenGL() method. Locate line 14 and copy what is shown in listing 1.
Listing 1. Loading the images into texture objects
void SkyBox::setupOpenGL(){
//…
//14. Activate GL_TEXTURE0
glActiveTexture(GL_TEXTURE0);
//15.a Generate a texture buffer
glGenTextures(1, &textureID[0]);
//16. Bind texture0
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID[0]);
//17. Simple For loop to get each image reference
for(int i=0; i<cubeMapTextures.size(); i++) {
//17a.Decode each cube map image into its raw image data.
if(convertImageToRawImage(cubeMapTextures.at(i))){
//17b. if decompression was successful, set the texture parameters
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
//17c. load the image data into the current bound texture buffer
//cubeMapTarget[] contains the cube map targets
glTexImage2D(cubeMapTarget[i], 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, &image[0]);
}
image.clear();
}
//18. Get the location of the Uniform Sampler2D
UVMapUniformLocation=glGetUniformLocation(programObject, "SkyBoxTexture");
//…
}
Our first task is to activate a texture unit as shown in line 1. A texture unit is the section in the GPU responsible for texturing operations.
Lines 15 & 16 simply create and bind a texture object. However, the texture object’s behavior is set to behave as if it is carrying a cube map image instead of a 2D image (line 16).
Since we have six images to load, we setup a simple For loop (line 17). Each image is decompressed into raw-format and their texture parameters set (lines 17a & 17b).
The reference to each image is stored in a C++ vector as shown in the SkyBox() constructor line 1.
The skybox requires an image for each face of the cube. During the loading of these images, how do we link an image to a particular face?
OpenGL provides a set of targets specifically for this purpose. Each face of the cube is represented as follows in OpenGL:
- GL_TEXTURE_CUBE_MAP_POSITIVE_X
- GL_TEXTURE_CUBE_MAP_NEGATIVE_X
- GL_TEXTURE_CUBE_MAP_POSITIVE_Y
- GL_TEXTURE_CUBE_MAP_NEGATIVE_Y
- GL_TEXTURE_CUBE_MAP_POSITIVE_Z
- GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
Each of these targets is declared in the cubeMapTarget[] array in the SkyBox.h file.
During the image loading process, we iterate through each of these targets and apply the correct image to a cube face. This is shown in line 17c.
Finally, we get the location of the uniform Sampler2D (line 18). This uniform will contain a reference to our texture data in the fragment shader.
Rendering the skybox
For the skybox to render on the screen, we need to activate the texture unit. A texture unit holds a reference to our texture object. Once the texture unit is activated, we bind the texture object with the target of _GL_TEXTURE_CUBEMAP. Finally we call the glDrawElements() function, which will render our skybox.
Open up the SkyBox.mm file. Locate the draw() method and copy what is shown in lines 3-8 in listing 2.
Listing 2. Rendering the skybox
void SkyBox::draw(){
//…
//3. Activate the texture unit
glActiveTexture(GL_TEXTURE0);
//4 Bind the texture object
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID[0]);
//5. Specify the value of the UV Map uniform
glUniform1i(UVMapUniformLocation, 0);
//6. draw the pixels if the incoming depth value is less than or equal to the stored depth value.
glDepthFunc(GL_LEQUAL);
//7. Start the rendering process
glDrawElements(GL_TRIANGLES, sizeof(sky_index)/4, GL_UNSIGNED_INT,(void*)0);
//8. draw the pixels if the incoming depth value is less than the stored depth value
glDepthFunc(GL_LESS);
//…
}
The texture unit is activated in line 3. The binding of the texture object occurs in line 4 and finally we start the rendering in line 7.
A skybox appears to always be rendered behind any other object in a scene. For this to occur we need to compare the incoming pixel-depth with the currently present in the frame buffer.
In line 6 we use the depth test condition of _GLLEQUAL. This allows the skybox to be render behind any other object in the scene. Line 7 renders the skybox as usual. The depth condition is then set to the default condition of _GLLESS, which allows objects to be render in front of any other in the scene (line 8).
The depth comparison is performed only if depth testing is enabled. This is set in the constructor method line 3.
Implementing the vertex shader
We will now implement the vertex shader for the skybox. Open up the SkyShader.vsh file and copy what is shown in listing 3.
Listing 3. Implementing the Vertex Shader
//1. declare attributes
attribute vec4 position;
//2. declare varying type which will transfer the texture coordinates to the fragment shader
varying mediump vec3 vTexCoordinates;
//3. declare a uniform that contains the model-View-projection
uniform mat4 modelViewProjectionMatrix;
void main()
{
//4. Generate the UV coordinates
vTexCoordinates=normalize(position.xyz);
//5. transform every position vertex by the model-view-projection matrix
gl_Position=modelViewProjectionMatrix * position;
//6. Trick to place the skybox behind any other 3D model
gl_Position=gl_Position.xyww;
}
You may have noticed that we never loaded UV coordinates into our OpenGL buffers in method setupOpenGL(). Loading UV coordinates is required when applying a texture to a 3D model, but they are not required when implementing a skybox.
Instead of loading UV coordinates into OpenGL buffers, we simply generate them in the vertex shader. The UV coordinates are simply the normalize vertex locations as shown in line 4.
As state before, one of the characteristics of a skybox is that it always appears to be rendered behind any other object in a scene. This is simply a trick that is performed by setting the z component of the vertex shader output, _glPosition, equal to the homogeneous coordinate w as shown in line 6.
Implementing the fragment shader
Implementing the fragment shader is very simple. It is very similar to the fragment shader implemented when a texture is applied to a 3D model.
The only difference is that the texture is sampled using the function textureCube() instead of texture2D() as shown in line 3.
Open up file SkyShader.fsh. Copy what is shown in listing 4.
Listing 4. Implementing the Fragment Shader
precision highp float;
//1. declare a uniform sampler2d that contains the texture data
uniform samplerCube SkyBoxTexture;
//2. declare varying type which will transfer the texture coordinates from the vertex shader
varying mediump vec3 vTexCoordinates;
void main()
{
//3. set the final color to the output of the fragment shader
gl_FragColor = textureCube(SkyBoxTexture,vTexCoordinates);
}
Creating an instance of the Skybox
We are almost done. We need to create an instance of the SkyBox class and call its setupOpenGL() method.
Open up the ViewController.mm file. Locate method viewDidLoad and head to line 8. Copy what is shown in listing 5.
Listing 5. Create an instance of SkyBox
(void)viewDidLoad
{
//…
//8. create the Skybox class instance
skyBox=new SkyBox( "LeftImage.png","RightImage.png", "TopImage.png","BottomImage.png", "FrontImage.png", "BackImage.png",self.view.bounds.size.height,self.view.bounds.size.width);
//9. Begin the OpenGL setup for the skybox
skyBox->setupOpenGL();
//…
}
In line 8 we simply create an instance of the SkyBox class and provide the name of the images which will be used. Line 9 simply starts the openGL setup operations.
Final Result
Run the project. You should now see a skybox loaded on your mobile device. Swipe your fingers horizontally across the screen. You should be able to navigate through the skybox.
Bonus: If you want to see a character rendered in the skybox, simply uncomment line 3 in the > glkView()> method in the > ViewController.mm> file as shown in listing 6.
Listing 6. Rendering a 3D model in the skybox
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
//...
//3. Render the character
character->draw();
//...
}
Source code
The final source code for this project can be found here.
Questions?
So, do you have any questions? Is there something you need me to clarify? Did this project help you? Please let me know. Add a comment below and subscribe to receive our latest game development projects.
Note:
If you are using a newer Xcode version, you may get the following error:
"Couldn't decode the image. decoder error 29: first chunk is not the header chunk"
If you are getting this error message, the settings in Xcode is preventing the loading of png images.
To fix this, click on the project name, and select "Build Settings". Search for "Compress PNG Files". Set this option (debugger/Release) to NO.
Right below this option, you should see "Remove Text Metadata From PNG FIles". Set this option to NO.
When you build and run the project, the error mentioned above should go away and the png images should show up.
If you need more help, please visit my support page or contact me.
Update:
In newer Xcode versions, you may get this error while running the project demos:
"No such file or directory: ...xxxx-Prefix.pch"
This error means that the project does not have a PCH file. The fix is very simple:
In Xcode, go to new->file->PCH File.
Name the PCH file: 'NameOfProject-Prefix' where "NameOfProject" is the name of the demo you are trying to open. In the OpenGL demo projects, I usually named the projects as "openglesinc."
So the name of the PCH file should be "openglesinc-Prefix"
Make sure the file is saved within the 'NameOfProject' folder. i.e, within 'openglesinc' folder.
Click create and run the project.