16
\$\begingroup\$

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.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ 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. \$\endgroup\$
    – msell
    Commented Aug 6, 2013 at 7:27
  • \$\begingroup\$ It might be a culling issue. Have you tried disabling backface culling to try and see if you get the full box? \$\endgroup\$
    – anthonyvd
    Commented Aug 6, 2013 at 14:02
  • \$\begingroup\$ @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. \$\endgroup\$
    – sm81095
    Commented Aug 6, 2013 at 16:20
  • \$\begingroup\$ @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. \$\endgroup\$
    – sm81095
    Commented Aug 6, 2013 at 16:22

1 Answer 1

33
\$\begingroup\$

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

Traditional way (textured cube)

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.

Cube with cubemap texture

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 (better) 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 transposition. 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.

\$\endgroup\$
7
  • \$\begingroup\$ I think you should also mention, that the matrix inversion is a costly process, so it better takes place in the client-side code. \$\endgroup\$
    – akaltar
    Commented Aug 7, 2013 at 8:52
  • 1
    \$\begingroup\$ 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). \$\endgroup\$ Commented Aug 7, 2013 at 20:26
  • 1
    \$\begingroup\$ Just a helpful note to folks, GLSL ES 1.0 (used for GL ES 2.0) does not implement inverse() \$\endgroup\$
    – Steven Lu
    Commented Dec 26, 2014 at 1:28
  • \$\begingroup\$ is the uWorldToCameraMatrix the MVP of the camera transform object? \$\endgroup\$
    – Sidar
    Commented May 12, 2017 at 14:10
  • \$\begingroup\$ @Sidar No, it's just the ModelView matrix, Projection is separate. \$\endgroup\$
    – msell
    Commented May 12, 2017 at 16:25

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .