Shaders in the Environment¶
A game’s own materials – terrain, buildings, water, characters – should be
lit by the same sun, shadowed by the same cascades and clouds, and fade into
the same haze as the sky and the trees. acron.shaderlib is the GLSL that
makes that possible: a set of include files with the functions the sky, the
clouds and the forest use themselves, and a small assembler that compiles a
shader with the right version line and #define s. This page is for
writing such a shader; Sky, Weather and Forests describes the environment it
lights against.
The reference is lit_basic in acron/sky/shaders/, a complete lit
material in some thirty lines. A game copies its calls into its own shaders.
Compiling a shader¶
Shader sources include library files by name and carry no #version line:
#include "atmosphere.glsl"
#include "shadows.glsl"
Environment.make_shader() compiles a vertex and fragment file pair
against the environment. It adds #version 450 core (or 410 core on a
macOS development machine), every frame slot’s number (FRAME_SUN_DIR,
…), the sky’s table sizes, the defines the clouds added, and the caller’s
own; then it pastes each #include in place, each file once. Files are
looked up in the folders given as search first, then in the library:
shader = env.make_shader("terrain.vert", "terrain.frag",
{"ENV_PROBE": True, "DETAIL_LAYERS": 4},
search=("assets/shaders",))
terrain.set_shader(shader)
A define of True becomes a bare #define; False and None leave it
out; floats keep their decimal point and tuples become vecN(...). The
engine caches shaders by their text, so switching an option back and forth
does not recompile.
Without an environment (a tool, a test), acron.shaderlib does the same
work: source(text_or_name, defines) returns one complete source,
assemble_sources(vertex, fragment, defines) the pair without touching
the GPU (so a test can check that every variant resolves), make()
compiles, and glsl_version(gsg) says which version a context gets.
What a lit shader can call¶
Include |
Functions |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
p is always a world-space position and n a world-space unit
normal; the frame values hold the camera and every matrix in world space, so
the vertex shader passes acron_ModelMatrix * acron_Vertex down. Including
atmosphere.glsl brings frame.glsl and common.glsl with it.
The key light. frame_value(FRAME_KEY_DIR).xyz points towards the key
light – the sun, or the moon at night. key_light_at(p) is the light’s
illuminance arriving at p after the air above it: reddened at sunset,
gone when the light is under the horizon. sun_visibility(p, n) is how
much of it reaches the surface, 0 to 1: the shadow cascades (with a normal
offset growing with the cascade’s texel size), the clouds’ shadow when
acron.clouds runs, and the look’s shadow strength.
The sky’s light. sky_ambient_irradiance(n) is the irradiance from the
sky on a surface facing n – up, down, towards and away from the sun,
blended – dimmed and greyed under clouds. With an environment probe, the
ENV_PROBE define brings in probe.glsl: env_irradiance(n) is the
real sky’s diffuse light, clouds and all, and env_specular(r, roughness)
its GGX-prefiltered reflection, combined with Karis’s split-sum
approximation:
vec3 r = reflect(-v, n);
vec2 ab = env_brdf_approx(max(dot(n, v), 0.0), roughness);
vec3 specular = env_specular(r, roughness) * (f0 * ab.x + ab.y);
The air. apply_atmosphere(color, p) puts the aerial perspective (the
light scattered into the view ray, and what the ray lets through) and the
height fog between the camera and a lit surface – with acron.fog, the
froxel volume’s shadowed, lamp-lit fog (Volumetric Fog).
apply_light_shafts takes back the in-scatter where clouds and the sun’s
cascades shadow the air, and apply_clouds fades geometry inside or behind
the cloud layer into it. They go in this order, last.
A lit shader¶
A complete fragment shader, Lambert plus the probe’s reflections, with every optional part behind its define:
#include "atmosphere.glsl"
#include "shadows.glsl"
#ifdef ENV_PROBE
#include "probe.glsl"
#endif
#ifdef LIGHT_SHAFTS
#include "light_shafts.glsl"
#endif
#ifdef CLOUDS_FOG
#include "cloud_fog.glsl"
#endif
uniform sampler2D acron_Texture0;
uniform float roughness;
in vec3 v_world;
in vec3 v_normal;
in vec2 v_uv;
layout(location = 0) out vec4 o_color;
void main() {
vec3 albedo = texture(acron_Texture0, v_uv).rgb;
vec3 n = normalize(v_normal);
vec3 v = normalize(frame_camera() - v_world);
vec3 l = normalize(frame_value(FRAME_KEY_DIR).xyz);
vec3 key = key_light_at(v_world) * sun_visibility(v_world, n);
vec3 color = albedo / PI * key * max(dot(n, l), 0.0);
#ifdef ENV_PROBE
color += albedo * env_irradiance(n) / PI;
vec2 ab = env_brdf_approx(max(dot(n, v), 0.0), roughness);
color += env_specular(reflect(-v, n), roughness) * (0.04 * ab.x + ab.y);
#else
color += albedo * sky_ambient_irradiance(n) / PI;
#endif
color = apply_atmosphere(color, v_world);
#ifdef LIGHT_SHAFTS
color = apply_light_shafts(color, v_world);
#endif
#ifdef CLOUDS_FOG
color = apply_clouds(color, v_world);
#endif
o_color = vec4(frame_display(color), 1.0);
}
The defines are set by whoever owns the feature: ENV_PROBE by the game
when it made an EnvironmentProbe; CLOUD_SHADOWS,
CLOUDS_FOG and LIGHT_SHAFTS (with CLOUD_FOG_STEPS,
SHAFT_SAMPLES, SHAFT_RANGE_M and the density constants) by
CloudRenderer, into Environment.defines() – which
is why the clouds are made before lit shaders are compiled. A shader compiled
before them works, without the clouds.
The exposure contract¶
Every function returns light unexposed, in the scene’s light units (the
sun outside the air has Look.sun_illuminance). Do all the lighting and
the air in those units, and finish with frame_display(color) once, at
the very end: the frame’s exposure and, without the frame pipeline, the
night’s rod shift (purkinje(); under the pipeline its composite does
it for the whole picture). Write linear colour: the window converts it to
sRGB (framebuffer-srgb true). Do not apply the exposure twice – the
environment’s DirectionalLight and AmbientLight already carry it, and
a shader that reads them (acron_LightSource) must not multiply again.
Tone mapping happens after, on the exposed colour: in the frame pipeline’s
composite (The Frame Pipeline), or in a game’s own post pass.
sky_ambient_irradiance(n) includes the night sky’s own light
(starlight, airglow, light pollution: the NIGHT_AMBIENT frame slot),
so a shader lit by it is never black on a moonless night.
The inputs¶
The environment binds everything these functions read once, on the scene
root (render): the frame values (frame_values), the sky’s tables
(sky_transmittance; sky_view, the sun’s and the moon’s sky-view tables
side by side; sky_aerial, the aerial perspective with the sky’s irradiance
in its top row), the cascades (sun_shadow_atlas: a 1 x 1 one that reads as
lit without SunShadows), the probe’s maps (probe_atlas
and probe_layout) and the clouds’ weather map and noise. When a module is
destroyed it leaves a 1 x 1 stand-in for each of its inputs (a shader compiled
while it ran still declares them), and Environment.destroy() does the
same for its own. A node under the root inherits them, so a shader needs no
set_shader_input calls of its own for the environment.
A node drawn outside the scene root – an offscreen pass, a second scene –
gets them with the owners’ bind methods:
env.frame.bind(card) # the frame values
env.passes.bind(card) # the sky's tables
probe.bind(card) # the probe's maps
Per-frame values a game adds go into the frame values, not into shader inputs on the root (see Frame values in The Sky, Day and Night).
Wind and foliage¶
wind.glsl and foliage.glsl are the forest’s vegetation shading, usable
for any plant: grass cards, bushes, a game’s own tree models. The wind reads
two vertex columns that acron.proctree’s mesher writes – wind0 (bend
weight, phase, flutter, level) and wind1 (the pivot: the stem’s
centreline point or the leaf’s anchor; stiffness) – and works in the
plant’s own frame, in metres, z up:
#include "frame.glsl"
#include "wind.glsl"
in vec4 wind0;
in vec4 wind1;
// in main(), with p and n in the plant's frame:
vec4 w = frame_value(FRAME_WIND); // world m/s, gust
// (turn it into the plant's frame if the plant is rotated)
vec3 dir = normalize(vec3(w.xy, 0.0) + vec3(1e-4, 0.0, 0.0));
p += wind_offset(p, n, wind0, plant_height, dir, length(w.xy), w.w,
frame_time(), plant_phase, leaf_random,
vec4(1.0)); // strength, bend, sway, flutter
A mesh from another tool can paint the same columns (a weight that grows from
the root, and a pivot) to sway the same way. foliage_wrap and
foliage_translucency light a crown as a volume and let the sun glow
through the leaves; foliage_season turns a colour towards autumn or
spring; foliage_coverage sharpens alpha for alpha-to-coverage.
Platform notes¶
The library targets GLSL 4.50 core on Windows and Linux and compiles as GLSL
4.10 core on macOS for development (ACRON_GLSL holds the version, for a
shader that wants to branch on it). It does not compile for OpenGL ES or
WebGL, and the Vulkan renderer is not supported yet.