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.skyThe 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.cloudsVolumetric 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.treesProcedural 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.ecosystemWhere 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.fogVolumetric 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.shaderlibThe 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:
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.CloudRenderer– before any lit shader is compiled: it registers its frame slots and addsCLOUD_SHADOWS,CLOUDS_FOGandLIGHT_SHAFTStoEnvironment.defines(), so thatsun_visibility()includes the clouds’ shadow andapply_clouds()andapply_light_shafts()exist. A lit shader compiled earlier simply does not see the clouds.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 includesshadows.glslstill draws (only the clouds’ shadow then falls on the ground).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 addsFROXEL_FOGandLOCAL_LIGHTS.The game’s lit shaders, compiled with
Environment.make_shader().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 |
|---|---|---|---|
|
141 |
54 |
scene 45, cascades 1.7 |
|
151 |
65 |
scene 47, clouds 6.4, cascades 3.0 |
|
170 |
73 |
scene 50, clouds 8.2, cascades 6.0 |
|
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_basicthroughacron.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.