Backgrounds and Post-Processing¶
Apart from the ray-traced render itself, there are two important steps which shape how your output will look:
Backgrounds: What fills empty pixels where no geometry is present.
Post-processing: Image filters and effects (like bloom, anti-aliasing, and tonemapping) that run over rendered frames before video encoding.
Backgrounds¶
A Scene’s background can be a solid color, an image, or a procedural function.
Set it for one
render by passing background to
save_video(), or for the Scene as a whole with
set_background():
Scene.save_video("my_video", background=BLUE) # For this render only
Scene.set_background(BLUE) # Across the whole Scene
Note
The background is Scene state, not timeline state: it applies to the entire video rather than from a point onwards. It cannot be animated, but a procedural background can vary with time – see below.
The default background is black.
A solid color¶
Any Algan color works, and arithmetic on colors is a convenient way to get a dark tint:
Scene.save_video("my_video", background=Color([0.05, 0.05, 0.15]))
Important
Be careful with BLUE * 0.15. Colors have five components including
opacity, so scaling one scales its alpha too, and an alpha below 1 makes
the whole output transparent, which changes the container to .mov.
Build a dark color with an explicit Color, or use
(BLUE * 0.15).set_opacity(1.0) after scaling.
An image¶
Pass a path and the image is scaled to the frame:
Example: BackgroundsImage ¶
from algan import *
Circle(color=YELLOW, glow=0.4).scale(0.8).spawn()
Scene.wait(1)
Scene.save_video(background='world_map.png')
Paths resolve against the working directory and then your script’s directory.
For a background that is part of the 3-D world (i.e. visible in reflections and refractions, and correct as the camera turns) you want an environment map instead, not a background image. See Lighting and Shadows.
A procedural background¶
Pass a callable (x, y, time) -> color for a background computed per pixel per
frame. x and y are normalized screen coordinates and time is in
seconds; all three arrive as broadcastable torch tensors:
Example: BackgroundsProcedural ¶
from algan import *
import torch
def sunset(x, y, t):
# x, y and t broadcast together, creating a [frames, H, W, 1] tensor base
base = torch.zeros_like(t + y + x)
return torch.cat([base + 0.35 * y + 0.05, # Red
base + 0.10 * y + 0.02, # Green
base + 0.30 * (1 - y) + 0.06, # Blue
base, # Glow
base + 1.0], -1) # Opacity
Circle(color=YELLOW).scale(0.8).spawn()
Scene.wait(1)
Scene.save_video(background=sunset)
Because time is passed in, a procedural background can animate (e.g. a drifting
gradient, a pulse, a scrolling pattern) even though the background itself is not
on the animation timeline.
For maximum speed a background callable can be a Taichi @ti.func instead, which
receives scalar normalized coordinates and a time and returns a color vector; it is
evaluated for the whole render batch by one Taichi kernel writing directly into the
output buffer.
Note
A procedural background is treated as opaque, because its alpha cannot be known without evaluating the render. For transparent output use a color with alpha below 1 – see Transparent Backgrounds.
Post-Processing¶
Post-processing passes run on each finished frame. The default is bloom (a soft glow around bright areas), and you choose the set per render:
Scene.save_video("my_video") # Default: bloom enabled
Scene.save_video("my_video", post_processes=()) # Disable all post-processing
Bloom and Glow¶
Bloom is what makes a Mob’s glow attribute light up surrounding pixels:
Example: BackgroundsGlow ¶
from algan import *
dot = Dot(color=YELLOW).scale(2).spawn()
with Seq(runtime=2):
dot.glow = 1.0
dot.glow = 0.0
Scene.save_video()
Tip
A little glow goes a long way. Values between 0.3 and 0.5 give a nice
luminous shine; 1.0 on large objects will wash out the frame.
To customise the bloom filter, pass the pass itself with different arguments:
from algan.rendering.post_processing.bloom import bloom_filter
from functools import partial
Scene.save_video("my_video", post_processes=(partial(bloom_filter, strength=8),))
strength scales the effect, kernel_size and scale_factor trade quality
for speed, and glow_spread sets how far the glow reaches.
Bloom blurs the glow at two scales and sums them: a tight rim (rim_frac) that
hugs the source outline, and a wide, faint tail (glow_spread, defaulting to
10% of the frame height) weighted by tail_weight. The tail is what makes a
single glowing Mob look luminous rather than outlined. In a
scene with many emitters the tails sum into a haze that fills the gaps between
them, and raising glow to compensate only deepens it. When you want glow that
is bright and local, narrow the tail and pay for it in strength:
tight = partial(bloom_filter, glow_spread=0.015, tail_weight=0.15, strength=60)
Scene.save_video("out", post_processes=(tight,))
Anti-aliasing¶
Algan supports three anti-aliasing techniques:
Supersampling (SSAA):
SETTINGS.video.supersampling(default2) renders at 2x resolution and downsamples. It is writtenssaaorSSAAjust as often – both are the same setting.Analytic AA:
SETTINGS.raytracing.analytic_aa(on by default) resolves vector edges analytically inside the rasterizer with almost zero overhead.FXAA:
SETTINGS.video.fxaa(off by default) is a cheap post-pass that smooths remaining edges. Useful when you have had to drop the supersampling level for speed.
SETTINGS.video.set(ssaa=1, fxaa=True) # Fast draft mode
SETTINGS.video.set(ssaa=2, fxaa=False) # High quality default
Tonemapping¶
By default, tonemapping is off so authored RGB values remain exact (e.g.
WHITE is exactly 255).
If your scene features high-dynamic-range lighting (bright specular highlights, intense glow, environment maps), enable filmic tonemapping to smoothly roll off highlights:
SETTINGS.raytracing.set(tonemapping=True)
SETTINGS.raytracing.set(tonemap_method="agx") # "neutral" or "agx"
SETTINGS.raytracing.set(tonemap_exposure=1.2)
Custom Post-Processing Passes¶
A custom post-processing pass is any callable that takes a batch of frames and returns modified frames:
Example: BackgroundsCustomPass ¶
from algan import *
from algan.rendering.post_processing.bloom import bloom_filter
def desaturate(frames, memory=None):
# Convert RGB channels to grayscale
grey = frames[..., :3].mean(-1, keepdim=True)
frames[..., :3] = grey
return frames
square = Square(color=RED).scale(1.5).spawn()
square.rotate(180, OUT)
Scene.save_video(post_processes=(bloom_filter, desaturate))
Frames arrive as a torch tensor on the render device.
The memory keyword is required, not optional: Algan always calls a pass as
process(frames, memory=arena), so a pass declared as def desaturate(frames)
raises TypeError part way through the render. Give it a default of None and
ignore it, as above, or accept **kwargs. Use it when you want to allocate from
Algan’s render arena rather than the torch allocator – see
Performance and Quality.
See Also¶
Transparent Backgrounds – rendering with an alpha channel.
Lighting and Shadows – environment maps, the 3-D equivalent of a background image.
Images and Textures – painting an image onto geometry rather than behind it.
Performance and Quality – what anti-aliasing and bloom cost.
Renderer Limitations – what analytic anti-aliasing does and does not resolve.
Settings – the settings system these knobs live in.
Saving Videos and Images – passing
backgroundandpost_processesper render.