Skip to content

Skybox Rendering Technique (Equirectangular)ยถ

The engine uses direct Equirectangular mapping for the environment background. This is more memory-efficient as it avoids converting to and storing a generated cubemap.

Early-Z Optimizationยถ

To maximize performance on integrated GPUs, the skybox is rendered after the scene objects.

  1. Vertex Shader: Positions the skybox triangles exactly on the far plane (z = 1.0).
  2. Depth Test: By using glDepthFunc(GL_LEQUAL), the GPU automatically rejects skybox fragments that are occluded by 3D objects (like the icosphere) before launching the fragment shader.
  3. Fragment Shader: Performs an inverse spherical projection to sample the 2D HDR texture.

Optimization Diagramยถ

Graphviz Diagram
// Inverse Equirectangular Projection
const vec2 invAtan = vec2(0.1591, 0.3183);
vec2 SampleEquirectangular(vec3 v) {
    vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
    uv *= invAtan;
    uv.x += 0.5;
    uv.y = 0.5 - uv.y;
    return uv;
}

void skybox_main() {
    vec3 dir = normalize(v_direction);
    vec2 uv = SampleEquirectangular(dir);
    out_color_val = textureLod(environmentMap, uv, blur_lod);
}

C Implementation (View Matrix)ยถ

We remove the translation component from the view matrix so the skybox appears infinitely far away (centered on the camera):

/* Copy view and strip translation to keep skybox at infinity */
mat4 view_sky;
glm_mat4_copy(view, view_sky);
view_sky[3][0] = 0.0f;
view_sky[3][1] = 0.0f;
view_sky[3][2] = 0.0f;

/* Compute inverse view-projection for equirect sampling */
mat4 inv_vp_sky;
glm_mat4_mul(proj, view_sky, inv_vp_sky);
glm_mat4_inv(inv_vp_sky, inv_vp_sky);

๐Ÿ” Technical Detailsยถ

Mipmap Samplingยถ

Using textureLod with an equirectangular texture allows precise control over bluriness: - LOD 0: Sharp environment. - LOD > 0: Blurred environment (useful for PBR background or debugging).

Orientation Correctionยถ

The inversion uv.y = 0.5 - uv.y is crucial to map the "top" of the HDR image to the "top" of the 3D world space.

๐ŸŽจ Full Workflowยถ

// Main render loop integration
void render_scene_example(App* app) {
    // 1. View Matrix without translation
    mat4 view_sky;
    glm_mat4_copy(app->view, view_sky);
    view_sky[3][0] = 0.0f;
    view_sky[3][1] = 0.0f;
    view_sky[3][2] = 0.0f;

    mat4 inv_vp_sky;
    glm_mat4_mul(app->proj, view_sky, inv_vp_sky);
    glm_mat4_inv(inv_vp_sky, inv_vp_sky);

    // 2. Render via skybox module
    skybox_render(&app->skybox, app->skybox_shader,
                  app->hdr_texture, inv_vp_sky, app->env_lod);
}

๐ŸŒŸ Advantagesยถ

  1. Performance: No complex matrix math, just zeroing 3 floats.
  2. Simplicity: Easy to understand and maintain.
  3. Robustness: Standard industry technique.
  4. Quality: Seamless infinite background.

๐Ÿ“ Important Notesยถ

  • Use glDepthFunc(GL_LEQUAL) so the skybox is drawn at the back processing.
  • The skybox does not write significant depth.
  • The LOD (blur_lod) allows controlling the environment blur.

๐Ÿ”— Python โ†’ C Equivalenceยถ

Python (moderngl)ยถ

view_m = camera.matrix
view_m[3][0] = 0
view_m[3][1] = 0
view_m[3][2] = 0
inv_view_proj_sky = glm.inverse(projection_matrix * view_m)

C (cglm)ยถ

mat4 view_m;
glm_lookat(camera_pos, target, up, view_m);
view_m[3][0] = 0.0f;
view_m[3][1] = 0.0f;
view_m[3][2] = 0.0f;

mat4 inv_view_proj_sky;
glm_mat4_mul(proj_matrix, view_m, inv_view_proj_sky);
glm_mat4_inv(inv_view_proj_sky, inv_view_proj_sky);

Perfectly equivalent! โœ