Randomize time of day in dynamic sky

@nshymberg After checking this a few times I think I have an idea of what’s happening here.

There’s an optimization we’ve encountered where attributes aren’t accessible until they’re ‘touched’ by the GUI. In the case of headless, this isn’t possible. So here’s the workaround I use …

You can write some python that checks the validity of the attributes on the script run (which only happens once at the beginning). If the attribute isn’t valid, you create that attribute. This won’t override it if it actually exists, but will make it accessible in the script now.

Below is my script example for doing this with textures, but the principle should be the same for you.

Pay specific attention to:

def make_asset_attr_accessible(prim, attribute_name, attr_value):
    if not prim.GetAttribute(attribute_name).IsValid():
        prim.CreateAttribute(attribute_name, Sdf.ValueTypeNames.Asset, custom=True).Set(attr_value)
        print(f'Set prim for {attribute_name}')

and

    # Create the primvars on the material - this is to work around an optimization where attributes aren't settable via script until they're viewed in GUI. The workaround is to create them on the prim, then they can be set.
    make_asset_attr_accessible(pbr_shader_prim, "inputs:normalmap_texture", "")
    make_asset_attr_accessible(pbr_shader_prim, "inputs:diffuse_texture", "")
    make_asset_attr_accessible(pbr_shader_prim, "inputs:reflectionroughness_texture", "")

Full script here. Hopefully this resolves your issue!

import glob
import re
import os
from pathlib import Path
from pxr import Usd, UsdGeom, Gf, Sdf, Vt
import omni.usd
import omni.replicator.core as rep

def make_asset_attr_accessible(prim, attribute_name, attr_value):
    if not prim.GetAttribute(attribute_name).IsValid():
        prim.CreateAttribute(attribute_name, Sdf.ValueTypeNames.Asset, custom=True).Set(attr_value)
        print(f'Set prim for {attribute_name}')

def hide_default_light():
    # Hide the default light
    default_light_prim = stage.GetPrimAtPath("/Environment/defaultLight")
    if default_light_prim.IsValid():
        xform = UsdGeom.Xformable(default_light_prim)
        xform.MakeInvisible()

def get_matching_files(directory, regex_pattern, file_extension):
    matching_files = []
    
    # Compile the regex pattern for efficient repeated use
    pattern = re.compile(regex_pattern)
    
    # List all files in the given directory
    for filename in os.listdir(directory):
        # Check if the file has the required extension
        if filename.endswith(file_extension):
            # Check if the file name matches the regex pattern
            if pattern.search(filename):
                # Add the matching file name to the list
                matching_files.append(os.path.join(directory, filename))
                
    return matching_files

stage = omni.usd.get_context().get_stage()

directory = Path("C:/YOURDIR/texture_lib_small/").as_posix()
file_extension = ".jpg"  # Example file extension
patterns = ['_Color', '_Displacement', '_NormalGL', '_Roughness']

# Make lists of all texture channels
diffuse_files = get_matching_files(directory, patterns[0], file_extension)
normal_files = get_matching_files(directory, patterns[2], file_extension)
roughness_files = get_matching_files(directory, patterns[3], file_extension)

# Scene settings
rep.settings.set_stage_up_axis("Y")
rep.settings.set_stage_meters_per_unit(0.01)

hide_default_light()

# Creating a new layer in the stage ensures all changes done can be isolated, and we do not accidentally change our original scene.
with rep.new_layer():
    
    # Create a material
    pbr_mat = rep.create.material_omnipbr(count=1)
    pbr_primpath = '/Replicator/Looks/OmniPBR/Shader'
    pbr_shader_prim = stage.GetPrimAtPath(pbr_primpath)
    pbr_shader = rep.get.prims(path_pattern=pbr_primpath)
    
    # Create the primvars on the material - this is to work around an optimization where attributes aren't settable via script until they're viewed in GUI. The workaround is to create them on the prim, then they can be set.
    make_asset_attr_accessible(pbr_shader_prim, "inputs:normalmap_texture", "")
    make_asset_attr_accessible(pbr_shader_prim, "inputs:diffuse_texture", "")
    make_asset_attr_accessible(pbr_shader_prim, "inputs:reflectionroughness_texture", "")
    
    # Create basic replicator objects in the stage - These are created under "Replicator/"
    floor = rep.create.cube(position=(0,0,0), scale=(10,0.1,10))
    cone = rep.create.cone(position=(0,100,0), scale=2)
    camera = rep.create.camera(position=(0,600,1500), rotation=(-22,0,0), focus_distance=200,f_stop=8)
    distance_light = rep.create.light(rotation=(-45,0,0), intensity=1500, temperature=6500, light_type="distant")
    
    # Apply the materials to the floor
    with floor:
        rep.modify.material(pbr_mat)
    
    # This is a randomizer, a special replicator way to organizer the graph logic (not a python function!)
    def randomize_texture(shader):
        with shader:
            rep.modify.attribute("inputs:diffuse_texture", rep.distribution.sequence(diffuse_files))
            rep.modify.attribute("inputs:reflectionroughness_texture", rep.distribution.sequence(roughness_files))
            rep.modify.attribute("inputs:normalmap_texture", rep.distribution.sequence(normal_files))
        return shader.node
    
    rep.randomizer.register(randomize_texture)
    
    # Create the render product and Writer
    render_product = rep.create.render_product(camera, (1920, 1080))
    writer = rep.WriterRegistry.get("BasicWriter")
    writer.initialize(output_dir="_random_textures", rgb=True)
    writer.attach([render_product])
    
    # In the created graph, this is what executes for N number of frames
    with rep.trigger.on_frame(num_frames=10, rt_subframes=50):
        rep.randomizer.randomize_texture(pbr_shader)
    
    # Preview the scene by executing the graph once, without writing.
    rep.orchestrator.preview()

1 Like