Lighting and shadows

Add directional lights and ambient lighting to illuminate 3D meshes with realistic shadows.

Features:

  • viser.SceneApi.set_ambient_light() for global illumination

  • viser.SceneApi.add_directional_light() for sun-like directional lighting

  • viser.SceneApi.configure_default_lights() for quick setup

  • viser.SceneNodeHandle.cast_shadow to enable shadow casting

  • viser.SceneApi.add_light_spot() for focused cone lighting with direction control

  • Dynamic light control with GUI sliders

Note

This example requires external assets. To download them, run:

git clone -b v1.0.24 https://github.com/viser-project/viser.git
cd viser/examples
./assets/download_assets.sh
python 01_scene/06_lighting.py  # With viser installed.

Source: examples/01_scene/06_lighting.py

Lighting and shadows

Code

  1import time
  2from pathlib import Path
  3
  4import numpy as np
  5import trimesh
  6
  7import viser
  8import viser.transforms as tf
  9
 10
 11def main() -> None:
 12    # Load mesh.
 13    mesh = trimesh.load_mesh(str(Path(__file__).parent / "../assets/dragon.obj"))
 14    assert isinstance(mesh, trimesh.Trimesh)
 15    mesh.apply_scale(0.05)
 16    vertices = mesh.vertices
 17    faces = mesh.faces
 18    print(f"Loaded mesh with {vertices.shape} vertices, {faces.shape} faces")
 19    print(mesh)
 20
 21    # Start Viser server with mesh.
 22    server = viser.ViserServer()
 23
 24    server.scene.add_mesh_simple(
 25        name="/simple",
 26        vertices=vertices,
 27        faces=faces,
 28        wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz,
 29        position=(0.0, 2.0, 0.0),
 30    )
 31    server.scene.add_mesh_trimesh(
 32        name="/trimesh",
 33        mesh=mesh,
 34        wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz,
 35        position=(0.0, -2.0, 0.0),
 36    )
 37    grid = server.scene.add_grid(
 38        "grid",
 39        width=20.0,
 40        height=20.0,
 41        position=np.array([0.0, 0.0, -2.0]),
 42    )
 43
 44    # adding controls to custom lights in the scene
 45    server.scene.add_transform_controls(
 46        "/control0", position=(0.0, 10.0, 5.0), scale=2.0
 47    )
 48    server.scene.add_label("/control0/label", "Directional")
 49    server.scene.add_transform_controls(
 50        "/control1", position=(0.0, -5.0, 5.0), scale=2.0
 51    )
 52    server.scene.add_label("/control1/label", "Point")
 53    server.scene.add_transform_controls(
 54        "/control2", position=(5.0, 0.0, 5.0), scale=2.0
 55    )
 56    server.scene.add_label("/control2/label", "Spot")
 57
 58    directional_light = server.scene.add_light_directional(
 59        name="/control0/directional_light",
 60        color=(186, 219, 173),
 61        cast_shadow=True,
 62    )
 63    point_light = server.scene.add_light_point(
 64        name="/control1/point_light",
 65        color=(192, 255, 238),
 66        intensity=30.0,
 67        cast_shadow=True,
 68    )
 69    spot_light = server.scene.add_light_spot(
 70        name="/control2/spot_light",
 71        color=(255, 200, 150),
 72        intensity=80.0,
 73        distance=15.0,
 74        angle=np.pi / 6,
 75        penumbra=0.4,
 76        cast_shadow=True,
 77        # direction is in the light's local frame; rotating the
 78        # transform control will rotate the cone accordingly.
 79        direction=(0.0, 0.0, -1.0),
 80    )
 81
 82    with server.gui.add_folder("Grid Shadows"):
 83        # Create grid shadows toggle
 84        grid_shadows = server.gui.add_slider(
 85            "Intensity",
 86            min=0.0,
 87            max=1.0,
 88            step=0.01,
 89            initial_value=grid.shadow_opacity,
 90        )
 91
 92        @grid_shadows.on_update
 93        def _(_) -> None:
 94            grid.shadow_opacity = grid_shadows.value
 95
 96    # Create default light toggle.
 97    gui_default_lights = server.gui.add_checkbox("Default lights", initial_value=True)
 98    gui_default_shadows = server.gui.add_checkbox(
 99        "Default shadows", initial_value=False
100    )
101
102    gui_default_lights.on_update(
103        lambda _: server.scene.configure_default_lights(
104            gui_default_lights.value, gui_default_shadows.value
105        )
106    )
107    gui_default_shadows.on_update(
108        lambda _: server.scene.configure_default_lights(
109            gui_default_lights.value, gui_default_shadows.value
110        )
111    )
112
113    # Create light control inputs.
114    with server.gui.add_folder("Directional light"):
115        gui_directional_color = server.gui.add_rgb(
116            "Color", initial_value=directional_light.color
117        )
118        gui_directional_intensity = server.gui.add_slider(
119            "Intensity",
120            min=0.0,
121            max=20.0,
122            step=0.01,
123            initial_value=directional_light.intensity,
124        )
125        gui_directional_shadows = server.gui.add_checkbox("Shadows", True)
126
127        @gui_directional_color.on_update
128        def _(_) -> None:
129            directional_light.color = gui_directional_color.value
130
131        @gui_directional_intensity.on_update
132        def _(_) -> None:
133            directional_light.intensity = gui_directional_intensity.value
134
135        @gui_directional_shadows.on_update
136        def _(_) -> None:
137            directional_light.cast_shadow = gui_directional_shadows.value
138
139    with server.gui.add_folder("Point light"):
140        gui_point_color = server.gui.add_rgb("Color", initial_value=point_light.color)
141        gui_point_intensity = server.gui.add_slider(
142            "Intensity",
143            min=0.0,
144            max=200.0,
145            step=0.01,
146            initial_value=point_light.intensity,
147        )
148        gui_point_shadows = server.gui.add_checkbox("Shadows", True)
149
150        @gui_point_color.on_update
151        def _(_) -> None:
152            point_light.color = gui_point_color.value
153
154        @gui_point_intensity.on_update
155        def _(_) -> None:
156            point_light.intensity = gui_point_intensity.value
157
158        @gui_point_shadows.on_update
159        def _(_) -> None:
160            point_light.cast_shadow = gui_point_shadows.value
161
162    with server.gui.add_folder("Spot light"):
163        gui_spot_color = server.gui.add_rgb("Color", initial_value=spot_light.color)
164        gui_spot_intensity = server.gui.add_slider(
165            "Intensity",
166            min=0.0,
167            max=200.0,
168            step=1.0,
169            initial_value=spot_light.intensity,
170        )
171        gui_spot_angle = server.gui.add_slider(
172            "Cone angle (deg)",
173            min=5.0,
174            max=89.0,
175            step=1.0,
176            initial_value=np.rad2deg(spot_light.angle),
177        )
178        gui_spot_penumbra = server.gui.add_slider(
179            "Penumbra",
180            min=0.0,
181            max=1.0,
182            step=0.01,
183            initial_value=spot_light.penumbra,
184        )
185        gui_spot_shadows = server.gui.add_checkbox("Shadows", True)
186
187        @gui_spot_color.on_update
188        def _(_) -> None:
189            spot_light.color = gui_spot_color.value
190
191        @gui_spot_intensity.on_update
192        def _(_) -> None:
193            spot_light.intensity = gui_spot_intensity.value
194
195        @gui_spot_angle.on_update
196        def _(_) -> None:
197            spot_light.angle = np.deg2rad(gui_spot_angle.value)
198
199        @gui_spot_penumbra.on_update
200        def _(_) -> None:
201            spot_light.penumbra = gui_spot_penumbra.value
202
203        @gui_spot_shadows.on_update
204        def _(_) -> None:
205            spot_light.cast_shadow = gui_spot_shadows.value
206
207    # Create GUI elements for controlling environment map.
208    with server.gui.add_folder("Environment map"):
209        gui_env_preset = server.gui.add_dropdown(
210            "Preset",
211            (
212                "None",
213                "apartment",
214                "city",
215                "dawn",
216                "forest",
217                "lobby",
218                "night",
219                "park",
220                "studio",
221                "sunset",
222                "warehouse",
223            ),
224            initial_value="city",
225        )
226        gui_background = server.gui.add_checkbox("Background", False)
227        gui_bg_blurriness = server.gui.add_slider(
228            "Bg Blurriness",
229            min=0.0,
230            max=1.0,
231            step=0.01,
232            initial_value=0.0,
233        )
234        gui_bg_intensity = server.gui.add_slider(
235            "Bg Intensity",
236            min=0.0,
237            max=1.0,
238            step=0.01,
239            initial_value=1.0,
240        )
241        gui_env_intensity = server.gui.add_slider(
242            "Env Intensity",
243            min=0.0,
244            max=1.0,
245            step=0.01,
246            initial_value=0.3,
247        )
248
249    def update_environment_map(_) -> None:
250        server.scene.configure_environment_map(
251            gui_env_preset.value if gui_env_preset.value != "None" else None,
252            background=gui_background.value,
253            background_blurriness=gui_bg_blurriness.value,
254            background_intensity=gui_bg_intensity.value,
255            environment_intensity=gui_env_intensity.value,
256        )
257
258    update_environment_map(None)
259    gui_env_preset.on_update(update_environment_map)
260    gui_background.on_update(update_environment_map)
261    gui_bg_blurriness.on_update(update_environment_map)
262    gui_bg_intensity.on_update(update_environment_map)
263    gui_env_intensity.on_update(update_environment_map)
264
265    while True:
266        time.sleep(10.0)
267
268
269if __name__ == "__main__":
270    main()