Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I am trying to get a skybox working with OpenGL 3.3 and GLSL version 330.

I could not find a completely modern OGL skybox tutorial anywhere on the web, so I modernised an older one (using glVertexAttribPointer() instead of gl_Vertex for vertices, etc.). It's mostly working, but for 2 major details:

The skyboxes are more like sky triangles, and the textures are badly warped and stretched (they are supposed to be star fields, I get while lines on a black background). I'm 99% sure that this is because I didn't port the old tutorials completely correctly.

Here is my skybox class:

static ShaderProgram* cubeMapShader = nullptr;

static const GLfloat vertices[] = 
{
    1.0f, -1.0f,  1.0f,
    1.0f,  1.0f,  1.0f,
    1.0f,  1.0f, -1.0f,
    -1.0f, -1.0f,  1.0f,
    -1.0f, -1.0f, -1.0f,
    -1.0f,  1.0f, -1.0f,
    -1.0f,  1.0f,  1.0f,
    -1.0f,  1.0f, -1.0f,
    1.0f,  1.0f, -1.0f,
    1.0f,  1.0f,  1.0f,
    -1.0f,  1.0f,  1.0f,
    -1.0f, -1.0f,  1.0f,
    1.0f, -1.0f,  1.0f,
    1.0f, -1.0f, -1.0f,
    -1.0f, -1.0f, -1.0f,
    1.0f, -1.0f,  1.0f,
    -1.0f, -1.0f,  1.0f,
    -1.0f,  1.0f,  1.0f,
    1.0f,  1.0f,  1.0f,
    -1.0f, -1.0f, -1.0f,
    1.0f, -1.0f, -1.0f,
    1.0f,  1.0f, -1.0f,
    -1.0f,  1.0f, -1.0f
};

Skybox::Skybox(const char* xp, const char* xn, const char* yp, const char* yn, const        char* zp, const char* zn)
{
if (cubeMapShader == nullptr)
    cubeMapShader = new ShaderProgram("cubemap.vert", "cubemap.frag");

    texture = SOIL_load_OGL_cubemap(xp, xn, yp, yn, zp, zn, SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_MIPMAPS);

    glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_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);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

    glGenVertexArrays(1, &vaoID);
    glBindVertexArray(vaoID);
    glGenBuffers(1, &vboID);
    glBindBuffer(GL_ARRAY_BUFFER, vboID);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
    glBindVertexArray(0);

    scale = 1.0f;
}

Skybox::~Skybox()
{

}

void Skybox::Render()
{
    ShaderProgram::SetActive(cubeMapShader);
    glDisable(GL_DEPTH_TEST);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
    cubeMapShader->Uniform1i("SkyTexture", 0);
    cubeMapShader->UniformVec3("CameraPosition", Camera::ActiveCameraPosition());
    cubeMapShader->UniformMat4("MVP", 1, GL_FALSE, Camera::GetActiveCamera()->GetProjectionMatrix() * Camera::GetActiveCamera()->GetViewMatrix() * glm::mat4(1.0));
    glBindVertexArray(vaoID);
    glDrawArrays(GL_QUADS, 0, 24);
    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
}

Vertex Shader:

#version 330 
layout(location = 0) in vec3 Vertex;

uniform vec3 CameraPosition;
uniform mat4 MVP;

out vec3 Position;

void main()
{
    Position = Vertex.xyz;
    gl_Position = MVP * vec4(Vertex.xyz + CameraPosition, 1.0);
}

Fragment Shader:

#version 330 compatibility

uniform samplerCube SkyTexture;

in vec3 Position;

void main()
{
    gl_FragColor = textureCube(SkyTexture, Position);
}

Here's an example of the glitches. If anyone could take a look who knows GLSL well (I'm still learning it), or skyboxes, I would appreciate any help you could give. Also, kudos if you can teach me how to use non-deprecated functions in the fragment shader so I don't have to use the compatibility profile of glsl 330.


EDIT: Immediately found the problem with the stretching textures: I was using Position = Vertex.xyx instead of Position = Vertex.xyz in the vertex shader. Oops. But the triangle error still exists.

share|improve this question
1  
You only need 4 vertices (fullscreen quad) to render a skybox with a cubemap texture. You just need a vertex shader which calculates correct texture coordinates based on camera and projection. –  msell Aug 6 '13 at 7:27
    
It might be a culling issue. Have you tried disabling backface culling to try and see if you get the full box? –  pwny Aug 6 '13 at 14:02
    
@pwny, I didn't think of that. I tried it, and it didn't work, but I can see how that could have thrown it off. Thanks for the suggestion. –  sm81095 Aug 6 '13 at 16:20
    
@msell, I have heard of this approach, but I didn't find a tutorial online for this, and I'm still in the process of learning glsl. If you could provide an example or a link to an example about how to do this, I would greatly appreciate that. –  sm81095 Aug 6 '13 at 16:22

1 Answer 1

up vote 11 down vote accepted

While this answer does not tell what is wrong with your approach, it presents a simpler way to render skyboxes.

Traditional way

A straightforward way for creating skyboxes is to render a textured cube centered to the camera position. Each face of the cube consists of two triangles and a 2D texture (or part of an atlas). Due to texture coordinates each face requires own vertices. This approach has problems in the seams of adjacent faces, where the texture values are not interpolated properly.

Cubemap

Like in the traditional way, a textured cube is rendered around the camera. Instead of using six 2D textures, a single cubemap texture is used. Because the camera is centered inside the cube, the vertex coordinates map one to one with the cubemap sampling vectors. Thus texture coordinates are not needed for the mesh data and the vertices can be shared between faces by using index buffer.

This approach also fixes the problem of seams when GL_TEXTURE_CUBE_MAP_SEAMLESS is enabled.

Simpler way

When rendering a cube and the camera lies inside it, whole viewport gets filled. Up to five faces of the skybox can be partially visible at any time. The triangles of cube faces are projected and clipped to the viewport and cubemap sampling vectors are interpolated between the vertices. This work is unnecessary.

It's possible to fill a single quad filling the whole viewport and calculate the cubemap sampling vectors in the corners. Since the cubemap sampling vectors match the vertex coordinates, they can be calculated by unprojecting the viewport coordinates to the world space. This is the opposite of projecting world coordinates to the viewport and can be achieved by inverting the matrices. Also make sure you either disable z-buffer write or write a value that is far enough.

Below is the vertex shader that accomplishes this:

#version 330
uniform mat4 uProjectionMatrix;
uniform mat4 uWorldToCameraMatrix;

in vec4 aPosition;

smooth out vec3 eyeDirection;

void main() {
    mat4 inverseProjection = inverse(uProjectionMatrix);
    mat3 inverseModelview = transpose(mat3(uWorldToCameraMatrix));
    vec3 unprojected = (inverseProjection * aPosition).xyz;
    eyeDirection = inverseModelview * unprojected;

    gl_Position = aPosition;
} 

aPosition is the vertex coordinates {-1,-1; 1,-1; 1,1; -1,1}. The shader calculates eyeDirection with the inverse of model-view-projection matrix. However the inversion is split for projection and world-to-camera matrices. This is because only the 3x3 part of the camera matrix should be used to eliminate the position of the camera. This aligns the camera to the center of the skybox. In addition as my camera doesn't have any scaling or shearing, the inversion can be simplified to transposion. The inversion of the projection matrix is a costly operation and could be precalculated, but as this code is executed by the vertex shader typically just four times per frame, it's usually a non-issue.

The fragment shader simply performs a texture lookup using eyeDirection vector:

#version 330
uniform samplerCube uTexture;

smooth in vec3 eyeDirection;

out vec4 fragmentColor;

void main() {
    fragmentColor = texture(uTexture, eyeDirection);
}

Note that to get rid of the compatibility mode you need to replace textureCube with just texture and specify the output variable yourself.

share|improve this answer
    
I think you should also mention, that the matrix inversion is a costly process, so it better takes place in the client-side code. –  akaltar Aug 7 '13 at 8:52
    
For the 4 verts of a fullscreen quad I don't think we need worry much about the cost of inversion (especially as the GPU doing it 4 times will still likely be faster than the CPU doing it once). –  Darth Satan Aug 7 '13 at 20:26
    
This is brilliant. Thanks! –  blissfreak Sep 9 at 3:53

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.