├── dome_grid_2k.exr ├── .editorconfig ├── .gitattributes ├── test_linear_vertical.ies ├── luxtest_const.py ├── husk_pre_render.py ├── .flake8 ├── .gitignore ├── .vscode ├── settings.json └── extensions.json ├── bega_50988.6k3.ies ├── pyproject.toml ├── luxtest_utils.py ├── test_stripes_nonuniform.ies ├── set_customLayerData.py ├── luxtest.css ├── pip_import.py ├── bega_84693k4.ies ├── .pylintrc ├── print_ies_lines.py ├── test_stripes_uniform.ies ├── archive_web.py ├── README.md ├── combine_ies_test_images.py ├── customLayerData.usda ├── time_test.py ├── usd ├── ies_scale.usda ├── dome.usda ├── visibleRect.usda ├── iesTest.usda └── distant.usda ├── genhoudini.py ├── bega_84659K4.ies ├── gendiffs.py ├── LICENSE.md ├── genembree.py └── luxtest_hou_utils.py /dome_grid_2k.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anderslanglands/luxtest/main/dome_grid_2k.exr -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Unix-style newlines by default 2 | [*] 3 | end_of_line = lf 4 | trim_trailing_whitespace = true 5 | indent_style = space 6 | indent_size = 4 7 | 8 | [*.{bat,cmd,ps1}] 9 | end_of_line = crlf -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Tell git to stop altering line endings. Check things out in the state they're 2 | # saved in the repo. Modern text editors can deal with either LF or CRLF on any 3 | # platform. 4 | * -text 5 | -------------------------------------------------------------------------------- /test_linear_vertical.ies: -------------------------------------------------------------------------------- 1 | IESNA:LM-63-1995 2 | [DESCRIPTION] ies file for testing - linear map from vertical angle, 0=>1 and 180=>0 3 | TILT=NONE 4 | 1 -1 1.0 2 1 1 2 1.000 1.000 1.000 5 | 1.0 1.0 100 6 | 0.0 180.0 7 | 0.0 8 | 1.0 0.0 9 | -------------------------------------------------------------------------------- /luxtest_const.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Constants 3 | ############################################################################### 4 | 5 | DEFAULT_OVERRIDES = { 6 | "inputs:shaping:cone:angle": 180, 7 | } 8 | -------------------------------------------------------------------------------- /husk_pre_render.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # If we don't clear out the QT environment variables, the AdskLicensingAgent 4 | # used by HtoA will error, which prevents it from getting the license 5 | for key in os.environ: 6 | if key.startswith("QT_"): 7 | del os.environ[key] 8 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | 4 | ignore = 5 | # E203 = whitespace before ':' - black will format like `"mystr"[2 + 1 :]`, which flake8 doesn't like 6 | E203, 7 | # E402 = module level import not at top of file 8 | E402, 9 | # E501 = line too long 10 | E501, 11 | # W503 = line break before binary operator - black will break (long_condition and other_long_condition) 12 | # in a way that this check doesn't like 13 | W503, 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.deps 2 | /.thumbs 3 | /backup 4 | /web 5 | /web.archive 6 | /renders 7 | **/*.rat 8 | **/*.tx 9 | **/*.autosave 10 | **/*.nk~ 11 | **/*.exr 12 | !/dome_grid_2k.exr 13 | **/*.png 14 | 15 | 16 | # python byte-compiled / optimized files 17 | __pycache__/ 18 | *.py[co] 19 | 20 | # Virtual environments 21 | .tox/ 22 | .venv/ 23 | _venv/ 24 | 25 | # Linters / tools 26 | .bash_history 27 | .conan 28 | .mypy_cache 29 | 30 | # IDEs 31 | .idea/ 32 | .vs/ 33 | 34 | # OS - specific 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[bat]": { 3 | "files.eol": "\r\n" 4 | }, 5 | "[python]": { 6 | "editor.codeActionsOnSave": { 7 | "source.organizeImports": "explicit" 8 | }, 9 | "editor.rulers": [ 10 | 80, 11 | 120 12 | ] 13 | }, 14 | "editor.formatOnSave": true, 15 | "editor.insertSpaces": true, 16 | "editor.tabSize": 4, 17 | "editor.wordWrapColumn": 120, 18 | "files.trimTrailingWhitespace": true, 19 | "python.analysis.typeCheckingMode": "off", // use mypy for typing, not pylance 20 | "rewrap.wrappingColumn": 120, 21 | "ruff.organizeImports": false 22 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "ms-python.black-formatter", 7 | "ms-python.isort", 8 | "ms-python.mypy-type-checker", 9 | "ms-python.pylint", 10 | "ms-python.python", 11 | "ms-python.vscode-pylance" 12 | ], 13 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 14 | "unwantedRecommendations": [] 15 | } 16 | -------------------------------------------------------------------------------- /bega_50988.6k3.ies: -------------------------------------------------------------------------------- 1 | IESNA:LM-63-1995 2 | [TEST] 3 | [MANUFAC] BEGA 4 | [MORE] Copyright LUMCat V 5 | [LUMCAT] 6 | [LUMINAIRE] 50988.6K3 7 | [LAMPCAT] LED 11,5W 8 | [LAMP] 1096 lm,14 W 9 | TILT=NONE 10 | 1 -1 1.0 37 1 1 2 -0.120 0.000 0.000 11 | 1.0 1.0 14 12 | 0.0 2.5 5.0 7.5 10.0 12.5 15.0 17.5 20.0 22.5 25.0 27.5 30.0 13 | 32.5 35.0 37.5 40.0 42.5 45.0 47.5 50.0 52.5 55.0 57.5 60.0 62.5 14 | 65.0 67.5 70.0 72.5 75.0 77.5 80.0 82.5 85.0 87.5 90.0 15 | 0.0 16 | 1645.4 1617.0 1552.5 1461.3 1357.8 1247.8 1131.7 1011.0 17 | 892.4 780.7 686.1 610.2 547.5 475.8 384.0 295.1 18 | 224.6 157.0 84.5 32.4 12.2 8.6 7.5 6.7 19 | 5.8 4.9 4.0 3.1 2.4 1.7 1.1 0.6 20 | 0.3 0.1 0.0 0.0 0.0 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | exclude = ''' 3 | ^/( 4 | ( 5 | # root directories to exclude 6 | \.DS_Store 7 | | \.conan 8 | | \.git 9 | | \.hg 10 | | \.mypy_cache 11 | | \.tox 12 | | \.venv 13 | | \.vs 14 | | \.vscode 15 | | _venv 16 | )/ 17 | # any rooted files to ignore would go here 18 | ) 19 | # unrooted paths here 20 | | __pycache__/ 21 | ''' 22 | # In different versions of black, there were 3 different ways to turn on 23 | # the same thing - splitting up of long string literals 24 | experimental-string-processing = true 25 | preview = true 26 | enable-unstable-feature = ['string_processing'] 27 | 28 | line-length = 120 29 | 30 | [tool.isort] 31 | profile = 'black' 32 | lines_between_types = 1 33 | combine_as_imports = true 34 | line_length = 120 35 | 36 | [tool.ruff] 37 | line-length = 120 38 | ignore = [ 39 | "E402", # Module level import not at top of file 40 | ] 41 | -------------------------------------------------------------------------------- /luxtest_utils.py: -------------------------------------------------------------------------------- 1 | import locale 2 | import sys 3 | 4 | 5 | def make_unique(*objs): 6 | return tuple(dict.fromkeys(x for x in objs if x is not None)) 7 | 8 | 9 | CODEC_LIST = make_unique( 10 | # list of codecs to try, in order... 11 | # use getattr because some stream wrappers (ie, houdinit's hou.ShellIO) 12 | # may not have a .encoding attr 13 | getattr(sys.stdout, "encoding", None), 14 | getattr(sys.stderr, "encoding", None), 15 | getattr(sys.stdin, "encoding", None), 16 | getattr(sys.__stdout__, "encoding", None), 17 | getattr(sys.__stderr__, "encoding", None), 18 | getattr(sys.__stdin__, "encoding", None), 19 | locale.getpreferredencoding(), 20 | "utf8", 21 | ) 22 | 23 | 24 | def try_decode(input_bytes): 25 | for codec in CODEC_LIST: 26 | try: 27 | return input_bytes.decode(codec) 28 | except UnicodeDecodeError: 29 | pass 30 | return input_bytes 31 | -------------------------------------------------------------------------------- /test_stripes_nonuniform.ies: -------------------------------------------------------------------------------- 1 | IESNA:LM-63-1995 2 | [DESCRIPTION] ies test file - horizontal black/white stripes every 10 degrees. 3 | [DESCRIPTION2] Bottom band/cap (vertical angle = 0 to 9) is .25, and top band/ 4 | [DESCRIPTION3] cap (vertical angle = 170 to 180) is .75. 5 | [DESCRIPTION4] Uses non-uniform vertical angle interval. Should behave 6 | [DESCRIPTION4] to identically to test_stripes_uniform.ies 7 | TILT=NONE 8 | 1 -1 1.0 36 1 1 2 1.000 1.000 1.000 9 | 1.0 1.0 100 10 | 0.0 9.0 10.0 19.0 20.0 29.0 30.0 39.0 40.0 49.0 11 | 50.0 59.0 60.0 69.0 70.0 79.0 80.0 89.0 90.0 99.0 12 | 100.0 109.0 110.0 119.0 120.0 129.0 130.0 139.0 140.0 149.0 13 | 150.0 159.0 160.0 169.0 170.0 180.0 14 | 0.0 15 | 0.25 0.25 1.00 1.00 0.00 0.00 1.00 1.00 0.00 0.00 16 | 1.00 1.00 0.00 0.00 1.00 1.00 0.00 0.00 1.00 1.00 17 | 0.00 0.00 1.00 1.00 0.00 0.00 1.00 1.00 0.00 0.00 18 | 1.00 1.00 0.00 0.00 0.75 0.75 -------------------------------------------------------------------------------- /set_customLayerData.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import hou 4 | 5 | from pxr import Sdf 6 | 7 | node = hou.pwd() 8 | stage = node.editableStage() 9 | 10 | this_hip_file = os.path.abspath(hou.hipFile.path()) 11 | this_dir = os.path.dirname(this_hip_file) 12 | custom_laya_data_usda = os.path.join(this_dir, "customLayerData.usda") 13 | 14 | metadata_layer = Sdf.Layer.FindOrOpen(custom_laya_data_usda) 15 | new_metadata = metadata_layer.customLayerData 16 | 17 | light_name = node.name().rsplit("_", 1)[-1] 18 | 19 | usd_rop_node = hou.node(f"/stage/usd_rop_{light_name}") 20 | start_frame = round(usd_rop_node.parm("f1").eval()) 21 | end_frame = round(usd_rop_node.parm("f2").eval()) 22 | 23 | new_metadata["MovieCaptureSettings"]["capture_frame_start"] = start_frame 24 | new_metadata["MovieCaptureSettings"]["capture_frame_end"] = end_frame 25 | new_metadata["MovieCaptureSettings"]["capture_name"] = f"{light_name}-rtx" 26 | new_metadata["omni_layer"]["authoring_layer"] = f"./{light_name}.usda" 27 | new_metadata["renderSettings"]["rtx:externalFrameCounter"] = end_frame 28 | 29 | stage.GetSessionLayer().customLayerData = new_metadata 30 | -------------------------------------------------------------------------------- /luxtest.css: -------------------------------------------------------------------------------- 1 | body { 2 | max-width: 1600px; 3 | margin: 40px auto; 4 | padding: 0 10px; 5 | font: 18px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | color: #444 7 | } 8 | 9 | .wrapper { 10 | display: flex; 11 | } 12 | 13 | .navbar { 14 | height: 100%; 15 | position: -webkit-sticky; 16 | position: sticky; 17 | top: 0; 18 | } 19 | 20 | .main { 21 | margin-left: 20px; 22 | } 23 | 24 | h1, 25 | h2, 26 | h3 { 27 | line-height: 1.2 28 | } 29 | 30 | img { 31 | transition: transform .1s; 32 | width: 128px; 33 | height: 128px; 34 | margin: 0 auto; 35 | } 36 | 37 | img:hover { 38 | transform: scale(4); 39 | } 40 | 41 | td { 42 | padding-left: 10px; 43 | padding-right: 10px; 44 | padding-top: 10px; 45 | padding-bottom: 10px; 46 | } 47 | 48 | @media (prefers-color-scheme: dark) { 49 | body { 50 | color: #d1d1d1; 51 | background: #111 52 | } 53 | 54 | a:link { 55 | color: #58a6ff 56 | } 57 | 58 | a:visited { 59 | color: #8e96f0 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /pip_import.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import inspect 3 | import os 4 | import subprocess 5 | import sys 6 | 7 | ############################################################################### 8 | # Constants 9 | ############################################################################### 10 | 11 | THIS_FILE = os.path.abspath(inspect.getsourcefile(lambda: None) or __file__) 12 | THIS_DIR = os.path.dirname(THIS_FILE) 13 | 14 | DEPS_DIR = os.path.join(THIS_DIR, ".deps") 15 | PY_DEPS_DIR = os.path.join(DEPS_DIR, "python") 16 | 17 | ############################################################################### 18 | # Functions 19 | ############################################################################### 20 | 21 | 22 | def pip_import(module_name, pip_package_name=None): 23 | try: 24 | return importlib.import_module(module_name) 25 | except ImportError: 26 | pass 27 | 28 | # couldn't import - first, ensure PY_DEPS_DIR is on path, and retry 29 | os.makedirs(PY_DEPS_DIR, exist_ok=True) 30 | if PY_DEPS_DIR not in sys.path: 31 | sys.path.insert(0, PY_DEPS_DIR) 32 | try: 33 | return importlib.import_module(module_name) 34 | except ImportError: 35 | pass 36 | 37 | # still couldn't import - install via pip 38 | if pip_package_name is None: 39 | pip_package_name = module_name 40 | os.makedirs(PY_DEPS_DIR, exist_ok=True) 41 | 42 | # ensurepip not necessary, houdini's python install includes it... and it was erroring 43 | # subprocess.check_call([sys.executable, "-m", "ensurepip"]) 44 | subprocess.check_call([sys.executable, "-m", "pip", "install", "--target", PY_DEPS_DIR, pip_package_name]) 45 | print("=" * 80) 46 | 47 | return importlib.import_module(module_name) 48 | -------------------------------------------------------------------------------- /bega_84693k4.ies: -------------------------------------------------------------------------------- 1 | IESNA:LM-63-1995 2 | [TEST] 3 | [MANUFAC] BEGA 4 | [MORE] Copyright LUMCat V 5 | [LUMCAT] 6 | [LUMINAIRE] 84693K4 7 | [LAMPCAT] LED 38,1W 8 | [LAMP] 2044 lm,43 W 9 | TILT=NONE 10 | 1 -1 1.0 91 1 1 2 -0.150 0.000 0.000 11 | 1.0 1.0 43 12 | 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 11.0 12.0 13 | 13.0 14.0 15.0 16.0 17.0 18.0 19.0 20.0 21.0 22.0 23.0 24.0 25.0 14 | 26.0 27.0 28.0 29.0 30.0 31.0 32.0 33.0 34.0 35.0 36.0 37.0 38.0 15 | 39.0 40.0 41.0 42.0 43.0 44.0 45.0 46.0 47.0 48.0 49.0 50.0 51.0 16 | 52.0 53.0 54.0 55.0 56.0 57.0 58.0 59.0 60.0 61.0 62.0 63.0 64.0 17 | 65.0 66.0 67.0 68.0 69.0 70.0 71.0 72.0 73.0 74.0 75.0 76.0 77.0 18 | 78.0 79.0 80.0 81.0 82.0 83.0 84.0 85.0 86.0 87.0 88.0 89.0 90.0 19 | 0.0 20 | 39295.9 38713.4 37147.7 33638.1 27559.3 19932.5 12767.2 7497.8 21 | 4413.2 3045.6 2509.0 2229.4 2020.8 1834.4 1655.2 1477.9 22 | 1318.1 1177.7 1051.8 939.5 838.2 747.1 669.9 609.3 23 | 560.3 509.1 456.1 407.7 365.4 325.3 294.9 272.6 24 | 251.2 224.7 192.4 159.0 132.4 117.7 106.8 97.6 25 | 90.0 83.2 77.0 71.4 66.4 62.6 59.4 56.2 26 | 53.5 50.8 47.9 44.8 41.4 38.3 35.4 32.9 27 | 28.2 22.7 18.8 16.0 13.7 11.7 9.7 7.9 28 | 6.2 4.8 3.7 2.9 2.3 1.8 1.3 1.0 29 | 0.7 0.4 0.2 0.1 0.0 0.0 0.0 0.0 30 | 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 31 | 0.0 0.0 0.0 32 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Files or directories to be skipped. They should be base names, not 11 | # paths. 12 | ignore=.DS_Store, 13 | .conan, 14 | .git, 15 | .hg, 16 | .mypy_cache, 17 | .vs, 18 | .vscode, 19 | __pycache__, 20 | CVS, 21 | 22 | # Add files or directories matching the regex patterns to the ignore-list. The 23 | # regex matches against paths and can be in Posix or Windows format. 24 | ignore-paths=^\.tox/.*$, 25 | ^\.venv/.*$ 26 | ^\_venv/.*$ 27 | 28 | 29 | [MESSAGES CONTROL] 30 | 31 | # Disable the message, report, category or checker with the given id(s). You 32 | # can either give multiple identifiers separated by comma (,) or put this 33 | # option multiple times (only on the command line, not in the configuration 34 | # file where it should appear only once). You can also use "--disable=all" to 35 | # disable everything first and then reenable specific checks. For example, if 36 | # you want to run only the similarities checker, you can use "--disable=all 37 | # --enable=similarities". If you want to run only the classes checker, but have 38 | # no Warning level messages displayed, use "--disable=all --enable=classes 39 | # --disable=W". 40 | disable=import-outside-toplevel, 41 | missing-class-docstring, 42 | missing-function-docstring, 43 | subprocess-run-check, 44 | wrong-import-position, 45 | # Until VSCode linting respects the .env file (or has some other way to add paths), "can't import" errors 46 | # are useless: 47 | # https://github.com/microsoft/vscode-python/issues/9185 48 | import-error 49 | 50 | [FORMAT] 51 | 52 | # Maximum number of characters on a single line. 53 | max-line-length=120 54 | 55 | [TYPECHECK] 56 | # List of module names for which member attributes should not be checked 57 | # (useful for modules/projects where namespaces are manipulated during runtime 58 | # and thus existing member attributes cannot be deduced by static analysis. It 59 | # supports qualified module names, as well as Unix pattern matching. 60 | ignored-modules=winreg -------------------------------------------------------------------------------- /print_ies_lines.py: -------------------------------------------------------------------------------- 1 | # Funcs used to help generate custom .ies profiles 2 | 3 | 4 | def print_angles_values(angles, values, precision=1): 5 | assert len(angles) == len(values) 6 | 7 | print(f"num: {len(angles)}") 8 | 9 | for i, angle in enumerate(angles): 10 | print(f"{angle:>6.1f} ", end="") 11 | if (i + 1) % 10 == 0: 12 | print() 13 | if (i + 1) % 10 != 0: 14 | print() 15 | print("0.0") # horizontal angles 16 | 17 | for i, val in enumerate(values): 18 | print(f"{val:>6.{precision}f} ", end="") 19 | if (i + 1) % 10 == 0: 20 | print() 21 | 22 | 23 | def vertical_bands2(): 24 | color = 1.0 25 | 26 | angles = [] 27 | values = [] 28 | 29 | for theta in range(0, 171, 10): 30 | angles.append(theta) 31 | if theta == 170: 32 | angles.append(180) 33 | else: 34 | angles.append(theta + 9.9) 35 | values.append(color) 36 | values.append(color) 37 | color = 1 - color 38 | print_angles_values(angles, values) 39 | 40 | 41 | def vertical_bands3(): 42 | v_num = 181 # 0-180, inclusive 43 | 44 | angles = [] 45 | values = [] 46 | for v in range(v_num): 47 | angles.append(v) 48 | if v < 10: 49 | value = 0.25 50 | elif v >= 170: 51 | value = 0.75 52 | else: 53 | value = (v // 10) % 2 54 | values.append(value) 55 | 56 | print_angles_values(angles, values, precision=2) 57 | 58 | 59 | def vertical_bands4(): 60 | # in theory, should give identical results to vertical_bands3, but without 61 | # uniform spacing 62 | 63 | angles = [] 64 | values = [] 65 | for v in range(181): 66 | # skip the repeating interior elements 67 | 68 | # bands generally start/end at 0's and 9s, modulo 10 69 | # exception is last row, which goes from 170 to 180 - so we skip the 70 | # final "9", 179 71 | if v == 179: 72 | continue 73 | if v % 10 not in (0, 9): 74 | continue 75 | 76 | angles.append(v) 77 | if v < 10: 78 | value = 0.25 79 | elif v >= 170: 80 | value = 0.75 81 | else: 82 | value = (v // 10) % 2 83 | values.append(value) 84 | 85 | print_angles_values(angles, values, precision=2) 86 | -------------------------------------------------------------------------------- /test_stripes_uniform.ies: -------------------------------------------------------------------------------- 1 | IESNA:LM-63-1995 2 | [DESCRIPTION] ies test file - horizontal black/white stripes every 10 degrees. 3 | [DESCRIPTION2] Bottom band/cap (vertical angle = 0 to 9) is .25, and top band/ 4 | [DESCRIPTION3] cap (vertical angle = 170 to 180) is .75. 5 | [DESCRIPTION4] Uses uniform vertical samples angle interval, every 1.0 degrees. 6 | [DESCRIPTION4] Should behave identically to test_stripes_nonuniform.ies 7 | 8 | 9 | TILT=NONE 10 | 1 -1 1.0 181 1 1 2 1.000 1.000 1.000 11 | 1.0 1.0 100 12 | 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 13 | 10.0 11.0 12.0 13.0 14.0 15.0 16.0 17.0 18.0 19.0 14 | 20.0 21.0 22.0 23.0 24.0 25.0 26.0 27.0 28.0 29.0 15 | 30.0 31.0 32.0 33.0 34.0 35.0 36.0 37.0 38.0 39.0 16 | 40.0 41.0 42.0 43.0 44.0 45.0 46.0 47.0 48.0 49.0 17 | 50.0 51.0 52.0 53.0 54.0 55.0 56.0 57.0 58.0 59.0 18 | 60.0 61.0 62.0 63.0 64.0 65.0 66.0 67.0 68.0 69.0 19 | 70.0 71.0 72.0 73.0 74.0 75.0 76.0 77.0 78.0 79.0 20 | 80.0 81.0 82.0 83.0 84.0 85.0 86.0 87.0 88.0 89.0 21 | 90.0 91.0 92.0 93.0 94.0 95.0 96.0 97.0 98.0 99.0 22 | 100.0 101.0 102.0 103.0 104.0 105.0 106.0 107.0 108.0 109.0 23 | 110.0 111.0 112.0 113.0 114.0 115.0 116.0 117.0 118.0 119.0 24 | 120.0 121.0 122.0 123.0 124.0 125.0 126.0 127.0 128.0 129.0 25 | 130.0 131.0 132.0 133.0 134.0 135.0 136.0 137.0 138.0 139.0 26 | 140.0 141.0 142.0 143.0 144.0 145.0 146.0 147.0 148.0 149.0 27 | 150.0 151.0 152.0 153.0 154.0 155.0 156.0 157.0 158.0 159.0 28 | 160.0 161.0 162.0 163.0 164.0 165.0 166.0 167.0 168.0 169.0 29 | 170.0 171.0 172.0 173.0 174.0 175.0 176.0 177.0 178.0 179.0 30 | 180.0 31 | 0.0 32 | 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 33 | 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 34 | 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 35 | 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 36 | 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 37 | 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 38 | 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 39 | 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 40 | 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 41 | 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 42 | 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 43 | 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 44 | 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 45 | 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 46 | 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 47 | 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 48 | 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 49 | 0.75 0.75 0.75 0.75 0.75 0.75 0.75 0.75 0.75 0.75 50 | 0.75 -------------------------------------------------------------------------------- /archive_web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Moves the "web" folder into webdiffs, giving it a unique name to identify it""" 4 | 5 | import argparse 6 | import datetime 7 | import inspect 8 | import os 9 | import shutil 10 | import subprocess 11 | import sys 12 | import traceback 13 | 14 | ############################################################################### 15 | # Constants 16 | ############################################################################### 17 | 18 | THIS_FILE = os.path.abspath(inspect.getsourcefile(lambda: None) or __file__) 19 | THIS_DIR = os.path.dirname(THIS_FILE) 20 | 21 | WEB_DIR = os.path.join(THIS_DIR, "web") 22 | WEB_ARCHIVE_DIR = os.path.join(THIS_DIR, "web.archive") 23 | 24 | DEFAULT_USD_REPO = os.path.join(os.path.dirname(THIS_DIR), "usd-ci", "USD") 25 | USD_REPO = os.environ.get("USD_ROOT", DEFAULT_USD_REPO) 26 | 27 | RENDERS_REPO = os.path.join(THIS_DIR, "renders") 28 | 29 | ############################################################################### 30 | # Utilities 31 | ############################################################################### 32 | 33 | 34 | def is_ipython(): 35 | try: 36 | __IPYTHON__ # type: ignore 37 | except NameError: 38 | return False 39 | return True 40 | 41 | 42 | def get_git_hash(repo_dir, n=8): 43 | proc = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo_dir, text=True, capture_output=True) 44 | hash = proc.stdout.strip() 45 | if n: 46 | hash = hash[:n] 47 | return hash 48 | 49 | 50 | ############################################################################### 51 | # Core functions 52 | ############################################################################### 53 | 54 | 55 | def archive_web(name: str): 56 | now = datetime.datetime.now() 57 | date = now.strftime("%Y-%m-%d") 58 | luxtest_hash = get_git_hash(THIS_DIR) 59 | renders_hash = get_git_hash(RENDERS_REPO) 60 | usd_hash = get_git_hash(USD_REPO) 61 | dest_name = f"{date}.{name}.luxtest-{luxtest_hash}.usd-{usd_hash}.renders-{renders_hash}" 62 | dest_path = os.path.join(WEB_ARCHIVE_DIR, dest_name) 63 | 64 | print("moving:") 65 | print(f" {WEB_DIR}") 66 | print("to:") 67 | print(f" {dest_path}") 68 | 69 | shutil.move(WEB_DIR, dest_path) 70 | 71 | 72 | ############################################################################### 73 | # CLI 74 | ############################################################################### 75 | 76 | 77 | def get_parser(): 78 | parser = argparse.ArgumentParser( 79 | description=__doc__, 80 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 81 | ) 82 | parser.add_argument("name", help="short descriptive tag to help identify this set of diffs") 83 | return parser 84 | 85 | 86 | def main(argv=None): 87 | if argv is None: 88 | argv = sys.argv[1:] 89 | parser = get_parser() 90 | args = parser.parse_args(argv) 91 | try: 92 | archive_web(name=args.name) 93 | except Exception: # pylint: disable=broad-except 94 | 95 | traceback.print_exc() 96 | return 1 97 | return 0 98 | 99 | 100 | if __name__ == "__main__" and not is_ipython(): 101 | sys.exit(main()) 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Problem Statement 2 | 3 | The current specifications of the various UsdLux prims + attributes are imprecise or vague in many places, and as a result, actual implementations of them by various renderers have diverged, sometimes quite significantly. For instance, here is Intel's [4004 Moore Lane](https://dpel.aswf.io/4004-moore-lane/) scene, with the same UsdLux lights defined, in 3 different renderers: 4 | 5 | | Karma | Arnold | Omniverse RTX | 6 | | --------------------------------------- | ----------------------------------------- | --------------------------------------------- | 7 | | ![4004 Moore Lane - Karma][moore-karma] | ![4004 Moore Lane - Arnold][moore-arnold] | ![4004 Moore Lane - Omniverse RTX][moore-rtx] | 8 | 9 | # Solution 10 | 11 | We need to update UsdLux to specify exactly what quantities should be emitted for each light and combination of its attributes so that lighting can be shared between applications and renderers. 12 | 13 | A PR with proposals for these changes may be found here: 14 | - https://github.com/PixarAnimationStudios/OpenUSD/pull/3182 15 | 16 | Further, we propose adding a reference implementation of UsdLux support to hdEmbree, in a similar way that hdStorm provides a reference implementation of a Hydra Render Delegate. A PR chain adding such an implementation can be found here: 17 | 18 | - https://github.com/PixarAnimationStudios/OpenUSD/pull/3199 19 | 20 | 21 | # A Test Suite For UsdLux 22 | 23 | Further, this repo aims to provide tooling to: 24 | - generate standardized UsdLux test scenes (in .usda format) 25 | - generate renders of these test scenes 26 | - compare rendered images between different renderers 27 | 28 | ## Prerequisites 29 | 30 | - Install Houdini 31 | - Apprentice editions will not work 32 | - Check RenderMan compatible versions: 33 | - RenderMan 26: 34 | - https://rmanwiki.pixar.com/display/RFH26/Installation+of+RenderMan+for+Houdini 35 | - > RenderMan for Houdini 26.0 supports: 20.0.653, 19.5.805, 19.0.720 36 | - > RenderMan for Houdini on Linux will only operate with the gcc9.3 houdini build. 37 | - Check Arnold compatible versions: 38 | - See below for download instructions, and check available builds 39 | - Install RenderMan + RenderMan for Houdini 40 | - Register on pixar forums 41 | - https://renderman.pixar.com/forum/ 42 | - Download: 43 | - https://renderman.pixar.com/install 44 | - Install HtoA (Houdini to Arnold) 45 | - Purchase Arnold license, or sign up for free trial: 46 | - https://www.autodesk.com/products/arnold/overview 47 | - Download HtoA from [https://manage.autodesk.com/products/updates] (filter by "HtoA") 48 | 49 | ## Usage 50 | 51 | ### Automatically rendering all lights using hython 52 | - Make sure the `bin` folder for houdini is on your `PATH` 53 | - run `hython genhoudini.py` 54 | 55 | ### Manually rendering individual lights in Houdini GUI 56 | - Open `luxtest.hip` in Houdini 57 | - In the Solaris /stage pane, scroll down to the section corresponding to the 58 | lighting setup you wish to render: 59 | - distant 60 | - rect 61 | - sphere 62 | - disk 63 | - cylinder 64 | - dome 65 | - visibleRect 66 | - Select the USD Render ROP corresponding to the renderer you wish to render 67 | - will be of format `render_{light}_{renderer} 68 | - ie, `render_rect_ris` 69 | - In the parameters pane, click "Render to Disk" 70 | 71 | ## Renders repo 72 | 73 | Result rendered images are available in a 74 | [separate repo](https://github.com/pmolodo/luxtest_renders). To add it to 75 | this repo, first ensure "renders" subdir does not already exist, then from 76 | the repo root, run: 77 | 78 | ```shell 79 | git clone https://github.com/pmolodo/luxtest_renders renders 80 | ``` 81 | 82 | 83 | [moore-karma]: https://github.com/anderslanglands/light_comparison/blob/main/renders/moore-lane/moore-lane_karma.jpg?raw=true "Karma" 84 | [moore-arnold]: https://github.com/anderslanglands/light_comparison/blob/main/renders/moore-lane/moore-lane_arnold.jpg?raw=true "Arnold" 85 | [moore-rtx]: https://github.com/anderslanglands/light_comparison/blob/main/renders/moore-lane/moore-lane_rtx.jpg?raw=true "Omniverse RTX" 86 | -------------------------------------------------------------------------------- /combine_ies_test_images.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """CLI interface to does_something""" 4 | 5 | import argparse 6 | import inspect 7 | import os 8 | import re 9 | import subprocess 10 | import sys 11 | import traceback 12 | 13 | 14 | def does_something(arg1, opt_arg="blah"): 15 | print(arg1, opt_arg) 16 | 17 | 18 | THIS_FILE = os.path.abspath(inspect.getsourcefile(lambda: None) or __file__) 19 | THIS_DIR = os.path.dirname(THIS_FILE) 20 | 21 | RENDERS_DIR = os.path.join(THIS_DIR, "renders") 22 | 23 | RENDERERS = ("embree", "arnold", "karma", "ris") 24 | 25 | INPUT_NAME_RE = re.compile(r"""^iesTest-(?P.*)\.(?PiesTop|iesBottom).(?P\d{4}).exr$""") 26 | ############################################################################### 27 | # Utilities 28 | ############################################################################### 29 | 30 | 31 | def is_ipython(): 32 | try: 33 | __IPYTHON__ # type: ignore 34 | except NameError: 35 | return False 36 | return True 37 | 38 | 39 | if sys.platform == "win32": 40 | to_shell_cmd = subprocess.list2cmdline 41 | else: 42 | 43 | def to_shell_cmd(cmd_list): 44 | import shlex 45 | 46 | return " ".join(shlex.quote(x) for x in cmd_list) 47 | 48 | 49 | ############################################################################### 50 | # Core functions 51 | ############################################################################### 52 | 53 | 54 | def combine_ies_test_images(renderers=(), delete=True): 55 | to_delete = [] 56 | if not renderers: 57 | renderers = RENDERERS 58 | for renderer in renderers: 59 | renderer_dir = os.path.join(RENDERS_DIR, renderer) 60 | 61 | top_frames = {} 62 | bottom_frames = {} 63 | for entry in os.scandir(renderer_dir): 64 | if not entry.is_file(): 65 | continue 66 | match = INPUT_NAME_RE.match(entry.name) 67 | if not match: 68 | continue 69 | frame_dict = { 70 | "iesTop": top_frames, 71 | "iesBottom": bottom_frames, 72 | }[match.group("camera")] 73 | if match.group("renderer") != renderer: 74 | raise RuntimeError( 75 | f"found file {entry.path} with renderer {match.group('renderer')} in renderer dir {renderer_dir}" 76 | ) 77 | frame = match.group("frame") 78 | frame_dict[frame] = entry 79 | 80 | top_set = set(top_frames) 81 | bottom_set = set(bottom_frames) 82 | 83 | top_only = top_set - bottom_set 84 | bottom_only = bottom_set - top_set 85 | 86 | mismatched = [] 87 | mismatched.extend(top_frames[x] for x in top_only) 88 | mismatched.extend(bottom_frames[x] for x in bottom_only) 89 | if mismatched: 90 | print("WARNING: found mismatched frames without accompanying top or bottom frame:") 91 | print("=" * 80) 92 | for entry in mismatched: 93 | print(f" {entry.path}") 94 | print() 95 | both = sorted(top_set.intersection(bottom_set)) 96 | for frame in both: 97 | top_path = top_frames[frame].path 98 | bottom_path = bottom_frames[frame].path 99 | output_path = os.path.join(renderer_dir, f"iesTest-{renderer}.{frame}.exr") 100 | args = ["oiiotool", top_path, bottom_path, "--mosaic", "1x2", "-o", output_path] 101 | print(to_shell_cmd(args), flush=True) 102 | subprocess.check_call(args) 103 | print(f"Output: {output_path}") 104 | to_delete.append(top_path) 105 | to_delete.append(bottom_path) 106 | if delete: 107 | for path in to_delete: 108 | print(f"removing: {path}") 109 | os.remove(path) 110 | 111 | 112 | ############################################################################### 113 | # CLI 114 | ############################################################################### 115 | 116 | 117 | def get_parser(): 118 | parser = argparse.ArgumentParser( 119 | description=__doc__, 120 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 121 | ) 122 | parser.add_argument( 123 | "-r", 124 | "--renderers", 125 | choices=RENDERERS, 126 | nargs="+", 127 | help="Only combine images for the given renderers; if not specified, combine images for all renderers.", 128 | ) 129 | parser.add_argument( 130 | "-k", "--keep", action="store_true", help="Keep source half-image files after generating combined image" 131 | ) 132 | return parser 133 | 134 | 135 | def main(argv=None): 136 | if argv is None: 137 | argv = sys.argv[1:] 138 | parser = get_parser() 139 | args = parser.parse_args(argv) 140 | try: 141 | combine_ies_test_images(renderers=args.renderers, delete=not args.keep) 142 | except Exception: # pylint: disable=broad-except 143 | traceback.print_exc() 144 | return 1 145 | return 0 146 | 147 | 148 | if __name__ == "__main__" and not is_ipython(): 149 | sys.exit(main()) 150 | -------------------------------------------------------------------------------- /customLayerData.usda: -------------------------------------------------------------------------------- 1 | #usda 1.0 2 | ( 3 | customLayerData = { 4 | dictionary MovieCaptureSettings = { 5 | double animation_fps = 24 6 | int batch_count = 1 7 | string camera_name = "/cameras/camera1" 8 | bool capture_application = 0 9 | int capture_every_nth_frames = 20 10 | bool capture_every_nth_frames_checked = 0 11 | int capture_frame_end = 40 12 | int capture_frame_start = 1 13 | string capture_name = "sphere-rtx" 14 | string capture_range = "Custom Range - Frames" 15 | double capture_time_end = 10 16 | double capture_time_start = 0 17 | string exr_compression_method = "zips" 18 | double fps = 24 19 | bool generate_shader_cache = 0 20 | bool hdr_for_exr_checked = 1 21 | bool hdr_for_exr_visible = 0 22 | int iray_pathtrace_spp = 1 23 | int iray_subframes_per_frame = 32 24 | string movie_type = "Sequence" 25 | int mp4_encoding_bitrate = 16777216 26 | int mp4_encoding_iframe_interval = 60 27 | string mp4_encoding_preset = "PRESET_DEFAULT" 28 | string mp4_encoding_profile = "H264_PROFILE_HIGH" 29 | string mp4_encoding_rc_mode = "RC_VBR" 30 | int mp4_encoding_rc_target_quality = 0 31 | bool mp4_encoding_video_full_range_flag = 0 32 | string output_format = ".exr" 33 | string output_path = "./renders/rtx" 34 | bool overwrite_existing_frame_checked = 0 35 | bool pathtrace_mb_checked = 0 36 | double pathtrace_mb_frame_shutter_close = 0.5 37 | double pathtrace_mb_frame_shutter_open = 0 38 | int pathtrace_mb_subframes = 64 39 | int pathtrace_spp_per_iteration_mgpu = 1 40 | int pathtrace_spp_per_subframe = 128 41 | string queue_instance = "localhost Queue" 42 | int realtime_settle_latency = 0 43 | string render_preset = "PathTracing" 44 | bool render_style = 1 45 | bool renumber_negtive_frames_checked = 0 46 | int resolution_aspect_ratio_selected = 0 47 | string resolution_aspect_ratios = '["1.000:1", "16:9", "4:3"]' 48 | int resolution_height = 512 49 | string resolution_type = "Custom" 50 | bool resolution_w_h_linked = 0 51 | int resolution_width = 512 52 | int run_n_frames_before_start = 20 53 | bool run_n_frames_before_start_checked = 0 54 | bool save_alpha_checked = 0 55 | bool skip_upload_to_s3 = 0 56 | int start_delay_seconds = 10 57 | double sunstudy_end = 18 58 | int sunstudy_movie_minutes = 1 59 | int sunstudy_movie_seconds = 1 60 | double sunstudy_start = 6 61 | string task_comment = "" 62 | string task_priority = "" 63 | string task_type = "create-render" 64 | bool upload_to_s3 = 0 65 | } 66 | dictionary cameraSettings = { 67 | dictionary Front = { 68 | double3 position = (0, 0, 500) 69 | double radius = 500 70 | } 71 | dictionary Perspective = { 72 | double3 position = (500, 500, 500) 73 | double3 target = (-0.00000397803842133726, 0.000007956076785831101, -0.000003978038307650422) 74 | } 75 | dictionary Right = { 76 | double3 position = (-500, 0, 0) 77 | double radius = 500 78 | } 79 | dictionary Top = { 80 | double3 position = (0, 500, 0) 81 | double radius = 500 82 | } 83 | string boundCamera = "/cameras/camera1" 84 | } 85 | dictionary navmeshSettings = { 86 | double agentHeight = 180 87 | double agentRadius = 20 88 | bool excludeRigidBodies = 1 89 | uint64 ver = 1 90 | double voxelCeiling = 460 91 | } 92 | dictionary omni_layer = { 93 | string authoring_layer = "./sphere.usda" 94 | } 95 | dictionary renderSettings = { 96 | float3 "rtx:debugView:pixelDebug:textColor" = (0, 1e18, 0) 97 | int "rtx:externalFrameCounter" = 40 98 | float3 "rtx:fog:fogColor" = (0.75, 0.75, 0.75) 99 | float3 "rtx:index:regionOfInterestMax" = (0, 0, 0) 100 | float3 "rtx:index:regionOfInterestMin" = (0, 0, 0) 101 | float3 "rtx:iray:environment_dome_ground_position" = (0, 0, 0) 102 | float3 "rtx:iray:environment_dome_ground_reflectivity" = (0, 0, 0) 103 | float3 "rtx:iray:environment_dome_rotation_axis" = (3.4028235e38, 3.4028235e38, 3.4028235e38) 104 | float3 "rtx:post:backgroundZeroAlpha:backgroundDefaultColor" = (0, 0, 0) 105 | float3 "rtx:post:colorcorr:contrast" = (1, 1, 1) 106 | float3 "rtx:post:colorcorr:gain" = (1, 1, 1) 107 | float3 "rtx:post:colorcorr:gamma" = (1, 1, 1) 108 | float3 "rtx:post:colorcorr:offset" = (0, 0, 0) 109 | float3 "rtx:post:colorcorr:saturation" = (1, 1, 1) 110 | float3 "rtx:post:colorgrad:blackpoint" = (0, 0, 0) 111 | float3 "rtx:post:colorgrad:contrast" = (1, 1, 1) 112 | float3 "rtx:post:colorgrad:gain" = (1, 1, 1) 113 | float3 "rtx:post:colorgrad:gamma" = (1, 1, 1) 114 | float3 "rtx:post:colorgrad:lift" = (0, 0, 0) 115 | float3 "rtx:post:colorgrad:multiply" = (1, 1, 1) 116 | float3 "rtx:post:colorgrad:offset" = (0, 0, 0) 117 | float3 "rtx:post:colorgrad:whitepoint" = (1, 1, 1) 118 | float3 "rtx:post:lensDistortion:lensFocalLengthArray" = (10, 30, 50) 119 | float3 "rtx:post:lensFlares:anisoFlareFalloffX" = (450, 475, 500) 120 | float3 "rtx:post:lensFlares:anisoFlareFalloffY" = (10, 10, 10) 121 | float3 "rtx:post:lensFlares:cutoffPoint" = (2, 2, 2) 122 | float3 "rtx:post:lensFlares:haloFlareFalloff" = (10, 10, 10) 123 | float3 "rtx:post:lensFlares:haloFlareRadius" = (75, 75, 75) 124 | float3 "rtx:post:lensFlares:isotropicFlareFalloff" = (50, 50, 50) 125 | double "rtx:post:tonemap:filmIso" = 0 126 | float3 "rtx:post:tonemap:whitepoint" = (1, 1, 1) 127 | float3 "rtx:raytracing:inscattering:singleScatteringAlbedo" = (0.9, 0.9, 0.9) 128 | float3 "rtx:raytracing:inscattering:transmittanceColor" = (0.5, 0.5, 0.5) 129 | token "rtx:rendermode" = "PathTracing" 130 | float3 "rtx:sceneDb:ambientLightColor" = (0.1, 0.1, 0.1) 131 | } 132 | } 133 | ) 134 | -------------------------------------------------------------------------------- /time_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Runs time trials""" 4 | 5 | import argparse 6 | import datetime 7 | import inspect 8 | import os 9 | import subprocess 10 | import sys 11 | import traceback 12 | 13 | from typing import Dict, Iterable, List, NamedTuple, Tuple 14 | 15 | THIS_FILE = os.path.abspath(inspect.getsourcefile(lambda: None) or __file__) 16 | THIS_DIR = os.path.dirname(THIS_FILE) 17 | 18 | GEN_EMBREE = os.path.join(THIS_DIR, "genembree.py") 19 | DEFAULT_TEST_USDA = os.path.join(THIS_DIR, "usd", "test", "embree_test_01.usda") 20 | DEFAULT_OUTPUT_DIR = os.path.join(THIS_DIR, "renders", "test") 21 | 22 | ############################################################################### 23 | # Utilities 24 | ############################################################################### 25 | 26 | 27 | def is_ipython(): 28 | try: 29 | __IPYTHON__ # type: ignore 30 | except NameError: 31 | return False 32 | return True 33 | 34 | 35 | if sys.platform == "win32": 36 | to_shell_cmd = subprocess.list2cmdline 37 | else: 38 | 39 | def to_shell_cmd(cmd_list): 40 | import shlex 41 | 42 | return " ".join(shlex.quote(x) for x in cmd_list) 43 | 44 | 45 | ############################################################################### 46 | # Core functions 47 | ############################################################################### 48 | 49 | 50 | class FrameRange(NamedTuple): 51 | start: int 52 | end: int 53 | 54 | @property 55 | def num_frames(self): 56 | return self.end - self.start + 1 57 | 58 | def __str__(self): 59 | return f"{self.start}:{self.end}" 60 | 61 | 62 | class TrialInfo(NamedTuple): 63 | frames: FrameRange 64 | num_runs: int 65 | 66 | 67 | class TrialResult(NamedTuple): 68 | exitcode: int 69 | elapsed: datetime.timedelta 70 | args: List[str] 71 | 72 | 73 | class Average(NamedTuple): 74 | average: float 75 | num: int 76 | 77 | @classmethod 78 | def from_total(cls, total, num): 79 | average = total / num 80 | return cls(average=average, num=num) 81 | 82 | def __str__(self): 83 | return f"Average: {self.average} (over {self.num} results)" 84 | 85 | 86 | class Overhead(NamedTuple): 87 | overhead: float 88 | per_frame: float 89 | 90 | @classmethod 91 | def from_two_averages(cls, num_frames1: int, average1: float, num_frames2: int, average2: float): 92 | if num_frames1 == num_frames2: 93 | raise Exception("same num frames") 94 | 95 | per_frame = (average1 - average2) / (num_frames1 - num_frames2) 96 | overhead = average1 - num_frames1 * per_frame 97 | return cls(overhead=overhead, per_frame=per_frame) 98 | 99 | def __str__(self): 100 | return f"Overhead: {self.overhead} - Per-frame: {self.per_frame}" 101 | 102 | 103 | def run_trial(frames: FrameRange): 104 | print() 105 | print("-" * 60) 106 | args = [sys.executable, GEN_EMBREE, "-f", str(frames), "-i", DEFAULT_TEST_USDA, "-o", DEFAULT_OUTPUT_DIR] 107 | print(to_shell_cmd(args)) 108 | start = datetime.datetime.now() 109 | exitcode = subprocess.call(args) 110 | elapsed = datetime.datetime.now() - start 111 | return TrialResult(exitcode=exitcode, elapsed=elapsed, args=args) 112 | 113 | 114 | def run_trials(trials: Iterable[TrialInfo], stop_on_error=True): 115 | # frames -> list of results 116 | results: Dict[FrameRange, TrialResult] = {} 117 | 118 | num_failures = 0 119 | num_successes = 0 120 | total_frames = 0 121 | 122 | start = datetime.datetime.now() 123 | for trial in trials: 124 | results_for_frame = results.setdefault(trial.frames, []) 125 | for i in range(trial.num_runs): 126 | result = run_trial(trial.frames) 127 | results_for_frame.append(result) 128 | total_frames += trial.frames.num_frames 129 | if result.exitcode: 130 | print("!" * 80) 131 | print(f"ERROR running trial - exitcode: {result.exitcode}") 132 | print("!" * 80) 133 | num_failures += 1 134 | if stop_on_error: 135 | raise subprocess.CalledProcessError(result.exitcode, result.args, "", "") 136 | else: 137 | num_successes += 1 138 | print() 139 | print(f"Frames: {trial.frames} - Run {i + 1} / {trial.num_runs} - took: {result.elapsed}") 140 | elapsed = datetime.datetime.now() - start 141 | 142 | print(f"Ran {num_successes + num_failures} total renders, and {total_frames} total frames") 143 | print(f"Total elapsed time: {elapsed}") 144 | 145 | if not num_successes: 146 | print() 147 | print("!" * 80) 148 | print("!" * 80) 149 | print(f"No successful runs - had {num_failures} failures!") 150 | print("!" * 80) 151 | print("!" * 80) 152 | return 153 | 154 | print() 155 | print("=" * 80) 156 | print(f"Succesful runs: {num_successes} - Failures: {num_failures}") 157 | print() 158 | print_data(results) 159 | 160 | 161 | def print_data(results: Dict[FrameRange, TrialResult]): 162 | if not results: 163 | print("No results - cannot print anything") 164 | return 165 | 166 | averages = {} 167 | for frames, trial_results in results.items(): 168 | 169 | successes = [x for x in trial_results if x.exitcode == 0] 170 | if not successes: 171 | continue 172 | total_timedelta = sum((x.elapsed for x in successes), start=datetime.timedelta()) 173 | total_seconds = total_timedelta.total_seconds() 174 | average = Average.from_total(total_seconds, len(successes)) 175 | averages[frames] = average 176 | print(f"{tuple(frames)} - Avg: {average}") 177 | 178 | # want to estimate the how much startup overhead there is, and how much 179 | # per-frame time there is. To do that, we need results for two FrameRanges, 180 | # with an unequal number of total frames. 181 | 182 | frame_range_set = set(averages) 183 | 184 | # want to find the frame ranges with the biggest having the greatest frame 185 | # range, with the first tiebreaker being number of successful results we 186 | # got for that range 187 | def sort_key(x: FrameRange): 188 | average = averages[x] 189 | return (x.num_frames, average.num, average.average, x.start) 190 | 191 | print() 192 | frame_range_list = sorted(frame_range_set, key=sort_key) 193 | 194 | # first will have the largest possible number of frames 195 | frange1 = frame_range_list.pop() 196 | different = [x for x in frame_range_list if x.num_frames != frange1.num_frames] 197 | if not different: 198 | # couldn't find two frame ranges with unequal num_frames... can't 199 | # estimate startup + per-frame time 200 | return 201 | frange2 = different[-1] 202 | overhead = Overhead.from_two_averages( 203 | frange1.num_frames, 204 | averages[frange1].average, 205 | frange2.num_frames, 206 | averages[frange2].average, 207 | ) 208 | 209 | print(f"Estimated breakdown: {overhead}") 210 | print() 211 | 212 | 213 | ############################################################################### 214 | # CLI 215 | ############################################################################### 216 | 217 | 218 | def get_parser(): 219 | parser = argparse.ArgumentParser( 220 | description=__doc__, 221 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 222 | ) 223 | return parser 224 | 225 | 226 | def main(argv=None): 227 | if argv is None: 228 | argv = sys.argv[1:] 229 | parser = get_parser() 230 | args = parser.parse_args(argv) 231 | try: 232 | # hardcoding trials for now 233 | run_trials( 234 | # for testing: 235 | # [ 236 | # TrialInfo(frames=FrameRange(1, 1), num_runs=1), 237 | # TrialInfo(frames=FrameRange(1, 2), num_runs=1), 238 | # ] 239 | # for farily quick results 240 | [ 241 | TrialInfo(frames=FrameRange(1, 4), num_runs=5), 242 | TrialInfo(frames=FrameRange(1, 1), num_runs=5), 243 | ] 244 | ) 245 | except Exception: # pylint: disable=broad-except 246 | 247 | traceback.print_exc() 248 | return 1 249 | return 0 250 | 251 | 252 | if __name__ == "__main__" and not is_ipython(): 253 | sys.exit(main()) 254 | -------------------------------------------------------------------------------- /usd/ies_scale.usda: -------------------------------------------------------------------------------- 1 | #usda 1.0 2 | ( 3 | defaultPrim = "grid1" 4 | endTimeCode = 20 5 | framesPerSecond = 24 6 | metersPerUnit = 1 7 | startTimeCode = 1 8 | timeCodesPerSecond = 24 9 | upAxis = "Y" 10 | ) 11 | 12 | def Xform "cameras" 13 | { 14 | def Camera "camera1" ( 15 | prepend apiSchemas = ["HoudiniCameraPlateAPI", "HoudiniViewportGuideAPI"] 16 | ) 17 | { 18 | float2 clippingRange = (1, 1000000) 19 | float exposure = 0 20 | float focalLength = 0.5 21 | float focusDistance = 5 22 | float fStop = 0 23 | float horizontalAperture = 0.20955 24 | float horizontalApertureOffset = 0 25 | asset houdini:backgroundimage = @@ 26 | asset houdini:foregroundimage = @@ 27 | float houdini:guidescale = 1 28 | bool houdini:inviewermenu = 1 29 | token projection = "perspective" 30 | double shutter:close = 0.25 31 | double shutter:open = -0.25 32 | float verticalAperture = 0.20955 33 | float verticalApertureOffset = 0 34 | matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 30, 0, 1) ) 35 | uniform token[] xformOpOrder = ["xformOp:transform"] 36 | } 37 | } 38 | 39 | def Xform "grid1" ( 40 | prepend apiSchemas = ["MaterialBindingAPI"] 41 | kind = "component" 42 | ) 43 | { 44 | rel material:binding = 45 | matrix4d xformOp:transform:xform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) 46 | uniform token[] xformOpOrder = ["xformOp:transform:xform"] 47 | 48 | def Mesh "mesh_0" 49 | { 50 | float3[] extent = [(-5, 0, -5), (5, 0, 5)] 51 | int[] faceVertexCounts = [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] 52 | int[] faceVertexIndices = [0, 1, 11, 10, 1, 2, 12, 11, 2, 3, 13, 12, 3, 4, 14, 13, 4, 5, 15, 14, 5, 6, 16, 15, 6, 7, 17, 16, 7, 8, 18, 17, 8, 9, 19, 18, 10, 11, 21, 20, 11, 12, 22, 21, 12, 13, 23, 22, 13, 14, 24, 23, 14, 15, 25, 24, 15, 16, 26, 25, 16, 17, 27, 26, 17, 18, 28, 27, 18, 19, 29, 28, 20, 21, 31, 30, 21, 22, 32, 31, 22, 23, 33, 32, 23, 24, 34, 33, 24, 25, 35, 34, 25, 26, 36, 35, 26, 27, 37, 36, 27, 28, 38, 37, 28, 29, 39, 38, 30, 31, 41, 40, 31, 32, 42, 41, 32, 33, 43, 42, 33, 34, 44, 43, 34, 35, 45, 44, 35, 36, 46, 45, 36, 37, 47, 46, 37, 38, 48, 47, 38, 39, 49, 48, 40, 41, 51, 50, 41, 42, 52, 51, 42, 43, 53, 52, 43, 44, 54, 53, 44, 45, 55, 54, 45, 46, 56, 55, 46, 47, 57, 56, 47, 48, 58, 57, 48, 49, 59, 58, 50, 51, 61, 60, 51, 52, 62, 61, 52, 53, 63, 62, 53, 54, 64, 63, 54, 55, 65, 64, 55, 56, 66, 65, 56, 57, 67, 66, 57, 58, 68, 67, 58, 59, 69, 68, 60, 61, 71, 70, 61, 62, 72, 71, 62, 63, 73, 72, 63, 64, 74, 73, 64, 65, 75, 74, 65, 66, 76, 75, 66, 67, 77, 76, 67, 68, 78, 77, 68, 69, 79, 78, 70, 71, 81, 80, 71, 72, 82, 81, 72, 73, 83, 82, 73, 74, 84, 83, 74, 75, 85, 84, 75, 76, 86, 85, 76, 77, 87, 86, 77, 78, 88, 87, 78, 79, 89, 88, 80, 81, 91, 90, 81, 82, 92, 91, 82, 83, 93, 92, 83, 84, 94, 93, 84, 85, 95, 94, 85, 86, 96, 95, 86, 87, 97, 96, 87, 88, 98, 97, 88, 89, 99, 98] 53 | uniform token orientation = "leftHanded" 54 | point3f[] points = [(-5, 0, -5), (-3.8888888, 0, -5), (-2.7777777, 0, -5), (-1.6666665, 0, -5), (-0.55555534, 0, -5), (0.5555558, 0, -5), (1.666667, 0, -5), (2.7777781, 0, -5), (3.8888893, 0, -5), (5, 0, -5), (-5, 0, -3.8888888), (-3.8888888, 0, -3.8888888), (-2.7777777, 0, -3.8888888), (-1.6666665, 0, -3.8888888), (-0.55555534, 0, -3.8888888), (0.5555558, 0, -3.8888888), (1.666667, 0, -3.8888888), (2.7777781, 0, -3.8888888), (3.8888893, 0, -3.8888888), (5, 0, -3.8888888), (-5, 0, -2.7777777), (-3.8888888, 0, -2.7777777), (-2.7777777, 0, -2.7777777), (-1.6666665, 0, -2.7777777), (-0.55555534, 0, -2.7777777), (0.5555558, 0, -2.7777777), (1.666667, 0, -2.7777777), (2.7777781, 0, -2.7777777), (3.8888893, 0, -2.7777777), (5, 0, -2.7777777), (-5, 0, -1.6666665), (-3.8888888, 0, -1.6666665), (-2.7777777, 0, -1.6666665), (-1.6666665, 0, -1.6666665), (-0.55555534, 0, -1.6666665), (0.5555558, 0, -1.6666665), (1.666667, 0, -1.6666665), (2.7777781, 0, -1.6666665), (3.8888893, 0, -1.6666665), (5, 0, -1.6666665), (-5, 0, -0.55555534), (-3.8888888, 0, -0.55555534), (-2.7777777, 0, -0.55555534), (-1.6666665, 0, -0.55555534), (-0.55555534, 0, -0.55555534), (0.5555558, 0, -0.55555534), (1.666667, 0, -0.55555534), (2.7777781, 0, -0.55555534), (3.8888893, 0, -0.55555534), (5, 0, -0.55555534), (-5, 0, 0.5555558), (-3.8888888, 0, 0.5555558), (-2.7777777, 0, 0.5555558), (-1.6666665, 0, 0.5555558), (-0.55555534, 0, 0.5555558), (0.5555558, 0, 0.5555558), (1.666667, 0, 0.5555558), (2.7777781, 0, 0.5555558), (3.8888893, 0, 0.5555558), (5, 0, 0.5555558), (-5, 0, 1.666667), (-3.8888888, 0, 1.666667), (-2.7777777, 0, 1.666667), (-1.6666665, 0, 1.666667), (-0.55555534, 0, 1.666667), (0.5555558, 0, 1.666667), (1.666667, 0, 1.666667), (2.7777781, 0, 1.666667), (3.8888893, 0, 1.666667), (5, 0, 1.666667), (-5, 0, 2.7777781), (-3.8888888, 0, 2.7777781), (-2.7777777, 0, 2.7777781), (-1.6666665, 0, 2.7777781), (-0.55555534, 0, 2.7777781), (0.5555558, 0, 2.7777781), (1.666667, 0, 2.7777781), (2.7777781, 0, 2.7777781), (3.8888893, 0, 2.7777781), (5, 0, 2.7777781), (-5, 0, 3.8888893), (-3.8888888, 0, 3.8888893), (-2.7777777, 0, 3.8888893), (-1.6666665, 0, 3.8888893), (-0.55555534, 0, 3.8888893), (0.5555558, 0, 3.8888893), (1.666667, 0, 3.8888893), (2.7777781, 0, 3.8888893), (3.8888893, 0, 3.8888893), (5, 0, 3.8888893), (-5, 0, 5), (-3.8888888, 0, 5), (-2.7777777, 0, 5), (-1.6666665, 0, 5), (-0.55555534, 0, 5), (0.5555558, 0, 5), (1.666667, 0, 5), (2.7777781, 0, 5), (3.8888893, 0, 5), (5, 0, 5)] ( 55 | interpolation = "vertex" 56 | ) 57 | uniform token subdivisionScheme = "none" 58 | } 59 | } 60 | 61 | def Scope "materials" 62 | { 63 | def Material "usdpreviewsurface1" 64 | { 65 | token outputs:displacement.connect = 66 | token outputs:surface.connect = 67 | 68 | def Shader "usdpreviewsurface1" 69 | { 70 | uniform token info:id = "UsdPreviewSurface" 71 | color3f inputs:diffuseColor = (1, 1, 1) 72 | float inputs:ior = 1 73 | int inputs:useSpecularWorkflow = 1 74 | token outputs:displacement 75 | token outputs:surface 76 | } 77 | } 78 | } 79 | 80 | def Scope "Render" 81 | { 82 | def RenderSettings "Settings" ( 83 | prepend apiSchemas = ["KarmaRenderSettingsAPI"] 84 | ) 85 | { 86 | custom int arnold:global:AA_samples = 6 87 | custom bool arnold:global:enable_progressive_render = 1 88 | custom int arnold:global:GI_total_depth = 0 89 | rel camera = 90 | float4 dataWindowNDC = (0, 0, 1, 1) 91 | token[] includedPurposes = ["default", "render"] 92 | bool instantaneousShutter = 1 93 | int karma:global:pathtracedsamples = 64 94 | int karma:global:samplesperpixel = 9 95 | custom float karma:object:diffuselimit = 0 96 | custom float karma:object:reflectlimit = 0 97 | custom float karma:object:refractlimit = 0 98 | custom float karma:object:ssslimit = 0 99 | token[] materialBindingPurposes = ["full", "allPurpose"] 100 | float pixelAspectRatio = 1 101 | int2 resolution = (512, 512) 102 | custom string ri:integrator:name = "PxrDirectLighting" 103 | } 104 | } 105 | 106 | def Xform "lights" 107 | { 108 | def SphereLight "ies_scale_light" ( 109 | prepend apiSchemas = ["ShapingAPI", "HoudiniViewportGuideAPI"] 110 | ) 111 | { 112 | bool houdini:inviewermenu = 1 113 | color3f inputs:color = (1, 1, 1) 114 | float inputs:diffuse = 1 115 | bool inputs:enableColorTemperature = 0 116 | float inputs:exposure = 5.4 117 | float inputs:intensity = 1 118 | bool inputs:normalize = 0 119 | float inputs:radius = 0.05 120 | float inputs:shaping:ies:angleScale.timeSamples = { 121 | 1: -1, 122 | 2: -0.9765559, 123 | 3: -0.91177285, 124 | 4: -0.81397456, 125 | 5: -0.691485, 126 | 6: -0.5526278, 127 | 7: -0.4057269, 128 | 8: -0.25910613, 129 | 9: -0.121089205, 130 | 10: 0, 131 | 11: 0.11697911, 132 | 12: 0.24460895, 133 | 13: 0.3774806, 134 | 14: 0.51018506, 135 | 15: 0.6373134, 136 | 16: 0.7534567, 137 | 17: 0.853206, 138 | 18: 0.9311522, 139 | 19: 0.98188657, 140 | 20: 1, 141 | } 142 | asset inputs:shaping:ies:file = @../bega_84693k4.ies@ 143 | bool inputs:shaping:ies:normalize = 0 144 | float inputs:specular = 1 145 | rel light:filters = None 146 | matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, -0.01613485813140869, 0.6823078393936157, 1) ) 147 | uniform token[] xformOpOrder = ["xformOp:transform"] 148 | } 149 | } 150 | 151 | -------------------------------------------------------------------------------- /genhoudini.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env hython 2 | 3 | """Render all lights for all renderers in luxtest.hip""" 4 | 5 | import argparse 6 | import inspect 7 | import os 8 | import re 9 | import sys 10 | import traceback 11 | 12 | from typing import Iterable, Optional, Tuple 13 | 14 | ############################################################################### 15 | # Constants 16 | ############################################################################### 17 | 18 | THIS_FILE = os.path.abspath(inspect.getsourcefile(lambda: None) or __file__) 19 | THIS_DIR = os.path.dirname(THIS_FILE) 20 | 21 | if THIS_DIR not in sys.path: 22 | sys.path.append(THIS_DIR) 23 | 24 | import combine_ies_test_images 25 | import genLightParamDescriptions 26 | import luxtest_hou_utils 27 | 28 | from genLightParamDescriptions import FrameRange 29 | 30 | LUXTEST_HIP = os.path.join(THIS_DIR, "luxtest.hip") 31 | HUSK_PRE_RENDER = os.path.join(THIS_DIR, "husk_pre_render.py") 32 | 33 | HOUDINI_ATTR_RE = re.compile(r"""^\s*[A-Za-z_][A-Za-z_0-9]* houdini:[A-Za-z_][A-Za-z_0-9:]*.*""") 34 | 35 | # if we can't read light_descriptions, use this 36 | FALLBACK_LIGHTS = ( 37 | "cylinder", 38 | "disk", 39 | "distant", 40 | "dome", 41 | "rect", 42 | "sphere", 43 | "visibleRect", 44 | ) 45 | 46 | ############################################################################### 47 | # Utilities 48 | ############################################################################### 49 | 50 | 51 | def is_ipython(): 52 | try: 53 | __IPYTHON__ # type: ignore 54 | except NameError: 55 | return False 56 | return True 57 | 58 | 59 | def filter_lights(rop_nodes: Iterable["hou.Node"], lights: Iterable[str]): 60 | if not lights: 61 | return rop_nodes 62 | light_suffixes = tuple(f"_{l}" for l in lights) 63 | return [x for x in rop_nodes if x.name().endswith(light_suffixes)] 64 | 65 | 66 | def filter_renderers(rop_nodes: Iterable["hou.Node"], renderers: Iterable[str]): 67 | renderers = set(renderers) 68 | if not renderers: 69 | return rop_nodes 70 | 71 | return [x for x in rop_nodes if luxtest_hou_utils.get_renderer(x) in renderers] 72 | 73 | 74 | ############################################################################### 75 | # Core functions 76 | ############################################################################### 77 | 78 | 79 | def render_luxtest( 80 | hip_path=LUXTEST_HIP, 81 | renderers: Optional[Iterable[str]] = None, 82 | lights: Optional[Iterable[str]] = None, 83 | frame_range: Optional[FrameRange] = None, 84 | images=True, 85 | usd=True, 86 | ): 87 | import hou 88 | 89 | print(f"Loading: {hip_path}") 90 | hou.hipFile.load(hip_path) 91 | 92 | renderers = list(renderers) if renderers else [] 93 | lights = list(lights) if lights else [] 94 | 95 | if usd: 96 | output_usd(lights=lights) 97 | if images: 98 | render_images(renderers=renderers, lights=lights, frame_range=frame_range) 99 | 100 | 101 | def output_usd(lights: Iterable[str] = ()): 102 | import hou 103 | 104 | usdrop_type = hou.lopNodeTypeCategory().nodeType("usd_rop") 105 | rop_nodes = [x for x in hou.node("/stage").allSubChildren() if x.type() == usdrop_type] 106 | rop_nodes = filter_lights(rop_nodes, lights) 107 | rop_nodes.sort(key=lambda node: node.name()) 108 | num_rops = len(rop_nodes) 109 | print() 110 | print("=" * 80) 111 | print(f"Found {num_rops} usd ROP nodes:") 112 | for rop_node in rop_nodes: 113 | print(rop_node.name()) 114 | print("=" * 80) 115 | print() 116 | 117 | for i, rop_node in enumerate(rop_nodes): 118 | print(f"Outputing USD node {i + 1}/{num_rops}: {rop_node.name()}") 119 | rop_node.render() 120 | 121 | # strip out houdini-specific attributes 122 | outpath = rop_node.parm("lopoutput").eval() 123 | with open(outpath, "r", encoding="utf8") as reader: 124 | lines = reader.readlines() 125 | newlines = [x for x in lines if not HOUDINI_ATTR_RE.match(x)] 126 | if len(newlines) != len(lines): 127 | with open(outpath, "w", encoding="utf8", newline="\n") as writer: 128 | writer.writelines(newlines) 129 | 130 | 131 | def render_images( 132 | renderers: Iterable[str] = (), 133 | lights: Iterable[str] = (), 134 | frame_range: Optional[FrameRange] = None, 135 | ): 136 | import hou 137 | 138 | usdrender_type = hou.lopNodeTypeCategory().nodeType("usdrender_rop") 139 | 140 | rop_nodes = [x for x in hou.node("/stage").allSubChildren() if x.type() == usdrender_type] 141 | rop_nodes = filter_lights(rop_nodes, lights) 142 | rop_nodes = filter_renderers(rop_nodes, renderers) 143 | rop_nodes.sort(key=lambda node: node.name()) 144 | num_rops = len(rop_nodes) 145 | print() 146 | print("=" * 80) 147 | print(f"Found {num_rops} usdrender ROP nodes:") 148 | for rop_node in rop_nodes: 149 | print(rop_node.name()) 150 | print("=" * 80) 151 | print() 152 | 153 | render_kwargs = { 154 | "output_progress": True, 155 | "verbose": True, 156 | } 157 | if frame_range is not None: 158 | render_kwargs["frame_range"] = tuple(frame_range) 159 | 160 | ies_renderer_count = {} 161 | for i, rop_node in enumerate(rop_nodes): 162 | print() 163 | print("=" * 80) 164 | print(f"Rendering node {i + 1}/{num_rops}: {rop_node.path()}") 165 | print() 166 | 167 | rop_node.parm("husk_prerender").set(HUSK_PRE_RENDER) 168 | rop_node.render(**render_kwargs) 169 | 170 | light_name = rop_node.name().rsplit("_", 1)[-1] 171 | 172 | # auto-combine iesTest images if this is the second time we're render 173 | # an iesTest image for this renderer (which should mean it's the 174 | # second camera) 175 | if light_name == "iesTest": 176 | renderer = luxtest_hou_utils.get_renderer(rop_node) 177 | new_renderer_count = ies_renderer_count.get(renderer, 0) + 1 178 | ies_renderer_count[renderer] = new_renderer_count 179 | if new_renderer_count == 2: 180 | combine_ies_test_images.combine_ies_test_images(renderers=[renderer]) 181 | elif new_renderer_count > 2: 182 | raise RuntimeError("Unexpected result - should only have 2 cameras per iesTest per renderer") 183 | 184 | 185 | ############################################################################### 186 | # CLI 187 | ############################################################################### 188 | 189 | 190 | def get_parser(): 191 | parser = argparse.ArgumentParser( 192 | description=__doc__, 193 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 194 | ) 195 | 196 | try: 197 | light_descriptions = genLightParamDescriptions.read_descriptions() 198 | light_names = sorted(light_descriptions) 199 | except Exception as err: 200 | print("Error reading light names from light_descriptions.json:") 201 | print(err) 202 | print("...using fallback light names") 203 | light_names = FALLBACK_LIGHTS 204 | 205 | parser.add_argument( 206 | "hip_file", 207 | nargs="?", 208 | default=LUXTEST_HIP, 209 | help="path to the .hip file to render", 210 | ) 211 | parser.add_argument("--no-images", dest="images", action="store_false", help="Disable rendering output images") 212 | parser.add_argument("--no-usd", dest="usd", action="store_false", help="Disable writing out usda files") 213 | parser.add_argument( 214 | "-r", 215 | "--renderer", 216 | choices=("karma", "ris", "arnold"), 217 | action="append", 218 | dest="renderers", 219 | help=( 220 | "Only render images for the given renderer; if not" 221 | " specified, render images for all renderers. May be" 222 | " repeated." 223 | ), 224 | ) 225 | parser.add_argument( 226 | "-l", 227 | "--light", 228 | choices=light_names, 229 | action="append", 230 | dest="lights", 231 | help=( 232 | "Only render images for the given lights; if not specified, render images for all lights. May be repeated." 233 | ), 234 | ) 235 | parser.add_argument( 236 | "-f", 237 | "--frames", 238 | type=FrameRange.from_str, 239 | help=( 240 | "Only render the given frame or frame range; may be single digit, or inclusive range specified as start:end" 241 | ), 242 | ) 243 | return parser 244 | 245 | 246 | def main(argv=None): 247 | if argv is None: 248 | argv = sys.argv[1:] 249 | parser = get_parser() 250 | args = parser.parse_args(argv) 251 | try: 252 | render_luxtest( 253 | args.hip_file, 254 | lights=args.lights, 255 | renderers=args.renderers, 256 | frame_range=args.frames, 257 | images=args.images, 258 | usd=args.usd, 259 | ) 260 | except Exception: # pylint: disable=broad-except 261 | 262 | traceback.print_exc() 263 | return 1 264 | return 0 265 | 266 | 267 | if __name__ == "__main__" and not is_ipython(): 268 | sys.exit(main()) 269 | -------------------------------------------------------------------------------- /usd/dome.usda: -------------------------------------------------------------------------------- 1 | #usda 1.0 2 | ( 3 | customLayerData = { 4 | dictionary MovieCaptureSettings = { 5 | double animation_fps = 24 6 | int batch_count = 1 7 | string camera_name = "/cameras/camera1" 8 | bool capture_application = 0 9 | int capture_every_nth_frames = 20 10 | bool capture_every_nth_frames_checked = 0 11 | int capture_frame_end = 1 12 | int capture_frame_start = 1 13 | string capture_name = "dome-rtx" 14 | string capture_range = "Custom Range - Frames" 15 | double capture_time_end = 10 16 | double capture_time_start = 0 17 | string exr_compression_method = "zips" 18 | double fps = 24 19 | bool generate_shader_cache = 0 20 | bool hdr_for_exr_checked = 1 21 | bool hdr_for_exr_visible = 0 22 | int iray_pathtrace_spp = 1 23 | int iray_subframes_per_frame = 32 24 | string movie_type = "Sequence" 25 | int mp4_encoding_bitrate = 16777216 26 | int mp4_encoding_iframe_interval = 60 27 | string mp4_encoding_preset = "PRESET_DEFAULT" 28 | string mp4_encoding_profile = "H264_PROFILE_HIGH" 29 | string mp4_encoding_rc_mode = "RC_VBR" 30 | int mp4_encoding_rc_target_quality = 0 31 | bool mp4_encoding_video_full_range_flag = 0 32 | string output_format = ".exr" 33 | string output_path = "./renders/rtx" 34 | bool overwrite_existing_frame_checked = 0 35 | bool pathtrace_mb_checked = 0 36 | double pathtrace_mb_frame_shutter_close = 0.5 37 | double pathtrace_mb_frame_shutter_open = 0 38 | int pathtrace_mb_subframes = 64 39 | int pathtrace_spp_per_iteration_mgpu = 1 40 | int pathtrace_spp_per_subframe = 128 41 | string queue_instance = "localhost Queue" 42 | int realtime_settle_latency = 0 43 | string render_preset = "PathTracing" 44 | bool render_style = 1 45 | bool renumber_negtive_frames_checked = 0 46 | int resolution_aspect_ratio_selected = 0 47 | string resolution_aspect_ratios = '["1.000:1", "16:9", "4:3"]' 48 | int resolution_height = 512 49 | string resolution_type = "Custom" 50 | bool resolution_w_h_linked = 0 51 | int resolution_width = 512 52 | int run_n_frames_before_start = 20 53 | bool run_n_frames_before_start_checked = 0 54 | bool save_alpha_checked = 0 55 | bool skip_upload_to_s3 = 0 56 | int start_delay_seconds = 10 57 | double sunstudy_end = 18 58 | int sunstudy_movie_minutes = 1 59 | int sunstudy_movie_seconds = 1 60 | double sunstudy_start = 6 61 | string task_comment = "" 62 | string task_priority = "" 63 | string task_type = "create-render" 64 | bool upload_to_s3 = 0 65 | } 66 | dictionary cameraSettings = { 67 | dictionary Front = { 68 | double3 position = (0, 0, 500) 69 | double radius = 500 70 | } 71 | dictionary Perspective = { 72 | double3 position = (500, 500, 500) 73 | double3 target = (-0.00000397803842133726, 0.000007956076785831101, -0.000003978038307650422) 74 | } 75 | dictionary Right = { 76 | double3 position = (-500, 0, 0) 77 | double radius = 500 78 | } 79 | dictionary Top = { 80 | double3 position = (0, 500, 0) 81 | double radius = 500 82 | } 83 | string boundCamera = "/cameras/camera1" 84 | } 85 | dictionary navmeshSettings = { 86 | double agentHeight = 180 87 | double agentRadius = 20 88 | bool excludeRigidBodies = 1 89 | int ver = 1 90 | double voxelCeiling = 460 91 | } 92 | dictionary omni_layer = { 93 | string authoring_layer = "./dome.usda" 94 | } 95 | dictionary renderSettings = { 96 | float3 "rtx:debugView:pixelDebug:textColor" = (0, 1e18, 0) 97 | int "rtx:externalFrameCounter" = 1 98 | float3 "rtx:fog:fogColor" = (0.75, 0.75, 0.75) 99 | float3 "rtx:index:regionOfInterestMax" = (0, 0, 0) 100 | float3 "rtx:index:regionOfInterestMin" = (0, 0, 0) 101 | float3 "rtx:iray:environment_dome_ground_position" = (0, 0, 0) 102 | float3 "rtx:iray:environment_dome_ground_reflectivity" = (0, 0, 0) 103 | float3 "rtx:iray:environment_dome_rotation_axis" = (3.4028235e38, 3.4028235e38, 3.4028235e38) 104 | float3 "rtx:post:backgroundZeroAlpha:backgroundDefaultColor" = (0, 0, 0) 105 | float3 "rtx:post:colorcorr:contrast" = (1, 1, 1) 106 | float3 "rtx:post:colorcorr:gain" = (1, 1, 1) 107 | float3 "rtx:post:colorcorr:gamma" = (1, 1, 1) 108 | float3 "rtx:post:colorcorr:offset" = (0, 0, 0) 109 | float3 "rtx:post:colorcorr:saturation" = (1, 1, 1) 110 | float3 "rtx:post:colorgrad:blackpoint" = (0, 0, 0) 111 | float3 "rtx:post:colorgrad:contrast" = (1, 1, 1) 112 | float3 "rtx:post:colorgrad:gain" = (1, 1, 1) 113 | float3 "rtx:post:colorgrad:gamma" = (1, 1, 1) 114 | float3 "rtx:post:colorgrad:lift" = (0, 0, 0) 115 | float3 "rtx:post:colorgrad:multiply" = (1, 1, 1) 116 | float3 "rtx:post:colorgrad:offset" = (0, 0, 0) 117 | float3 "rtx:post:colorgrad:whitepoint" = (1, 1, 1) 118 | float3 "rtx:post:lensDistortion:lensFocalLengthArray" = (10, 30, 50) 119 | float3 "rtx:post:lensFlares:anisoFlareFalloffX" = (450, 475, 500) 120 | float3 "rtx:post:lensFlares:anisoFlareFalloffY" = (10, 10, 10) 121 | float3 "rtx:post:lensFlares:cutoffPoint" = (2, 2, 2) 122 | float3 "rtx:post:lensFlares:haloFlareFalloff" = (10, 10, 10) 123 | float3 "rtx:post:lensFlares:haloFlareRadius" = (75, 75, 75) 124 | float3 "rtx:post:lensFlares:isotropicFlareFalloff" = (50, 50, 50) 125 | double "rtx:post:tonemap:filmIso" = 0 126 | float3 "rtx:post:tonemap:whitepoint" = (1, 1, 1) 127 | float3 "rtx:raytracing:inscattering:singleScatteringAlbedo" = (0.9, 0.9, 0.9) 128 | float3 "rtx:raytracing:inscattering:transmittanceColor" = (0.5, 0.5, 0.5) 129 | string "rtx:rendermode" = "PathTracing" 130 | float3 "rtx:sceneDb:ambientLightColor" = (0.1, 0.1, 0.1) 131 | } 132 | } 133 | endTimeCode = 1 134 | framesPerSecond = 24 135 | metersPerUnit = 1 136 | startTimeCode = 1 137 | timeCodesPerSecond = 24 138 | upAxis = "Y" 139 | ) 140 | 141 | def Scope "Render" 142 | { 143 | def RenderSettings "Settings" ( 144 | prepend apiSchemas = ["KarmaRenderSettingsAPI"] 145 | ) 146 | { 147 | custom int arnold:global:AA_samples = 6 148 | custom bool arnold:global:enable_progressive_render = 1 149 | custom int arnold:global:GI_total_depth = 0 150 | rel camera = 151 | float4 dataWindowNDC = (0, 0, 1, 1) 152 | token[] includedPurposes = ["default", "render"] 153 | bool instantaneousShutter = 1 154 | int karma:global:pathtracedsamples = 64 155 | int karma:global:samplesperpixel = 9 156 | custom float karma:object:diffuselimit = 0 157 | custom float karma:object:reflectlimit = 0 158 | custom float karma:object:refractlimit = 0 159 | custom float karma:object:ssslimit = 0 160 | token[] materialBindingPurposes = ["full", "allPurpose"] 161 | float pixelAspectRatio = 1 162 | int2 resolution = (512, 512) 163 | custom string ri:integrator:name = "PxrDirectLighting" 164 | } 165 | } 166 | 167 | def Scope "materials" 168 | { 169 | def Material "usdpreviewsurface1" 170 | { 171 | token outputs:displacement.connect = 172 | token outputs:surface.connect = 173 | 174 | def Shader "usdpreviewsurface1" 175 | { 176 | uniform token info:id = "UsdPreviewSurface" 177 | color3f inputs:diffuseColor = (1, 1, 1) 178 | float inputs:ior = 1 179 | int inputs:useSpecularWorkflow = 1 180 | token outputs:displacement 181 | token outputs:surface 182 | } 183 | } 184 | } 185 | 186 | def Xform "cameras" 187 | { 188 | def Camera "camera1" ( 189 | prepend apiSchemas = ["HoudiniCameraPlateAPI", "HoudiniViewportGuideAPI"] 190 | ) 191 | { 192 | float2 clippingRange = (1, 1000000) 193 | float exposure = 0 194 | float focalLength = 0.5 195 | float focusDistance = 5 196 | float fStop = 0 197 | float horizontalAperture = 0.20955 198 | float horizontalApertureOffset = 0 199 | token projection = "perspective" 200 | double shutter:close = 0.25 201 | double shutter:open = -0.25 202 | float verticalAperture = 0.20955 203 | float verticalApertureOffset = 0 204 | matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 30, 0, 1) ) 205 | matrix4d xformOp:transform:xform1 = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) 206 | uniform token[] xformOpOrder = ["xformOp:transform:xform1"] 207 | } 208 | } 209 | 210 | def Xform "lights" 211 | { 212 | def DomeLight "dome_light" ( 213 | prepend apiSchemas = ["KarmaLightAPI", "HoudiniViewportGuideAPI", "LightAPI"] 214 | ) 215 | { 216 | custom rel filters = None 217 | color3f inputs:color = (1, 1, 1) 218 | float inputs:colorTemperature = 6500 219 | float inputs:diffuse = 1 220 | bool inputs:enableColorTemperature = 0 221 | float inputs:exposure = 0 222 | float inputs:intensity = 1 223 | bool inputs:normalize = 0 224 | float inputs:specular = 1 225 | asset inputs:texture:file = @../dome_grid_2k.exr@ 226 | token inputs:texture:format = "latlong" 227 | bool karma:light:renderlightgeo = 1 228 | rel light:filters = None 229 | rel portals = None 230 | matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) 231 | uniform token[] xformOpOrder = ["xformOp:transform"] 232 | } 233 | } 234 | 235 | -------------------------------------------------------------------------------- /usd/visibleRect.usda: -------------------------------------------------------------------------------- 1 | #usda 1.0 2 | ( 3 | customLayerData = { 4 | dictionary MovieCaptureSettings = { 5 | double animation_fps = 24 6 | int batch_count = 1 7 | string camera_name = "/cameras/camera1" 8 | bool capture_application = 0 9 | int capture_every_nth_frames = 20 10 | bool capture_every_nth_frames_checked = 0 11 | int capture_frame_end = 1 12 | int capture_frame_start = 1 13 | string capture_name = "visibleRect-rtx" 14 | string capture_range = "Custom Range - Frames" 15 | double capture_time_end = 10 16 | double capture_time_start = 0 17 | string exr_compression_method = "zips" 18 | double fps = 24 19 | bool generate_shader_cache = 0 20 | bool hdr_for_exr_checked = 1 21 | bool hdr_for_exr_visible = 0 22 | int iray_pathtrace_spp = 1 23 | int iray_subframes_per_frame = 32 24 | string movie_type = "Sequence" 25 | int mp4_encoding_bitrate = 16777216 26 | int mp4_encoding_iframe_interval = 60 27 | string mp4_encoding_preset = "PRESET_DEFAULT" 28 | string mp4_encoding_profile = "H264_PROFILE_HIGH" 29 | string mp4_encoding_rc_mode = "RC_VBR" 30 | int mp4_encoding_rc_target_quality = 0 31 | bool mp4_encoding_video_full_range_flag = 0 32 | string output_format = ".exr" 33 | string output_path = "./renders/rtx" 34 | bool overwrite_existing_frame_checked = 0 35 | bool pathtrace_mb_checked = 0 36 | double pathtrace_mb_frame_shutter_close = 0.5 37 | double pathtrace_mb_frame_shutter_open = 0 38 | int pathtrace_mb_subframes = 64 39 | int pathtrace_spp_per_iteration_mgpu = 1 40 | int pathtrace_spp_per_subframe = 128 41 | string queue_instance = "localhost Queue" 42 | int realtime_settle_latency = 0 43 | string render_preset = "PathTracing" 44 | bool render_style = 1 45 | bool renumber_negtive_frames_checked = 0 46 | int resolution_aspect_ratio_selected = 0 47 | string resolution_aspect_ratios = '["1.000:1", "16:9", "4:3"]' 48 | int resolution_height = 512 49 | string resolution_type = "Custom" 50 | bool resolution_w_h_linked = 0 51 | int resolution_width = 512 52 | int run_n_frames_before_start = 20 53 | bool run_n_frames_before_start_checked = 0 54 | bool save_alpha_checked = 0 55 | bool skip_upload_to_s3 = 0 56 | int start_delay_seconds = 10 57 | double sunstudy_end = 18 58 | int sunstudy_movie_minutes = 1 59 | int sunstudy_movie_seconds = 1 60 | double sunstudy_start = 6 61 | string task_comment = "" 62 | string task_priority = "" 63 | string task_type = "create-render" 64 | bool upload_to_s3 = 0 65 | } 66 | dictionary cameraSettings = { 67 | dictionary Front = { 68 | double3 position = (0, 0, 500) 69 | double radius = 500 70 | } 71 | dictionary Perspective = { 72 | double3 position = (500, 500, 500) 73 | double3 target = (-0.00000397803842133726, 0.000007956076785831101, -0.000003978038307650422) 74 | } 75 | dictionary Right = { 76 | double3 position = (-500, 0, 0) 77 | double radius = 500 78 | } 79 | dictionary Top = { 80 | double3 position = (0, 500, 0) 81 | double radius = 500 82 | } 83 | string boundCamera = "/cameras/camera1" 84 | } 85 | dictionary navmeshSettings = { 86 | double agentHeight = 180 87 | double agentRadius = 20 88 | bool excludeRigidBodies = 1 89 | int ver = 1 90 | double voxelCeiling = 460 91 | } 92 | dictionary omni_layer = { 93 | string authoring_layer = "./visibleRect.usda" 94 | } 95 | dictionary renderSettings = { 96 | float3 "rtx:debugView:pixelDebug:textColor" = (0, 1e18, 0) 97 | int "rtx:externalFrameCounter" = 1 98 | float3 "rtx:fog:fogColor" = (0.75, 0.75, 0.75) 99 | float3 "rtx:index:regionOfInterestMax" = (0, 0, 0) 100 | float3 "rtx:index:regionOfInterestMin" = (0, 0, 0) 101 | float3 "rtx:iray:environment_dome_ground_position" = (0, 0, 0) 102 | float3 "rtx:iray:environment_dome_ground_reflectivity" = (0, 0, 0) 103 | float3 "rtx:iray:environment_dome_rotation_axis" = (3.4028235e38, 3.4028235e38, 3.4028235e38) 104 | float3 "rtx:post:backgroundZeroAlpha:backgroundDefaultColor" = (0, 0, 0) 105 | float3 "rtx:post:colorcorr:contrast" = (1, 1, 1) 106 | float3 "rtx:post:colorcorr:gain" = (1, 1, 1) 107 | float3 "rtx:post:colorcorr:gamma" = (1, 1, 1) 108 | float3 "rtx:post:colorcorr:offset" = (0, 0, 0) 109 | float3 "rtx:post:colorcorr:saturation" = (1, 1, 1) 110 | float3 "rtx:post:colorgrad:blackpoint" = (0, 0, 0) 111 | float3 "rtx:post:colorgrad:contrast" = (1, 1, 1) 112 | float3 "rtx:post:colorgrad:gain" = (1, 1, 1) 113 | float3 "rtx:post:colorgrad:gamma" = (1, 1, 1) 114 | float3 "rtx:post:colorgrad:lift" = (0, 0, 0) 115 | float3 "rtx:post:colorgrad:multiply" = (1, 1, 1) 116 | float3 "rtx:post:colorgrad:offset" = (0, 0, 0) 117 | float3 "rtx:post:colorgrad:whitepoint" = (1, 1, 1) 118 | float3 "rtx:post:lensDistortion:lensFocalLengthArray" = (10, 30, 50) 119 | float3 "rtx:post:lensFlares:anisoFlareFalloffX" = (450, 475, 500) 120 | float3 "rtx:post:lensFlares:anisoFlareFalloffY" = (10, 10, 10) 121 | float3 "rtx:post:lensFlares:cutoffPoint" = (2, 2, 2) 122 | float3 "rtx:post:lensFlares:haloFlareFalloff" = (10, 10, 10) 123 | float3 "rtx:post:lensFlares:haloFlareRadius" = (75, 75, 75) 124 | float3 "rtx:post:lensFlares:isotropicFlareFalloff" = (50, 50, 50) 125 | double "rtx:post:tonemap:filmIso" = 0 126 | float3 "rtx:post:tonemap:whitepoint" = (1, 1, 1) 127 | float3 "rtx:raytracing:inscattering:singleScatteringAlbedo" = (0.9, 0.9, 0.9) 128 | float3 "rtx:raytracing:inscattering:transmittanceColor" = (0.5, 0.5, 0.5) 129 | string "rtx:rendermode" = "PathTracing" 130 | float3 "rtx:sceneDb:ambientLightColor" = (0.1, 0.1, 0.1) 131 | } 132 | } 133 | endTimeCode = 1 134 | framesPerSecond = 24 135 | metersPerUnit = 1 136 | startTimeCode = 1 137 | timeCodesPerSecond = 24 138 | upAxis = "Y" 139 | ) 140 | 141 | def Scope "Render" 142 | { 143 | def RenderSettings "Settings" ( 144 | prepend apiSchemas = ["KarmaRenderSettingsAPI"] 145 | ) 146 | { 147 | custom int arnold:global:AA_samples = 6 148 | custom bool arnold:global:enable_progressive_render = 1 149 | custom int arnold:global:GI_total_depth = 0 150 | rel camera = 151 | float4 dataWindowNDC = (0, 0, 1, 1) 152 | token[] includedPurposes = ["default", "render"] 153 | bool instantaneousShutter = 1 154 | int karma:global:pathtracedsamples = 64 155 | int karma:global:samplesperpixel = 9 156 | custom float karma:object:diffuselimit = 0 157 | custom float karma:object:reflectlimit = 0 158 | custom float karma:object:refractlimit = 0 159 | custom float karma:object:ssslimit = 0 160 | token[] materialBindingPurposes = ["full", "allPurpose"] 161 | float pixelAspectRatio = 1 162 | int2 resolution = (512, 512) 163 | custom string ri:integrator:name = "PxrDirectLighting" 164 | } 165 | } 166 | 167 | def Scope "materials" 168 | { 169 | def Material "usdpreviewsurface1" 170 | { 171 | token outputs:displacement.connect = 172 | token outputs:surface.connect = 173 | 174 | def Shader "usdpreviewsurface1" 175 | { 176 | uniform token info:id = "UsdPreviewSurface" 177 | color3f inputs:diffuseColor = (1, 1, 1) 178 | float inputs:ior = 1 179 | int inputs:useSpecularWorkflow = 1 180 | token outputs:displacement 181 | token outputs:surface 182 | } 183 | } 184 | } 185 | 186 | def Xform "cameras" 187 | { 188 | def Camera "camera1" ( 189 | prepend apiSchemas = ["HoudiniCameraPlateAPI", "HoudiniViewportGuideAPI"] 190 | ) 191 | { 192 | float2 clippingRange = (1, 1000000) 193 | float exposure = 0 194 | float focalLength = 0.5 195 | float focusDistance = 5 196 | float fStop = 0 197 | float horizontalAperture = 0.20955 198 | float horizontalApertureOffset = 0 199 | token projection = "perspective" 200 | double shutter:close = 0.25 201 | double shutter:open = -0.25 202 | float verticalAperture = 0.20955 203 | float verticalApertureOffset = 0 204 | matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 30, 0, 1) ) 205 | matrix4d xformOp:transform:xform1 = ( (-1, 0, -0, 0), (0, 1, -0, 0), (0, 0, -1, 0), (0, 0, -3, 1) ) 206 | uniform token[] xformOpOrder = ["xformOp:transform:xform1"] 207 | } 208 | } 209 | 210 | def Xform "lights" 211 | { 212 | def RectLight "visibleRect_light" ( 213 | prepend apiSchemas = ["HoudiniViewportLightAPI", "KarmaLightAPI", "HoudiniViewportGuideAPI", "LightAPI"] 214 | ) 215 | { 216 | float3[] extent = [(-0.5, -0.5, -0), (0.5, 0.5, 0)] 217 | color3f inputs:color = (1, 1, 1) 218 | float inputs:colorTemperature = 6500 219 | float inputs:diffuse = 1 220 | bool inputs:enableColorTemperature = 0 221 | float inputs:exposure = 0 222 | float inputs:height = 1 223 | float inputs:intensity = 1 224 | bool inputs:normalize = 0 225 | float inputs:specular = 1 226 | asset inputs:texture:file = @../dome_grid_2k.exr@ 227 | custom bool inputs:visibility:camera = 1 228 | float inputs:width = 1 229 | bool karma:light:renderlightgeo = 1 230 | rel light:filters = None 231 | float primvars:arnold:camera = 1 ( 232 | interpolation = "constant" 233 | ) 234 | int primvars:ri:attributes:visibility:camera = 1 ( 235 | interpolation = "constant" 236 | ) 237 | custom bool visibleInPrimaryRay = 1 238 | matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) 239 | uniform token[] xformOpOrder = ["xformOp:transform"] 240 | } 241 | } 242 | 243 | -------------------------------------------------------------------------------- /bega_84659K4.ies: -------------------------------------------------------------------------------- 1 | IESNA:LM-63-1995 2 | [TEST] 3 | [MANUFAC] BEGA 4 | [MORE] Copyright LUMCat V 5 | [LUMCAT] 6 | [LUMINAIRE] 84659K4 (Preliminary) 7 | [LAMPCAT] LED 62W 8 | [LAMP] 9600 lm,68 W 9 | TILT=NONE 10 | 1 -1 1.0 37 37 1 2 0.240 0.270 0.000 11 | 1.0 1.0 68 12 | 0.0 2.5 5.0 7.5 10.0 12.5 15.0 17.5 20.0 22.5 25.0 27.5 30.0 13 | 32.5 35.0 37.5 40.0 42.5 45.0 47.5 50.0 52.5 55.0 57.5 60.0 62.5 14 | 65.0 67.5 70.0 72.5 75.0 77.5 80.0 82.5 85.0 87.5 90.0 15 | 90.0 95.0 100.0 105.0 110.0 115.0 120.0 125.0 130.0 135.0 140.0 145.0 150.0 16 | 155.0 160.0 165.0 170.0 175.0 180.0 185.0 190.0 195.0 200.0 205.0 210.0 215.0 17 | 220.0 225.0 230.0 235.0 240.0 245.0 250.0 255.0 260.0 265.0 270.0 18 | 1739.8 1748.5 1754.2 1758.7 1775.5 1813.8 1888.7 2057.8 19 | 2268.7 2530.0 2653.0 2727.6 2847.9 2943.6 3012.0 3010.7 20 | 2925.3 2695.3 2336.5 1929.2 1501.3 1136.6 808.5 532.9 21 | 280.7 122.6 64.3 37.0 26.4 18.3 12.9 8.2 22 | 5.1 2.4 0.3 0.0 0.0 23 | 1739.8 1748.6 1755.1 1761.4 1779.4 1810.3 1897.9 2049.0 24 | 2262.6 2522.0 2673.8 2761.2 2883.7 2983.2 3028.5 3050.3 25 | 2988.3 2746.1 2362.9 1967.1 1541.8 1172.7 839.4 558.2 26 | 309.8 141.1 80.4 50.8 35.6 24.5 16.9 10.8 27 | 7.1 4.0 1.2 0.0 0.0 28 | 1739.8 1749.1 1757.4 1767.4 1785.7 1823.3 1899.7 2048.4 29 | 2256.2 2498.9 2694.0 2773.9 2844.4 2922.8 2988.7 3031.0 30 | 3027.4 2881.6 2567.2 2188.7 1761.0 1375.4 1018.7 699.3 31 | 400.3 196.2 113.3 71.3 47.6 32.5 22.1 14.8 32 | 9.7 5.2 2.1 0.1 0.0 33 | 1739.8 1749.8 1760.2 1773.9 1795.1 1832.1 1900.1 2045.2 34 | 2239.3 2482.5 2660.9 2744.7 2861.5 3034.0 3184.8 3305.3 35 | 3327.2 3210.1 2878.7 2442.9 1979.1 1517.9 1151.7 808.6 36 | 498.3 261.5 152.9 99.1 66.3 42.9 26.9 18.1 37 | 11.5 6.5 2.5 0.0 0.0 38 | 1739.8 1750.6 1763.0 1780.3 1803.0 1833.0 1894.3 2008.2 39 | 2199.0 2444.7 2696.9 2888.1 3087.5 3267.2 3389.1 3432.1 40 | 3418.0 3298.7 3024.6 2609.8 2146.7 1683.6 1305.7 944.7 41 | 626.2 363.9 214.5 137.1 87.4 54.7 34.9 23.2 42 | 15.2 8.4 2.5 0.0 0.0 43 | 1739.8 1751.6 1766.6 1786.2 1806.4 1826.5 1860.2 1973.2 44 | 2177.0 2478.9 2808.4 3035.9 3161.6 3281.4 3384.3 3448.8 45 | 3438.0 3361.0 3159.2 2800.0 2365.0 1909.0 1518.8 1156.2 46 | 839.7 537.7 333.0 229.1 153.9 98.5 60.9 38.2 47 | 22.2 10.8 3.6 0.0 0.0 48 | 1739.8 1752.7 1771.7 1792.3 1802.7 1808.7 1832.9 1970.3 49 | 2231.9 2557.8 2848.8 3046.8 3145.9 3246.5 3374.8 3453.3 50 | 3465.7 3410.5 3291.8 3011.3 2624.9 2203.5 1792.9 1450.0 51 | 1136.5 840.7 606.6 481.8 364.3 246.9 150.7 88.9 52 | 48.1 22.0 6.9 0.3 0.0 53 | 1739.8 1753.8 1777.0 1796.5 1794.8 1791.9 1841.4 2021.7 54 | 2280.7 2542.2 2771.2 2975.5 3121.1 3248.2 3391.1 3511.0 55 | 3531.5 3484.4 3392.6 3203.7 2913.7 2548.4 2163.0 1821.9 56 | 1540.7 1327.8 1160.0 1022.5 800.9 563.3 370.4 228.6 57 | 125.4 51.8 15.1 1.2 0.1 58 | 1739.8 1754.8 1781.0 1795.7 1781.8 1792.6 1886.5 2075.7 59 | 2261.2 2450.6 2649.5 2876.7 3093.7 3285.2 3444.9 3600.0 60 | 3677.9 3661.2 3584.7 3460.9 3265.7 2991.9 2661.8 2352.5 61 | 2166.2 2101.0 2042.7 1810.4 1435.5 1092.7 774.0 501.6 62 | 276.8 103.1 26.9 2.6 0.0 63 | 1739.8 1755.4 1782.8 1790.5 1772.5 1813.9 1953.0 2096.5 64 | 2208.0 2334.6 2514.0 2750.9 3033.0 3305.6 3497.4 3638.5 65 | 3780.3 3871.8 3869.9 3803.3 3693.5 3534.0 3313.6 3128.5 66 | 3083.1 3162.1 3100.2 2758.2 2262.6 1809.0 1341.3 899.8 67 | 500.0 187.6 46.7 4.3 0.0 68 | 1739.8 1755.7 1782.8 1783.3 1768.3 1848.9 2006.2 2097.5 69 | 2154.6 2236.7 2392.4 2608.1 2899.4 3231.2 3514.3 3713.1 70 | 3856.4 3983.7 4070.7 4087.8 4080.7 4098.0 4077.7 4091.0 71 | 4192.6 4272.4 4144.6 3754.2 3197.0 2607.4 2005.6 1386.9 72 | 778.1 293.9 79.5 7.5 0.0 73 | 1739.8 1755.4 1782.2 1774.3 1772.9 1890.9 2037.3 2090.6 74 | 2116.9 2172.1 2286.5 2481.7 2751.7 3076.9 3413.4 3703.8 75 | 3904.5 4053.3 4183.4 4293.1 4376.2 4499.1 4733.8 5032.2 76 | 5271.9 5261.5 5017.0 4611.3 4064.9 3373.1 2651.0 1878.4 77 | 1053.3 386.3 107.3 13.3 0.0 78 | 1739.8 1754.8 1781.2 1763.5 1782.8 1925.4 2052.3 2085.7 79 | 2109.9 2148.2 2230.5 2373.6 2592.0 2872.4 3193.1 3524.8 80 | 3813.4 4054.3 4243.0 4393.5 4537.7 4758.7 5191.2 5738.1 81 | 6099.8 6004.9 5742.4 5359.5 4807.0 4117.3 3298.1 2376.1 82 | 1374.9 499.6 137.2 19.0 0.1 83 | 1739.8 1754.2 1779.0 1753.3 1795.5 1954.6 2057.7 2081.8 84 | 2106.0 2140.8 2198.8 2293.9 2431.6 2638.7 2872.8 3146.3 85 | 3442.6 3729.7 4009.0 4277.9 4534.4 4841.2 5334.3 5971.2 86 | 6458.4 6427.8 6214.9 5945.5 5405.8 4726.0 3924.8 2910.2 87 | 1731.6 647.9 172.7 24.4 0.2 88 | 1739.8 1753.7 1775.5 1747.2 1811.0 1975.6 2058.3 2078.1 89 | 2100.7 2143.1 2192.8 2252.8 2332.3 2443.4 2578.3 2735.2 90 | 2900.4 3095.8 3322.8 3611.9 3979.1 4473.8 5057.7 5592.9 91 | 6039.2 6254.0 6274.6 6086.8 5637.3 5050.5 4315.5 3337.6 92 | 2052.6 777.2 188.6 27.0 0.1 93 | 1739.8 1753.5 1772.1 1743.4 1825.3 1988.7 2055.7 2075.2 94 | 2104.0 2147.1 2182.2 2230.3 2278.6 2338.0 2407.8 2488.4 95 | 2561.1 2649.0 2758.2 2933.2 3221.1 3689.9 4315.9 4887.8 96 | 5217.3 5403.3 5578.3 5593.9 5285.6 4859.4 4234.2 3286.5 97 | 2092.1 833.6 192.9 25.7 0.1 98 | 1739.8 1753.4 1770.3 1741.1 1834.5 1998.4 2060.2 2074.5 99 | 2102.5 2137.9 2177.5 2212.0 2239.4 2273.8 2324.5 2374.0 100 | 2422.9 2478.7 2555.6 2672.0 2890.8 3264.9 3773.6 4189.1 101 | 4395.8 4538.9 4636.8 4609.0 4417.1 4064.6 3531.1 2726.5 102 | 1743.9 712.5 163.7 22.3 0.0 103 | 1739.8 1753.2 1769.9 1740.7 1844.3 2003.6 2059.3 2074.3 104 | 2101.9 2140.5 2174.2 2200.8 2225.4 2242.9 2274.6 2316.0 105 | 2375.6 2439.1 2522.0 2651.4 2890.5 3269.0 3713.2 4051.6 106 | 4168.6 4152.3 4098.0 3932.4 3645.8 3237.7 2704.4 2024.6 107 | 1266.2 505.7 118.2 17.2 0.2 108 | 1739.8 1752.7 1768.9 1739.4 1848.0 2005.0 2057.8 2076.1 109 | 2105.3 2138.8 2172.1 2193.3 2208.4 2232.2 2264.7 2306.5 110 | 2370.7 2456.0 2566.0 2716.8 2996.1 3411.2 3879.6 4247.8 111 | 4401.7 4315.5 4107.0 3814.8 3470.1 3017.1 2489.7 1881.1 112 | 1180.9 457.8 104.1 14.4 0.2 113 | 1739.8 1751.9 1766.5 1736.6 1841.4 1999.8 2051.3 2064.5 114 | 2091.3 2125.1 2159.2 2184.1 2191.0 2207.5 2233.7 2281.7 115 | 2347.3 2428.6 2529.8 2693.0 2968.7 3391.4 3882.6 4271.4 116 | 4468.1 4373.3 4162.7 3880.4 3514.7 2965.6 2326.5 1745.4 117 | 1081.3 418.7 96.3 13.4 1.0 118 | 1739.8 1750.6 1763.4 1733.9 1826.0 1984.8 2039.0 2046.3 119 | 2062.0 2088.3 2113.7 2133.3 2141.8 2161.8 2185.5 2224.5 120 | 2273.4 2341.5 2413.9 2555.7 2779.9 3119.7 3534.7 3883.8 121 | 4018.8 3886.8 3657.9 3358.8 2911.8 2312.5 1679.0 1171.2 122 | 650.0 219.8 51.6 7.1 0.1 123 | 1739.8 1748.8 1761.1 1728.9 1802.9 1961.8 2019.1 2024.3 124 | 2035.5 2051.2 2063.9 2066.3 2069.3 2082.4 2104.1 2127.4 125 | 2161.4 2207.3 2257.9 2335.9 2458.5 2688.2 2954.9 3149.0 126 | 3185.9 2991.3 2712.6 2412.9 1981.8 1449.2 948.1 615.6 127 | 320.2 97.1 23.3 3.3 0.2 128 | 1739.8 1746.5 1759.4 1723.6 1777.0 1932.0 1998.8 1995.7 129 | 1994.2 2001.1 2002.5 1998.9 1987.0 1981.7 1985.9 1995.0 130 | 1999.0 1996.4 1977.9 1933.3 1936.8 2021.7 2181.6 2300.9 131 | 2256.1 2053.8 1778.0 1471.1 1135.3 799.3 514.5 323.3 132 | 164.0 54.3 15.1 1.6 0.1 133 | 1739.8 1744.0 1757.0 1721.8 1748.6 1894.2 1976.7 1971.4 134 | 1956.9 1944.0 1936.1 1924.3 1898.2 1872.1 1842.7 1794.3 135 | 1718.2 1646.3 1576.3 1483.8 1417.8 1401.1 1440.9 1460.8 136 | 1416.0 1272.8 1065.0 836.9 632.9 445.5 286.8 178.6 137 | 94.5 37.6 12.6 1.2 0.3 138 | 1739.8 1741.7 1753.4 1722.7 1722.0 1842.1 1944.5 1941.7 139 | 1918.0 1891.7 1868.5 1851.0 1816.6 1752.6 1640.2 1519.0 140 | 1434.8 1324.8 1200.9 1070.9 944.0 869.6 846.3 831.3 141 | 790.4 737.8 618.3 463.7 343.7 247.9 165.4 107.8 142 | 60.6 28.2 10.6 1.1 0.1 143 | 1739.8 1739.8 1748.9 1724.2 1698.3 1788.1 1901.0 1907.5 144 | 1882.0 1856.4 1822.8 1785.5 1710.2 1563.1 1442.7 1331.9 145 | 1185.6 1054.8 883.0 738.7 614.5 520.0 465.7 457.4 146 | 447.6 406.0 346.2 275.1 205.1 152.5 114.7 80.8 147 | 48.6 23.4 9.0 1.0 0.0 148 | 1739.8 1738.1 1743.9 1725.3 1681.1 1728.5 1845.0 1873.7 149 | 1849.8 1813.1 1773.9 1679.5 1531.6 1417.5 1288.7 1126.7 150 | 980.5 804.6 659.8 523.7 411.4 329.1 333.5 349.1 151 | 341.6 296.6 255.7 218.4 169.0 130.9 111.9 85.7 152 | 54.1 24.0 7.9 0.6 0.4 153 | 1739.8 1736.4 1738.6 1726.7 1675.0 1674.9 1765.5 1832.5 154 | 1821.7 1779.7 1679.0 1525.9 1406.7 1277.8 1119.9 973.6 155 | 776.8 629.6 489.6 373.0 292.8 297.0 338.4 373.3 156 | 360.6 319.8 289.8 241.5 177.2 139.2 116.7 85.4 157 | 50.4 21.6 5.9 0.6 0.2 158 | 1739.8 1734.9 1733.0 1726.6 1678.5 1639.2 1676.6 1759.3 159 | 1781.8 1728.1 1560.6 1415.1 1296.8 1138.6 994.1 791.0 160 | 639.3 488.9 362.5 271.5 278.4 342.3 430.8 470.2 161 | 433.8 400.4 375.7 296.9 194.9 131.9 99.3 72.7 162 | 46.8 21.9 6.2 0.3 0.1 163 | 1739.8 1733.4 1726.2 1721.4 1683.0 1628.9 1608.0 1656.3 164 | 1704.4 1642.1 1461.3 1338.6 1178.5 1036.7 842.3 665.5 165 | 509.1 370.1 248.1 237.4 299.4 424.0 538.7 550.9 166 | 505.8 511.8 478.4 333.3 185.0 107.6 77.9 62.2 167 | 39.9 17.1 4.6 0.4 0.7 168 | 1739.8 1731.9 1718.6 1711.6 1685.5 1634.5 1578.3 1566.8 169 | 1578.6 1503.9 1391.7 1255.9 1088.7 918.0 716.7 559.7 170 | 401.4 255.8 187.5 211.9 297.5 428.9 500.2 478.8 171 | 461.1 488.9 417.1 244.3 117.7 65.4 46.5 33.1 172 | 19.9 8.9 2.5 0.5 0.7 173 | 1739.8 1730.4 1712.0 1698.4 1680.5 1643.5 1584.6 1522.5 174 | 1448.6 1328.1 1281.5 1153.1 1024.0 806.2 630.3 450.3 175 | 309.5 184.1 158.9 180.9 253.0 347.8 373.9 338.1 176 | 321.6 318.9 245.2 135.1 69.1 41.8 29.2 21.0 177 | 13.5 6.3 1.6 0.2 0.5 178 | 1739.8 1728.7 1707.5 1686.4 1669.4 1640.1 1597.6 1534.1 179 | 1390.3 1215.9 1131.7 1016.4 911.0 711.6 558.9 394.2 180 | 239.7 152.3 141.0 158.0 216.4 290.9 300.0 255.6 181 | 228.7 224.1 172.4 88.1 36.6 18.1 12.7 8.7 182 | 5.8 3.1 0.9 0.3 0.3 183 | 1739.8 1727.3 1704.7 1677.5 1654.2 1625.8 1594.5 1538.4 184 | 1391.4 1223.5 1086.3 937.0 788.1 619.2 476.3 340.5 185 | 195.2 136.2 124.3 136.7 186.6 250.5 253.2 209.8 186 | 198.9 210.2 154.6 57.6 19.1 7.5 2.6 0.4 187 | 0.3 0.1 0.3 0.1 0.1 188 | 1739.8 1726.1 1702.3 1671.4 1641.1 1613.2 1577.6 1520.4 189 | 1376.1 1245.4 1102.8 950.4 762.0 587.5 432.1 303.3 190 | 173.0 126.7 111.5 113.1 144.8 183.9 180.1 150.1 191 | 148.3 152.5 110.7 58.0 29.4 14.6 6.5 3.5 192 | 2.2 1.2 0.3 0.2 0.3 193 | 1739.8 1725.3 1699.4 1669.8 1633.6 1599.7 1563.7 1498.5 194 | 1345.3 1223.2 1074.8 935.8 757.2 592.9 431.9 290.9 195 | 166.9 125.1 107.4 101.7 108.6 116.4 100.5 77.0 196 | 69.3 66.1 49.9 31.9 17.8 9.2 6.1 5.1 197 | 3.8 2.4 0.4 0.2 0.1 198 | 1739.8 1725.1 1697.8 1670.7 1629.3 1595.2 1555.4 1496.7 199 | 1324.7 1209.7 1067.7 938.0 736.8 576.8 423.8 288.7 200 | 163.9 124.3 105.5 94.1 90.2 83.9 64.6 44.4 201 | 32.4 26.8 19.6 12.9 7.9 4.1 2.8 2.2 202 | 1.1 0.2 0.2 0.1 0.2 203 | -------------------------------------------------------------------------------- /gendiffs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | """Generate a web page showing UsdLux image diffs 5 | 6 | ...between the reference embree implementation and other renderers (ie, arnold, 7 | karma, and RenderMan RIS) 8 | """ 9 | 10 | 11 | import argparse 12 | import asyncio 13 | import datetime 14 | import inspect 15 | import multiprocessing 16 | import os 17 | import shutil 18 | import subprocess 19 | import sys 20 | import textwrap 21 | import traceback 22 | 23 | from typing import Dict, Iterable 24 | 25 | ############################################################################### 26 | # Constants 27 | ############################################################################### 28 | 29 | THIS_FILE = os.path.abspath(inspect.getsourcefile(lambda: None) or __file__) 30 | THIS_DIR = os.path.dirname(THIS_FILE) 31 | 32 | if THIS_DIR not in sys.path: 33 | sys.path.append(THIS_DIR) 34 | 35 | import genLightParamDescriptions 36 | import luxtest_utils 37 | import pip_import 38 | 39 | pip_import.pip_import("tqdm") 40 | import tqdm.asyncio 41 | 42 | RENDERS_DIR_NAME = "renders" 43 | RENDERS_ROOT = os.path.join(THIS_DIR, RENDERS_DIR_NAME) 44 | WEB_DIR_NAME = "web" 45 | WEB_ROOT = os.path.join(THIS_DIR, WEB_DIR_NAME) 46 | WEB_IMG_ROOT = os.path.join(WEB_ROOT, "img") 47 | 48 | RENDERERS = [ 49 | "karma", 50 | "ris", 51 | "arnold", 52 | ] 53 | 54 | OUTPUT_DIR = "diff" 55 | 56 | MAP = "magma" 57 | 58 | HTML_START = """ 59 | 60 | 61 | 62 | UsdLux Comparison 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 | 75 |
76 | """ 77 | 78 | HTML_END = """ 79 |
80 |
81 | 82 | """ 83 | 84 | OIIOTOOL = os.environ.get("LUXTEST_OIIOTOOL", "oiiotool") 85 | 86 | NUM_CPUS = multiprocessing.cpu_count() 87 | 88 | # if we can't read light_descriptions, use this 89 | FALLBACK_LIGHTS = ( 90 | "cylinder", 91 | "disk", 92 | "distant", 93 | "dome", 94 | "rect", 95 | "sphere", 96 | "visibleRect", 97 | ) 98 | 99 | SKIP_LIGHTS = ("ies_scale",) 100 | 101 | 102 | ############################################################################### 103 | # Utilities 104 | ############################################################################### 105 | 106 | 107 | def is_ipython(): 108 | try: 109 | __IPYTHON__ # type: ignore 110 | except NameError: 111 | return False 112 | return True 113 | 114 | 115 | if sys.platform == "win32": 116 | to_shell_cmd = subprocess.list2cmdline 117 | else: 118 | 119 | def to_shell_cmd(cmd_list): 120 | import shlex 121 | 122 | return " ".join(shlex.quote(x) for x in cmd_list) 123 | 124 | 125 | def normalize_concurrency(concurrency: int): 126 | if concurrency < 0: 127 | concurrency += NUM_CPUS 128 | return max(concurrency, 1) 129 | 130 | 131 | def needs_update(existing, dependent): 132 | if os.path.exists(dependent): 133 | return os.path.getmtime(existing) > os.path.getmtime(dependent) 134 | return True 135 | 136 | 137 | def iter_frames(light_description): 138 | start, end = light_description.frames 139 | return range(start, end + 1) 140 | 141 | 142 | def get_image_path(light_name, renderer: str, frame: int, ext: str, prefix=""): 143 | ext = ext.lstrip(".") 144 | filename = f"{prefix}{light_name}-{renderer}.{frame:04}.{ext}" 145 | if ext == "png": 146 | base_dir = WEB_IMG_ROOT 147 | elif ext == "exr": 148 | base_dir = os.path.join(RENDERS_ROOT, renderer) 149 | else: 150 | raise ValueError(f"unrecognized extension: {ext}") 151 | return os.path.join(base_dir, filename) 152 | 153 | 154 | def get_image_url(light_name, renderer: str, frame: int, ext: str, prefix=""): 155 | image_path = get_image_path(light_name, renderer, frame, ext, prefix=prefix) 156 | rel_path = os.path.relpath(image_path, WEB_ROOT) 157 | return rel_path.replace(os.sep, "/") 158 | 159 | 160 | def print_streams(proc: subprocess.CompletedProcess): 161 | for stream_name in ("stdout", "stderr"): 162 | stream = getattr(proc, stream_name, None) 163 | print("=" * 80) 164 | print(f"{stream_name}:") 165 | print() 166 | print(luxtest_utils.try_decode(stream)) 167 | 168 | 169 | def raise_proc_error(proc: subprocess.CompletedProcess, verbose: bool): 170 | print() 171 | if not verbose: 172 | # if not verbose, we haven't printed output yet - do so now 173 | print_streams(proc) 174 | print("=" * 80) 175 | print("Error running commmand:") 176 | print(to_shell_cmd(proc.args)) 177 | print(f"Exitcode: {proc.returncode}") 178 | print("=" * 80) 179 | raise subprocess.CalledProcessError(proc.returncode, proc.args, proc.stdout, proc.stderr) 180 | 181 | 182 | async def run(args: Iterable[str], check=False, verbose=False): 183 | if verbose: 184 | print(f"Running: {to_shell_cmd(args)}") 185 | proc = await asyncio.create_subprocess_exec(args[0], *args[1:], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 186 | stdout, stderr = await proc.communicate() 187 | completed_proc = subprocess.CompletedProcess(args=args, returncode=proc.returncode, stdout=stdout, stderr=stderr) 188 | if verbose: 189 | print_streams(completed_proc) 190 | print("=" * 80) 191 | if completed_proc.returncode: 192 | if check: 193 | raise_proc_error(completed_proc, verbose) 194 | if verbose: 195 | print(f"Exitcode: {completed_proc.returncode}") 196 | return completed_proc 197 | 198 | 199 | ############################################################################### 200 | # Core functions 201 | ############################################################################### 202 | 203 | 204 | async def update_png(exr_path, png_path, verbose=False): 205 | 206 | if needs_update(exr_path, png_path): 207 | if verbose: 208 | print(f"Creating png: {png_path}") 209 | cmd = [ 210 | OIIOTOOL, 211 | exr_path, 212 | "--ch", 213 | "R,G,B", 214 | "--colorconvert", 215 | "linear", 216 | "sRGB", 217 | "-o", 218 | png_path, 219 | ] 220 | proc = await run(cmd, verbose=verbose, check=True) 221 | if not os.path.isfile: 222 | print(f"Error - output png did not exist: {png_path}") 223 | raise_proc_error(proc, verbose) 224 | 225 | 226 | async def update_diff(exr_path1, exr_path2, diff_path, verbose=False): 227 | cmd = [ 228 | OIIOTOOL, 229 | exr_path1, 230 | exr_path2, 231 | "--diff", 232 | "--absdiff", 233 | "--mulc", 234 | "2,2,2,1", 235 | "--colormap", 236 | MAP, 237 | "--colorconvert", 238 | "linear", 239 | "sRGB", 240 | "-o", 241 | diff_path, 242 | ] 243 | proc = await run(cmd, verbose=verbose) 244 | if not os.path.isfile: 245 | print(f"Error - output diff png did not exist: {diff_path}") 246 | raise_proc_error(proc, verbose) 247 | 248 | 249 | async def gen_images_async(light_descriptions, verbose=False, max_concurrency=-1): 250 | flat_frames = [] 251 | for name, description in light_descriptions.items(): 252 | for frame in iter_frames(description): 253 | flat_frames.append((name, description, frame)) 254 | 255 | all_tasks = [] 256 | num_possible_images = 0 257 | 258 | def queue_png_update(exr_path, png_path): 259 | nonlocal num_possible_images 260 | num_possible_images += 1 261 | if needs_update(exr_path, png_path): 262 | all_tasks.append(update_png(exr_path, png_path, verbose=verbose)) 263 | 264 | def queue_diff_update(exr_path1, exr_path2, diff_path): 265 | nonlocal num_possible_images 266 | num_possible_images += 1 267 | if needs_update(exr_path1, diff_path) or needs_update(exr_path2, diff_path): 268 | all_tasks.append(update_diff(exr_path1, exr_path2, diff_path, verbose=verbose)) 269 | 270 | print("Finding how many images need to be updated:") 271 | progress = tqdm.tqdm(flat_frames) 272 | for name, description, frame in progress: 273 | progress.set_postfix({"name": name, "frame": frame}) 274 | embree_exr_path = get_image_path(name, "embree", frame, "exr") 275 | embree_png_path = get_image_path(name, "embree", frame, "png") 276 | queue_png_update(embree_exr_path, embree_png_path) 277 | 278 | for renderer in RENDERERS: 279 | renderer_exr_path = get_image_path(name, renderer, frame, "exr") 280 | renderer_png_path = get_image_path(name, renderer, frame, "png") 281 | queue_png_update(renderer_exr_path, renderer_png_path) 282 | 283 | diff_png_path = get_image_path(name, renderer, frame, "png", prefix="diff-") 284 | queue_diff_update(embree_exr_path, renderer_exr_path, diff_png_path) 285 | 286 | print(f"Generating {len(all_tasks)} images (out of possible {num_possible_images}):") 287 | 288 | # dont' want to overwhelm system by launching too many subprocess 289 | max_concurrency = normalize_concurrency(max_concurrency) 290 | proc_limiter = asyncio.Semaphore(max_concurrency) 291 | 292 | async def proc_limited_coroutine(coroutine): 293 | async with proc_limiter: 294 | return await coroutine 295 | 296 | limited_tasks = [proc_limited_coroutine(x) for x in all_tasks] 297 | 298 | await tqdm.asyncio.tqdm_asyncio.gather(*limited_tasks) 299 | 300 | 301 | def gen_images(light_descriptions, verbose=False, max_concurrency=-1): 302 | asyncio.run(gen_images_async(light_descriptions, verbose, max_concurrency=max_concurrency)) 303 | 304 | 305 | def gen_html(light_descriptions: Dict[str, genLightParamDescriptions.LightParamDescription]): 306 | html = HTML_START 307 | num_cols = len(RENDERERS) * 2 + 1 308 | 309 | # sort first by number of frames (so tests with, ie, only one frame appear at top and are easy to find), then 310 | # alphabetically 311 | def sort_key(name_desc_pair): 312 | name, description = name_desc_pair 313 | return (description.frames.num_frames, description.frames.start, name) 314 | 315 | sorted_lights = sorted(light_descriptions.items(), key=sort_key) 316 | 317 | for name, description in sorted_lights: 318 | # add navbar links first 319 | html += f"""

{name}

""" 320 | 321 | html += HTML_NAVBAR_END 322 | 323 | for name, description in sorted_lights: 324 | # now add main body 325 | summaries_by_start_frame = genLightParamDescriptions.get_light_group_summaries(name, description) 326 | 327 | html += textwrap.dedent( 328 | f"""

{name}

329 | 330 | 331 | 332 | 333 | 334 | """ 335 | ) 336 | for renderer in RENDERERS: 337 | html += textwrap.dedent( 338 | f""" 339 | 340 | 341 | """ 342 | ) 343 | 344 | html += "\n" 345 | for frame in iter_frames(description): 346 | 347 | if frame in summaries_by_start_frame: 348 | desc = summaries_by_start_frame[frame] 349 | html += " \n" 350 | html += " \n" 351 | html += f" \n" 352 | html += " \n" 353 | 354 | html += " \n" 355 | html += f" " 356 | 357 | embree_url = get_image_url(name, "embree", frame, "png") 358 | 359 | html += f' " 369 | 370 | html += "
FrameRef{renderer}{renderer} diff
{desc}
{frame:04}\n' 360 | 361 | for renderer in RENDERERS: 362 | renderer_url = get_image_url(name, renderer, frame, "png") 363 | diff_url = get_image_url(name, renderer, frame, "png", prefix="diff-") 364 | 365 | html += f' \n' 366 | html += f' \n' 367 | 368 | html += "
\n" 371 | html += HTML_END 372 | 373 | with open(os.path.join(WEB_ROOT, "luxtest.html"), "w", encoding="utf8") as f: 374 | f.write(html) 375 | 376 | shutil.copyfile("luxtest.css", os.path.join(WEB_ROOT, "luxtest.css")) 377 | 378 | 379 | def gen_diffs(verbose=False, max_concurrency=-1, lights: Iterable[str] = ()): 380 | start = datetime.datetime.now() 381 | lights = tuple(lights) # in case it's an iterable 382 | light_descriptions = genLightParamDescriptions.read_descriptions() 383 | light_descriptions = {light: desc for light, desc in light_descriptions.items() if light not in SKIP_LIGHTS} 384 | if lights: 385 | light_descriptions = {light: desc for light, desc in light_descriptions.items() if light in lights} 386 | 387 | os.makedirs(WEB_IMG_ROOT, exist_ok=True) 388 | gen_images(light_descriptions, verbose=verbose, max_concurrency=max_concurrency) 389 | gen_html(light_descriptions) 390 | elapsed = datetime.datetime.now() - start 391 | print(f"Done generating diffs - took: {elapsed}") 392 | 393 | 394 | ############################################################################### 395 | # CLI 396 | ############################################################################### 397 | 398 | 399 | def get_parser(): 400 | try: 401 | light_descriptions = genLightParamDescriptions.read_descriptions() 402 | light_names = sorted(light_descriptions) 403 | except Exception as err: 404 | print("Error reading light names from light_descriptions.json:") 405 | print(err) 406 | print("...using fallback light names") 407 | light_names = FALLBACK_LIGHTS 408 | 409 | parser = argparse.ArgumentParser( 410 | description=__doc__, 411 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 412 | ) 413 | parser.add_argument("-v", "--verbose", action="store_true") 414 | parser.add_argument( 415 | "-j", 416 | type=int, 417 | default="-1", 418 | help=( 419 | "Number of oiiotool procs to run in parallel; negative values are subtracted from" 420 | " multiprocessing.cpu_count()" 421 | ), 422 | ) 423 | parser.add_argument( 424 | "-l", 425 | "--light", 426 | choices=light_names, 427 | action="append", 428 | dest="lights", 429 | help=( 430 | "Only render images for the given lights; if not specified, render images for all lights. May be repeated." 431 | ), 432 | ) 433 | return parser 434 | 435 | 436 | def main(argv=None): 437 | if argv is None: 438 | argv = sys.argv[1:] 439 | parser = get_parser() 440 | args = parser.parse_args(argv) 441 | try: 442 | gen_diffs(verbose=args.verbose, max_concurrency=args.j, lights=args.lights or ()) 443 | except Exception: # pylint: disable=broad-except 444 | 445 | traceback.print_exc() 446 | return 1 447 | return 0 448 | 449 | 450 | if __name__ == "__main__" and not is_ipython(): 451 | sys.exit(main()) 452 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Creative Commons Attribution 4.0 International 2 | 3 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 4 | 5 | **Using Creative Commons Public Licenses** 6 | 7 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 8 | 9 | * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 10 | 11 | * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 12 | 13 | ## Creative Commons Attribution 4.0 International Public License 14 | 15 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 16 | 17 | ### Section 1 – Definitions. 18 | 19 | a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 20 | 21 | b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 22 | 23 | c. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 24 | 25 | d. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 26 | 27 | e. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 28 | 29 | f. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 30 | 31 | g. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 32 | 33 | h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. 34 | 35 | i. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 36 | 37 | j. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 38 | 39 | k. __You__ means the individual or entity exercising the Licensed Rights under this Public License. __Your__ has a corresponding meaning. 40 | 41 | ### Section 2 – Scope. 42 | 43 | a. ___License grant.___ 44 | 45 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 46 | 47 | A. reproduce and Share the Licensed Material, in whole or in part; and 48 | 49 | B. produce, reproduce, and Share Adapted Material. 50 | 51 | 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 52 | 53 | 3. __Term.__ The term of this Public License is specified in Section 6(a). 54 | 55 | 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 56 | 57 | 5. __Downstream recipients.__ 58 | 59 | A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 60 | 61 | B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 62 | 63 | 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 64 | 65 | b. ___Other rights.___ 66 | 67 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 68 | 69 | 2. Patent and trademark rights are not licensed under this Public License. 70 | 71 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. 72 | 73 | ### Section 3 – License Conditions. 74 | 75 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 76 | 77 | a. ___Attribution.___ 78 | 79 | 1. If You Share the Licensed Material (including in modified form), You must: 80 | 81 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 82 | 83 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 84 | 85 | ii. a copyright notice; 86 | 87 | iii. a notice that refers to this Public License; 88 | 89 | iv. a notice that refers to the disclaimer of warranties; 90 | 91 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 92 | 93 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 94 | 95 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 96 | 97 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 98 | 99 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 100 | 101 | 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. 102 | 103 | ### Section 4 – Sui Generis Database Rights. 104 | 105 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 106 | 107 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; 108 | 109 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 110 | 111 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 112 | 113 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 114 | 115 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 116 | 117 | a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ 118 | 119 | b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ 120 | 121 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 122 | 123 | ### Section 6 – Term and Termination. 124 | 125 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 126 | 127 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 128 | 129 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 130 | 131 | 2. upon express reinstatement by the Licensor. 132 | 133 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 134 | 135 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 136 | 137 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 138 | 139 | ### Section 7 – Other Terms and Conditions. 140 | 141 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 142 | 143 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 144 | 145 | ### Section 8 – Interpretation. 146 | 147 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 148 | 149 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 150 | 151 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 152 | 153 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 154 | 155 | > Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 156 | > 157 | > Creative Commons may be contacted at creativecommons.org -------------------------------------------------------------------------------- /genembree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Run the UsdLux_2 test suite""" 4 | 5 | import argparse 6 | import fnmatch 7 | import inspect 8 | import os 9 | import re 10 | import subprocess 11 | import sys 12 | import traceback 13 | 14 | from glob import glob 15 | from typing import Callable, Iterable, List, NamedTuple, Optional, Tuple 16 | 17 | ############################################################################### 18 | # Constants 19 | ############################################################################### 20 | 21 | THIS_FILE = os.path.abspath(inspect.getsourcefile(lambda: None) or __file__) 22 | THIS_DIR = os.path.dirname(THIS_FILE) 23 | 24 | if THIS_DIR not in sys.path: 25 | sys.path.append(THIS_DIR) 26 | 27 | import combine_ies_test_images 28 | import genLightParamDescriptions 29 | import luxtest_utils 30 | import pip_import 31 | 32 | pip_import.pip_import("tqdm") 33 | 34 | from tqdm import tqdm 35 | 36 | from genLightParamDescriptions import FrameRange 37 | 38 | EMBREE_DELEGATE = "Embree" 39 | DEFAULT_DELEGATES = (EMBREE_DELEGATE,) 40 | DEFAULT_INCLUDE_GLOBS = (os.path.join(THIS_DIR, "usd", "*.usda"),) 41 | DEFAULT_EXCLUDE_GLOBS = () 42 | DEFAULT_OUTPUT_DIR = os.path.join(THIS_DIR, "renders") 43 | DEFAULT_RESOLUTION = 512 44 | DEFAULT_CAMERAS = ("/cameras/camera1",) 45 | DEFAULT_CAMERAS_BY_USD = { 46 | "iesTest": ("/cameras/iesTop", "/cameras/iesBottom"), 47 | } 48 | 49 | DEFAULT_SEED = 1 50 | 51 | USD_RECORD_FRAME_RE = re.compile(rb"""^Recording time code: .*""") 52 | 53 | ############################################################################### 54 | # Utilities 55 | ############################################################################### 56 | 57 | 58 | def is_ipython(): 59 | try: 60 | __IPYTHON__ # type: ignore 61 | except NameError: 62 | return False 63 | return True 64 | 65 | 66 | if sys.platform == "win32": 67 | to_shell_cmd = subprocess.list2cmdline 68 | else: 69 | 70 | def to_shell_cmd(cmd_list): 71 | import shlex 72 | 73 | return " ".join(shlex.quote(x) for x in cmd_list) 74 | 75 | 76 | ############################################################################### 77 | # Dataclasses 78 | ############################################################################### 79 | 80 | 81 | class UsdRecordCommand(NamedTuple): 82 | """Data needed for a single invocation of usdrecord""" 83 | 84 | name: str # base name of the file - ie, the light name 85 | usd_path: str 86 | output_path: str 87 | renderer: str 88 | camera: str 89 | frames: FrameRange 90 | resolution: int 91 | samples: Optional[int] 92 | seed: int 93 | 94 | def render(self, frame_callback: Optional[Callable[[], None]] = None): 95 | return run_test( 96 | self.usd_path, 97 | self.output_path, 98 | delegate=self.renderer, 99 | resolution=self.resolution, 100 | samples=self.samples, 101 | camera=self.camera, 102 | frames=self.frames, 103 | seed=self.seed, 104 | frame_callback=frame_callback, 105 | ) 106 | 107 | 108 | ############################################################################### 109 | # Core functions 110 | ############################################################################### 111 | 112 | 113 | def run_tests( 114 | include_globs: Iterable[str] = DEFAULT_INCLUDE_GLOBS, 115 | exclude_globs: Iterable[str] = DEFAULT_EXCLUDE_GLOBS, 116 | output_dir: str = DEFAULT_OUTPUT_DIR, 117 | delegates: Iterable[str] = DEFAULT_DELEGATES, 118 | resolution: int = DEFAULT_RESOLUTION, 119 | samples: Optional[int] = None, 120 | cameras: Iterable[str] = (), 121 | frames: Optional[FrameRange] = None, 122 | seed: int = DEFAULT_SEED, 123 | progress_bar: bool = True, 124 | ) -> List[str]: 125 | """Runs tests on input usds matching given glob, for all given delegates 126 | 127 | Returns 128 | ------- 129 | failures: List[UsdRecordCommand] 130 | """ 131 | nullLight = genLightParamDescriptions.LightParamDescription.empty() 132 | 133 | light_descriptions = genLightParamDescriptions.read_descriptions() 134 | 135 | # strip empty globs, and convert iterables into tuples 136 | include_globs = tuple(x for x in include_globs if x) 137 | exclude_globs = tuple(x for x in exclude_globs if x) 138 | 139 | exclude_res = [re.compile(fnmatch.translate(x)) for x in exclude_globs] 140 | 141 | def is_excluded(path): 142 | return any(x.match(path) for x in exclude_res) 143 | 144 | if not include_globs: 145 | errmsg = ( 146 | "ERROR: no input usd glob patterns specified. Please specify a glob pattern to match usd layers to render" 147 | ) 148 | print(errmsg) 149 | return [errmsg] 150 | 151 | input_layers = [] 152 | for pattern in include_globs: 153 | print(f"globbing: {pattern}") 154 | new_layers = [] 155 | new_layers = [x for x in glob(pattern) if not is_excluded(x)] 156 | 157 | print(f"found {len(new_layers)} layers") 158 | for f in new_layers: 159 | print(f" {f}") 160 | input_layers.extend(new_layers) 161 | 162 | if not input_layers: 163 | errmsg = f"ERROR: input glob patterns {include_globs} did not match any files" 164 | if exclude_res: 165 | errmsg += f", after excluding glob patterns {exclude_globs}" 166 | print(errmsg) 167 | return [errmsg] 168 | 169 | flat_list: List[UsdRecordCommand] = [] 170 | total_frames = 0 171 | unique_cameras = set() 172 | 173 | for delegate in delegates: 174 | delegate_output_dir = os.path.join(output_dir, delegate.lower()) 175 | os.makedirs(delegate_output_dir, exist_ok=True) 176 | 177 | for layer in input_layers: 178 | input_file = os.path.basename(layer) 179 | base = os.path.splitext(input_file)[0] 180 | 181 | if frames is None: 182 | test_frames = light_descriptions.get(base, nullLight).frames 183 | else: 184 | test_frames = frames 185 | 186 | if cameras: 187 | light_cameras = cameras 188 | else: 189 | light_cameras = DEFAULT_CAMERAS_BY_USD.get(base, DEFAULT_CAMERAS) 190 | unique_cameras.update(light_cameras) 191 | 192 | for camera in light_cameras: 193 | total_frames += test_frames.num_frames 194 | 195 | if len(light_cameras) > 1: 196 | camera_filename_part = "." + os.path.basename(camera) 197 | else: 198 | camera_filename_part = "" 199 | output_file = f"{base}-{delegate.lower()}{camera_filename_part}.####.exr" 200 | output_path = os.path.join(delegate_output_dir, output_file) 201 | 202 | flat_list.append( 203 | UsdRecordCommand( 204 | name=base, 205 | usd_path=layer, 206 | output_path=output_path, 207 | renderer=delegate, 208 | camera=camera, 209 | frames=test_frames, 210 | resolution=resolution, 211 | samples=samples, 212 | seed=seed, 213 | ) 214 | ) 215 | 216 | num_commands = len(flat_list) 217 | print(f"Found: {len(delegates)} delegates - {len(input_layers)} files - {len(unique_cameras)} cameras ") 218 | print(f"Total usdrecord render commands: {num_commands} - Total frames: {total_frames}") 219 | print() 220 | 221 | if progress_bar: 222 | render_command_progress = tqdm(desc="Render commands", unit="command", total=num_commands, position=0) 223 | total_frames_progress = tqdm(desc="Total frames ", unit="frame", total=total_frames, position=1) 224 | current_frames_progress = tqdm(desc="Command frames ", unit="frame", position=2) 225 | try: 226 | if progress_bar: 227 | 228 | def frame_progress_update(): 229 | render_command_progress.refresh() 230 | total_frames_progress.update(1) 231 | current_frames_progress.update(1) 232 | 233 | frame_callback = frame_progress_update 234 | else: 235 | frame_callback = None 236 | 237 | failures = [] 238 | for command in flat_list: 239 | if progress_bar: 240 | current_frames_progress.reset(total=command.frames.num_frames) 241 | current_frames_progress.clear() 242 | print() 243 | exitcode = command.render(frame_callback=frame_callback) 244 | if progress_bar: 245 | render_command_progress.update(1) 246 | if exitcode: 247 | failures.append(command) 248 | 249 | # auto-combine iesTest images if we did all cameras 250 | if command.name == "iesTest" and not cameras: 251 | print() 252 | print(f"Combining {base} images") 253 | combine_ies_test_images.combine_ies_test_images(renderers=["embree"]) 254 | 255 | return failures 256 | finally: 257 | if progress_bar: 258 | render_command_progress.close() 259 | total_frames_progress.close() 260 | current_frames_progress.close() 261 | 262 | 263 | def run_test( 264 | usd_path: str, 265 | output_path: str, 266 | delegate: str, 267 | resolution: int, 268 | samples: Optional[int], 269 | camera: str, 270 | frames: FrameRange, 271 | seed: int, 272 | frame_callback: Optional[Callable[[], None]] = None, 273 | ): 274 | 275 | usdrecord = "usdrecord" 276 | if sys.platform == "win32": 277 | usdrecord += ".cmd" 278 | 279 | cmd = [ 280 | usdrecord, 281 | "--disableCameraLight", 282 | "--disableGpu", 283 | "--imageWidth", 284 | str(resolution), 285 | "--renderer", 286 | delegate, 287 | "--colorCorrectionMode=disabled", 288 | ] 289 | if frames is not None: 290 | cmd.extend(["--frames", str(frames)]) 291 | if camera: 292 | cmd.extend(["--camera", camera]) 293 | cmd.extend([usd_path, output_path]) 294 | try: 295 | print(to_shell_cmd(cmd)) 296 | except Exception: 297 | print(cmd) 298 | raise 299 | env = dict(os.environ) 300 | env["PYTHONUNBUFFERED"] = "1" 301 | env["HDEMBREE_RANDOM_NUMBER_SEED"] = str(seed) 302 | if samples is not None: 303 | env["HDEMBREE_SAMPLES_TO_CONVERGENCE"] = str(samples) 304 | 305 | if frame_callback is None: 306 | # skip progress bar 307 | return subprocess.call(cmd, env=env) 308 | 309 | current_line = [] 310 | 311 | # we watch for usdrecording lines like "Recording time code: 1.0000" 312 | # ...however, it prints these when it STARTS each frame, not when it finishes 313 | # we don't want to update progress each time we get this, as this would result 314 | # in a first frame "finish" time that would be very fast, and would skew 315 | # early time estimations. 316 | # So, instead we skip the frame_callback() the FIRST time we see this 317 | # message... and then add in one last frame_callback() when the process 318 | # finishes 319 | read_first_frame = False 320 | 321 | def process_finished_line(line): 322 | nonlocal read_first_frame 323 | match = USD_RECORD_FRAME_RE.match(line) 324 | if match and frame_callback is not None: 325 | if read_first_frame: 326 | frame_callback() 327 | else: 328 | read_first_frame = True 329 | else: 330 | print(luxtest_utils.try_decode(line)) 331 | 332 | def process_text(newtext): 333 | if not newtext: 334 | return 335 | lines = newtext.split(b"\n") 336 | unfinished_line = lines.pop() 337 | for line in lines: 338 | line = line.rstrip(b"\r") 339 | if current_line: 340 | current_line.append(line) 341 | finished_line = b"".join(current_line) 342 | current_line.clear() 343 | else: 344 | finished_line = line 345 | process_finished_line(finished_line) 346 | if unfinished_line: 347 | current_line.append(unfinished_line) 348 | 349 | proc = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE) 350 | while proc.poll() is None: 351 | newtext = proc.stdout.read1() 352 | process_text(newtext) 353 | 354 | # ok, proc finished, but we still might have text to read 355 | while True: 356 | newtext = proc.stdout.read1() 357 | if not newtext: 358 | break 359 | process_text(newtext) 360 | if frame_callback is not None: 361 | # callback for "finishing" the last frame 362 | frame_callback() 363 | 364 | if current_line: 365 | process_text("".join(current_line)) 366 | return proc.returncode 367 | 368 | 369 | ############################################################################### 370 | # CLI 371 | ############################################################################### 372 | 373 | 374 | def get_parser(): 375 | parser = argparse.ArgumentParser( 376 | description=__doc__, 377 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 378 | ) 379 | parser.add_argument( 380 | "-d", 381 | "--delegates", 382 | nargs="+", 383 | default=DEFAULT_DELEGATES, 384 | help=( 385 | "Delegates to use to render the test suite. Can specify multiple delegates, which will run each specified" 386 | " delegate sequentially." 387 | ), 388 | ) 389 | parser.add_argument( 390 | "-r", 391 | "--resolution", 392 | type=int, 393 | default=DEFAULT_RESOLUTION, 394 | help="Resolution of the rendered test images", 395 | ) 396 | parser.add_argument( 397 | "-s", 398 | "--samples", 399 | type=int, 400 | help=( 401 | "Number of samples per-pixel for the embree render delegate (if not specified, uses embree's default," 402 | " currently 100)" 403 | ), 404 | ) 405 | parser.add_argument( 406 | "-c", 407 | "--cameras", 408 | nargs="+", 409 | help="Prim paths to cameras to render from", 410 | ) 411 | parser.add_argument( 412 | "-i", 413 | "--include", 414 | nargs="+", 415 | default=DEFAULT_INCLUDE_GLOBS, 416 | help="Glob pattern(s) to match input usd filepaths to render", 417 | ) 418 | parser.add_argument( 419 | "-e", 420 | "--exclude", 421 | nargs="+", 422 | default=DEFAULT_EXCLUDE_GLOBS, 423 | help="Glob pattern(s) that will exclude usd filepaths to render - overrides a match against an include pattern", 424 | ) 425 | parser.add_argument( 426 | "-o", 427 | "--output-dir", 428 | default=DEFAULT_OUTPUT_DIR, 429 | help=( 430 | "Base directory under which to write the rendered images. Subdirectories will be created for each render" 431 | " delegate." 432 | ), 433 | ) 434 | parser.add_argument( 435 | "-f", 436 | "--frames", 437 | type=FrameRange.from_str, 438 | help=( 439 | "Only render the given frame or frame range; may be single digit, or inclusive range specified as start:end" 440 | ), 441 | ) 442 | parser.add_argument( 443 | "-x", 444 | "--seed", 445 | type=int, 446 | default=DEFAULT_SEED, 447 | help="Set a random number seed, for repeatable results; set to -1 to use a different seed on each invocation", 448 | ) 449 | parser.add_argument( 450 | "--no-progress-bar", 451 | dest="progress_bar", 452 | action="store_false", 453 | help="Disable the progress bar - may be useful for debugging", 454 | ) 455 | 456 | return parser 457 | 458 | 459 | def main(argv=None): 460 | if argv is None: 461 | argv = sys.argv[1:] 462 | parser = get_parser() 463 | args = parser.parse_args(argv) 464 | 465 | try: 466 | failures = run_tests( 467 | include_globs=args.include, 468 | exclude_globs=args.exclude, 469 | output_dir=args.output_dir, 470 | delegates=args.delegates, 471 | resolution=args.resolution, 472 | samples=args.samples, 473 | cameras=args.cameras, 474 | frames=args.frames, 475 | seed=args.seed, 476 | progress_bar=args.progress_bar, 477 | ) 478 | except Exception: # pylint: disable=broad-except 479 | traceback.print_exc() 480 | return 1 481 | 482 | print() 483 | if failures: 484 | print("!" * 80) 485 | print(f"Enountered {len(failures)} failures:") 486 | for f in failures: 487 | print(f) 488 | print("!" * 80) 489 | return 1 490 | print("All lights successfully rendered") 491 | return 0 492 | 493 | 494 | if __name__ == "__main__" and not is_ipython(): 495 | sys.exit(main()) 496 | -------------------------------------------------------------------------------- /usd/iesTest.usda: -------------------------------------------------------------------------------- 1 | #usda 1.0 2 | ( 3 | customLayerData = { 4 | dictionary MovieCaptureSettings = { 5 | double animation_fps = 24 6 | int batch_count = 1 7 | string camera_name = "/cameras/camera1" 8 | bool capture_application = 0 9 | int capture_every_nth_frames = 20 10 | bool capture_every_nth_frames_checked = 0 11 | int capture_frame_end = 41 12 | int capture_frame_start = 1 13 | string capture_name = "iesTest-rtx" 14 | string capture_range = "Custom Range - Frames" 15 | double capture_time_end = 10 16 | double capture_time_start = 0 17 | string exr_compression_method = "zips" 18 | double fps = 24 19 | bool generate_shader_cache = 0 20 | bool hdr_for_exr_checked = 1 21 | bool hdr_for_exr_visible = 0 22 | int iray_pathtrace_spp = 1 23 | int iray_subframes_per_frame = 32 24 | string movie_type = "Sequence" 25 | int mp4_encoding_bitrate = 16777216 26 | int mp4_encoding_iframe_interval = 60 27 | string mp4_encoding_preset = "PRESET_DEFAULT" 28 | string mp4_encoding_profile = "H264_PROFILE_HIGH" 29 | string mp4_encoding_rc_mode = "RC_VBR" 30 | int mp4_encoding_rc_target_quality = 0 31 | bool mp4_encoding_video_full_range_flag = 0 32 | string output_format = ".exr" 33 | string output_path = "./renders/rtx" 34 | bool overwrite_existing_frame_checked = 0 35 | bool pathtrace_mb_checked = 0 36 | double pathtrace_mb_frame_shutter_close = 0.5 37 | double pathtrace_mb_frame_shutter_open = 0 38 | int pathtrace_mb_subframes = 64 39 | int pathtrace_spp_per_iteration_mgpu = 1 40 | int pathtrace_spp_per_subframe = 128 41 | string queue_instance = "localhost Queue" 42 | int realtime_settle_latency = 0 43 | string render_preset = "PathTracing" 44 | bool render_style = 1 45 | bool renumber_negtive_frames_checked = 0 46 | int resolution_aspect_ratio_selected = 0 47 | string resolution_aspect_ratios = '["1.000:1", "16:9", "4:3"]' 48 | int resolution_height = 512 49 | string resolution_type = "Custom" 50 | bool resolution_w_h_linked = 0 51 | int resolution_width = 512 52 | int run_n_frames_before_start = 20 53 | bool run_n_frames_before_start_checked = 0 54 | bool save_alpha_checked = 0 55 | bool skip_upload_to_s3 = 0 56 | int start_delay_seconds = 10 57 | double sunstudy_end = 18 58 | int sunstudy_movie_minutes = 1 59 | int sunstudy_movie_seconds = 1 60 | double sunstudy_start = 6 61 | string task_comment = "" 62 | string task_priority = "" 63 | string task_type = "create-render" 64 | bool upload_to_s3 = 0 65 | } 66 | dictionary cameraSettings = { 67 | dictionary Front = { 68 | double3 position = (0, 0, 500) 69 | double radius = 500 70 | } 71 | dictionary Perspective = { 72 | double3 position = (500, 500, 500) 73 | double3 target = (-0.00000397803842133726, 0.000007956076785831101, -0.000003978038307650422) 74 | } 75 | dictionary Right = { 76 | double3 position = (-500, 0, 0) 77 | double radius = 500 78 | } 79 | dictionary Top = { 80 | double3 position = (0, 500, 0) 81 | double radius = 500 82 | } 83 | string boundCamera = "/cameras/camera1" 84 | } 85 | dictionary navmeshSettings = { 86 | double agentHeight = 180 87 | double agentRadius = 20 88 | bool excludeRigidBodies = 1 89 | int ver = 1 90 | double voxelCeiling = 460 91 | } 92 | dictionary omni_layer = { 93 | string authoring_layer = "./iesTest.usda" 94 | } 95 | dictionary renderSettings = { 96 | float3 "rtx:debugView:pixelDebug:textColor" = (0, 1e18, 0) 97 | int "rtx:externalFrameCounter" = 41 98 | float3 "rtx:fog:fogColor" = (0.75, 0.75, 0.75) 99 | float3 "rtx:index:regionOfInterestMax" = (0, 0, 0) 100 | float3 "rtx:index:regionOfInterestMin" = (0, 0, 0) 101 | float3 "rtx:iray:environment_dome_ground_position" = (0, 0, 0) 102 | float3 "rtx:iray:environment_dome_ground_reflectivity" = (0, 0, 0) 103 | float3 "rtx:iray:environment_dome_rotation_axis" = (3.4028235e38, 3.4028235e38, 3.4028235e38) 104 | float3 "rtx:post:backgroundZeroAlpha:backgroundDefaultColor" = (0, 0, 0) 105 | float3 "rtx:post:colorcorr:contrast" = (1, 1, 1) 106 | float3 "rtx:post:colorcorr:gain" = (1, 1, 1) 107 | float3 "rtx:post:colorcorr:gamma" = (1, 1, 1) 108 | float3 "rtx:post:colorcorr:offset" = (0, 0, 0) 109 | float3 "rtx:post:colorcorr:saturation" = (1, 1, 1) 110 | float3 "rtx:post:colorgrad:blackpoint" = (0, 0, 0) 111 | float3 "rtx:post:colorgrad:contrast" = (1, 1, 1) 112 | float3 "rtx:post:colorgrad:gain" = (1, 1, 1) 113 | float3 "rtx:post:colorgrad:gamma" = (1, 1, 1) 114 | float3 "rtx:post:colorgrad:lift" = (0, 0, 0) 115 | float3 "rtx:post:colorgrad:multiply" = (1, 1, 1) 116 | float3 "rtx:post:colorgrad:offset" = (0, 0, 0) 117 | float3 "rtx:post:colorgrad:whitepoint" = (1, 1, 1) 118 | float3 "rtx:post:lensDistortion:lensFocalLengthArray" = (10, 30, 50) 119 | float3 "rtx:post:lensFlares:anisoFlareFalloffX" = (450, 475, 500) 120 | float3 "rtx:post:lensFlares:anisoFlareFalloffY" = (10, 10, 10) 121 | float3 "rtx:post:lensFlares:cutoffPoint" = (2, 2, 2) 122 | float3 "rtx:post:lensFlares:haloFlareFalloff" = (10, 10, 10) 123 | float3 "rtx:post:lensFlares:haloFlareRadius" = (75, 75, 75) 124 | float3 "rtx:post:lensFlares:isotropicFlareFalloff" = (50, 50, 50) 125 | double "rtx:post:tonemap:filmIso" = 0 126 | float3 "rtx:post:tonemap:whitepoint" = (1, 1, 1) 127 | float3 "rtx:raytracing:inscattering:singleScatteringAlbedo" = (0.9, 0.9, 0.9) 128 | float3 "rtx:raytracing:inscattering:transmittanceColor" = (0.5, 0.5, 0.5) 129 | string "rtx:rendermode" = "PathTracing" 130 | float3 "rtx:sceneDb:ambientLightColor" = (0.1, 0.1, 0.1) 131 | } 132 | } 133 | defaultPrim = "sopcreate_iesTest" 134 | endTimeCode = 41 135 | framesPerSecond = 24 136 | metersPerUnit = 1 137 | startTimeCode = 1 138 | timeCodesPerSecond = 24 139 | upAxis = "Y" 140 | ) 141 | 142 | def Scope "Render" 143 | { 144 | def RenderSettings "Settings" ( 145 | prepend apiSchemas = ["KarmaRenderSettingsAPI"] 146 | ) 147 | { 148 | custom int arnold:global:AA_samples = 6 149 | custom bool arnold:global:enable_progressive_render = 1 150 | custom int arnold:global:GI_total_depth = 0 151 | rel camera = 152 | float4 dataWindowNDC = (0, 0, 1, 1) 153 | token[] includedPurposes = ["default", "render"] 154 | bool instantaneousShutter = 1 155 | int karma:global:pathtracedsamples = 64 156 | int karma:global:samplesperpixel = 9 157 | custom float karma:object:diffuselimit = 0 158 | custom float karma:object:reflectlimit = 0 159 | custom float karma:object:refractlimit = 0 160 | custom float karma:object:ssslimit = 0 161 | token[] materialBindingPurposes = ["full", "allPurpose"] 162 | float pixelAspectRatio = 1 163 | int2 resolution = (512, 512) 164 | custom string ri:integrator:name = "PxrDirectLighting" 165 | } 166 | } 167 | 168 | def Scope "materials" 169 | { 170 | def Material "usdpreviewsurface1" 171 | { 172 | token outputs:displacement.connect = 173 | token outputs:surface.connect = 174 | 175 | def Shader "usdpreviewsurface1" 176 | { 177 | uniform token info:id = "UsdPreviewSurface" 178 | color3f inputs:diffuseColor = (1, 1, 1) 179 | float inputs:ior = 1 180 | int inputs:useSpecularWorkflow = 1 181 | token outputs:displacement 182 | token outputs:surface 183 | } 184 | } 185 | } 186 | 187 | def Xform "cameras" 188 | { 189 | def Camera "iesBottom" ( 190 | prepend apiSchemas = ["HoudiniCameraPlateAPI", "HoudiniViewportGuideAPI"] 191 | ) 192 | { 193 | float2 clippingRange = (0.001, 1000000) 194 | float exposure = 0 195 | float focalLength = 0.0001 196 | float focusDistance = 5 197 | float fStop = 0 198 | float horizontalAperture = 0.0002 199 | float horizontalApertureOffset = 0 200 | token projection = "perspective" 201 | double shutter:close = 0 202 | double shutter:open = 0 203 | float verticalAperture = 0.0002 204 | float verticalApertureOffset = 0 205 | matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 0.7071067811865476, 0.7071067811865476, 0), (0, -0.7071067811865476, 0.7071067811865476, 0), (0, 0, 0, 1) ) 206 | uniform token[] xformOpOrder = ["xformOp:transform"] 207 | } 208 | 209 | def Camera "iesTop" ( 210 | prepend apiSchemas = ["HoudiniCameraPlateAPI", "HoudiniViewportGuideAPI"] 211 | ) 212 | { 213 | float2 clippingRange = (0.001, 1000000) 214 | float exposure = 0 215 | float focalLength = 0.0001 216 | float focusDistance = 5 217 | float fStop = 0 218 | float horizontalAperture = 0.0002 219 | float horizontalApertureOffset = 0 220 | token projection = "perspective" 221 | double shutter:close = 0 222 | double shutter:open = 0 223 | float verticalAperture = 0.0002 224 | float verticalApertureOffset = 0 225 | matrix4d xformOp:transform = ( (1, -0, 0, 0), (0, -0.7071067811865475, 0.7071067811865476, 0), (0, -0.7071067811865476, -0.7071067811865475, 0), (0, -0, 0, 1) ) 226 | uniform token[] xformOpOrder = ["xformOp:transform"] 227 | } 228 | } 229 | 230 | def Xform "sopcreate_iesTest" ( 231 | prepend apiSchemas = ["MaterialBindingAPI"] 232 | kind = "component" 233 | ) 234 | { 235 | rel material:binding = 236 | matrix4d xformOp:transform:xform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) 237 | uniform token[] xformOpOrder = ["xformOp:transform:xform"] 238 | 239 | def Mesh "mesh_0" 240 | { 241 | float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] 242 | int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] 243 | int[] faceVertexIndices = [0, 2, 3, 1, 4, 6, 7, 5, 6, 3, 2, 7, 5, 0, 1, 4, 5, 7, 2, 0, 1, 3, 6, 4] 244 | uniform token orientation = "leftHanded" 245 | point3f[] points = [(0.5, -0.5, 0.5), (-0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5), (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (-0.5, 0.5, -0.5), (0.5, 0.5, -0.5)] ( 246 | interpolation = "vertex" 247 | ) 248 | uniform token subdivisionScheme = "catmullClark" 249 | } 250 | } 251 | 252 | def Xform "lights" 253 | { 254 | def SphereLight "iesTest_light" ( 255 | prepend apiSchemas = ["ShapingAPI", "HoudiniViewportLightAPI", "HoudiniViewportGuideAPI", "LightAPI"] 256 | ) 257 | { 258 | custom float barndoorbottom = 0 259 | custom float barndoorbottomedge = 0 260 | custom float barndoorleft = 0 261 | custom float barndoorleftedge = 0 262 | custom float barndoorright = 0 263 | custom float barndoorrightedge = 0 264 | custom float barndoortop = 0 265 | custom float barndoortopedge = 0 266 | float3[] extent = [(-0.005, -0.005, -0.005), (0.005, 0.005, 0.005)] 267 | color3f inputs:color = (1, 1, 1) 268 | float inputs:colorTemperature = 6500 269 | float inputs:diffuse = 1 270 | bool inputs:enableColorTemperature = 0 271 | float inputs:exposure = 0 272 | float inputs:intensity.timeSamples = { 273 | 1: 1000, 274 | 2: 1000, 275 | 3: 1000, 276 | 4: 1000, 277 | 5: 1000, 278 | 6: 1000, 279 | 7: 1000, 280 | 8: 1000, 281 | 9: 1000, 282 | 10: 1000, 283 | 11: 1000, 284 | 12: 1000, 285 | 13: 1000, 286 | 14: 1000, 287 | 15: 1000, 288 | 16: 1000, 289 | 17: 1000, 290 | 18: 1000, 291 | 19: 1000, 292 | 20: 1000, 293 | 21: 1000, 294 | 22: 1000, 295 | 23: 1000, 296 | 24: 1000, 297 | 25: 1000, 298 | 26: 1000, 299 | 27: 1000, 300 | 28: 1000, 301 | 29: 1000, 302 | 30: 1000, 303 | 31: 1000000, 304 | 32: 1000000, 305 | 33: 1000000, 306 | 34: 1000000, 307 | 35: 1000000, 308 | 36: 1000000, 309 | 37: 1000000, 310 | 38: 1000000, 311 | 39: 1000000, 312 | 40: 1000000, 313 | 41: 1000000, 314 | } 315 | bool inputs:normalize = 0 316 | float inputs:radius = 0.005 317 | float inputs:shaping:cone:angle = 180 318 | float inputs:shaping:cone:softness = 0 319 | float inputs:shaping:ies:angleScale.timeSamples = { 320 | 1: 0, 321 | 2: -1, 322 | 3: -0.75, 323 | 4: -0.5, 324 | 5: -0.25, 325 | 6: 4.440892e-16, 326 | 7: 0.25, 327 | 8: 0.5, 328 | 9: 0.75, 329 | 10: 1, 330 | 11: 0, 331 | 12: -1, 332 | 13: -0.75, 333 | 14: -0.5, 334 | 15: -0.25, 335 | 16: 0, 336 | 17: 0.25, 337 | 18: 0.5, 338 | 19: 0.75, 339 | 20: 1, 340 | 21: 0, 341 | 22: -1, 342 | 23: -0.75, 343 | 24: -0.5, 344 | 25: -0.25, 345 | 26: 0, 346 | 27: 0.25, 347 | 28: 0.5, 348 | 29: 0.75, 349 | 30: 1, 350 | 31: 0, 351 | 32: -1, 352 | 33: -0.75, 353 | 34: -0.5, 354 | 35: -0.25, 355 | 36: -6.661338e-16, 356 | 37: 0.25, 357 | 38: 0.5, 358 | 39: 0.75, 359 | 40: 1, 360 | 41: 0, 361 | } 362 | asset inputs:shaping:ies:file.timeSamples = { 363 | 1: @../test_stripes_nonuniform.ies@, 364 | 2: @../test_stripes_nonuniform.ies@, 365 | 3: @../test_stripes_nonuniform.ies@, 366 | 4: @../test_stripes_nonuniform.ies@, 367 | 5: @../test_stripes_nonuniform.ies@, 368 | 6: @../test_stripes_nonuniform.ies@, 369 | 7: @../test_stripes_nonuniform.ies@, 370 | 8: @../test_stripes_nonuniform.ies@, 371 | 9: @../test_stripes_nonuniform.ies@, 372 | 10: @../test_stripes_nonuniform.ies@, 373 | 11: @../test_stripes_uniform.ies@, 374 | 12: @../test_stripes_uniform.ies@, 375 | 13: @../test_stripes_uniform.ies@, 376 | 14: @../test_stripes_uniform.ies@, 377 | 15: @../test_stripes_uniform.ies@, 378 | 16: @../test_stripes_uniform.ies@, 379 | 17: @../test_stripes_uniform.ies@, 380 | 18: @../test_stripes_uniform.ies@, 381 | 19: @../test_stripes_uniform.ies@, 382 | 20: @../test_stripes_uniform.ies@, 383 | 21: @../test_stripes_uniform.ies@, 384 | 22: @../test_stripes_uniform.ies@, 385 | 23: @../test_stripes_uniform.ies@, 386 | 24: @../test_stripes_uniform.ies@, 387 | 25: @../test_stripes_uniform.ies@, 388 | 26: @../test_stripes_uniform.ies@, 389 | 27: @../test_stripes_uniform.ies@, 390 | 28: @../test_stripes_uniform.ies@, 391 | 29: @../test_stripes_uniform.ies@, 392 | 30: @../test_stripes_uniform.ies@, 393 | 31: @../test_stripes_uniform.ies@, 394 | 32: @../test_stripes_uniform.ies@, 395 | 33: @../test_stripes_uniform.ies@, 396 | 34: @../test_stripes_uniform.ies@, 397 | 35: @../test_stripes_uniform.ies@, 398 | 36: @../test_stripes_uniform.ies@, 399 | 37: @../test_stripes_uniform.ies@, 400 | 38: @../test_stripes_uniform.ies@, 401 | 39: @../test_stripes_uniform.ies@, 402 | 40: @../test_stripes_uniform.ies@, 403 | 41: @@, 404 | } 405 | bool inputs:shaping:ies:normalize.timeSamples = { 406 | 1: 0, 407 | 2: 0, 408 | 3: 0, 409 | 4: 0, 410 | 5: 0, 411 | 6: 0, 412 | 7: 0, 413 | 8: 0, 414 | 9: 0, 415 | 10: 0, 416 | 11: 0, 417 | 12: 0, 418 | 13: 0, 419 | 14: 0, 420 | 15: 0, 421 | 16: 0, 422 | 17: 0, 423 | 18: 0, 424 | 19: 0, 425 | 20: 0, 426 | 21: 1, 427 | 22: 1, 428 | 23: 1, 429 | 24: 1, 430 | 25: 1, 431 | 26: 1, 432 | 27: 1, 433 | 28: 1, 434 | 29: 1, 435 | 30: 1, 436 | 31: 0, 437 | 32: 0, 438 | 33: 0, 439 | 34: 0, 440 | 35: 0, 441 | 36: 0, 442 | 37: 0, 443 | 38: 0, 444 | 39: 0, 445 | 40: 0, 446 | 41: 0, 447 | } 448 | float inputs:specular = 1 449 | rel light:filters = None 450 | bool primvars:arnold:visibility:camera = 0 ( 451 | interpolation = "constant" 452 | ) 453 | string primvars:karma:object:rendervisibility = "-primary" ( 454 | interpolation = "constant" 455 | ) 456 | int primvars:ri:attributes:visibility:camera = 0 ( 457 | interpolation = "constant" 458 | ) 459 | matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) 460 | uniform token[] xformOpOrder = ["xformOp:transform"] 461 | } 462 | } 463 | 464 | -------------------------------------------------------------------------------- /luxtest_hou_utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import math 3 | import os 4 | import re 5 | import sys 6 | 7 | from typing import Iterable, NamedTuple, Optional, TypeAlias, Union 8 | 9 | import hou 10 | 11 | NodeOrRe: TypeAlias = Union[hou.Node, re.Pattern] 12 | NodeOrReOrIterable = Union[NodeOrRe, Iterable[NodeOrRe]] 13 | 14 | 15 | THIS_FILE = os.path.abspath(inspect.getsourcefile(lambda: None) or __file__) 16 | THIS_DIR = os.path.dirname(THIS_FILE) 17 | 18 | if THIS_DIR not in sys.path: 19 | sys.path.append(THIS_DIR) 20 | 21 | import luxtest_const 22 | 23 | ############################################################################### 24 | # Constants 25 | ############################################################################### 26 | 27 | THIS_FILE = os.path.abspath(inspect.getsourcefile(lambda: None) or __file__) 28 | THIS_DIR = os.path.dirname(THIS_FILE) 29 | 30 | ############################################################################### 31 | # General Houdini Utilities 32 | ############################################################################### 33 | 34 | 35 | def top_stage_nodes(type=""): 36 | nodes = hou.node("/stage").children() 37 | if type: 38 | if isinstance(type, str): 39 | type = lop_type(type) 40 | nodes = [x for x in nodes if x.type() == type] 41 | return nodes 42 | 43 | 44 | RENDERER_SHORT_NAMES = { 45 | "BRAY_HdKarma": "karma", 46 | "HdArnoldRendererPlugin": "arnold", 47 | "HdPrmanLoaderRendererPlugin": "ris", 48 | } 49 | 50 | 51 | # Because dealing with encoded parm names (but with not-encoded-tuple-member-suffixes) is annoying... 52 | # ie, you can't just reliably do hou.text.decode(parm.name())! 53 | class ParmName(NamedTuple): 54 | tuplename: str 55 | suffix: str = "" 56 | 57 | def __str__(self): 58 | return self.encoded 59 | 60 | def __repr__(self): 61 | # want to get a compact repr... 62 | if self.suffix == "": 63 | return f"PN({self.tuplename!r})" 64 | return f"PN{tuple(self)!r}" 65 | 66 | @property 67 | def pretty(self): 68 | if self.suffix: 69 | return f"{self.tuplename},{self.suffix}" 70 | else: 71 | return self.tuplename 72 | 73 | @property 74 | def encoded_tuplename(self): 75 | return hou.text.encode(self.tuplename) 76 | 77 | @property 78 | def encoded(self): 79 | return self.encoded_tuplename + self.suffix 80 | 81 | @classmethod 82 | def from_parm(cls, parm: Union[hou.Parm, hou.ParmTuple]): 83 | if isinstance(parm, hou.ParmTuple): 84 | parm_tuple = parm 85 | parm = None 86 | else: 87 | parm_tuple = parm.tuple() 88 | encoded_tuplename = parm_tuple.name() 89 | if parm is None: 90 | suffix = "" 91 | else: 92 | encoded = parm.name() 93 | assert encoded.startswith(encoded_tuplename) 94 | suffix = encoded[len(encoded_tuplename) :] 95 | return cls(hou.text.decode(encoded_tuplename), suffix) 96 | 97 | 98 | PN = ParmName 99 | 100 | 101 | def pretty_parm_path(parm: hou.Parm): 102 | parmName = ParmName.from_parm(parm) 103 | return f"{parm.node().path()}/{parmName.pretty}" 104 | 105 | 106 | def get_parm_tuple(node, name): 107 | parmTuple = node.parmTuple(name) 108 | if not parmTuple: 109 | parmTuple = node.parmTuple(hou.text.encode(name)) 110 | if not parmTuple: 111 | # ok, it couldn't be easy - try tuple names... 112 | raise ValueError(f"node {node.path()!r} has no parmTuple {name!r}") 113 | return parmTuple 114 | 115 | 116 | def get_parm(node, name, suffix=""): 117 | if isinstance(node, str): 118 | result = hou.node(node) 119 | if result: 120 | node = result 121 | else: 122 | result = get_light(node) 123 | if not result: 124 | raise ValueError(f"could not find node: {node}") 125 | node = result 126 | try: 127 | parmTuple = node.parmTuple(name) 128 | except ValueError: 129 | parmTuple = None 130 | if not parmTuple: 131 | if suffix: 132 | raise ValueError( 133 | f"could not find parmTuple for node {node.path()}: {name} (so cannot find suffix {suffix})" 134 | ) 135 | # could have been given an encoded name with suffix... 136 | parm = node.parm(name) 137 | if parm: 138 | return parm 139 | # last ditch - try encoded? 140 | parm = node.parm(hou.text.encode(name)) 141 | if parm: 142 | return parm 143 | raise ValueError(f"Could not find a parmTuple for node {node.path()} with name: {name}") 144 | if len(parmTuple) > 1: 145 | if not suffix: 146 | raise ValueError(f"{parmTuple.path()} was a parmTuple - specify a suffix, or use get_parm_tuple") 147 | full_encoded_name = parmTuple.name() + suffix 148 | parm = node.parm(full_encoded_name) 149 | if not parm: 150 | raise ValueError( 151 | f"Found parmTuple {parmTuple.name()} for node {node.path()}, but could not find with suffix {suffix}" 152 | ) 153 | return parm 154 | 155 | if suffix: 156 | raise ValueError( 157 | f"Found parmTuple {parmTuple.name()} for node {node.path()}, but only had one member, so could not find" 158 | f" suffix {suffix}" 159 | ) 160 | return parmTuple[0] 161 | 162 | 163 | def get_renderer(node): 164 | return RENDERER_SHORT_NAMES[node.parm("renderer").eval()] 165 | 166 | 167 | def lop_type(name): 168 | return hou.lopNodeTypeCategory().nodeType(name) 169 | 170 | 171 | def base_type(nodetype): 172 | if isinstance(nodetype, hou.Node): 173 | nodetype = nodetype.type() 174 | return nodetype.namespaceOrder()[-1] 175 | 176 | 177 | def is_light_type(nodetype): 178 | return base_type(nodetype) in ("light", "distantlight", "domelight") 179 | 180 | 181 | def is_area_light_type(nodetype): 182 | return base_type(nodetype) == "light" 183 | 184 | 185 | def is_light(node): 186 | return is_light_type(node.type()) 187 | 188 | 189 | def is_area_light(node): 190 | return is_area_light_type(node.type()) 191 | 192 | 193 | def get_nodes_referencing(node, include_self=False, node_refs=True, parm_refs=True): 194 | all_refs = set() 195 | if node_refs: 196 | all_refs.update(x.node() for x in node.parmsReferencingThis()) 197 | if parm_refs: 198 | for parm in node.parms(): 199 | all_refs.update(x.node() for x in parm.parmsReferencingThis()) 200 | if not include_self: 201 | all_refs.discard(node) 202 | return all_refs 203 | 204 | 205 | def make_parm_tuple_refs(tuple_name, source_node, dest_nodes): 206 | source_parm_tuple = get_parm_tuple(source_node, tuple_name) 207 | 208 | for dest_node in dest_nodes: 209 | dest_parm_tuple = get_parm_tuple(dest_node, source_parm_tuple.name()) 210 | assert len(source_parm_tuple) == len(dest_parm_tuple) 211 | for source_parm, dest_parm in zip(source_parm_tuple, dest_parm_tuple): 212 | source_parm_name = pretty_parm_path(source_parm) 213 | dest_parm_name = pretty_parm_path(dest_parm) 214 | if dest_parm.getReferencedParm() == source_parm: 215 | print(f"{dest_parm_name} already connected to {source_parm_name} - skipped") 216 | else: 217 | ref_exp = dest_parm.referenceExpression(source_parm) 218 | dest_parm.setExpression(ref_exp, language=hou.exprLanguage.Hscript) 219 | print(f"{dest_parm_name} now references {source_parm_name}") 220 | 221 | 222 | def get_all_animated_parms(root_node="/", include: NodeOrReOrIterable = (), exclude: NodeOrReOrIterable = ()): 223 | if isinstance(root_node, str): 224 | root_node = hou.node(root_node) 225 | all = [] 226 | 227 | def make_matcher(obj: NodeOrRe): 228 | if isinstance(obj, hou.Node): 229 | return lambda x: x == obj 230 | elif isinstance(obj, re.Pattern): 231 | return lambda x: obj.search(x.path()) 232 | raise TypeError(f"obj must be hou.Node or re.Pattern - got: {obj}") 233 | 234 | if include: 235 | if isinstance(include, (re.Pattern, hou.Node)): 236 | include = [include] 237 | include_matchers = [make_matcher(x) for x in include] 238 | 239 | def is_included(obj): 240 | return any(x(obj) for x in include_matchers) 241 | 242 | else: 243 | 244 | def is_included(obj): 245 | return True 246 | 247 | if exclude: 248 | if isinstance(exclude, (re.Pattern, hou.Node)): 249 | exclude = [exclude] 250 | exclude_matchers = [make_matcher(x) for x in exclude] 251 | 252 | def is_excluded(obj): 253 | return any(x(obj) for x in exclude_matchers) 254 | 255 | else: 256 | 257 | def is_excluded(obj): 258 | return False 259 | 260 | for node in (root_node,) + root_node.allSubChildren(): 261 | if is_excluded(node) or not is_included(node): 262 | continue 263 | all.extend(parm for parm in node.parms() if len(parm.keyframes()) > 1) 264 | return all 265 | 266 | 267 | def select_all_animated_parms(root_node="/"): 268 | parms = get_all_animated_parms(root_node) 269 | for p in parms: 270 | p.setSelect(True) 271 | return parms 272 | 273 | 274 | def get_keyframe_value(keyframe): 275 | if isinstance(keyframe, hou.StringKeyframe): 276 | return keyframe.expression() 277 | return keyframe.value() 278 | 279 | 280 | def insert_anim_gap(frame, num_frames, include: Iterable[NodeOrRe] = (), exclude: Iterable[NodeOrRe] = ()): 281 | """Shifts all keyframes >= frame by num_frames. 282 | 283 | Sets new keyframe at the frame to keep animation constant 284 | during it (if needed) 285 | """ 286 | to_shift = {} 287 | to_key = {} 288 | 289 | for parm in get_all_animated_parms(include=include, exclude=exclude): 290 | before_keys = parm.keyframesBefore(frame) 291 | after_keys = parm.keyframesAfter(frame) 292 | if before_keys and before_keys[-1].frame() == frame: 293 | if not after_keys or after_keys[0].frame() != frame: 294 | after_keys.insert(0, before_keys[-1]) 295 | before_keys = before_keys[:-1] 296 | if not after_keys: 297 | # nothing to move, carry on! 298 | continue 299 | to_shift[parm] = after_keys 300 | if not before_keys: 301 | # no before keys, don't need a new keyframe 302 | continue 303 | # we have both before and after keys... 304 | before = before_keys[-1] 305 | after = after_keys[0] 306 | after_val = get_keyframe_value(after) 307 | if get_keyframe_value(before) == after_val: 308 | # constant value, don't need a new keyframe 309 | continue 310 | # different values - abort, don't want to insert gap in the middle 311 | # of an animated value change 312 | if not math.isclose(after.frame(), frame): 313 | raise RuntimeError(f"Cannot insert anim gap at frame {frame} - {parm.path()} has animation") 314 | new_keys = [] 315 | for new_frame in (frame, frame + num_frames - 1): 316 | if isinstance(after, hou.StringKeyframe): 317 | keyframe = hou.StringKeyframe(after) 318 | keyframe.setFrame(new_frame) 319 | else: 320 | keyframe = hou.Keyframe(after_val, hou.frameToTime(new_frame)) 321 | keyframe.setExpression("linear()") 322 | new_keys.append(keyframe) 323 | 324 | to_key[parm] = new_keys 325 | for parm, keys in to_shift.items(): 326 | for key in keys: 327 | frame = key.frame() 328 | parm.deleteKeyframeAtFrame(frame) 329 | key.setFrame(frame + num_frames) 330 | parm.setKeyframes(keys) 331 | 332 | for parm, keys in to_key.items(): 333 | parm.setKeyframes(keys) 334 | 335 | 336 | def make_sphere_tuple_refs(tuple_name): 337 | sphere = hou.node("/stage/sphere_light") 338 | sphere_refs = [x for x in get_nodes_referencing(sphere) if is_light(x)] 339 | make_parm_tuple_refs(tuple_name, sphere, sphere_refs) 340 | 341 | 342 | def get_connected_recursive(node, predicate, direction, visited=None): 343 | results = set() 344 | if predicate(node): 345 | results.add(node) 346 | if visited is None: 347 | visited = set([node]) 348 | else: 349 | visited.add(node) 350 | if direction == "outputs": 351 | get_next = node.outputs 352 | elif direction == "inputs": 353 | get_next = node.inputs 354 | for other_node in get_next(): 355 | if other_node not in visited: 356 | results.update(get_connected_recursive(other_node, predicate, direction, visited=visited)) 357 | return results 358 | 359 | 360 | def get_upstream(node, predicate): 361 | return get_connected_recursive(node, predicate, "inputs") 362 | 363 | 364 | def get_downstream(node, predicate): 365 | return get_connected_recursive(node, predicate, "outputs") 366 | 367 | 368 | def get_downstream_lights(node): 369 | return get_downstream(node, is_light) 370 | 371 | 372 | def get_upstream_lights(node): 373 | return get_upstream(node, is_light) 374 | 375 | 376 | def get_connected_lights(node): 377 | if is_light(node): 378 | return set([node]) 379 | input_lights = get_upstream_lights(node) 380 | if input_lights: 381 | return input_lights 382 | return get_downstream_lights(node) 383 | 384 | 385 | def get_rop_out_parm(node): 386 | if node.type() == lop_type("usd_rop"): 387 | return node.parm("lopoutput") 388 | elif node.type() == lop_type("usdrender_rop"): 389 | return node.parm("outputimage") 390 | else: 391 | raise TypeError(f"Unrecognized rop node type: {node} - {node.type()}") 392 | 393 | 394 | def parm_at_default(parm): 395 | tuple_name = ParmName.from_parm(parm).tuplename 396 | missing = object() 397 | default = luxtest_const.DEFAULT_OVERRIDES.get(tuple_name, missing) 398 | if default is not missing: 399 | current = parm.eval() 400 | if isinstance(current, float) or isinstance(default, float): 401 | return math.isclose(current, default) 402 | return current == default 403 | return parm.isAtDefault() 404 | 405 | 406 | def get_non_default_parms(nodeOrParms, frames: Optional[Iterable[Union[float, int]]] = None): 407 | if isinstance(nodeOrParms, hou.Node): 408 | parms = nodeOrParms.parms() 409 | else: 410 | parms = list(nodeOrParms) 411 | 412 | if frames is None: 413 | return set([x for x in parms if not parm_at_default(x)]) 414 | orig_frame = hou.frame() 415 | non_default = None 416 | try: 417 | for frame in frames: 418 | hou.setFrame(frame) 419 | current_non_defaults = get_non_default_parms(parms) 420 | if non_default is None: 421 | non_default = current_non_defaults 422 | else: 423 | non_default.update(current_non_defaults) 424 | finally: 425 | hou.setFrame(orig_frame) 426 | return non_default 427 | 428 | 429 | def parmgrep(node, pattern): 430 | regex = re.compile(pattern, re.IGNORECASE) 431 | parms = parm_tuple_dict(node) 432 | return {name: node for name, node in parms.items() if regex.search(name) or regex.search(hou.text.encode(name))} 433 | 434 | 435 | def parm_tuple_dict(node, controls=False): 436 | """Returns a dict from decoded parm tuple name to the parmTuple""" 437 | pairs = [(hou.text.decode(p.name()), p) for p in node.parmTuples()] 438 | if not controls: 439 | pairs = [(name, node) for name, node in pairs if not name.endswith("_control")] 440 | pairs.sort(key=lambda pair: pair[0]) 441 | return dict(pairs) 442 | 443 | 444 | def input_parm_tuples(node, controls=False): 445 | all_parms_dict = parm_tuple_dict(node, controls=controls) 446 | return {name: node for name, node in all_parms_dict.items() if name.startswith("inputs:")} 447 | 448 | 449 | def houdini_range(start, stop=None, step=1): 450 | if stop is None: 451 | start = 0 452 | stop = start - 1 453 | current = start 454 | while current <= stop: 455 | yield current 456 | current += step 457 | 458 | 459 | def get_frames(node): 460 | start = node.parm("f1").eval() 461 | stop = node.parm("f2").eval() 462 | step = node.parm("f3").eval() 463 | return houdini_range(start, stop, step) 464 | 465 | 466 | ############################################################################### 467 | # Naming Utils 468 | ############################################################################### 469 | 470 | NODE_BASE_TYPE_TO_CATEGORY = { 471 | "usd_rop": "usd_rop", 472 | "usdrender_rop": "render", 473 | "camera": "camera", 474 | "distantlight": "light", 475 | "domelight": "light", 476 | "light": "light", 477 | "editproperties": "editproperties", 478 | "xform": "xform", 479 | } 480 | 481 | 482 | def node_category(node): 483 | base = base_type(node) 484 | return NODE_BASE_TYPE_TO_CATEGORY.get(base, base) 485 | 486 | 487 | LIGHT_NAME_RE = re.compile("^(?P.*)_light$") 488 | 489 | 490 | def parse_light_name(node_or_name): 491 | if isinstance(node_or_name, hou.Node): 492 | nodename = node_or_name.name() 493 | else: 494 | nodename = node_or_name 495 | return LIGHT_NAME_RE.match(nodename).group("name") 496 | 497 | 498 | def get_light(name): 499 | return hou.node(f"/stage/{name}_light") 500 | 501 | 502 | def get_rop_override_cam(rop): 503 | return rop.parm("override_camera").eval().rstrip("/").rsplit("/", 1)[-1] 504 | 505 | 506 | def get_standardized_name(node, associated_light_node): 507 | light_name = parse_light_name(associated_light_node) 508 | category = node_category(node) 509 | if category == "light": 510 | return f"{light_name}_light" 511 | elif category == "render": 512 | renderer = get_renderer(node) 513 | cam = get_rop_override_cam(node) 514 | if cam: 515 | cam = f"_{cam}" 516 | return f"{category}{cam}_{renderer}_{light_name}" 517 | elif category == "camera": 518 | cam_node_type = { 519 | 0: "edit", 520 | 1: "create", 521 | 2: "forceedit", 522 | }[node.parm("createprims").eval()] 523 | if cam_node_type == "create": 524 | prim_path = node.parm("primpath").eval() 525 | else: 526 | prim_path = node.parm("primpattern").eval() 527 | prim_name = prim_path.rstrip("/").rsplit("/", 1)[-1] 528 | return f"{category}_{cam_node_type}_{prim_name}_{light_name}" 529 | elif category == "xform": 530 | prim_name = node.parm("primpattern").eval().rstrip("/").rsplit("/", 1)[-1] 531 | return f"{category}_{prim_name}_{light_name}" 532 | else: 533 | return f"{category}_{light_name}" 534 | 535 | 536 | def standardize_node_names(dry_run=True): 537 | renames = [] 538 | for node in top_stage_nodes(): 539 | lights = get_connected_lights(node) 540 | if len(lights) != 1: 541 | continue 542 | light = lights.pop() 543 | new_name = get_standardized_name(node, light) 544 | old_name = node.name() 545 | if old_name != new_name: 546 | renames.append((old_name, new_name, node)) 547 | print() 548 | print("=" * 80) 549 | if not renames: 550 | print("Found no nodes to rename") 551 | return 552 | renames.sort() 553 | print(f"Found {len(renames)} nodes to rename:") 554 | print("=" * 80) 555 | for old_name, new_name, node in renames: 556 | print(f"rename: {old_name} => {new_name}") 557 | if not dry_run: 558 | node.setName(new_name) 559 | print("=" * 80) 560 | if dry_run: 561 | print("dry_run=True - nothing changed - to change names, use:") 562 | print(" standardize_node_names(dry_run=False)") 563 | 564 | 565 | def get_standardized_output_path(node, light_node): 566 | light_name = parse_light_name(light_node) 567 | if node.type() == lop_type("usd_rop"): 568 | return f"$HIP/usd/{light_name}.usda" 569 | elif node.type() == lop_type("usdrender_rop"): 570 | renderer = get_renderer(node) 571 | cam = get_rop_override_cam(node) 572 | if cam: 573 | cam = f".{cam}" 574 | return f"$HIP/renders/{renderer}/{light_name}-{renderer}{cam}.$F4.exr" 575 | 576 | 577 | def standardize_output_names(dry_run=True): 578 | renames = [] 579 | rop_nodes = [x for x in top_stage_nodes() if isinstance(x, hou.RopNode)] 580 | for rop in rop_nodes: 581 | lights = get_connected_lights(rop) 582 | if len(lights) != 1: 583 | print(f"found rop that couldn't be associated with one light: {rop} - {lights}") 584 | continue 585 | light = lights.pop() 586 | new_output_path = get_standardized_output_path(rop, light) 587 | parm = get_rop_out_parm(rop) 588 | old_output_path = parm.rawValue() 589 | if old_output_path != new_output_path: 590 | renames.append((rop.name(), parm, old_output_path, new_output_path)) 591 | print() 592 | print("=" * 80) 593 | if not renames: 594 | print("Found no node output paths to change") 595 | return 596 | renames.sort() 597 | print(f"Found {len(renames)} nodes with output paths to change:") 598 | print("=" * 80) 599 | for node_name, parm, old_output_path, new_output_path in renames: 600 | print(f"rename output for {node_name}:") 601 | print(f" {old_output_path}") 602 | print(f" {new_output_path}") 603 | if not dry_run: 604 | parm.set(new_output_path) 605 | print("=" * 80) 606 | if dry_run: 607 | print("dry_run=True - nothing changed - to change names, use:") 608 | print(" standardize_output_names(dry_run=False)") 609 | 610 | 611 | ############################################################################### 612 | # Summaries 613 | ############################################################################### 614 | 615 | SUMMARY_LINE_RE = re.compile("^\d+(?:-\d+)?:\s+.+", re.MULTILINE) 616 | 617 | 618 | def reset_net_box_summaries(dry_run=True): 619 | if THIS_DIR not in sys.path: 620 | sys.path.append(THIS_DIR) 621 | 622 | import genLightParamDescriptions 623 | 624 | param_descriptions = genLightParamDescriptions.read_descriptions() 625 | 626 | stage = hou.node("/stage") 627 | 628 | to_update = [] 629 | for box in stage.networkBoxes(): 630 | # find the light 631 | lights = [x for x in box.nodes() if is_light(x)] 632 | if not lights: 633 | continue 634 | if len(lights) > 1: 635 | raise RuntimeError("more than one light? oh noes!") 636 | light = lights[0] 637 | light_name = parse_light_name(light) 638 | usd_light_name = light_name.replace("-", "_") 639 | 640 | # now find the sticky note with the summary 641 | stickies = [x for x in box.stickyNotes() if SUMMARY_LINE_RE.search(x.text())] 642 | if len(stickies) != 1: 643 | raise RuntimeError(f"can't find right sticky for net box for light {light_name}") 644 | sticky = stickies[0] 645 | 646 | # now get the new summary text 647 | desc = param_descriptions[usd_light_name] 648 | try: 649 | summary = genLightParamDescriptions.summarize_light(light_name, desc) 650 | except Exception: 651 | print(desc) 652 | print(f"error summarizing light: {light_name}") 653 | raise 654 | if not summary: 655 | continue 656 | 657 | if sticky.text() != summary: 658 | to_update.append((light_name, sticky, summary)) 659 | 660 | print() 661 | print("=" * 80) 662 | if not to_update: 663 | print("Found no network box summaries to update") 664 | return 665 | to_update.sort() 666 | print(f"Found {len(to_update)} network box summaries to update:") 667 | print("=" * 80) 668 | for light_name, sticky, summary in to_update: 669 | print() 670 | print(f"{light_name}:") 671 | print(summary) 672 | if not dry_run: 673 | sticky.setText(summary) 674 | print("=" * 80) 675 | if dry_run: 676 | print("dry_run=True - nothing changed - to change summaries, use:") 677 | print(" reset_net_box_summaries(dry_run=False)") 678 | -------------------------------------------------------------------------------- /usd/distant.usda: -------------------------------------------------------------------------------- 1 | #usda 1.0 2 | ( 3 | customLayerData = { 4 | dictionary MovieCaptureSettings = { 5 | double animation_fps = 24 6 | int batch_count = 1 7 | string camera_name = "/cameras/camera1" 8 | bool capture_application = 0 9 | int capture_every_nth_frames = 20 10 | bool capture_every_nth_frames_checked = 0 11 | int capture_frame_end = 35 12 | int capture_frame_start = 1 13 | string capture_name = "distant-rtx" 14 | string capture_range = "Custom Range - Frames" 15 | double capture_time_end = 10 16 | double capture_time_start = 0 17 | string exr_compression_method = "zips" 18 | double fps = 24 19 | bool generate_shader_cache = 0 20 | bool hdr_for_exr_checked = 1 21 | bool hdr_for_exr_visible = 0 22 | int iray_pathtrace_spp = 1 23 | int iray_subframes_per_frame = 32 24 | string movie_type = "Sequence" 25 | int mp4_encoding_bitrate = 16777216 26 | int mp4_encoding_iframe_interval = 60 27 | string mp4_encoding_preset = "PRESET_DEFAULT" 28 | string mp4_encoding_profile = "H264_PROFILE_HIGH" 29 | string mp4_encoding_rc_mode = "RC_VBR" 30 | int mp4_encoding_rc_target_quality = 0 31 | bool mp4_encoding_video_full_range_flag = 0 32 | string output_format = ".exr" 33 | string output_path = "./renders/rtx" 34 | bool overwrite_existing_frame_checked = 0 35 | bool pathtrace_mb_checked = 0 36 | double pathtrace_mb_frame_shutter_close = 0.5 37 | double pathtrace_mb_frame_shutter_open = 0 38 | int pathtrace_mb_subframes = 64 39 | int pathtrace_spp_per_iteration_mgpu = 1 40 | int pathtrace_spp_per_subframe = 128 41 | string queue_instance = "localhost Queue" 42 | int realtime_settle_latency = 0 43 | string render_preset = "PathTracing" 44 | bool render_style = 1 45 | bool renumber_negtive_frames_checked = 0 46 | int resolution_aspect_ratio_selected = 0 47 | string resolution_aspect_ratios = '["1.000:1", "16:9", "4:3"]' 48 | int resolution_height = 512 49 | string resolution_type = "Custom" 50 | bool resolution_w_h_linked = 0 51 | int resolution_width = 512 52 | int run_n_frames_before_start = 20 53 | bool run_n_frames_before_start_checked = 0 54 | bool save_alpha_checked = 0 55 | bool skip_upload_to_s3 = 0 56 | int start_delay_seconds = 10 57 | double sunstudy_end = 18 58 | int sunstudy_movie_minutes = 1 59 | int sunstudy_movie_seconds = 1 60 | double sunstudy_start = 6 61 | string task_comment = "" 62 | string task_priority = "" 63 | string task_type = "create-render" 64 | bool upload_to_s3 = 0 65 | } 66 | dictionary cameraSettings = { 67 | dictionary Front = { 68 | double3 position = (0, 0, 500) 69 | double radius = 500 70 | } 71 | dictionary Perspective = { 72 | double3 position = (500, 500, 500) 73 | double3 target = (-0.00000397803842133726, 0.000007956076785831101, -0.000003978038307650422) 74 | } 75 | dictionary Right = { 76 | double3 position = (-500, 0, 0) 77 | double radius = 500 78 | } 79 | dictionary Top = { 80 | double3 position = (0, 500, 0) 81 | double radius = 500 82 | } 83 | string boundCamera = "/cameras/camera1" 84 | } 85 | dictionary navmeshSettings = { 86 | double agentHeight = 180 87 | double agentRadius = 20 88 | bool excludeRigidBodies = 1 89 | int ver = 1 90 | double voxelCeiling = 460 91 | } 92 | dictionary omni_layer = { 93 | string authoring_layer = "./distant.usda" 94 | } 95 | dictionary renderSettings = { 96 | float3 "rtx:debugView:pixelDebug:textColor" = (0, 1e18, 0) 97 | int "rtx:externalFrameCounter" = 35 98 | float3 "rtx:fog:fogColor" = (0.75, 0.75, 0.75) 99 | float3 "rtx:index:regionOfInterestMax" = (0, 0, 0) 100 | float3 "rtx:index:regionOfInterestMin" = (0, 0, 0) 101 | float3 "rtx:iray:environment_dome_ground_position" = (0, 0, 0) 102 | float3 "rtx:iray:environment_dome_ground_reflectivity" = (0, 0, 0) 103 | float3 "rtx:iray:environment_dome_rotation_axis" = (3.4028235e38, 3.4028235e38, 3.4028235e38) 104 | float3 "rtx:post:backgroundZeroAlpha:backgroundDefaultColor" = (0, 0, 0) 105 | float3 "rtx:post:colorcorr:contrast" = (1, 1, 1) 106 | float3 "rtx:post:colorcorr:gain" = (1, 1, 1) 107 | float3 "rtx:post:colorcorr:gamma" = (1, 1, 1) 108 | float3 "rtx:post:colorcorr:offset" = (0, 0, 0) 109 | float3 "rtx:post:colorcorr:saturation" = (1, 1, 1) 110 | float3 "rtx:post:colorgrad:blackpoint" = (0, 0, 0) 111 | float3 "rtx:post:colorgrad:contrast" = (1, 1, 1) 112 | float3 "rtx:post:colorgrad:gain" = (1, 1, 1) 113 | float3 "rtx:post:colorgrad:gamma" = (1, 1, 1) 114 | float3 "rtx:post:colorgrad:lift" = (0, 0, 0) 115 | float3 "rtx:post:colorgrad:multiply" = (1, 1, 1) 116 | float3 "rtx:post:colorgrad:offset" = (0, 0, 0) 117 | float3 "rtx:post:colorgrad:whitepoint" = (1, 1, 1) 118 | float3 "rtx:post:lensDistortion:lensFocalLengthArray" = (10, 30, 50) 119 | float3 "rtx:post:lensFlares:anisoFlareFalloffX" = (450, 475, 500) 120 | float3 "rtx:post:lensFlares:anisoFlareFalloffY" = (10, 10, 10) 121 | float3 "rtx:post:lensFlares:cutoffPoint" = (2, 2, 2) 122 | float3 "rtx:post:lensFlares:haloFlareFalloff" = (10, 10, 10) 123 | float3 "rtx:post:lensFlares:haloFlareRadius" = (75, 75, 75) 124 | float3 "rtx:post:lensFlares:isotropicFlareFalloff" = (50, 50, 50) 125 | double "rtx:post:tonemap:filmIso" = 0 126 | float3 "rtx:post:tonemap:whitepoint" = (1, 1, 1) 127 | float3 "rtx:raytracing:inscattering:singleScatteringAlbedo" = (0.9, 0.9, 0.9) 128 | float3 "rtx:raytracing:inscattering:transmittanceColor" = (0.5, 0.5, 0.5) 129 | string "rtx:rendermode" = "PathTracing" 130 | float3 "rtx:sceneDb:ambientLightColor" = (0.1, 0.1, 0.1) 131 | } 132 | } 133 | defaultPrim = "floor_plane" 134 | endTimeCode = 35 135 | framesPerSecond = 24 136 | metersPerUnit = 1 137 | startTimeCode = 1 138 | timeCodesPerSecond = 24 139 | upAxis = "Y" 140 | ) 141 | 142 | def Scope "Render" 143 | { 144 | def RenderSettings "Settings" ( 145 | prepend apiSchemas = ["KarmaRenderSettingsAPI"] 146 | ) 147 | { 148 | custom int arnold:global:AA_samples = 6 149 | custom bool arnold:global:enable_progressive_render = 1 150 | custom int arnold:global:GI_total_depth = 0 151 | rel camera = 152 | float4 dataWindowNDC = (0, 0, 1, 1) 153 | token[] includedPurposes = ["default", "render"] 154 | bool instantaneousShutter = 1 155 | int karma:global:pathtracedsamples = 64 156 | int karma:global:samplesperpixel = 9 157 | custom float karma:object:diffuselimit = 0 158 | custom float karma:object:reflectlimit = 0 159 | custom float karma:object:refractlimit = 0 160 | custom float karma:object:ssslimit = 0 161 | token[] materialBindingPurposes = ["full", "allPurpose"] 162 | float pixelAspectRatio = 1 163 | int2 resolution = (512, 512) 164 | custom string ri:integrator:name = "PxrDirectLighting" 165 | } 166 | } 167 | 168 | def Scope "materials" 169 | { 170 | def Material "usdpreviewsurface1" 171 | { 172 | token outputs:displacement.connect = 173 | token outputs:surface.connect = 174 | 175 | def Shader "usdpreviewsurface1" 176 | { 177 | uniform token info:id = "UsdPreviewSurface" 178 | color3f inputs:diffuseColor = (1, 1, 1) 179 | float inputs:ior = 1 180 | int inputs:useSpecularWorkflow = 1 181 | token outputs:displacement 182 | token outputs:surface 183 | } 184 | } 185 | } 186 | 187 | def Xform "cameras" 188 | { 189 | matrix4d xformOp:transform.timeSamples = { 190 | 1: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 191 | 2: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 192 | 3: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 193 | 4: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 194 | 5: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 195 | 6: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 196 | 7: ( (0.9396926207859083, 0.34202014332566877, 0, 0), (-0.34202014332566877, 0.9396926207859083, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 197 | 8: ( (0.7660444431189779, 0.6427876096865395, 0, 0), (-0.6427876096865395, 0.7660444431189779, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 198 | 9: ( (0.4999999999999999, 0.8660254037844387, 0, 0), (-0.8660254037844387, 0.4999999999999999, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 199 | 10: ( (0.17364817766693216, 0.9848077530122077, 0, 0), (-0.9848077530122077, 0.17364817766693216, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 200 | 11: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 201 | 12: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 202 | 13: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 203 | 14: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 204 | 15: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 205 | 16: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 206 | 17: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 207 | 18: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 208 | 19: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 209 | 20: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 210 | 21: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 211 | 22: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 212 | 23: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 213 | 24: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 214 | 25: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 215 | 26: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 216 | 27: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 217 | 28: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 218 | 29: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 219 | 30: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 220 | 31: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 221 | 32: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 222 | 33: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 223 | 34: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 224 | 35: ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ), 225 | } 226 | uniform token[] xformOpOrder = ["xformOp:transform"] 227 | 228 | def Camera "camera1" ( 229 | prepend apiSchemas = ["HoudiniCameraPlateAPI", "HoudiniViewportGuideAPI"] 230 | ) 231 | { 232 | float2 clippingRange = (1, 1000000) 233 | float exposure = 0 234 | float focalLength = 0.5 235 | float focusDistance = 5 236 | float fStop = 0 237 | float horizontalAperture = 0.20955 238 | float horizontalApertureOffset = 0 239 | token projection = "perspective" 240 | double shutter:close = 0.25 241 | double shutter:open = -0.25 242 | float verticalAperture = 0.20955 243 | float verticalApertureOffset = 0 244 | matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 30, 0, 1) ) 245 | uniform token[] xformOpOrder = ["xformOp:transform"] 246 | } 247 | } 248 | 249 | def Xform "floor_plane" ( 250 | prepend apiSchemas = ["MaterialBindingAPI"] 251 | kind = "component" 252 | ) 253 | { 254 | rel material:binding = 255 | matrix4d xformOp:transform:xform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) 256 | uniform token[] xformOpOrder = ["xformOp:transform:xform"] 257 | 258 | def Mesh "mesh_0" 259 | { 260 | float3[] extent = [(-5, 0, -5), (5, 0, 5)] 261 | int[] faceVertexCounts = [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] 262 | int[] faceVertexIndices = [0, 1, 11, 10, 1, 2, 12, 11, 2, 3, 13, 12, 3, 4, 14, 13, 4, 5, 15, 14, 5, 6, 16, 15, 6, 7, 17, 16, 7, 8, 18, 17, 8, 9, 19, 18, 10, 11, 21, 20, 11, 12, 22, 21, 12, 13, 23, 22, 13, 14, 24, 23, 14, 15, 25, 24, 15, 16, 26, 25, 16, 17, 27, 26, 17, 18, 28, 27, 18, 19, 29, 28, 20, 21, 31, 30, 21, 22, 32, 31, 22, 23, 33, 32, 23, 24, 34, 33, 24, 25, 35, 34, 25, 26, 36, 35, 26, 27, 37, 36, 27, 28, 38, 37, 28, 29, 39, 38, 30, 31, 41, 40, 31, 32, 42, 41, 32, 33, 43, 42, 33, 34, 44, 43, 34, 35, 45, 44, 35, 36, 46, 45, 36, 37, 47, 46, 37, 38, 48, 47, 38, 39, 49, 48, 40, 41, 51, 50, 41, 42, 52, 51, 42, 43, 53, 52, 43, 44, 54, 53, 44, 45, 55, 54, 45, 46, 56, 55, 46, 47, 57, 56, 47, 48, 58, 57, 48, 49, 59, 58, 50, 51, 61, 60, 51, 52, 62, 61, 52, 53, 63, 62, 53, 54, 64, 63, 54, 55, 65, 64, 55, 56, 66, 65, 56, 57, 67, 66, 57, 58, 68, 67, 58, 59, 69, 68, 60, 61, 71, 70, 61, 62, 72, 71, 62, 63, 73, 72, 63, 64, 74, 73, 64, 65, 75, 74, 65, 66, 76, 75, 66, 67, 77, 76, 67, 68, 78, 77, 68, 69, 79, 78, 70, 71, 81, 80, 71, 72, 82, 81, 72, 73, 83, 82, 73, 74, 84, 83, 74, 75, 85, 84, 75, 76, 86, 85, 76, 77, 87, 86, 77, 78, 88, 87, 78, 79, 89, 88, 80, 81, 91, 90, 81, 82, 92, 91, 82, 83, 93, 92, 83, 84, 94, 93, 84, 85, 95, 94, 85, 86, 96, 95, 86, 87, 97, 96, 87, 88, 98, 97, 88, 89, 99, 98] 263 | uniform token orientation = "leftHanded" 264 | point3f[] points = [(-5, 0, -5), (-3.8888888, 0, -5), (-2.7777777, 0, -5), (-1.6666665, 0, -5), (-0.55555534, 0, -5), (0.5555558, 0, -5), (1.666667, 0, -5), (2.7777781, 0, -5), (3.8888893, 0, -5), (5, 0, -5), (-5, 0, -3.8888888), (-3.8888888, 0, -3.8888888), (-2.7777777, 0, -3.8888888), (-1.6666665, 0, -3.8888888), (-0.55555534, 0, -3.8888888), (0.5555558, 0, -3.8888888), (1.666667, 0, -3.8888888), (2.7777781, 0, -3.8888888), (3.8888893, 0, -3.8888888), (5, 0, -3.8888888), (-5, 0, -2.7777777), (-3.8888888, 0, -2.7777777), (-2.7777777, 0, -2.7777777), (-1.6666665, 0, -2.7777777), (-0.55555534, 0, -2.7777777), (0.5555558, 0, -2.7777777), (1.666667, 0, -2.7777777), (2.7777781, 0, -2.7777777), (3.8888893, 0, -2.7777777), (5, 0, -2.7777777), (-5, 0, -1.6666665), (-3.8888888, 0, -1.6666665), (-2.7777777, 0, -1.6666665), (-1.6666665, 0, -1.6666665), (-0.55555534, 0, -1.6666665), (0.5555558, 0, -1.6666665), (1.666667, 0, -1.6666665), (2.7777781, 0, -1.6666665), (3.8888893, 0, -1.6666665), (5, 0, -1.6666665), (-5, 0, -0.55555534), (-3.8888888, 0, -0.55555534), (-2.7777777, 0, -0.55555534), (-1.6666665, 0, -0.55555534), (-0.55555534, 0, -0.55555534), (0.5555558, 0, -0.55555534), (1.666667, 0, -0.55555534), (2.7777781, 0, -0.55555534), (3.8888893, 0, -0.55555534), (5, 0, -0.55555534), (-5, 0, 0.5555558), (-3.8888888, 0, 0.5555558), (-2.7777777, 0, 0.5555558), (-1.6666665, 0, 0.5555558), (-0.55555534, 0, 0.5555558), (0.5555558, 0, 0.5555558), (1.666667, 0, 0.5555558), (2.7777781, 0, 0.5555558), (3.8888893, 0, 0.5555558), (5, 0, 0.5555558), (-5, 0, 1.666667), (-3.8888888, 0, 1.666667), (-2.7777777, 0, 1.666667), (-1.6666665, 0, 1.666667), (-0.55555534, 0, 1.666667), (0.5555558, 0, 1.666667), (1.666667, 0, 1.666667), (2.7777781, 0, 1.666667), (3.8888893, 0, 1.666667), (5, 0, 1.666667), (-5, 0, 2.7777781), (-3.8888888, 0, 2.7777781), (-2.7777777, 0, 2.7777781), (-1.6666665, 0, 2.7777781), (-0.55555534, 0, 2.7777781), (0.5555558, 0, 2.7777781), (1.666667, 0, 2.7777781), (2.7777781, 0, 2.7777781), (3.8888893, 0, 2.7777781), (5, 0, 2.7777781), (-5, 0, 3.8888893), (-3.8888888, 0, 3.8888893), (-2.7777777, 0, 3.8888893), (-1.6666665, 0, 3.8888893), (-0.55555534, 0, 3.8888893), (0.5555558, 0, 3.8888893), (1.666667, 0, 3.8888893), (2.7777781, 0, 3.8888893), (3.8888893, 0, 3.8888893), (5, 0, 3.8888893), (-5, 0, 5), (-3.8888888, 0, 5), (-2.7777777, 0, 5), (-1.6666665, 0, 5), (-0.55555534, 0, 5), (0.5555558, 0, 5), (1.666667, 0, 5), (2.7777781, 0, 5), (3.8888893, 0, 5), (5, 0, 5)] ( 265 | interpolation = "vertex" 266 | ) 267 | uniform token subdivisionScheme = "none" 268 | } 269 | } 270 | 271 | def Xform "lights" 272 | { 273 | def DistantLight "distant_light" ( 274 | prepend apiSchemas = ["HoudiniViewportLightAPI", "HoudiniViewportGuideAPI", "LightAPI"] 275 | ) 276 | { 277 | float inputs:angle.timeSamples = { 278 | 1: 0.53, 279 | 2: 0.53, 280 | 3: 0.53, 281 | 4: 0.53, 282 | 5: 0.53, 283 | 6: 0.53, 284 | 7: 0.53, 285 | 8: 0.53, 286 | 9: 0.53, 287 | 10: 0.53, 288 | 11: 0, 289 | 12: 20, 290 | 13: 40, 291 | 14: 60, 292 | 15: 80, 293 | 16: 100, 294 | 17: 120, 295 | 18: 140, 296 | 19: 160, 297 | 20: 180, 298 | 21: 0, 299 | 22: 20, 300 | 23: 40, 301 | 24: 60, 302 | 25: 80, 303 | 26: 100, 304 | 27: 120, 305 | 28: 140, 306 | 29: 160, 307 | 30: 180, 308 | 31: 0.53, 309 | 32: 0.53, 310 | 33: 0.53, 311 | 34: 0.53, 312 | 35: 0.53, 313 | } 314 | color3f inputs:color = (1, 1, 1) 315 | float inputs:colorTemperature.timeSamples = { 316 | 1: 6500, 317 | 2: 6500, 318 | 3: 6500, 319 | 4: 6500, 320 | 5: 6500, 321 | 6: 6500, 322 | 7: 6500, 323 | 8: 6500, 324 | 9: 6500, 325 | 10: 6500, 326 | 11: 6500, 327 | 12: 6500, 328 | 13: 6500, 329 | 14: 6500, 330 | 15: 6500, 331 | 16: 6500, 332 | 17: 6500, 333 | 18: 6500, 334 | 19: 6500, 335 | 20: 6500, 336 | 21: 6500, 337 | 22: 6500, 338 | 23: 6500, 339 | 24: 6500, 340 | 25: 6500, 341 | 26: 6500, 342 | 27: 6500, 343 | 28: 6500, 344 | 29: 6500, 345 | 30: 6500, 346 | 31: 2000, 347 | 32: 4250, 348 | 33: 6500, 349 | 34: 8750, 350 | 35: 11000, 351 | } 352 | float inputs:diffuse = 1 353 | bool inputs:enableColorTemperature.timeSamples = { 354 | 1: 0, 355 | 2: 0, 356 | 3: 0, 357 | 4: 0, 358 | 5: 0, 359 | 6: 0, 360 | 7: 0, 361 | 8: 0, 362 | 9: 0, 363 | 10: 0, 364 | 11: 0, 365 | 12: 0, 366 | 13: 0, 367 | 14: 0, 368 | 15: 0, 369 | 16: 0, 370 | 17: 0, 371 | 18: 0, 372 | 19: 0, 373 | 20: 0, 374 | 21: 0, 375 | 22: 0, 376 | 23: 0, 377 | 24: 0, 378 | 25: 0, 379 | 26: 0, 380 | 27: 0, 381 | 28: 0, 382 | 29: 0, 383 | 30: 0, 384 | 31: 1, 385 | 32: 1, 386 | 33: 1, 387 | 34: 1, 388 | 35: 1, 389 | } 390 | float inputs:exposure = 0 391 | float inputs:intensity.timeSamples = { 392 | 1: 3720, 393 | 2: 3720, 394 | 3: 3720, 395 | 4: 3720, 396 | 5: 3720, 397 | 6: 3720, 398 | 7: 3720, 399 | 8: 3720, 400 | 9: 3720, 401 | 10: 3720, 402 | 11: 0.3, 403 | 12: 0.3, 404 | 13: 0.3, 405 | 14: 0.3, 406 | 15: 0.3, 407 | 16: 0.3, 408 | 17: 0.3, 409 | 18: 0.3, 410 | 19: 0.3, 411 | 20: 0.3, 412 | 21: 0.3, 413 | 22: 0.3, 414 | 23: 0.3, 415 | 24: 0.3, 416 | 25: 0.3, 417 | 26: 0.3, 418 | 27: 0.3, 419 | 28: 0.3, 420 | 29: 0.3, 421 | 30: 0.3, 422 | 31: 3720, 423 | 32: 3720, 424 | 33: 3720, 425 | 34: 3720, 426 | 35: 3720, 427 | } 428 | bool inputs:normalize.timeSamples = { 429 | 1: 0, 430 | 2: 0, 431 | 3: 0, 432 | 4: 0, 433 | 5: 0, 434 | 6: 0, 435 | 7: 0, 436 | 8: 0, 437 | 9: 0, 438 | 10: 0, 439 | 11: 0, 440 | 12: 0, 441 | 13: 0, 442 | 14: 0, 443 | 15: 0, 444 | 16: 0, 445 | 17: 0, 446 | 18: 0, 447 | 19: 0, 448 | 20: 0, 449 | 21: 1, 450 | 22: 1, 451 | 23: 1, 452 | 24: 1, 453 | 25: 1, 454 | 26: 1, 455 | 27: 1, 456 | 28: 1, 457 | 29: 1, 458 | 30: 1, 459 | 31: 0, 460 | 32: 0, 461 | 33: 0, 462 | 34: 0, 463 | 35: 0, 464 | } 465 | float inputs:specular = 1 466 | rel light:filters = None 467 | matrix4d xformOp:transform.timeSamples = { 468 | 1: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 469 | 2: ( (1, 0, 0, 0), (0, 0.3420201433256688, -0.9396926207859083, 0), (0, 0.9396926207859083, 0.3420201433256688, 0), (0, 5, 0, 1) ), 470 | 3: ( (1, 0, 0, 0), (0, 0.6427876096865398, -0.7660444431189776, 0), (0, 0.7660444431189776, 0.6427876096865398, 0), (0, 5, 0, 1) ), 471 | 4: ( (1, 0, 0, 0), (0, 0.8660254037844393, -0.499999999999999, 0), (0, 0.499999999999999, 0.8660254037844393, 0), (0, 5, 0, 1) ), 472 | 5: ( (1, 0, 0, 0), (0, 0.9848077530122071, -0.17364817766693583, 0), (0, 0.17364817766693583, 0.9848077530122071, 0), (0, 5, 0, 1) ), 473 | 6: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 474 | 7: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 475 | 8: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 476 | 9: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 477 | 10: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 478 | 11: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 479 | 12: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 480 | 13: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 481 | 14: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 482 | 15: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 483 | 16: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 484 | 17: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 485 | 18: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 486 | 19: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 487 | 20: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 488 | 21: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 489 | 22: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 490 | 23: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 491 | 24: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 492 | 25: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 493 | 26: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 494 | 27: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 495 | 28: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 496 | 29: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 497 | 30: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 498 | 31: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 499 | 32: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 500 | 33: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 501 | 34: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 502 | 35: ( (1, 0, 0, 0), (0, 0, -1, 0), (0, 1, 0, 0), (0, 5, 0, 1) ), 503 | } 504 | uniform token[] xformOpOrder = ["xformOp:transform"] 505 | } 506 | } 507 | 508 | --------------------------------------------------------------------------------