SimulatorΒΆ

A slightly more complex example does not render static frames but instead uses the physics engine to run a simulation, and then renders the corresponding video. Let us look at examples/simulator.py (full source at the bottom of this page).

As we are generating a video, we also specify the framerate properties of renderer and simulator, how many frames to render, and attach a simulator to the scene:

scene = kb.Scene(resolution=(256, 256))
scene.frame_end = 48   #< numbers of frames to render
scene.frame_rate = 24  #< rendering framerate
scene.step_rate = 240  #< simulation framerate
simulator = KubricSimulator(scene)
renderer = KubricRenderer(scene, scratch_dir="./output")

And notice that when we specify the floor, the static=True argument ensures that the floor remains fixed during the simulation:

scene += kb.Cube(scale=(1, 1, 0.1), position=(0, 0, -0.1), static=True)
scene += kb.DirectionalLight(position=(-1, -0.5, 3), look_at=(0, 0, 0), intensity=1.5)
scene.camera = kb.PerspectiveCamera(position=(2, -0.5, 4), look_at=(0, 0, 0))

Let us add a couple of colorful balls (Sphere primitives) that bounce around; we use rng.uniform(low, high) to ensures that each ball is initialized at its own random random position:

spawn_region = [[-1, -1, 0], [1, 1, 1]]
rng = np.random.default_rng()
for i in range(8):
  position = rng.uniform(*spawn_region)
  velocity = rng.uniform([-1, -1, 0], [1, 1, 0])
  material = kb.PrincipledBSDFMaterial(color=kb.random_hue_color(rng=rng))
  sphere = kb.Sphere(scale=0.1, position=position, velocity=velocity, material=material)
  scene += sphere
  kb.move_until_no_overlap(sphere, simulator, spawn_region=spawn_region)

To color them, we use the PrincipledBSDFMaterial. This material is very versatile and can represent a wide range of materials including plastic, rubber, metal, wax, and glass (see e.g. these examples from the blender documentation).

The utility function move_until_no_overlap() jitters the objects position (and rotation) until the simulator no longer detects any collisions.

Now that we have all the objects in place, it is time to run the simulation. Once the simulation terminates (pybullet), the simulated object states are saved as keyframes within the renderer (blender).

simulator.run()

The gif below is generated via the convert tool from the ImageMagick package:

convert -delay 8 -loop 0 output/images/frame_*.png output/simulator.gif
../_images/simulator.gif
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import logging
import numpy as np
import kubric as kb
from kubric.renderer.blender import Blender as KubricBlender
from kubric.simulator.pybullet import PyBullet as KubricSimulator

logging.basicConfig(level="INFO")  # < CRITICAL, ERROR, WARNING, INFO, DEBUG

# --- create scene and attach a renderer and simulator
scene = kb.Scene(resolution=(256, 256))
scene.frame_end = 48   # < numbers of frames to render
scene.frame_rate = 24  # < rendering framerate
scene.step_rate = 240  # < simulation framerate
renderer = KubricBlender(scene)
simulator = KubricSimulator(scene)

# --- populate the scene with objects, lights, cameras
scene += kb.Cube(name="floor", scale=(3, 3, 0.1), position=(0, 0, -0.1), static=True, background=True)
scene += kb.DirectionalLight(name="sun", position=(-1, -0.5, 3), look_at=(0, 0, 0), intensity=1.5)
scene.camera = kb.PerspectiveCamera(name="camera", position=(2, -0.5, 4), look_at=(0, 0, 0))

# --- generates spheres randomly within a spawn region
spawn_region = [[-1, -1, 0], [1, 1, 1]]
rng = np.random.default_rng()
for i in range(8):
  velocity = rng.uniform([-1, -1, 0], [1, 1, 0])
  material = kb.PrincipledBSDFMaterial(color=kb.random_hue_color(rng=rng))
  sphere = kb.Sphere(scale=0.1, velocity=velocity, material=material)
  scene += sphere
  kb.move_until_no_overlap(sphere, simulator, spawn_region=spawn_region)

# --- executes the simulation (and store keyframes)
simulator.run()

# --- renders the output
kb.as_path("output").mkdir(exist_ok=True)
renderer.save_state("output/simulator.blend")
frames_dict = renderer.render()
kb.write_image_dict(frames_dict, "output")