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.setFoghere 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.
1. Ambient fill
Section titled “1. Ambient fill”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 RGBA lit shader reads it from the LightingData UBO tail (see §5). Debug view 9 (“Ambient only”,
Graphics.setDebugView(9)) isolates the ambient contribution.
2. Atmospheric distance fog
Section titled “2. Atmospheric distance fog”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 baseGraphics.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).
3. Procedural sky
Section titled “3. Procedural sky”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 §6Graphics.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.
4. Environment-cubemap sky
Section titled “4. Environment-cubemap sky”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 ray5. Reading ambient + fog in a lit shader
Section titled “5. Reading ambient + fog in a lit shader”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.
Verify it works
Section titled “Verify it works”plume3d apps/sky_demo # gradient sky + ambient + fog, then the cubemap sky; two screenshotsThe 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.
Gotchas & limits
Section titled “Gotchas & limits”- Cube faces must be square and equal-sized.
loadCubemapreturns0(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
SamplerCubein 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.
Related
Section titled “Related”- API: Graphics → Sky, fog & ambient (A#6) ·
Texture.loadCubemap. - Example: Sky Demo (
apps/sky_demo). - Companion guides: Post-effects & scene depth · Opaque-colour refraction.