Lighting and Shadows¶
Algan’s ray-traced renderer supports a full set of light sources modelled on Three.js’s lights, plus ray-traced shadows and environment maps (image-based lighting). This tutorial covers all of them.
Note
Lights, shadows and environment maps affect 3-D objects (those built on
Surface, such as Sphere, Cylinder and
Cone, and imported 3-D models). Flat 2-D shapes and text are drawn
with their own color and are not lit.
The Default Light¶
Every scene starts with a single white PointLight positioned above and
to the right of the camera. That is what gives 3-D shapes their shading.
Because lights are Mob s, you spawn, move, animate, and despawn them
just like any other shape. Spawning a light registers it with the Scene
automatically:
Example: LightingRegistering ¶
from algan import *
ball = Sphere(radius=1.2, color=WHITE).spawn()
# The scene's existing lights (index 0 is the default point light)
lights = Scene.get_light_sources()
with Off():
# Spawning a new light automatically adds it to the scene
PointLight(location=LEFT * 5 + OUT * 3, color=BLUE, intensity=2).spawn()
# Remove the default light
Scene.remove_light(lights[0])
ball.rotate(180, UP)
Scene.save_video()
To start with a blank slate call clear_lights(),
which removes all existing lights.
Important
Wrap scene setup in with Off():. Lights are Mobs, so each spawn()
records a one-second fade by default, and building a three-light rig outside
Off() would make your video open with three seconds of darkness.
Because lights are standard Mobs, you can animate them directly on the timeline:
Example: LightingAnimatedLight ¶
from algan import *
ball = Sphere(radius=1.2, color=WHITE).spawn()
light = Scene.get_light_sources()[0]
with Seq(runtime=4, easing=easings.identity):
light.orbit(360, OUT, about=ORIGIN)
light.color = BLUE
Scene.save_video()
Light Types¶
Every light takes an intensity multiplier, and every light’s location,
color and intensity are animatable. All of them are importable from
algan directly.
Point Light¶
PointLight emits light in all directions from a single point. By
default, it has no distance falloff (keeping scene lighting clean and even), but
you can opt into physically-correct attenuation with decay and a finite
distance range:
Example: LightingPointLight ¶
from algan import *
with Off():
Scene.clear_lights()
# Inverse-square falloff (decay=2), fading out by 20 units
PointLight(location=UP * 4, color=WHITE, intensity=30, decay=2,
distance=20).spawn()
Group([Sphere(radius=0.55, color=WHITE).move(RIGHT * x)
for x in (-1.8, 0, 1.8)]).spawn()
Scene.wait(2)
Scene.save_video()
Note that turning on decay usually means raising intensity a long way,
because the light now falls off with distance.
Directional Light¶
DirectionalLight models a distant source like the sun: all its rays are
parallel, pointing from the light toward its target (the origin by default).
Distance does not matter, only direction.
Example: LightingDirectionalLight ¶
from algan import *
with Off():
Scene.clear_lights()
DirectionalLight(location=UP * 10 + RIGHT * 6, target=ORIGIN,
color=WHITE, intensity=2).spawn()
Group([Sphere(radius=0.55, color=WHITE).move(RIGHT * x)
for x in (-1.8, 0, 1.8)]).spawn()
Scene.wait(2)
Scene.save_video()
Ambient Light¶
AmbientLight adds a flat, direction-less term to every surface. Use it
to lift shadows and fill in the dark side of objects so they never go fully black.
Almost every rig wants a little of it.
Example: LightingAmbientLight ¶
from algan import *
with Off():
Scene.clear_lights()
DirectionalLight(location=UP * 8 + RIGHT * 6 + OUT * 4, target=ORIGIN,
color=WHITE, intensity=2).spawn()
AmbientLight(color=WHITE, intensity=0.4).spawn()
ball = Sphere(radius=1.2, color=BLUE).spawn()
ball.rotate(180, UP)
Scene.save_video()
The ambient term is what keeps the side of the sphere facing away from the key light off pure black.
Animating Intensity¶
A light’s intensity is an animatable attribute like its color: writing
it after spawn records the change on the timeline, so a light can brighten or
dim over a shot.
Example: LightingAnimatedIntensity ¶
from algan import *
with Off():
Scene.clear_lights()
PointLight(location=UP * 4 + OUT * 2, color=WHITE).spawn()
Group([Sphere(radius=0.55, color=WHITE).move(RIGHT * x)
for x in (-1.8, 0, 1.8)]).spawn()
light = Scene.get_light_sources()[0]
with Seq(runtime=3):
light.intensity = 5
Scene.save_video()
Note
A light’s shape parameters (decay, distance, cone angles and
emitter sizes), are still plain per-light constants rather than animatable
attributes: they are read when a frame batch is prepared rather than
recorded on the timeline. To show two of those settings, render two videos.
Hemisphere Light¶
HemisphereLight is a soft outdoor fill: surfaces facing up receive
the light’s color (the “sky”), surfaces facing down receive its
ground_color, and side-facing surfaces blend between the two.
Example: LightingHemisphereLight ¶
from algan import *
with Off():
Scene.clear_lights()
HemisphereLight(color=BLUE, ground_color=(0.4, 0.3, 0.1),
intensity=0.8).spawn()
Group([Sphere(radius=0.55, color=WHITE).move(RIGHT * x)
for x in (-1.8, 0, 1.8)]).spawn()
Scene.wait(2)
Scene.save_video()
Spot Light¶
SpotLight is a cone of light aimed at a target. cone_angle sets
the cone’s half-angle in degrees and penumbra (0-1) softens its edge. Like a point
light, it supports decay and distance.
Example: LightingSpotHemisphere ¶
from algan import *
with Off():
Scene.clear_lights()
SpotLight(location=UP * 5 + OUT * 2, target=ORIGIN, color=WHITE,
intensity=40, cone_angle=28, penumbra=0.6, decay=2).spawn()
HemisphereLight(color=BLUE, ground_color=(0.3, 0.2, 0.1), intensity=0.5).spawn()
Group([Sphere(radius=0.55, color=WHITE).move(RIGHT * x)
for x in (-1.8, 0, 1.8)]).spawn()
Cube(size=5, color=GREY).move(DOWN * 3.1).spawn()
Scene.wait(2)
Scene.save_video()
Rect-Area Light¶
RectAreaLight is a one-sided emitting rectangle (a softbox). Its
emitted radiance does not change with viewing distance. Illumination falls off
through geometry, with no artificial range cutoff. The path tracer shows an
opaque panel with an emitting front and a black back; it can appear in mirrors
and through glass, and can occlude other objects. The deterministic renderer
integrates a grid of samples emitting cells without drawing the panel.
More cells improve that integration and its shadows at a proportional cost;
the path tracer samples two triangles regardless of the cell count.
intensity retains Algan’s area-light power normalization. For area A,
the panel’s linear-RGB radiance is color * intensity / A (including the
usual opacity/glow multipliers). Enlarging it at fixed intensity spreads the
same power over a larger, dimmer surface. A nearby large panel does not obey
a point source’s inverse-square law as a whole: each surface element contributes
its own distance and cosine factors.
Omit decay and distance in new area-light code. Only their physical
values, decay=2 and distance=0, are accepted, including later assignment
or set calls. Other values raise AlganConfigurationError rather
than silently reverting to nonphysical emission. Existing scenes that explicitly
used those physical values retain their intensity normalization. Scenes using
the old no-falloff default need their intensity retuned; there is no single
brightness conversion that preserves a nonphysical distance law everywhere.
Point and spot lights retain their separate artistic falloff controls.
Example: LightingRectAreaLight ¶
from algan import *
SETTINGS.raytracing.set(shadows=True)
with Off():
Scene.clear_lights()
# 9 samples is a 3x3 emitter grid, which spends 9 of the 16 shadow
# slots and leaves room for the ambient light (see below).
RectAreaLight(location=UP * 5, target=ORIGIN, width=4, height=4,
samples=9, color=WHITE, intensity=30).spawn()
AmbientLight(color=WHITE, intensity=0.2).spawn()
Sphere(radius=0.8, color=BLUE).move(UP * 0.7).spawn()
Cube(size=4, color=GREY).move(DOWN * 2.6).spawn()
Scene.wait(2)
Scene.save_video()
Ray-Traced Shadows¶
Shadows are off by default, because they cost render time. Turn them on with one setting before rendering:
SETTINGS.raytracing.set(shadows=True)
With shadows on, each lit surface point fires shadow rays toward every light; a light blocked by another object does not contribute to that point.
Soft Shadows¶
By default shadows are hard-edged. To get a soft penumbra, give the light a non-zero emitter size:
Point and spot lights take a
shadow_radius– the world-space radius of the emitting disk.Directional lights take a
shadow_angle– the angular size of the source in degrees, like the sun’s ~0.5°.Rect-area lights are soft automatically; their penumbra smoothness is set by
samples.
Example: LightingSoftShadow ¶
from algan import *
SETTINGS.raytracing.set(shadows=True)
with Off():
Scene.clear_lights()
DirectionalLight(location=UP * 8 + RIGHT * 4 + OUT * 4, target=ORIGIN,
color=WHITE, intensity=3, shadow_angle=3).spawn()
AmbientLight(color=WHITE, intensity=0.3).spawn()
Sphere(radius=0.8, color=BLUE).move(UP * 0.7).spawn()
Cube(size=4, color=GREY).move(DOWN * 2.6).spawn()
Scene.wait(2)
Scene.save_video()
Algan traces a fixed fan of shadow rays across the emitter to build the penumbra.
The number of rays comes from the environment variable
ALGAN_SOFT_SHADOW_SAMPLES (default 8), which is baked into the kernels and so
must be set before import algan. Raise it for smoother penumbras at a
proportional cost.
Note
Deterministic shadow rays do respect transparency: the light is multiplied through each occluder’s opacity, so stacked translucent surfaces compound and a fully opaque one blocks. Ambient and emissive terms are unaffected.
What neither renderer does is refractive shadow transport: a shadow ray travels a straight line through glass under both, so there are no caustics (see Renderer Limitations). The path tracer honours every light type on this page – area lights and emissive surfaces as true sampled emitters, the rest exactly as the deterministic stages define them – at the cost of a large increase in render time; see What each renderer supports for what it gives up.
How many lights can cast shadows?
Shadow-casting lights are collected into a fixed-size per-pixel list whose
length is a compile-time constant (default 16). Lights beyond that are still
lit, just not shadowed, and each sample of a RectAreaLight counts
toward the limit, so an under-capped area light simply gets a shallower shadow.
Sixteen slots is a key/fill/rim rig with an ambient fill several times over, or
a 3×3-sample area light plus that same rig – but a 4×4-sample area light spends
all sixteen on its own. If you need denser area-light penumbras or a larger rig,
set ALGAN_MAX_SHADOW_LIGHTS before the first render (more GPU registers,
slightly lower shadow-kernel occupancy).
Environment Maps¶
An environment map wraps the scene in a 360° image. It acts as a skybox (visible in the background and in reflections and refractions) and, optionally, as image-based lighting (the whole scene lit by the colors of the map).
Pass an equirectangular image (a longitude × latitude panorama, sky at the top) to
Scene.set_environment_map. Like every
Scene method, it is reachable on the class as well as on an instance, so a
module-level script writes Scene.set_environment_map(...) and it applies to
the active Scene:
Example: LightingEnvironmentMap ¶
from algan import *
Scene.set_environment_map("world_map.png", intensity=1.0, ambient=True)
# A mirror sphere reflects the environment; other objects are lit by it.
mirror = Sphere().move(LEFT * 1.5).set_material(
MeshStandardMaterial(metalness=1.0, roughness=0.05))
mirror.spawn()
Sphere().move(RIGHT * 1.5).spawn()
Scene.save_video()
Any equirectangular image works. A real studio panorama gives a much better result.
intensityscales the map’s brightness.ambient=True(the default) also lights surfaces from the map. Set it toFalseto use the map only as a backdrop and in reflections, without it contributing diffuse light.Pass
Noneto remove a previously-set environment map.
You can also pass a [height, width, 3] tensor or NumPy array instead of a file
path. Image paths resolve against the working directory and then your script’s
directory.
An environment map is the single biggest improvement you can make to a metal or glass object: a mirror with nothing to reflect renders black. See
Building a Rig¶
Real lighting is rarely one light. The standard three-point setup is a bright key with the shadow, a dimmer fill opposite it to keep the shadow side readable, and a rim from behind to separate the subject from the background, plus a little ambient:
Example: LightingThreePointRig ¶
from algan import *
SETTINGS.raytracing.set(shadows=True)
with Off():
Scene.clear_lights() # drop the default light
# Key light: bright, from above and to one side, with a soft shadow.
SpotLight(location=UP * 6 + RIGHT * 4 + OUT * 4, target=ORIGIN,
color=WHITE, intensity=60, cone_angle=30, penumbra=0.5,
decay=2, shadow_radius=0.3).spawn()
# Fill: dimmer, from the opposite side, no shadow.
PointLight(location=LEFT * 6 + OUT * 2, color=WHITE, intensity=4).spawn()
# Rim: from behind, slightly cool.
DirectionalLight(location=IN * 8 + UP * 4, target=ORIGIN,
color=Color((0.6, 0.7, 1.0))).spawn()
# Ambient: stops the shadow side going pure black.
AmbientLight(color=WHITE, intensity=0.25).spawn()
Sphere().spawn()
Cube(size=6, color=GREY).move(DOWN * 4).spawn() # Ground floor
Scene.wait(2)
Scene.save_video()
See Also¶
Your First 3-D Scene – the gentler introduction to lights and 3-D shapes.
Renderer Limitations – which objects and materials are lit and shadowed at all, and where the shadow approximations show.
Cameras – moving the camera through a lit scene.
Shaders and Materials – how materials respond to these lights.
Reflections and Glass – mirrors, metals and refraction.
Images and Textures – normal maps, which change how a surface responds to these lights per texel.
Performance and Quality – what shadows and extra lights cost.