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.

  • intensity scales the map’s brightness.

  • ambient=True (the default) also lights surfaces from the map. Set it to False to use the map only as a backdrop and in reflections, without it contributing diffuse light.

  • Pass None to 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

Reflections and Glass.

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