• Intro
  • Randomization

Randomization

By: SKY ENGINE AI
scroll down ↓to find out morerandomization_13_resourcesTutorial

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()
randomization_1_resourcesTutorial
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()
randomization_2_resourcesTutorial
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()
randomization_3_resourcesTutorial
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()
randomization_4_resourcesTutorial
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()
randomization_5_resourcesTutorial
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()
randomization_6_resourcesTutorial
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()
randomization_7_resourcesTutorial
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()
randomization_8_resourcesTutorial
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()
randomization_9_resourcesTutorial
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()
randomization_10_resourcesTutorial
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()
randomization_11_resourcesTutorial
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()
randomization_12_resourcesTutorial
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()
randomization_13_resourcesTutorial
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.