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:
- layer.fog: Boolean toggle enabling fog for the entire layer, stored in
src/project/scene/scene.cpp - layer.fogColorMode: Integer selecting the color source (0=material, 1=constant, 2=custom), stored in
src/project/scene/scene.cpp - layer.fogColor: RGBA vector for custom fog color when mode equals 2, stored in
src/project/scene/scene.cpp - layer.fogMin / fogMax: Float values defining the linear interpolation range for fog density, stored in
src/project/scene/scene.cpp - layer.fogMode: Byte encoding the active mode during binary export, written in
src/build/sceneBuilder.cpp
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
fogMinbegin blending towardfogColor, reaching full replacement atfogMax - 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 insrc/shader/ubo.glsl - The fragment shader in
src/shader/n64.frag.glsldefines the integration point where fog will blend with lit fragments through theblender_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →