Reflections and Glass

Algan’s renderer is a ray tracer, so reflection and refraction are not tricks – rays really do bounce off metal and bend through glass. This tutorial covers how to ask for them and how to make them look like something.

Everything here is driven by set_material(). There are no separate reflectivity or refractive-index setters: a MeshStandardMaterial’s metalness and roughness control reflection, and a MeshPhysicalMaterial’s transmission and ior control refraction. See Shaders and Materials for the material catalogue.

Important

set_material() must be called before the Mob is spawned, and reflection and refraction apply to 3-D objects only.

Reflections

metalness is how metallic a surface is: 0 is a dielectric (plastic, stone, painted wood), 1 is bare metal. roughness is how polished it is: 0 is a perfect mirror, 1 is fully diffuse. Keep it low where you want the scene to appear in the surface – see Glossy Reflections for what a high one does.

Example: ReflectionsMetalFloor

from algan import *

with Off():
    Prism(width=9, height=0.2, depth=9, color=GREY).move(DOWN * 1.4).set_material(
        MeshStandardMaterial(metalness=0.8, roughness=0.1)).spawn()
    balls = Group([Sphere(radius=0.5, color=c).move(RIGHT * x + DOWN * 0.6)
                   for c, x in ((RED, -1.8), (YELLOW, 0), (BLUE, 1.8))]).spawn()
    Scene.get_camera().move(UP * 1.2).look_at(ORIGIN)

with Seq(runtime=3):
    balls.move(UP * 1.4)
    balls.move(DOWN * 1.4)

Scene.save_video()

A flat polished floor is the easiest way to show off reflections, and a useful trick generally: it grounds objects that would otherwise float in a void.

Important

A mirror needs something to reflect. metalness=1 means the surface has no diffuse color of its own at all, so a fully metallic object in an otherwise empty scene renders black – it is faithfully reflecting a black background.

Three ways to fix that, in order of effectiveness:

  1. Give the scene an environment map, so there is always something to reflect (see Lighting and Shadows).

  2. Put other objects around it.

  3. Back the metalness off to 0.7-0.9 so some base color shows through.

Note also that a convex mirror shrinks whatever it reflects enormously. Flat or gently curved reflectors read much better than a mirrored sphere.

The reflection you see is of the neighbouring geometry as it is shaded, so a reflected object can legitimately look different from the object itself – the reflection sees a different side of it, lit differently.

Glass and Refraction

Refraction needs a MeshPhysicalMaterial with a non-zero transmission and an ior (index of refraction) above 1:

Example: ReflectionsGlass

from algan import *

with Off():
    Group([Square(color=c).scale(0.5).move(RIGHT * x + UP * y)
           for c, x, y in ((RED, -1.2, 0.8), (GREEN, 1.2, 0.8),
                           (YELLOW, -1.2, -0.8), (BLUE, 1.2, -0.8))]
          ).move(IN * 3).spawn()
    glass = Sphere(radius=1.1, color=WHITE).set_material(
        MeshPhysicalMaterial(transmission=1.0, ior=1.5, roughness=0.0)).spawn()

with Seq(runtime=3):
    glass.move(RIGHT * 1.5)
    glass.move(LEFT * 3)

Scene.save_video()

The sphere acts as a lens, inverting and displacing what is behind it. That displacement is the whole visual point of refraction – glass with nothing interesting behind it looks like nothing at all, so always put a patterned backdrop behind a glass object.

Useful indices of refraction:

Material

ior

Notes

Air / vacuum

1.0

No bending. Values <= 1 disable the dielectric lobe.

Water

1.33

Common glass

1.5

The MeshPhysicalMaterial default.

Sapphire

1.77

Diamond

2.42

Strong bending, obvious total internal reflection.

Transmission and opacity are independent

This trips people up, so it is worth stating plainly:

  • opacity is coverage – how much of the pixel the object occupies at all. Lower it and the object fades out; light passes straight through the part that is not there, undeviated.

  • transmission is transparency – how much light passes through the part of the object that is there. That light is refracted, tinted, and subject to total internal reflection.

For glass you want transmission=1.0 and opacity=1.0. Fading a glass object in and out is a change to opacity; making it more or less glassy is a change to transmission.

A transmissive material’s color tints the light passing through it, so MeshPhysicalMaterial(color=GREEN, transmission=1.0) gives green glass.

With path tracing (samples_per_pixel > 1), roughness blurs both reflected and refracted light. Rough glass also compensates for the light that the single-scatter microfacet model would lose between facets. Reflection and transmission share that recovered power, while the relative refractive indices and the existing absorption controls still apply. Neutral white glass therefore retains energy at high roughness instead of becoming artificially dark. The added broad lobe is an approximation, not a microscopic random walk; compensation of strongly tinted glass is deliberately conservative. Smooth interfaces and equal-index straight transmission retain their delta behavior. This does not fix darkness caused by too few scene bounces.

Controlling Bounce Depth

Every reflection or refraction spawns a new ray, and max_bounces caps how far that goes:

SETTINGS.raytracing.set(max_bounces=8)    # the default

A solid glass sphere needs at least four bounces to look right (in the front surface, out the back, and the internal reflections). Two mirrors facing each other will happily consume any budget you give them. Rays that run out of bounces stop contributing, which shows up as unexpectedly dark patches inside glass – if that is what you are seeing, raise max_bounces.

Lowering it is a good way to speed up a draft render of a scene full of glass.

Glossy Reflections

roughness decides how much of a surface’s specular energy is spent on a traced reflection at all, and that is what makes brushed metal look different from chrome:

Example: ReflectionsGlossy

from algan import *

with Off():
    Group([Square(color=c).scale(0.6).move(RIGHT * x + UP * y)
           for c, x, y in ((RED, -2, 1), (GREEN, 2, 1),
                           (YELLOW, -2, -1), (BLUE, 2, -1))]
          ).move(IN * 4).spawn()

    chrome = Sphere(radius=0.9, color=GREY).move(LEFT * 1.6).set_material(
        MeshStandardMaterial(metalness=1.0, roughness=0.0)).spawn()
    brushed = Sphere(radius=0.9, color=GREY).move(RIGHT * 1.6).set_material(
        MeshStandardMaterial(metalness=1.0, roughness=0.35)).spawn()

Scene.wait(2)

Scene.save_video()

The left sphere reflects the squares behind the camera’s subject; the right one does not.

A mirror (roughness=0) reflects the scene exactly. As roughness rises the reflected image fades out and the surface’s own shading – its broad specular highlight and the ambient/environment term – takes over: half the reflection is still traced at roughness=0.15, almost none of it by 0.35. So a rough metal reads as a rough metal, but it does not show you a picture of the room.

Note

The reason it fades rather than blurring is a sample budget. The default single-sample renderer spends at most four traced rays per reflective pixel, and four rays cannot integrate a wide lobe: they either dither (and the dither crawls as the object moves) or land as four ghost copies of the reflected image. Drawing the reflection sharp instead is worse still – a reflection is usually a heavily minified image, so a sharp one aliases into hard bright patches across the surface.

SETTINGS.raytracing.set(glossy_reflection=True) opts into a real blur, and it does not spend more rays to get one – it spends one. The reflection is traced in the mirror direction into a buffer of its own, and that buffer is blurred by how wide the lobe is on screen before it is composited back with the lobe’s exact energy (the split-sum approximation: DESIGN_glossy_prefilter.md in the renderer, Karis 2013 for the theory). Blurring a picture is what a wide lobe does; sampling it four times is not. Nothing dithers, nothing ghosts, and nothing crawls when the object moves, because a blur radius is a smooth function of position where a four-tap fan is not.

Two things to know before turning it on. It reflects what is on screen, so a reflector shows the background where it should show something behind the camera or outside the frame – give the scene an environment map (below) and it has something to reflect either way. And a rough metal will read darker than it does with this off: with it off the surface keeps its ambient fill in place of the reflection it is not drawing, and with it on that energy goes into the reflection, which is only as bright as the room around it.

The four-tap fan is still there for comparison – set(glossy_reflection=True, prefilter=False) – but it is not the recommended path.

For fully physical rough reflections, raise SETTINGS.raytracing.samples_per_pixel above 1 to switch to the path tracer, which importance-samples every bounce and has the samples to resolve a wide lobe honestly – glass, nested media and environment maps included, with the noise denoised by default. What it gives up is this page’s deterministic machinery (analytic coverage, the screen-space prefilter) and render time. See What each renderer supports.

Environment Maps

For any serious metal or glass work, an environment map is the highest-value change you can make. It gives every reflective surface something to reflect and every refractive one something to bend, and it lights the whole scene:

Example: ReflectionsEnvironmentMap

from algan import *

Scene.set_environment_map("world_map.png", intensity=1.0, ambient=True)

Sphere().set_material(
    MeshStandardMaterial(metalness=1.0, roughness=0.05)).spawn()

Scene.save_video()

Any equirectangular image works; a real studio panorama gives a far better result than the world map used here.

See Lighting and Shadows for the details.

Performance

Reflection and refraction are the most expensive features in the renderer, because each bounce is another full ray traversal:

  • In the deterministic renderer, a refractive object splits each ray into reflected and refracted rays, so glass costs more than metal. The path tracer samples one continuation per event, including rough-glass compensation; it does not split paths or grow their state with roughness.

  • Refraction is implemented by both renderers. Raising samples_per_pixel selects the path tracer, which refracts through the same nested-media stack and importance-samples the environment map instead of prefiltering it – see What each renderer supports.

  • max_bounces multiplies the cost of everything reflective in the scene.

While you are iterating on a shot, lower max_bounces and leave the resolution at the default; raise both for the final render. See Performance and Quality.

See Also