Cutout shadows¶
an alpha-tested material casts a holed shadow.
▶ Run in browserTags: 3d shadows cutoff alpha-test cutout
A material with blend="cutoff" discards every fragment whose albedo alpha is
below alpha_cutoff. The shadow passes of directional, point and spot lights
sample the same alpha, so a cut-out surface casts a shadow with the same holes
rather than a solid silhouette. Nothing else is needed: give the material an
albedo_map whose alpha channel is the mask, and turn on shadows for the
light (shadows are opt-in per light).
Here a flat panel textured with a checkerboard alpha mask hangs over the ground
under a DirectionalLight3D, which projects the checkerboard through the holes
onto the floor. A solid slab beside it casts an ordinary filled shadow for
contrast.
Source¶
1# /// simvx
2# tags = ["3d", "shadows", "cutoff", "alpha-test", "cutout"]
3# screenshot_frame = 30
4# ///
5"""Cutout shadows: an alpha-tested material casts a holed shadow.
6
7A material with ``blend="cutoff"`` discards every fragment whose albedo alpha is
8below ``alpha_cutoff``. The shadow passes of directional, point and spot lights
9sample the same alpha, so a cut-out surface casts a shadow with the same holes
10rather than a solid silhouette. Nothing else is needed: give the material an
11``albedo_map`` whose alpha channel is the mask, and turn on ``shadows`` for the
12light (shadows are opt-in per light).
13
14Here a flat panel textured with a checkerboard alpha mask hangs over the ground
15under a ``DirectionalLight3D``, which projects the checkerboard through the holes
16onto the floor. A solid slab beside it casts an ordinary filled shadow for
17contrast.
18"""
19
20import numpy as np
21
22from simvx.core import Camera3D, DirectionalLight3D, Material, Mesh, MeshInstance3D, Node3D, WorldEnvironment
23from simvx.graphics import App
24
25
26def checkerboard_alpha(cells: int = 6, cell_px: int = 24) -> np.ndarray:
27 """Near-white RGBA texture whose alpha is 255 on alternate cells and 0 on the rest."""
28 mask = np.indices((cells, cells)).sum(axis=0) % 2 == 0
29 alpha = mask.repeat(cell_px, axis=0).repeat(cell_px, axis=1)
30 tex = np.full((*alpha.shape, 4), 235, dtype=np.uint8)
31 tex[..., 3] = alpha * 255
32 return tex
33
34
35class CutoffShadowScene(Node3D):
36 def on_ready(self):
37 self.add_child(WorldEnvironment())
38 self.add_child(Camera3D(position=(0, 15, 22), fov=55, look_at=(0, 0, 0)))
39 sun = DirectionalLight3D(colour=(1.0, 0.96, 0.88), intensity=1.3, shadows=True, direction=(4, -14, -6))
40 self.add_child(sun)
41
42 ground = Material(colour=(0.55, 0.55, 0.6, 1), roughness=0.9)
43 self.add_child(MeshInstance3D(mesh=Mesh.box((34, 0.4, 34)), material=ground, position=(0, -0.5, 0)))
44
45 # Single-sided on purpose: a double-sided panel would show its underside
46 # through the holes at grazing angles, as strips inside the cutouts.
47 cutout = Material(albedo_map=checkerboard_alpha(), blend="cutoff", alpha_cutoff=0.5, roughness=0.7)
48 self.add_child(MeshInstance3D(mesh=Mesh.box((11, 0.2, 11)), material=cutout, position=(-5, 5, 0)))
49
50 solid = Material(colour=(0.8, 0.35, 0.25, 1), roughness=0.6, metallic=0.1)
51 self.add_child(MeshInstance3D(mesh=Mesh.box((5, 0.2, 5)), material=solid, position=(9, 5, 0)))
52
53
54if __name__ == "__main__":
55 App(title="Cutout Shadows", width=800, height=600).run(CutoffShadowScene())