Stage 4: Rendering#
Execution records what the robot and the blocks did, but it records no camera images. This stage produces them, by replaying each recorded episode frame by frame: the robot and objects are placed at their recorded poses, with no physics stepping and no replanning, and each frame is rendered.
A render is described by a flags YAML: how the scene is set up and how it is drawn. The
file is recorded in every problem’s render_params.json, so a dataset always carries the look
it was made with. There is no default, so the look is always named.
The simplest one is
render_flags_plain.yaml,
which ships with the repo. Every key in it is fixed, so each episode renders identically:
carb_settings: {...} # RTX path tracing
env_spacing: 500 # how far apart the envs sit
texture_cache: {...} # where decoded textures are kept
hide_objects: [backdrop] # the backdrop geometry is hidden...
skybox: # ...because this sky replaces it
names: [abandoned_bakery_2k]
rotation_deg: 0
intensity: 800
visible_background: true
Render a pass with it:
--render-pass src/toyblocks/domain_randomization/flags/render_flags_plain.yaml
Add sampled sections to that file and the same renderer produces a different look per episode: see Domain randomization. Ask for more than one look in a run with render passes.
The renderer is also a plain script you can run on a single GPU, either on a dataset that has no images yet or on one that does, replacing them. To run it as part of a pipeline run, see Through the pipeline and Running the pipeline locally.
Why the pixels are a separate stage#
Keeping rendering apart from execution has several benefits:
One expensive planning-and-physics run can be rendered many times with different visuals, for example N copies per episode or separate train and test letter sets, without re-planning.
Rendering can run later, or on a different GPU, separately from data generation.
The render settings sampled for each episode are recorded, so evaluation can reproduce an episode’s look.
Because execution does not spawn cameras by default, rendering is not an optional extra for a dataset with video: it is where the video comes from.
What rendering does not change#
The state and action trajectory is identical across renders, and only the pixels and the camera columns differ. Rendering cannot fix a bad episode, change what the robot did, or alter the success labels.
Rendered pixels match an original in-physics rendering closely but not exactly, because driver and scene-version differences remain. See DETERMINISM.md for exactly what is bit-reproducible and what is not.
Where it fits#
Input: the recorded datasets from stage 3, execution, the env packages they were recorded with, and a flags YAML describing the look.
Output: a rendered copy of each dataset, with camera videos, read by merge.
The stage is optional in the pipeline, and --render-pass is the only flag that turns it on.
It is the only way to get camera videos without recording them during execution.
Running it#
The quickest way to try it is to render a handful of episodes from an existing dataset on your own GPU. This is also the best way to preview a flags YAML before committing to a large run:
python scripts/render/render.py <dataset_dir> \
--env_file <env_package>/env.py \
--output <out_dir> \
--max_episodes 20 --num_envs 8 \
--flags src/toyblocks/domain_randomization/flags/render_flags_allrand_texcache_glyphs_trainsplit_symmetric.yaml
For an unmerged dataset, with one directory per problem, drop --env_file. The env is then
matched automatically per problem from <datasets_dir>/envs/<problem_name>/env.py. Use
--envs_dir when the env packages live somewhere other than next to the data. For a pipeline run
that is the experiment’s own envs/ directory:
python scripts/render/render.py data/datasets/unmerged/<experiment-id>/execution \
--envs_dir data/datasets/unmerged/<experiment-id>/envs \
--output <out_dir> --max_episodes 20 \
--flags src/toyblocks/domain_randomization/flags/render_flags_allrand_texcache.yaml
render.py often hangs during Isaac teardown after it finishes. See
Common pitfalls for how to run it safely.
Through the pipeline#
Add a --render-pass to a pipeline run. Each problem is rendered as soon as its execution
finishes, and the merge then uses the rendered episodes instead of the execution data:
python scripts/datagen/run_datagen_pipeline.py \
--render-pass src/toyblocks/domain_randomization/flags/render_flags_allrand_texcache_glyphs_trainsplit_symmetric.yaml \
--experiment-id my_run --output-root data \
--task-gen-script scripts/task_gen/flip.py --task-gen-count 4 \
--vecgen-problems-per-env 1 --exec-num-envs 8 --vecgen-episodes 16 \
--local-gpu-workers 2
Repeat --render-pass for more looks, and give each one a :N for several copies of every
episode. See Render passes for the rules and
Running the pipeline locally for how those renders are scheduled.
Every pass names a YAML, and the pipeline records that file in the problem’s
render_params.json, so a dataset always says which look it was made with. With no other render
flags the pass above renders render_flags_allrand_texcache_glyphs_trainsplit_symmetric.yaml
once per episode, with left_camera skipped, failure episodes skipped, and the 224-px RGB and
rg8_55_224 depth sidecars
(--extra_rgb 224 --extra_depth rg8_55_224 --extra_rgb_GOP 2 --rgb_GOP 2). Override most
render.py options with --render-extra, in the = form:
--render-extra="--extra_depth g12_224"
--render-schedule chooses how renders are scheduled against the executions. coupled, the
default, runs each render inside that problem’s execution job. pipelined queues it in the
shared GPU-slot pool so it can overlap the next execution. Both run the same renderer with the
same arguments and produce identical pixels, so this is a throughput and observability choice.
Running the pipeline locally has the measured difference, and
what counts as “rendered” is the gate that decides what the
merge ships.
The allrand flags YAMLs draw HDRI backdrops from the polyhaven and indoor-2018 pools.
Where to get each, and what happens when one is missing, is in
HDRI backdrops.
The pipeline flags for this stage are:
Flag |
Default |
Description |
|---|---|---|
|
none |
Add one render pass with this flags YAML, optionally |
|
|
|
|
|
Cameras not to render. Pass the flag with no names to render every camera. |
|
on |
Render only successful episodes. The merge keeps only those anyway. |
|
none |
Extra |
|
8 |
Envs per render wave. Must be at least |
|
3600 |
Kill a render that prints nothing for this many seconds. |
|
off |
Fail a problem whose encoder could not publish every video, instead of finalizing it as |
Every flag above except --render-pass applies to every pass, so one run cannot give two
passes different cameras, sidecars or schedules. Only the YAML and the copy count vary per pass,
and both live in the --render-pass value itself.
A render cannot be combined with --vecgen-record-videos.
Output#
Every rendered split gets a meta/render_params.json describing how it was rendered. Its
top-level keys are:
Key |
Meaning |
|---|---|
|
When the render ran, and which script ran it. |
|
Whether the render reached a final state, not whether it succeeded. See Common pitfalls. |
|
|
|
The exact |
|
Why a finished render recorded nothing, when it recorded nothing. |
|
The number of episodes in this split. |
|
The problem’s source episode count, across all splits. With N copies per episode the splits together hold N times this many. |
|
|
|
What was rendered, and from where. |
|
The run’s own arguments: the flags file, |
|
The randomization, the digest of the literal file, and the digest of the resolved dict. Group shards by the resolved digest. |
|
Which seeding mode produced these values. A consumer must branch on it. |
|
The scene as actually built: cameras, materials and lights. |
|
Per-episode records, including |
|
Present only for a |
completed and video_defects together give three states, so a gap in the output is neither
silently accepted nor treated as a total failure:
|
|
Meaning |
|---|---|---|
|
|
Terminal with no known holes. Check |
|
non-empty |
Usable but incomplete. The listed videos are missing and need a patch pass. |
|
any |
The render did not finish. This is also the live state of a render still running, because |
Validating the output#
Metadata alone cannot prove a render is good, so check the videos themselves:
python scripts/render/validate_dataset_videos.py <out_dir> --frames
Without --frames only the structural check runs, which catches an unopenable file but passes a
short one. Either way it can only inspect videos that exist, so a video that was never written is
caught by video_defects instead.
Domain randomization#
The plain YAML above fixes every value. Replace any of them with a set or a range and the renderer samples instead, giving each episode its own look from the same file:
skybox:
set: polyhaven/rooms # a pool of skies, not one name
rotation_deg: [0, 360] # a range, not 0
intensity: [500, 1000]
materials: {...} # table and block surfaces
scene_lights: {...} # fill, local lights, the sun
env_sphere: {...} # the visible backdrop sphere
randomize: {...} # camera pose and field of view
randomize_task_letters: {...} # the glyph on each block
randomize_block_colors: {...} # cuboid colours
--render-pass src/toyblocks/domain_randomization/flags/render_flags_allrand_texcache.yaml
Nothing else changes: same renderer, same stage, same output layout. The whole file is recorded
in every problem’s render_params.json as the dataset’s visual provenance.
Five flags YAMLs ship in src/toyblocks/domain_randomization/flags/:
File |
Use |
|---|---|
One fixed look, nothing sampled. The plain baseline. |
|
|
All-random lighting, materials and cameras, plus the train glyph split (symmetry-preserving) and per-env block colours. |
All-random lighting, materials and cameras, without glyph or colour swaps. |
|
A small camera preset for quick tests. It randomizes nothing. See Common pitfalls. |
|
Camera randomization only. |
The two allrand files sample HDRI backdrops from the polyhaven and indoor-2018 pools. See
HDRI backdrops.
To write your own, see the YAML reference. It covers every top-level key, the value forms the resolver accepts, and, importantly, when each value is drawn: per wave, per episode or per env.
Render passes#
One run can produce several looks of the same episodes. Each pass is one YAML, rendered over
every source episode into its own job_<name>/ directory, where <name> is the YAML’s file name
with any render_flags_ prefix stripped. The merge takes them all:
--render-pass src/toyblocks/domain_randomization/flags/render_flags_plain.yaml
--render-pass src/toyblocks/domain_randomization/flags/render_flags_allrand_texcache.yaml:3
That renders the plain look once into job_plain and the randomized look three times into
job_allrand_texcache, so the merged dataset is 4x the source episodes. The :N suffix is per
pass, and without it a pass renders once.
Passes are keyed by that job_<name>, so two YAMLs whose names reduce to the same one are not
both rendered. The run keeps the first and warns. The merge also reads every job_* directory
under render/out/, not only the passes this command asked for, so a directory left by an
earlier run of the same experiment is merged again. Delete it, or use a fresh --experiment-id.
Each pass is a separate renderer invocation rather than extra -N iterations within one,
because a process applies one flags dict before the sim is built, and the per-wave levers (dome,
skybox, exposure) are scene-global, so a differently-flagged episode sharing a wave would still
be lit by that wave’s draw. Separate job directories also mean each pass resumes independently,
and a problem counts as rendered only when every pass has landed. Otherwise a failed pass
would contribute 3 copies where 4 were promised and nothing would report it.
A plain pass keeps the scene and drops the draws. The point of rendering one beside
randomized passes is that they are comparable: same backdrop, same renderer, only the sampling
gone. That is why it takes a YAML rather than rendering with no flags at all. Dropping the YAML
would bring the backdrop geometry back and change the render mode, giving a different scene
rather than the same one unrandomized. render_flags_plain.yaml is that baseline, and any YAML of your own can
take its place.
N copies within one pass#
Often you want several visual variants of the same trajectory, for example to make a policy
robust to lighting. A pass written FLAGS.yaml:N replays each source episode N times, each time
with freshly sampled randomization. The state and action trajectory is identical across the
copies and only the pixels and camera columns differ, so the pass has N times the source’s
episodes and frames. Each output episode records the src_episode_index it came from, and its
iter when there is more than one copy.
Running render.py by hand, the same multiplier is -N / --renders_per_episode.
A plain pass beside a randomized one makes the merged dataset (N+1) times the source:
--render-pass src/toyblocks/domain_randomization/flags/render_flags_allrand_texcache_glyphs_trainsplit_symmetric.yaml:3 \
--render-pass src/toyblocks/domain_randomization/flags/render_flags_plain.yaml
That gives job_allrand_texcache_glyphs_trainsplit_symmetric with 3 copies per episode and
job_plain with 1, so the merge ships 4x the source episodes.
Reproducing a recorded look#
Because each episode’s sampled settings are recorded, a render can be reproduced later:
python scripts/render/render_from_params.py --help
Pass the dr_seeding mode recorded in the dataset’s own render_params.json.
Flags#
These are render.py’s own options. datasets_dir is positional and --output is required,
and everything else has a default. Isaac Lab’s AppLauncher adds its own on top, such as
--device, --kit_args and --rendering_mode. --headless and --enable_cameras are forced
on after parsing, so passing them changes nothing.
Source and output#
Flag |
Default |
Meaning |
|---|---|---|
|
(positional, required) |
The root containing the recorded dataset or datasets. |
|
(required) |
The output directory. It mirrors the structure of the input. |
|
|
The env package’s |
|
|
A root holding per-problem env packages as |
|
|
Extra asset search roots for HDRIs and skyboxes, searched in order before the repo default ( |
|
off |
Replace the recorder’s re-derived |
Episode selection#
Flag |
Default |
Meaning |
|---|---|---|
|
all |
The maximum number of episodes rendered per source dataset. |
|
off |
Render only |
|
|
Render only the listed episodes, for a targeted patch. Output episodes are renumbered per split, but each records its |
|
|
Select a fragment for parallel rendering: the global slice |
|
on |
Keep the full per-problem episode list inside a fragment, so render-uids, wave grouping and randomization draws match a single-process run. Reassembling the fragments then gives a merge that is byte-identical to one big run. |
|
off |
Render each member of a vecgen |
Cameras and video streams#
Flag |
Default |
Meaning |
|---|---|---|
|
|
Cameras not to spawn. They are dropped before the scene is created, so both their RGB and depth are absent and their render cost is never paid. Names work with or without the |
|
off |
Do not render or record the main depth stream. Cannot be combined with |
|
|
How the main depth is stored. |
|
|
|
|
|
The keyframe interval for the main depth. |
|
|
The keyframe interval for the main RGB. |
|
|
Also write a 224-px RGB video per camera, in the training sidecar layout. It is resized with Pillow BILINEAR, and ffmpeg |
|
|
Also write a 224-px depth video per camera: |
|
|
The keyframe interval for the |
|
|
Scale the preset cameras’ resolution while keeping the field of view. Requires |
|
|
Where each camera’s starting pose comes from before randomization. |
|
off |
(Experimental.) Override extrinsics and intrinsics with the recorded values. Camera overrides in the flags YAML always take precedence. |
Randomization and seeding#
Flag |
Default |
Meaning |
|---|---|---|
|
|
The flags YAML. |
|
|
The number of renders per source episode. See render passes. |
|
|
How per-episode draws are seeded: |
|
off |
Reset every writable material key to the asset’s own value before each draw. Without it, a key the chosen material leaves out keeps the previous episode’s value, so materials bleed across episodes. Off by default because it changes the rendered output. |
|
off |
Turn on RTX global illumination and reflections. Enabled automatically when the YAML has a |
The seeding modes differ in how an episode’s values are derived. stream seeds one RNG over the
whole episode list, so an episode’s values depend on how many episodes come before it. indexed
and hash derive each episode independently, which is what lets a patch jump straight to a
missing episode.
Performance and rendering#
Flag |
Default |
Meaning |
|---|---|---|
|
|
The number of parallel envs, which is also the replay batch size. Use a multiple of 8, or fewer than 8. |
|
|
Encoder processes per render process. Raise it when the encoder, not the GPU, is the bottleneck. |
|
|
Group episodes of similar length into a wave (a batch of episodes rendered together), within blocks of this many waves. A wave renders every env until its longest episode ends, so rendering in source order wasted about 36% of rendered env-frames on measured runs. |
|
on |
At each problem boundary, make the live scene match that problem’s block set. Costs nothing when consecutive problems share a set. |
|
|
Extra RTX passes after a scene swap, so the denoiser can converge. |
|
|
Extra render passes at |
|
preset (DLSS) |
|
|
off |
A shortcut for |
|
off |
Skip the physics settle at construction time. Safe, because the replay overrides all state on every frame. |
|
off |
Run one physics step per frame instead of rendering only. This removes occasional duplicate frames at the cost of slight drift. The default is to place everything and render, with no physics. |
|
|
Override the scene’s env spacing. Larger spacing stops a neighbouring env’s backdrop from leaking into a camera’s peripheral view when a skybox is used. A YAML |
Resume, integrity and debugging#
Flag |
Default |
Meaning |
|---|---|---|
|
off |
Skip problems whose output already has |
|
off |
Mark a problem incomplete, forcing a full render, if any video could not be published. Off by default: the problem stays complete with |
|
on |
Copy every column from the matching source episode, so the two datasets differ only where the render is authoritative. |
|
on |
Copy the source’s |
|
off |
Print the stage layout and exit. |
|
|
Write the applied material values, live carb settings and render products to JSON after the first wave, then continue. |
|
off |
Keep debug visualization markers in the render. Hidden by default. |
Common pitfalls#
render.py hangs after it finishes#
render.py prints all jobs done and then often hangs in Isaac teardown. Run it through
scripts/render/run_local_render.py, which watches for that marker and kills the process group
30 s later, as the pipeline always does. Never judge success from the exit status. Judge it from
the marker and the result on disk.
python scripts/render/run_local_render.py -- python scripts/render/render.py <dataset_dir> ...
completed: true does not mean the render produced data#
completed: true means the render reached a final state, not that it has data. It is written
so that a re-run does not retry the problem forever, so it is also written when the recorded block
size is not in the problem’s size table, or when every source parquet is unreadable. Those cases
write a render_params.json but no info.json, and info.json is what merge actually needs.
Note also that there is no total_episodes key, because the problem-wide count is
episodes_total.
A broken render can still look complete#
Metadata cannot see a broken render. A host-RAM out-of-memory error during encoding produces
videos that fail with moov atom not found, inside a dataset whose render_params.json still
says completed: true. Always run validate_dataset_videos.py, shown under Output,
before using a rendered dataset.
render_flags_camtest.yaml produces identical copies#
render_flags_camtest.yaml has no random section at all. A :4 pass with it produces four
copies of the same look, identical in depth and differing in RGB only by renderer noise, while
every size-based check still reports 4x. Use it only to test the camera rig, never to check that
randomization works.
--antialiasing has no effect under the shipped path-tracing files#
render_flags_plain.yaml and both allrand files select RTX Real-Time 2.0 path tracing through
their carb_settings, and that renderer does not let antialiasing be turned off.
--antialiasing and --disable_dlss set the simulation’s antialiasing mode, which the YAML’s
render mode then overrides, so the log prints the mode you asked for and the pixels do not
change. Verify any antialiasing change with a pixel difference, never with the log line. To get
a genuinely different mode, change the render mode in the YAML.
Mixing seeding modes in one dataset#
Never mix two --dr_seeding modes within one dataset, because each produces different
values. To reproduce or patch an existing dataset, pass the mode recorded in its
meta/render_params.json.
Two renders into the same output delete each other’s work#
render.py deletes a non-finalized <output>/<problem> before rendering it, on every run
and not only under --resume, because partial output can never be appended to safely. A
finalized directory is left alone. So two renders pointed at the same problem directory delete
each other’s work rather than failing. Never run two renders against the same output problem
directory.
Checking a multiplier by file size gives the wrong answer#
To confirm that :N really multiplied the dataset, count episodes, frames and distinct video
files, never bytes. Merge hardlinks videos into the merged tree, so du of the merged
directory is meaningless, and different draws compress differently (measured at 2.055x for N=2,
not 2x).
The failure a size check cannot catch is N copies that are pixel-identical because nothing was
randomized. Check that the copies actually differ: there should be distinct
observation.camera_pose.* values per episode under camera randomization, and differing
depth frames. Depth is deterministic and RGB is not, so RGB can prove a change but never its
absence.
Next#
Merge the rendered problems into one training dataset. Once this stage is marked
complete, the merge reads render/out/job_* and nothing else, so a problem that was never
rendered simply will not appear. See
what counts as “rendered” for how the local flow guards
against that.
See also#
Running the pipeline locally: the local pipeline, the render schedules, HDRI backdrops, and the gate that decides what gets merged
Execution: the stage that produces the trajectories this stage replays
Merge: how rendered problems become one training dataset
YAML reference: every randomization key
DETERMINISM.md: what is reproducible, and what is not