Skip to content
Martin Prazak edited this page Sep 30, 2018 · 3 revisions

Skybox (or skydome) is a simple technique for adding visual complexity to 3D scenes. Skyboxes have been widely used for outdoor environments in many 3D games, dating back all the way to DOOM.

In this tutorial, we will create a simple skybox setup in Possumwood, using a combination of a texture loaded from a file, a simple background plane drawing using glUnProject and a set of simple OpenGL shaders.

This tutorial uses concepts introduced in previous tutorials, particularly in GLSL Turntable.

Background plane

At the core of our skybox implementation is a simple quad, with a few additional attributes to make it usable in our intended way.

alt text

As you can see in the properties editor, the quad is build out of three attributes - P, iNearPositionVert and iFarPositionVert.

To allow for more experimentation, let's also add a standard shader setup:

alt text

The P attribute simply represents world position of the quad's vertices. The normal of the plane is facing the Z axis, while X and Y coordinates of the points are a combination of -1 and 1. To visualise its properties, let's replace the shader source code, passing the P attribute directly to the fragment shader, and colouring the quad accordingly.

Vertex shader:

#version 130

in vec3 P;                     // position attr from the vbo

uniform mat4 iProjection;      // projection matrix
uniform mat4 iModelView;       // modelview matrix

out vec3 vertexPosition;       // vertex position for the fragment shader

void main() {
	// pass the P attribute unchanged
	vertexPosition = P;

	// the position of each vertex in world space
	vec4 pos4 = vec4(P.x, P.y, P.z, 1);
	gl_Position = iProjection * iModelView * pos4;
}

Fragment shader:

#version 130

out vec4 color;

in vec3 vertexPosition;

void main() {
	color = vec4(
		fract(vertexPosition.x),
		fract(vertexPosition.y),
		0, 1
	);
}

This will explicitly visualise the value of the P attribute, on a quad in the world space.

alt text

Finally, to turn the quad into a "background plane", we need it to simply not respect the camera transformation. This effectively makes the quad follow the window coordinates, with [-1,-1] at the bottom left of the screen and [1,1] at top right. To do that, we simply use the P value directly as gl_Position in the vertex shader:

#version 130

in vec3 P;                     // position attr from the vbo

out vec3 vertexPosition;       // vertex position for the fragment shader

void main() {
	// pass the P attribute unchanged
	vertexPosition = P;

	// the position of each vertex in screen space
	//   Z = 1 will place the vertex at the far plane
	vec4 pos4 = vec4(P.x, P.y, 1, 1);
	gl_Position = pos4;
}

The quad now covers the whole screen, and does not respect the camera movement (i.e., "follows" the camera). Setting its Z coordinate to 1 will make the quad draw at the background plane - in effect placing it behind any object we would draw in the scene.

alt text

Displaying a texture

To load a texture, we will use a render/image/load and render/uniform/texture node. Into the filename attribute of the load node, let's fill a hdrihaven.com texture from the examples directory - $EXAMPLES/hdrihaven_envmaps/misty_pines_4k.png - and change the name to background.

alt text

The uniform attributes now contain a new entry - uniform sampler2D background; . Let's use this to display the texture on our plane in the fragment shader:

#version 130

uniform sampler2D background;

in vec3 vertexPosition;

out vec4 color;

void main() {
	color = texture(
		background,
		vec2(
			(vertexPosition.x / 2.0 + 0.5),
			-(vertexPosition.y / 2.0 + 0.5)
		)
	);
}

Which will replace our colour gradients with a texture:

alt text

Near and far plane positions via gluUnproject

Apart from the P attribute, the background vertex data node outputs two additional attributes - iNearPositionVert and iFarPositionVert. These are computed using the GLU's gluUnProject function, which allows to "unapply" a stack of OpenGL transformations. There are other approaches that can be used to achieve the same result, but for the sake of simplicity, let's just stick to this simple solution.

The Possumwood's render plugin applies this function on the vertex positions' near and far plane, computing the world space coordinates of each vertex at the near plane and at the far plane.

Subtracting these, we can get a world-space view vector:

vec3 dir = normalize(iFarPosition - iNearPosition);

which we can then map to the spherical coordinate system (also known as lat-long system), effectively converting the 3D vector into a 2D space representing the surface of a sphere.

To derive longitude, we can simply use the acos on the Y axis of the normalized view vector, which will lead to a value in range [-PI/2 .. PI/2]. To compute latitude, we can use the 2-argument arc tangent, implemented in GLSL simply as a tan() function with two parameters. This will lead to a value in range [-PI .. PI]. We then normalize these values to the expected ranges, arriving at the final form of the equations:

float lng = acos(dir.y) / 3.1415;
float lat = atan(dir.x, -dir.z) / 3.1415 / 2.0;

Skybox shaders

The final form of the vertex shader simply passes all parameters through unchanged, and sets gl_Position to the screen space far plane:

#version 130

in vec3 P;
in vec3 iNearPositionVert;
in vec3 iFarPositionVert;

out vec3 vertexPosition;
out vec3 iNearPosition;
out vec3 iFarPosition;

void main() {
	// pass all parameters unchanged
	vertexPosition = P;
	iNearPosition = iNearPositionVert;
	iFarPosition = iFarPositionVert;

	// the position of each vertex in screen space
	vec4 pos4 = vec4(P.x, P.y, 1, 1);
	gl_Position = pos4;
}

The fragment shader uses the equations from previous section to compute latitude and longitude, and uses these to fetch a pixel from the background texture:

#version 130

uniform sampler2D background;

in vec3 vertexPosition;
in vec3 iNearPosition;
in vec3 iFarPosition;

out vec4 color;

void main() {
	vec3 dir = normalize(iFarPosition - iNearPosition);

	float lng = acos(dir.y) / 3.1415;
	float lat = atan(dir.x, -dir.z) / 3.1415 / 2.0;

	color = texture(background, vec2(lat, lng));
}

This leads to the final result of this tutorial - a scene with a lat-long skybox texture in the background, reacting correctly to camera movement:

alt text