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.