WorldEnvironment¶
WorldEnvironment is the canonical way to configure post-processing, sky, fog, and global rendering settings. Add one as a child of any node in your scene tree and set its properties: the renderer syncs them every frame. Never reach into the renderer directly.
Basic Usage¶
from simvx.core import Node, WorldEnvironment, Camera3D
class Game(Node):
def on_ready(self):
env = self.add_child(WorldEnvironment())
env.bloom_enabled = True
env.bloom_threshold = 0.8
env.ssao_enabled = True
env.tonemap_mode = "aces"
env.tonemap_exposure = 1.2
self.add_child(Camera3D(position=(0, 3, 8)))
Only one WorldEnvironment should be active per scene; the renderer picks the first one it finds during scene submission.
Property Groups¶
All properties are real Property descriptors: inspector-visible, serializable, and groupable.
Group |
Properties |
|---|---|
Ambient |
|
Sky |
|
Fog |
|
Volumetric Fog |
|
Tonemap |
|
Bloom |
|
SSAO |
|
Depth of Field |
|
Motion Blur |
|
Film Effects |
|
Anti-Aliasing |
|
Colour Grading |
|
Shadows |
|
Quality |
|
Quality Tiers¶
quality_tier is one string dial resolved through the tier table in simvx.core.graphics_quality, so a tier means the same thing on the Vulkan and WebGPU backends. Two of its values are selectors rather than tiers: "auto" (the default) consults no table — the backend decides, and the per-dial properties below apply as they are — and "custom" is the explicit pass-through, where the dials are entirely yours. "high" is pinned to the engine defaults, so it renders exactly like "auto" on desktop.
Four dials are wired to the table today:
Dial |
|
|
|
|
|---|---|---|---|---|
|
0.5 |
1.0 |
1.0 |
1.0 |
|
0 |
2 |
2 |
8 |
|
64 |
64 |
128 |
128 |
|
0 |
1 |
1 |
4 |
A tier never overrules a decision you made: a dial you explicitly moved off its default wins over the tier value, and "custom" (or "auto") keeps the table out of it altogether. That is the manual override — there is no separate opt-out.
render_scalesizes the whole main-view HDR chain; the tonemap draw upscales to the window. 1.0 is native and byte-identical to the unscaled path.probe_blend_countis how many reflection probes blend per fragment. 0 turns probe capture and blending off entirely, and ambient falls back to the sky IBL or the flat fill.probe_face_sizeis the per-face capture resolution of those probes.shadow_caster_countis how many point lights, and how many spot lights, may cast a shadow at once (Vulkan only: the web forward shader samples only the directional cascades). Each slot is another row in both shadow atlases and another shadow map to draw, but a map is only drawn again when its light moves or changes, when a casting mesh in the scene moves, is added or is removed, or when a material changes something a shadow is drawn from: a still scene draws each of its shadow maps once and then costs almost nothing per frame for them (four point casters over 200 casting meshes spent around 15 ms a frame on those maps while one casting mesh moved and 0.2 ms once nothing did, taking the whole frame from around 21 ms to around 6 ms), which is what makes"ultra"afford four of each kind. Moving one light redraws that light’s map alone; moving one casting mesh redraws all of them. Lights past the budget still light the scene and simply cast nothing; the renderer reports that once. Which of them keep a shadow is decided byPointLight3D.shadow_priority/SpotLight3D.shadow_priority(higher wins) and then by how much each light matters to the camera:intensity * range / distance, the lit sphere as it subtends at the camera scaled by its brightness, so a wide lamp behind you outranks a pinprick at your feet. Selection is sticky: a light that already holds a shadow map keeps it until a challenger beats it by more than a tenth, so a moving camera does not swap maps back and forth across a boundary where two lights are worth the same. It keeps the same atlas row with it, so a light that stops casting, or that loses its slot to a stronger one, does not shuffle the rows of the lights around it; lowering the budget, or adding and removing light nodes, re-seats what it has to.
env.quality_tier = "ultra" # four positional casters, 8-probe blend
env.shadow_caster_count = 2 # explicit: wins over the tier
Custom Post-Processing¶
PostProcessEffect wraps a fragment shader as a fullscreen pass:
from simvx.core import PostProcessEffect
grayscale = PostProcessEffect(
shader_code="""
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec3 c = texture(u_colour_tex, uv).rgb;
frag_colour = vec4(vec3(dot(c, vec3(0.299, 0.587, 0.114))), 1.0);
}
""",
order=10,
)
grayscale.set_uniform("intensity", 0.5)
env.add_post_process(grayscale)
Standard uniforms wired automatically: u_time (float), u_resolution (vec2), u_colour_tex (sampler2D), u_depth_tex (sampler2D). Effects run in order ascending after the built-in post chain.
API Reference¶
See simvx.core.WorldEnvironment, simvx.core.Environment, and simvx.core.PostProcessEffect.