Rendering API

Basic Example

Rendering an animation with a single blender instance can be done like so:

from functools import partial
from pathlib import Path

from rich.progress import Progress

from visionsim.simulate.blender import BlenderClient

if __name__ == "__main__":
    with BlenderClient.spawn(timeout=30, log="logs") as client, Progress() as progress:
        client.initialize(Path("assets/monkey.blend").resolve(), Path("renders/monkey").resolve())
        client.include_frames()
        task = progress.add_task("Rendering monkey.blend...")
        client.render_animation(update_fn=partial(progress.update, task))

Parallelized Rendering

Making it use multiple instances is as easy as using BlenderClients instead of BlenderClient (notice the s)! Here’s all that needs to change:

from pathlib import Path

from rich.progress import Progress

from visionsim.simulate.blender import BlenderClients

if __name__ == "__main__":
    with BlenderClients.spawn(jobs=2, timeout=30, log="logs") as clients, Progress() as progress:
        clients.initialize(Path("assets/monkey.blend").resolve(), Path("renders/monkey").resolve())
        clients.include_frames()
        task = progress.add_task("Rendering monkey.blend...")
        clients.render_animation(update_fn=partial(progress.update, task))

Warning

In practice, the number of rendering jobs will be limited by the user’s system resources, most likely GPU VRAM. Start small, and increase accordingly.


Render Process Pool

As seen in above using multiple Blender instances to render a single scene is already faster than only using one, but it limits the user to rendering a single scene at a time. For more fine-grained control, you can use BlenderClients.pool, which returns a multiprocessing Pool-like instance which has had it’s applicator methods (map/imap/starmap/etc) monkey-patched to inject a client instance as first argument:

from pathlib import Path

from visionsim.simulate.blender import BlenderClients


def render(client, blend_file):
    root = Path("renders") / Path(blend_file).stem
    client.initialize(Path(blend_file).resolve(), root)
    client.include_frames()
    client.set_resolution((512, 512))
    client.move_keyframes(scale=0.5)
    client.render_animation()


if __name__ == "__main__":
    with BlenderClients.pool(2, log="logs", timeout=30) as pool:
        # Note: The client will be automagically passed to `render` here.
        pool.map(render, ["assets/monkey.blend", "assets/cube.blend", "assets/metaballs.blend"])

This enables much more flexibility, allowing users to render multiple scenes or parts-thereof simultaneously. However, tracking their progress is not as simple as in the previous example as each render is now it’s own process and needs to communicate it’s state to the parent process. This is tricky to do correctly, thankfully, we can use the PoolProgress utility for this:

from pathlib import Path

from visionsim.simulate.blender import BlenderClients
from visionsim.utils.progress import PoolProgress


def render(client, blend_file, tick):
    root = Path("renders") / Path(blend_file).stem
    client.initialize(blend_file, root)
    client.include_frames()
    client.set_resolution((512, 512))
    client.move_keyframes(scale=0.5)
    client.render_animation(update_fn=tick)


if __name__ == "__main__":
    with (
        BlenderClients.pool(2, log="logs", timeout=30) as pool,
        PoolProgress() as progress,
    ):
        for blend_file in ["assets/monkey.blend", "assets/cube.blend", "assets/metaballs.blend"]:
            tick = progress.add_task(f"Rendering {blend_file}...")

            # Note: The client will be automagically passed to `render` here.
            pool.apply_async(render, args=(blend_file, tick))
        progress.wait()
        pool.close()
        pool.join()

To properly track the progress, we queue render jobs with apply_async which gives control back to the main process. Then we call progress.wait(), which blocks until all jobs have completed and updates the progress bar accordingly.

Important

After queuing jobs, you must wait for them to terminate by calling PoolProgress.wait, otherwise the main process will exit and force-kill all child processes.