How Fog Settings and Lighting Component Interactions Shape Atmosphere in Pyrite64

Pyrite64 combines per-layer fog configuration with directional and ambient lighting through a unified GLSL blending pipeline, where distance-based fog interpolation modifies lit fragment colors before final compositing to create atmospheric depth.

The Pyrite64 rendering engine implements Nintendo 64-style graphics through sophisticated fog settings and lighting component interactions. This open-source project manages atmospheric effects via C++ scene serialization and real-time GLSL shader blending. Understanding how these systems converge in the rendering pipeline reveals how Pyrite64 achieves its characteristic low-poly aesthetic with realistic depth cues.

Fog Configuration Architecture

Layer-Level Fog Properties

In src/project/scene/scene.cpp, fog settings are stored as layer properties that control distance-based color blending. The system supports three color modes: material color (0), constant color (1), and custom color (2). Each layer defines fogMin and fogMax distances that establish the linear interpolation range for atmospheric density.

Key properties include:

Binary Export Pipeline

The src/build/sceneBuilder.cpp file handles serialization of fog data into binary scene files. During export, the builder writes the RGBA fog color, distance thresholds, and mode byte:

// src/build/sceneBuilder.cpp
uint8_t fogMode = layer.fog.value ? layer.fogColorMode.value : 0;
ctx.fileScene.writeRGBA(layer.fogColor.value);
ctx.fileScene.write<float>(layer.fogMin.value);
ctx.fileScene.write<float>(layer.fogMax.value);
ctx.fileScene.write<uint8_t>(fogMode);

Lighting Component Implementation

Per-Draw Uniform Population

Lighting data flows through src/renderer/n64Mesh.cpp, where the drawPart() method populates uniform buffers with scene lighting information. The system supports one ambient light and up to two directional lights:

// src/renderer/n64Mesh.cpp
float clip = uniforms.mat.lightDir[0].w;
const auto &lights = scene->getLights();
int lightIdx = 0;
for (auto &light : lights) {
    if (light.type == 0) {
        uniforms.mat.ambientColor = light.color;
    } else if (lightIdx < 2) {
        uniforms.mat.lightDir[lightIdx] = glm::vec4(light.dir, 0.0f);
        uniforms.mat.lightColor[lightIdx] = light.color;
        ++lightIdx;
    }
}
uniforms.mat.lightDir[0].w = clip;

Shader Uniform Blocks

The GLSL shaders access lighting through the UniformObject block defined in src/shader/ubo.glsl. This standardized layout ensures consistent data access across vertex and fragment stages, containing lightDir[2], lightColor[2], and ambientColor vectors.

Shader Integration: Where Fog Meets Lighting

The Blender Pipeline Architecture

Fog settings and lighting component interactions converge in src/shader/n64.frag.glsl within the blender_fetch() function. While full fog implementation remains pending, the architecture clearly defines the integration point:

// src/shader/n64.frag.glsl
else if (val == BLENDER_CLR_FOG) return colorCC;
...
vec4 colorFog = vec4(1.0, 0.0, 0.0, 1.0); // placeholder @TODO

When active, the shader will replace the placeholder with linear interpolation between colorFog and the lit fragment color based on view-space depth relative to the fogMin and fogMax values supplied by the binary scene file.

Atmospheric Depth Cues

The interaction creates realistic atmospheric perspective through three mechanisms:

  • Distance-based color wash: Fragments beyond fogMin begin blending toward fogColor, reaching full replacement at fogMax
  • Lighting preservation: Fog blends with already-lit surfaces, maintaining directional light gradients and ambient occlusion
  • Transparency consistency: Because fog participates in the same gamma-corrected blending pipeline as lighting, translucent materials receive consistent atmospheric treatment

Editor Workflow for Atmospheric Configuration

The src/editor/pages/parts/layerInspector.cpp file provides the ImGui interface for configuring these interactions:

// src/editor/pages/parts/layerInspector.cpp
ImTable::addProp("Fog", layer.fog);
if (layer.fog.value) {
    std::vector<ImTable::ComboEntry> fogColorModes{
        {"Material colour", 0},
        {"Constant colour", 1},
        {"Custom colour",   2}
    };
    ImTable::addVecComboBox("Fog‑Mode", fogColorModes, layer.fogColorMode.value);
    if (layer.fogColorMode.value == 2) {
        ImTable::addColor("Fog Color", layer.fogColor.value);
    }
    ImTable::addProp("Fog Min", layer.fogMin);
    ImTable::addProp("Fog Max", layer.fogMax);
}

Summary

  • Pyrite64 stores fog settings as layer properties in src/project/scene/scene.cpp, supporting material-based, constant, or custom color modes with configurable distance ranges
  • The scene builder serializes fog data to binary format in src/build/sceneBuilder.cpp, writing color values, min/max distances, and mode bytes
  • Lighting components are populated per-draw-call in src/renderer/n64Mesh.cpp, supporting one ambient and two directional lights passed via uniform buffers defined in src/shader/ubo.glsl
  • The fragment shader in src/shader/n64.frag.glsl defines the integration point where fog will blend with lit fragments through the blender_fetch() function, though full implementation remains pending
  • Together, these systems create atmospheric depth by applying distance-based color interpolation to already-lit surfaces, preserving lighting gradients while adding environmental haze

Frequently Asked Questions

How does Pyrite64 determine which fog color mode to use?

The fog color mode is stored as an integer in the layer properties (layer.fogColorMode) and serialized as a byte in the binary scene file. Mode 0 uses the material's inherent color, mode 1 uses a constant predefined color, and mode 2 allows artists to specify a custom RGBA value through the editor interface in src/editor/pages/parts/layerInspector.cpp.

What is the maximum number of lights supported per object?

Pyrite64 supports one ambient light and up to two directional lights per draw call. This limitation aligns with Nintendo 64 hardware constraints and is enforced in src/renderer/n64Mesh.cpp where the uniform buffer population loop explicitly checks if (lightIdx < 2) before assigning directional light data.

Where does the actual fog blending calculation occur?

While the architecture is prepared for fog blending in src/shader/n64.frag.glsl within the blender_fetch() function, the actual implementation remains a TODO placeholder. The current code defines vec4 colorFog = vec4(1.0, 0.0, 0.0, 1.0) as a placeholder, indicating that future development will implement the linear interpolation between lit fragments and fog color based on view-space depth.

How do fog settings interact with transparent materials?

Fog settings blend with fragment colors before the final two-stage blending operation in the shader pipeline. Because both fog and lighting participate in the same gamma-corrected blending system defined in src/shader/n64.frag.glsl, translucent materials receive consistent atmospheric treatment. The final alpha value is forced to the linearized new color's alpha, ensuring that fog-induced transparency modifications remain physically consistent with the rest of the rendering pipeline.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →