Skip to content

Sky, fog & ambient

Add a sky drawn behind the scene (a procedural gradient or an environment cubemap), a global atmospheric distance fog, and an ambient fill. These are neutral, raw-linear engine knobs (engine ADR 0076 / PLM-241); the sky shader and the time-of-day / weather curve are pack/game content. Every knob is additive and defaults to a no-op, so a scene that never sets them is unchanged.

Reference app: apps/sky_demo.

Not fog-of-war. Graphics.setFog here is atmospheric distance fog. It is not the gameplay fog-of-war (Graphics.fogSoftness / FogOfWar.*, ADR 0067) — those are unrelated. Distance fog fades geometry by camera distance; fog-of-war hides unexplored world.

Raw-linear ambient that lifts unlit / shadowed faces off black. The default is (0.03, 0.03, 0.03) — the old hardcoded constant — so doing nothing is identical to before.

Graphics.setAmbient(0.34, 0.37, 0.44) // raw linear RGB

A lit shader reads it from the LightingData UBO tail (see §5). Debug view 9 (“Ambient only”, Graphics.setDebugView(9)) isolates the ambient contribution.

A global fog term applied in the lit fragment shader. density > 0 selects exponential exp2(-density · dist); otherwise it is linear over [start, end] (world units of camera distance).

// linear fog: clear until 8 units, fully fogged by 30, colour matched to the sky base
Graphics.setFog(0.62, 0.70, 0.85, 0.0, 8.0, 30.0)
// or exponential:
Graphics.setFog(0.6, 0.7, 0.85, 0.04, 0.0, 0.0)

Fog is a shader term, not a post pass, so it composes with every scene path — it works even in fog-of-war scenes (where a post-stack fog would silently vanish).

Load a full-screen sky shader and enable it. mode 0 is a procedural horizon→zenith gradient; the params list is shader-interpreted (the demo uses params[0..2] = horizon RGB, params[4..6] = zenith RGB).

Graphics.loadShader("shaders/sky") // an app/pack sky shader — see §6
Graphics.setSky("shaders/sky", 0, [0.62, 0.70, 0.85, 0.0, 0.18, 0.32, 0.68, 0.0])

The sky is drawn first, behind the scene, at z=far (it never occludes geometry). A real perspective camera is required — the sky reconstructs a view ray from the inverse view/projection.

Load 6 face images (order +X, -X, +Y, -Y, +Z, -Z, all the same square size), point the engine at the cube, and select mode 1.

var cube = Texture.loadCubemap([
"textures/cube/px.png", "textures/cube/nx.png",
"textures/cube/py.png", "textures/cube/ny.png",
"textures/cube/pz.png", "textures/cube/nz.png"]) // returns a cube handle (0 on failure)
Graphics.setEnvironmentMap(cube)
Graphics.setSky("shaders/sky", 1, []) // mode 1 = sample the cube by the view ray

A lit shader declares the LightingData cbuffer at set 0 binding 1 and appends the A#6 tail (byte-identical to the engine UBO). Do not move any existing field.

[[vk::binding(1, 0)]]
cbuffer LightingData : register(b1) {
// ... existing header + lights[8] + shadowMatrices[4] (unchanged) ...
float4 ambientColor; // @1040 rgb ambient (raw linear)
float4 fogColorAndDensity; // @1056 rgb fog colour, w = density
float4 fogStartEnd; // @1072 x = start, y = end
};
// ... after lighting: color = albedo * (ambientColor.rgb + lightTerm);
// distance fog: fogF = density>0 ? saturate(1 - exp2(-density*dist))
// : saturate((dist - start) / (end - start));
// color = lerp(color, fogColorAndDensity.rgb, fogF);

Un-updated shaders that declare only the old 1040-byte layout keep working (a cbuffer reads only its declared prefix), so existing material shaders are unaffected.

6. Writing a sky shader (#include "plume3d_sky.slang")

Section titled “6. Writing a sky shader (#include "plume3d_sky.slang")”

The sky shader includes the shared sky ABI and provides only [shader("fragment")] (the full-screen vertex is in the include):

#include "plume3d_sky.slang" // set 0: b0 sky UBO, b1 env cubemap; the fullscreen vertex is here
[shader("fragment")]
float4 fragment(SkyVSOutput input) : SV_Target {
float3 dir = plume3d_skyRay(input.UV); // world-space view ray
if (plume3d_skyMode() == 1) return float4(plume3d_skyCube(dir), 1.0); // cubemap
float t = saturate(dir.y * 0.5 + 0.5); // gradient
return float4(lerp(plume3d_skyParam(0).xyz, plume3d_skyParam(1).xyz, t*t), 1.0);
}

Do not #include "plume3d.slang" in a sky shader — that is the mesh ABI (a different set-0 layout). The sky pipeline is built lazily from the loaded shader; loading it also attempts a harmless, one-time mesh-pipeline build that is skipped on MoltenVK — expect a single Could not create graphics pipeline log line for the sky shader, not per-frame spam.

Terminal window
plume3d apps/sky_demo # gradient sky + ambient + fog, then the cubemap sky; two screenshots

The reference (sky demo): a diagonal row of cubes recedes into the fog while the gradient sky sits behind, then the run switches to the cubemap sky — proving both sky modes and the shared ambient + fog.

  • Cube faces must be square and equal-sized. loadCubemap returns 0 (falsy) on any mismatch or read/decode failure — check the handle.
  • The cubemap sky renders, but sampling the env cube from lit shaders (IBL / reflections) is a follow-up — a combined SamplerCube in the crowded mesh set-0 fails MoltenVK MSL conversion (ADR 0076 §5).
  • Sky/fog reach the single-viewport post and plain paths in v1; fog-of-war / RTT-composite / split-screen scenes get the fog (a lit-shader term) but not the sky pass yet.