Randomization Basics
The randomization is defined by four elements: providers, receivers, strategies and randomization groups.
We will cover what are these elements in this introductory case in such manner it will allow us to use
randomization in SkyRenderer.
Agenda:
- Providers and Receivers
- Synchronization
- Custom synchronization
Providers and Receivers
Each asset source used in the rendering scene must have its provider. There are providers for:
textures from files:
- FileTextureProvider
- HDRTextureProvider
substance textures:
- SubstanceTextureProvider
mesh buffers:
- ObjBufferProvider
- AlembicBufferProvider
and others. Parameters are also a source of information, often randomized, so they are also handled by providers.
Each provider can have many receivers for its assets. For example, if there are 10 instances of an object
constructed from Alembic mesh and Substance textures, there will be one Alembic mesh provider,
one Substance texture provider, 10 materials (texture receivers) and 10 geometries (mesh buffers receivers).
Each receiver defines the strategy it uses for randomization and randomization group it's in.
Let's render a simple scene to demonstrate that:
from skyrenderer.cases.utils import RandomizationSceneComposer
scene_composer = RandomizationSceneComposer()
scene_composer.setup_scene()
scene_composer.visualize()
2025-02-04 14:13:54,036 | skyrenderer.scene.renderer_context | INFO: Root paths:
- root path: /dli/skyenvironment/skyrenderer/skyrenderer
- assets path: /dli/mount/assets
- config path: /dli/skyenvironment/skyrenderer/skyrenderer/config
- gpu sources path: /dli/skyenvironment/skyrenderer/skyrenderer/optix_sources/sources
- cache path: /dli/mount/cache
- ptx cache path: compiled_ptx/ptx
- ocio path: ocio_configs/aces_1.2/config.ocio
2025-02-04 14:13:54,705 | skyrenderer.utils.time_measurement | INFO: Setup time: 620 ms
2025-02-04 14:13:54,915 | skyrenderer.utils.time_measurement | INFO: Context update time: 210 ms
2025-02-04 14:13:58,077 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:13:58,078 | skyrenderer.utils.time_measurement | INFO: Render time: 3.16 seconds
We will now create a simple shader parameter provider.
from skyrenderer.basic_types.procedure import PBRShader
pbr_provider = PBRShader.create_parameter_provider(scene_composer.renderer_context, base_color=(1, 0, 0))
There are 7 spheres in each row and column and their names have the following structure: "sphere{row}{col}".
We want to assign the Material of one of them to be a receiver of newly created provider. Firstly, we have
to create an instance of MaterialDefinition, and then we can assign it to the proper sphere.
from skyrenderer.scene.scene_layout.layout_elements_definitions import MaterialDefinition
red_material = MaterialDefinition(scene_composer.renderer_context, parameter_set=pbr_provider)
scene_composer.renderer_context.set_material_definition("sphere_0_0", red_material)
Let's see the result of the render first.
scene_composer.visualize()
2025-02-04 14:13:58,517 | skyrenderer.utils.time_measurement | INFO: Setup time: 176 ms
2025-02-04 14:13:58,704 | skyrenderer.utils.time_measurement | INFO: Context update time: 187 ms
2025-02-04 14:14:00,988 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:00,990 | skyrenderer.utils.time_measurement | INFO: Render time: 2.28 seconds
To avoid verbosity from now on, we will use a custom method of scene composer called color_spheres() which wraps
above steps into a single line. So far there was no randomization involved. We will change that now.
Let's start by changing the red color to a random one. For this task we can use FloatInput.
Input is a special type used for creating random values.
from skyrenderer.basic_types.provider.provider_inputs import FloatInput
random_color = FloatInput(dims=3)
pbr_provider = PBRShader.create_parameter_provider(scene_composer.renderer_context, base_color=random_color)
sphere_name = "sphere_0_1"
material = MaterialDefinition(scene_composer.renderer_context, parameter_set=pbr_provider)
scene_composer.renderer_context.set_material_definition(sphere_name, material)
scene_composer.visualize()
2025-02-04 14:14:01,391 | skyrenderer.utils.time_measurement | INFO: Setup time: 164 ms
2025-02-04 14:14:01,603 | skyrenderer.utils.time_measurement | INFO: Context update time: 211 ms
2025-02-04 14:14:04,435 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:04,436 | skyrenderer.utils.time_measurement | INFO: Render time: 2.83 seconds
To make sure that it is indeed random let's assign random color to every sphere on the scene.
scene_composer.color_spheres(color=random_color)
scene_composer.visualize()
2025-02-04 14:14:04,828 | skyrenderer.utils.time_measurement | INFO: Setup time: 164 ms
2025-02-04 14:14:05,018 | skyrenderer.utils.time_measurement | INFO: Context update time: 189 ms
2025-02-04 14:14:07,234 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:07,235 | skyrenderer.utils.time_measurement | INFO: Render time: 2.22 seconds
Synchronization
To synchronize colors between spheres, we need to use randomization groups and randomization strategies.
By default, all the spheres are in the same group, so group synchronization will bind them all together.
That's why in the next example we don't have to set randomization groups and we can use a default one.
As for the strategies, there are quite a few predefined synchronization strategies to use out of the box:
- EQUAL
- EQUAL_IN_GROUP
- DISTINCT_EQUAL_GROUPS
- UNIQUE
- UNIQUE_IN_GROUP
- UNRESTRICTED
import skyrenderer.randomization.strategy as st
synchronization = st.SynchronizationDescription(in_strategy=st.Synchronization.EQUAL_IN_GROUP)
input_strategy = st.InputDrawingStrategy(
drawing_rule=st.DrawingRule.RANDOM, distribution=st.UniformDistribution(), synchronization=synchronization
)
strategy = st.DrawingStrategy(scene_composer.renderer_context, inputs_strategies={"base_color": input_strategy})
scene_composer.color_spheres(color=random_color, strategy=strategy)
scene_composer.visualize()
2025-02-04 14:14:07,632 | skyrenderer.utils.time_measurement | INFO: Setup time: 156 ms
2025-02-04 14:14:07,818 | skyrenderer.utils.time_measurement | INFO: Context update time: 185 ms
2025-02-04 14:14:10,755 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:10,756 | skyrenderer.utils.time_measurement | INFO: Render time: 2.94 seconds
And the same with a helper.
strategy = st.DrawingStrategy(
scene_composer.renderer_context, inputs_strategies={"base_color": st.EqualInGroupInput()}
)
scene_composer.color_spheres(color=random_color, strategy=strategy)
Now lets put each row to a different synchronization group.
groups = {(i, j): f"g{i}" for (i, j) in scene_composer.all_sphere_indices}
scene_composer.color_spheres(color=random_color, strategy=strategy, groups=groups)
scene_composer.visualize()
2025-02-04 14:14:11,218 | skyrenderer.utils.time_measurement | INFO: Setup time: 238 ms
2025-02-04 14:14:11,402 | skyrenderer.utils.time_measurement | INFO: Context update time: 183 ms
2025-02-04 14:14:13,603 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:13,604 | skyrenderer.utils.time_measurement | INFO: Render time: 2.20 seconds
Now with the same group assignment we can synchronize inputs for the whole strategy.
All the spheres use the same strategy, so they are all the same.
strategy = st.DrawingStrategy(scene_composer.renderer_context, default_input_strategy=st.EqualInStrategyInput())
scene_composer.color_spheres(color=random_color, strategy=strategy, groups=groups)
scene_composer.visualize()
2025-02-04 14:14:14,026 | skyrenderer.utils.time_measurement | INFO: Setup time: 198 ms
2025-02-04 14:14:14,241 | skyrenderer.utils.time_measurement | INFO: Context update time: 215 ms
2025-02-04 14:14:17,264 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:17,265 | skyrenderer.utils.time_measurement | INFO: Render time: 3.02 seconds
Now we can start playing with the shapes. With 7 possible radii and 7 rows, the difference will be visible.
Let's see the default, randomized strategy.
random_radius = FloatInput(min_value=0.1, max_value=3, number_of_steps=7)
scene_composer.reshape_spheres(radius=random_radius)
scene_composer.visualize()
2025-02-04 14:14:17,614 | skyrenderer.utils.time_measurement | INFO: Setup time: 153 ms
2025-02-04 14:14:17,802 | skyrenderer.utils.time_measurement | INFO: Context update time: 187 ms
2025-02-04 14:14:23,986 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:23,987 | skyrenderer.utils.time_measurement | INFO: Render time: 6.18 seconds
And now let's force different radii inside groups. Each row is a different group. Note, that there are no
radius repetitions in rows.
strategy = st.DrawingStrategy(
scene_composer.renderer_context, inputs_strategies={"radius": st.DifferentInGroupInput()}
)
scene_composer.reshape_spheres(radius=random_radius, strategy=strategy, groups=groups)
scene_composer.visualize()
2025-02-04 14:14:24,440 | skyrenderer.utils.time_measurement | INFO: Setup time: 152 ms
2025-02-04 14:14:24,629 | skyrenderer.utils.time_measurement | INFO: Context update time: 188 ms
2025-02-04 14:14:30,767 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:30,768 | skyrenderer.utils.time_measurement | INFO: Render time: 6.14 seconds
There is one additional option. The value can be drawn distinctly for each group, which means that
it's synchronized in a group, but different groups cannot share the same value.
synchronization = st.SynchronizationDescription(in_strategy=st.Synchronization.DISTINCT_EQUAL_GROUPS)
strategy = st.DrawingStrategy(
scene_composer.renderer_context, inputs_strategies={"radius": st.SynchronizedInput(synchronization)}
)
scene_composer.reshape_spheres(radius=random_radius, strategy=strategy, groups=groups)
scene_composer.visualize()
2025-02-04 14:14:31,214 | skyrenderer.utils.time_measurement | INFO: Setup time: 154 ms
2025-02-04 14:14:31,405 | skyrenderer.utils.time_measurement | INFO: Context update time: 191 ms
2025-02-04 14:14:36,616 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:36,617 | skyrenderer.utils.time_measurement | INFO: Render time: 5.21 seconds
For the examples above we were working with a simple 3-dimensional FloatInput to generate colors, but
there are more advanced options available too. For example, if you want to synchronize the color intensity
without changing its hue or saturation, the natural solution would be to use HSV model.
First, let's reset the geometry and strategies to default.
scene_composer.reshape_spheres(radius=1.0)
Randomized intensity grayscale:
from skyrenderer.basic_types.provider.provider_inputs import HSVColorInput
hsv_color = HSVColorInput(hue_range=(0, 0), saturation_range=(0, 0), value_range=(0, 1))
scene_composer.color_spheres(color=hsv_color)
scene_composer.visualize()
2025-02-04 14:14:37,048 | skyrenderer.utils.time_measurement | INFO: Setup time: 156 ms
2025-02-04 14:14:37,238 | skyrenderer.utils.time_measurement | INFO: Context update time: 189 ms
2025-02-04 14:14:40,052 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:40,053 | skyrenderer.utils.time_measurement | INFO: Render time: 2.81 seconds
Randomized intensity red:
hsv_color = HSVColorInput(hue_range=(0, 0), saturation_range=(1, 1), value_range=(0, 1))
scene_composer.color_spheres(color=hsv_color)
scene_composer.visualize()
2025-02-04 14:14:40,514 | skyrenderer.utils.time_measurement | INFO: Setup time: 249 ms
2025-02-04 14:14:40,703 | skyrenderer.utils.time_measurement | INFO: Context update time: 188 ms
2025-02-04 14:14:42,852 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:42,853 | skyrenderer.utils.time_measurement | INFO: Render time: 2.15 seconds
Custom synchronization
It's not possible to anticipate every possible synchronization problem, so if the provided options are not
sufficient, user can write their own synchronization rule. It must inherit a ICustomSynchronization interface
that enforces priority_value and forbidden_values methods.
In this example we'll write a special synchronization that will ensure that small spheres will be red and a large
ones - blue.
First, let's just create a parameter providers with 2 options (blue-red color and small-big radius).
hsv_color = HSVColorInput(hue_range=(0, 0.5), saturation_range=(1, 1), value_range=(1, 1), number_of_steps=2)
radius_input = FloatInput(min_value=0.5, max_value=1.5, number_of_steps=2)
Here a custom synchronization rule is defined.
class ColorRadiusSynchronization(st.ICustomSynchronization):
def priority_value(self, input_name, group_id, receiver_drawn_inputs, previously_drawn_values):
if input_name "base_color":
synced_input = "radius"
elif input_name "radius":
synced_input = "base_color"
else:
# not a synced input
return None
for inputs_dict in previously_drawn_valuessynced_input in inputs_dict:
return inputs_dict[synced_input].value
return None
def forbidden_values(self, input_name, group_id, receiver_drawn_inputs, previously_drawn_values):
return set()
And a strategy using this rule for 'base_color' and 'radius'.
input_strategy = st.SynchronizedInput(ColorRadiusSynchronization())
strategy = st.DrawingStrategy(
scene_composer.renderer_context, inputs_strategies={"base_color": input_strategy, "radius": input_strategy}
)
To be able to recognize an object by a group_id, we have to put each sphere in a separate randomization group.
groups = {(i, j): f"sphere{i}{j}" for (i, j) in scene_composer.all_sphere_indices}
scene_composer.color_spheres(color=hsv_color, strategy=strategy, groups=groups)
scene_composer.reshape_spheres(radius=radius_input, strategy=strategy, groups=groups)
scene_composer.visualize()
2025-02-04 14:14:43,231 | skyrenderer.utils.time_measurement | INFO: Setup time: 164 ms
2025-02-04 14:14:43,422 | skyrenderer.utils.time_measurement | INFO: Context update time: 191 ms
2025-02-04 14:14:47,362 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 14:14:47,363 | skyrenderer.utils.time_measurement | INFO: Render time: 3.94 seconds
Summary
In this section you have learnt:
- SkyRenderer has implemented randomization system relying on Providers and Receivers.
- Properties of objects in the scene can be randomized and synchronized.
- There are some randomization strategies predefined.
- In case provided solution are not enough, a custom one can be created.