Skybox - martin-pr/possumwood GitHub Wiki
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.
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:
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.
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.
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
.
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:
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: