@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()