Sky, Weather and Forests

Four packages of the acron framework build an outdoor world’s environment, and they are designed to be used together:

acron.sky

The time of day at a place on Earth, the sun, the moon and the stars where they would be, a physically based atmosphere, cascaded sun shadows and an environment probe for reflections and ambient light. See The Sky, Day and Night.

acron.clouds

Volumetric clouds and weather presets: clouds marched through every frame, their shadows on the ground, light shafts through their gaps, fog inside them, rain curtains and lightning, and CPU queries for gameplay. See Clouds and Weather.

acron.trees

Procedural tree species and forests of thousands of trees, instanced from a few prototypes per species, with wind, seasons, levels of detail and octahedral impostors. See Trees and Forests.

acron.ecosystem

Where the plants stand: woods grown over a terrain by an ecosystem simulation (light, site, seeding, mortality) and planted into the forest, and grass drawn blade by blade, thinned under the crowns and on the roads. See Ecosystems and Grass.

acron.fog

Volumetric fog near the camera: the look’s height fog as a froxel volume, shadowed by the sun and the clouds, lit by the game’s own point lights and spotlights (headlights, streetlamps), with local fog volumes. See Volumetric Fog.

acron.shaderlib

The GLSL the three share, which a game’s own shaders include to light their surfaces the same way. See Shaders in the Environment.

The heavy CPU work is native: the clouds’ noise, the weather and the sky’s light colours in acron.atmosphere, tree growth, meshing and the forest’s culling in acron.proctree. None of it uses numpy.

This page shows how the pieces fit together; the pages linked above describe each one.

Requirements

The environment modules need an OpenGL core profile: they render into floating-point buffers, read textures with texelFetch, use layout qualifiers and instance through gl_InstanceID. Every pass is a fragment pass – nothing needs compute shaders – so they run on:

  • Windows and Linux with OpenGL 4.5 (GLSL 450): the targets the modules are developed and tuned for;

  • macOS with OpenGL 4.1 (GLSL 410): supported for development, so a game can be worked on with a Mac. It is not a shipping target for these modules.

They are not available on OpenGL ES, WebGL (the web platform) or Android yet, nor on the Vulkan renderer. The CPU halves (acron.atmosphere, acron.proctree, the clock and the ephemerides, the weather presets, the tree species) run everywhere, so a server or a tool can use them.

Request the context before the window opens. acron.sky.PRC holds the line for the targets; on macOS ask for 4.1:

import sys
from acron.core import load_prc_file_data
from acron import sky

prc = sky.PRC                                  # "gl-version 4 5"
if sys.platform == "darwin":
    prc = "gl-version 4 1\n"                   # development only
load_prc_file_data("environment", prc + "framebuffer-srgb true\n")

acron.shaderlib writes #version 450 core where the driver offers GLSL 4.5 and #version 410 core otherwise, and refuses anything older with an error that names the setting.

Putting it together

The order of construction matters for one reason: shaders are compiled with the frame-value slots and the #define s that exist at that moment, and the clouds and the forest add both. So the environment comes first, then the clouds, then everything that compiles a lit shader:

  1. Environment – the clock, the sky, the frame values, the key and ambient lights. If the game uses the frame pipeline (FramePipeline: TAA, motion vectors, blue noise, ambient occlusion; The Frame Pipeline), it comes right after: it adds defines every lit shader needs.

  2. CloudRenderer – before any lit shader is compiled: it registers its frame slots and adds CLOUD_SHADOWS, CLOUDS_FOG and LIGHT_SHAFTS to Environment.defines(), so that sun_visibility() includes the clouds’ shadow and apply_clouds() and apply_light_shafts() exist. A lit shader compiled earlier simply does not see the clouds.

  3. SunShadows – the cascaded shadow maps. Optional: without it the environment keeps a 1 x 1 shadow atlas bound that reads as lit, so every shader that includes shadows.glsl still draws (only the clouds’ shadow then falls on the ground).

  4. EnvironmentProbe – a copy of the sky (clouds and all) as image-based light. VolumetricFog, if the game has it, also comes before the lit shaders: it adds FROXEL_FOG and LOCAL_LIGHTS.

  5. The game’s lit shaders, compiled with Environment.make_shader().

  6. Forest, which compiles its tree and impostor shaders when it is made.

A game that registers frame slots of its own (headlights, say) does so before step 5 as well.

from direct.showbase.ShowBase import ShowBase
from acron.sky import DayClock, Environment, EnvironmentProbe, SunShadows
from acron.clouds import CloudRenderer
from acron.trees import Forest, Species

base = ShowBase()
base.camLens.set_near_far(0.2, 30000)

clock = DayClock(latitude=45.81, longitude=15.98, utc_offset=2.0)
clock.set_date(2026, 6, 21, hour=15.0)
clock.time_scale = 60                       # a game hour a real minute

env = Environment(base, clock=clock, metres_per_unit=1.0)
clouds = CloudRenderer(env, quality="high", weather="fair")
shadows = SunShadows(env, resolution=2048, distance=400)
probe = EnvironmentProbe(env)

terrain.set_shader(env.make_shader("lit_basic.vert", "lit_basic.frag",
                                   {"ENV_PROBE": True}))

forest = Forest(env, shadows=shadows)
forest.plant(Species.load("oak"), oak_positions)
forest.plant(Species.load("spruce"), spruce_positions)

clouds.set_weather("storm", seconds=120)    # two minutes to build up

Everything then runs by itself: the environment, the clouds and the forest add their own tasks and frame writers, and each frame they advance the clock, move the sun, drift the clouds, cull the forest and upload one small texture of frame values before the frame renders. lit_basic is the reference material that comes with acron.sky; a game’s own shaders call the same functions (Shaders in the Environment).

The samples in the engine’s python/samples directory show each layer: sky_demo.py (the sky through a day, weather looks, shadows), clouds_demo.py (the weather presets cross-faded live, the quality presets, lightning, cloud fog and light shafts) and forest_demo.py (6000 trees of ten species, wind, seasons, levels of detail and impostors). Each takes --screenshot out.png to render a few frames offscreen and quit.

The units contract

The modules agree on a few units, and a game’s shaders and numbers must agree with them too.

Distances. The world keeps its own units; Environment(metres_per_unit=) says how big one is, and altitude how high world z = 0 lies above the sea (in metres). Everything physical is converted from there: the atmosphere works in kilometres, the clouds and the weather in metres above sea level, the trees in metres (their LOD distances too), and height fog takes its density per metre. The world is z-up; north_heading turns north away from +y.

Light. The sun outside the atmosphere has an illuminance of Look.sun_illuminance (3.0 by default) in the scene’s light units, and the moon Look.moon_illuminance. Every light the modules compute – the sky’s radiance, the key light reaching a point, the sky’s irradiance, the clouds, the probe – is in those units, unexposed. The default is chosen so that a white surface under a high sun comes out near 1.0 after exposure; a game that lights its own objects with other sources keeps them in the same units.

Exposure. frame_exposure() in a shader (the VIEW frame slot) is 2 ** Look.exposure_ev times the eye’s adaptation to the hour’s light: 1 by day, rising at dusk and at night with the light on the ground up to Look.night_exposure (The Sky, Day and Night), so a moonlit or starlit scene reads without every material knowing about the night. The contract is: compute in unexposed light units, and finish with frame_display(color) – the exposure, and without the frame pipeline the night’s rod shift – once, at the end, writing linear colour. The sky shader, the clouds, the trees, the grass and lit_basic do exactly that, and the DirectionalLight and AmbientLight the environment keeps for materials that light from engine lights only are given already-exposed colours.

Albedo. Colours are linear albedos: a lawn is about (0.085, 0.13, 0.04) (acron.ecosystem.GROUND_COLORS), concrete 0.3 to 0.45, fresh snow 0.85. An sRGB-looking value such as (0.29, 0.36, 0.17) used as an albedo is three times too bright and lights pale and washed out under the sun.

Output. The modules write linear colour and expect the window to encode it: framebuffer-srgb true. Without the frame pipeline nothing tone maps and very bright values clip. FramePipeline renders the scene into a float buffer and tone maps in its composite, after the exposure: tonemap is "shoulder" by default (linear up to 0.76, then Khronos PBR Neutral’s shoulder rolls highlights off to white instead of clipping each channel), "neutral" (the whole Khronos PBR Neutral, with its toe), "clamp" (the clip), "aces" or "reinhard" (The Frame Pipeline).

Texture units. The environment modules target Windows and Linux, whose GL 4.5 drivers commonly let a fragment shader read 32 textures (GL_MAX_TEXTURE_IMAGE_UNITS); macOS is a development platform only, and its GL 4.1 driver allows 16, the least any GL 4 driver may give. With every module on, lit_basic with ENV_PROBE reads exactly 16 (the probe’s six maps share one atlas, the two sky-view tables one texture, the sky’s irradiance sits in the aerial-perspective atlas, the AO is one texture), so on macOS a game material that adds textures of its own must leave out what it does not need there (CLOUDS_FOG, ENV_PROBE, LOCAL_LIGHTS). A shader over the driver’s limit fails to link and the engine draws the node without it, in its plain vertex colour; acron.shaderlib.make() warns (RuntimeWarning) when a shader it compiles reads more than 16.

Time. DayClock holds local civil time at its latitude and longitude; the environment’s shader time (frame_time(), for wind and twinkling) is real seconds and wraps hourly.

Performance

Targets. The desktop targets are budgeted on a GTX 1660-class GPU at 1920 x 1080: the whole environment stack (sky, probe, shadows, pipeline, clouds, fog, forest and grass) in about 8 ms on low, 12 ms on medium, 16.7 ms (60 Hz) on high and 33 ms (30 Hz) on ultra. On the GTX 660 (Kepler, about 2.6 times slower) that the engine is measured on, that is about 21, 31, 43 and 87 ms.

Measured (GPU time per frame from timer queries around every pass, samples/nature_stack.py at 1920 x 1080 at 15:00 in fair weather, GTX 660, NVIDIA 470, GL 4.6):

Preset

Before

Now

The largest passes now

low

141

54

scene 45, cascades 1.7

medium

151

65

scene 47, clouds 6.4, cascades 3.0

high

170

73

scene 50, clouds 8.2, cascades 6.0

ultra

271

113

scene 55, clouds 28.5, GTAO 12

(milliseconds). What brought it down: the forest’s depth pre-pass (its lighting ran for every layer of leaf cards: Trees and Forests), shadow filter taps by preset and staggered far cascades (The Sky, Day and Night), and the clouds’ presets retuned (Clouds and Weather). Still over the targets: the scene pass, in which the forest’s lighting is about 17 ms (the air and fog about 5, the sun’s shadow 3), the grass about 9 ms (its vertices), and the clouds’ shadows, fog and the sky’s cloud layer about 14 ms on low.

The macOS figures below (OpenGL 4.1, an Apple M4 Pro, 1920 x 1080) are older and a guide to proportions.

  • The sky has not been timed on its own yet. A whole frame of the sky (all its look-up tables, the dome, aerial perspective) over a simple lit scene took about 6 ms with a screenshot read back every frame: the baseline the cloud figures below are added to.

  • The clouds add about 1 to 1.5 ms at the low, medium and high quality presets, and 2 to 6 ms at ultra (overcast is the worst case: a closed deck is the longest march). Their CPU work is about 0.15 ms a frame. See Clouds and Weather for the table.

  • The forest’s culling, level choice and table uploads run natively in about 0.1 to 0.2 ms a frame for 600 to 6000 trees. Growing a species’ prototypes and painting its textures happens at load (about 50 ms of painting a species, plus the growth), and the prototypes can be cached on disk.

  • The environment probe redraws twice a second by default (EnvironmentProbe(interval=0.5)); the shadow cascades every frame.

The look-up tables, golden screenshots of the whole stack and a time-lapse soak that watches frame times, memory and the history buffers are described in Testing and Verification.

Known limitations

  • Core-profile desktop GL only: no OpenGL ES, WebGL, Android or Vulkan yet. The modules refuse the Vulkan renderer when they assemble their shaders (it reports no GLSL version). Forced through on a software Vulkan driver, the scene renders wrong (textures read in vertex shaders come back black, the sky’s tables are invalid), so the Vulkan path is not supported until the renderer itself is ready.

  • The world is flat for everything near the camera: the clouds sit on a small planet of their own (350 km) so they meet the horizon, but terrain is not curved.

  • One key light with shadows: the sun by day, the moon by night. The game’s point lights and spotlights light the fog and lit_basic through acron.fog (Volumetric Fog), without shadows.

  • Tone mapping only through the frame pipeline (see Output above).

  • macOS (development only) allows 16 textures per fragment shader, which every module together fills (see Texture units above).

  • Materials that light from engine lights only get the sun’s colour and a rough ambient term, not the shadows, the aerial perspective or the clouds; for those, the shader has to include acron.shaderlib.

  • The GPU timings above are from macOS; the Windows and Linux targets have not been profiled yet.

References

  • Sébastien Hillaire, “A Scalable and Production Ready Sky and Atmosphere Rendering Technique”, Computer Graphics Forum (EGSR) 2020.

  • Eric Bruneton and Fabrice Neyret, “Precomputed Atmospheric Scattering”, EGSR 2008.

  • Andrew Schneider, “The Real-time Volumetric Cloudscapes of Horizon: Zero Dawn” (SIGGRAPH 2015) and the later Nubis talks (2017–2023).

  • Jason Weber and Joseph Penn, “Creation and Rendering of Realistic Trees”, SIGGRAPH 1995.

  • Adam Runions, Brendan Lane and Przemyslaw Prusinkiewicz, “Modeling Trees with a Space Colonization Algorithm”, 2007.

  • Ryan Brucks, “Octahedral Impostors”, 2018.

  • Brian Karis, “Real Shading in Unreal Engine 4”, SIGGRAPH 2013, and “Physically Based Shading on Mobile”, 2014 (the split-sum approximation).

  • Jaroslav Křivánek and Mark Colbert, “Real-time Shading with Filtered Importance Sampling”, EGSR 2008.