Instantly share code, notes, and snippets.
Last active
June 16, 2026 14:42
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save kickbase/ca7b9a38d6a1fec6e3d06e774ab70d94 to your computer and use it in GitHub Desktop.
[Houdini] [Python] Customize menus
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?xml version="1.0" encoding="UTF-8"?> | |
| <menuDocument> | |
| <menu> | |
| <separatorItem/> | |
| <scriptItem id="h.pane.wsheet.reset_node_color"> | |
| <label>Reset color to default</label> | |
| <scriptCode><![CDATA[ | |
| import hou | |
| with hou.undos.group("Reset color to default"): | |
| for node in hou.selectedNodes(): | |
| node.setGenericFlag(hou.nodeFlag.ColorDefault, True) | |
| ]]></scriptCode> | |
| </scriptItem> | |
| <scriptItem id="h.pane.wsheet.focus_display_flag"> | |
| <label>Focus Display Flag</label> | |
| <expression><![CDATA[ | |
| pane = hou.ui.paneTabUnderCursor() | |
| if pane is None or pane.type() != hou.paneTabType.NetworkEditor: | |
| return False | |
| pwd = pane.pwd() | |
| if pwd is None or pwd.childTypeCategory() != hou.sopNodeTypeCategory(): | |
| return False | |
| return pwd.displayNode() is not None | |
| ]]></expression> | |
| <scriptCode><![CDATA[ | |
| import hou | |
| import hdefereval | |
| clicked_node = kwargs.get("node") | |
| def network_editor_for_pwd(pwd): | |
| pane = hou.ui.paneTabUnderCursor() | |
| if (pane is not None | |
| and pane.type() == hou.paneTabType.NetworkEditor | |
| and pane.pwd() == pwd): | |
| return pane | |
| for candidate in hou.ui.paneTabs(): | |
| if (candidate.type() == hou.paneTabType.NetworkEditor | |
| and candidate.pwd() == pwd): | |
| return candidate | |
| return None | |
| def focus_display_sop(pane, pwd): | |
| display_node = pwd.displayNode() | |
| if display_node is None: | |
| return | |
| original_selected = list(pwd.selectedItems()) | |
| original_current = pane.currentNode() | |
| try: | |
| display_node.setSelected(True, clear_all_selected=True) | |
| pane.homeToSelection() | |
| finally: | |
| if original_selected: | |
| for i, item in enumerate(original_selected): | |
| item.setSelected(True, clear_all_selected=(i == 0)) | |
| else: | |
| display_node.setSelected(False) | |
| if original_current is not None and original_current.parent() == pwd: | |
| pane.setCurrentNode(original_current) | |
| def run(): | |
| if clicked_node is None: | |
| return | |
| pwd = clicked_node.parent() | |
| if pwd is None or pwd.childTypeCategory() != hou.sopNodeTypeCategory(): | |
| return | |
| if pwd.displayNode() is None: | |
| return | |
| pane = network_editor_for_pwd(pwd) | |
| if pane is None: | |
| return | |
| focus_display_sop(pane, pwd) | |
| hdefereval.executeDeferred(run) | |
| ]]></scriptCode> | |
| </scriptItem> | |
| <separatorItem/> | |
| <scriptItem id="h.pane.wsheet.sop_to_geo"> | |
| <label>SOP to GEO</label> | |
| <expression><![CDATA[ | |
| return any( | |
| n.type().category() == hou.sopNodeTypeCategory() | |
| for n in hou.selectedNodes() | |
| ) | |
| ]]></expression> | |
| <scriptCode><![CDATA[ | |
| import hou | |
| import hdefereval | |
| def obj_container(node): | |
| """Walk up to the /obj-level container (geo, subnet, etc.).""" | |
| obj = hou.node("/obj") | |
| container = node | |
| while container is not None and container.parent() != obj: | |
| container = container.parent() | |
| if container is not None and container.parent() == obj: | |
| return container | |
| return None | |
| def run(): | |
| sop_nodes = [ | |
| n for n in hou.selectedNodes() | |
| if n.type().category() == hou.sopNodeTypeCategory() | |
| ] | |
| if not sop_nodes: | |
| hou.ui.displayMessage( | |
| "Select one or more SOP nodes.", | |
| severity=hou.severityType.Message, | |
| ) | |
| return | |
| obj = hou.node("/obj") | |
| num = 0 | |
| last_geo = None | |
| with hou.undos.group("SOP to GEO"): | |
| for node in sop_nodes: | |
| num += 1 | |
| geo_node = obj.createNode("geo", node.name(), run_init_scripts=False) | |
| obj_merge = geo_node.createNode("object_merge", "object_merge1") | |
| obj_merge.parm("xformtype").set(1) # Into This Object | |
| obj_merge.parm("objpath1").set(node.path()) | |
| obj_merge.setDisplayFlag(True) | |
| obj_merge.setRenderFlag(True) | |
| container = obj_container(node) | |
| if container is not None: | |
| geo_node.setPosition(container.position() + hou.Vector2(0, -num)) | |
| else: | |
| geo_node.setPosition(hou.Vector2(num, 0)) | |
| last_geo = geo_node | |
| if last_geo is not None: | |
| desktop = hou.ui.curDesktop() | |
| pane = desktop.paneTabOfType(hou.paneTabType.NetworkEditor) if desktop else None | |
| if pane is None: | |
| pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor) | |
| if pane is not None: | |
| pane.setPwd(obj) | |
| pane.setCurrentNode(last_geo) | |
| hdefereval.executeDeferred(run) | |
| ]]></scriptCode> | |
| </scriptItem> | |
| <scriptItem id="h.pane.wsheet.sop_to_rop"> | |
| <label>SOP to ROP</label> | |
| <expression><![CDATA[ | |
| return any( | |
| n.type().category() == hou.sopNodeTypeCategory() | |
| for n in hou.selectedNodes() | |
| ) | |
| ]]></expression> | |
| <scriptCode><![CDATA[ | |
| import hou | |
| import hdefereval | |
| PARM_PREFIX = "_reference" | |
| def create_geometry_rop(node, position=None): | |
| """Create a geometry ROP and file SOP wired to export the given SOP.""" | |
| name = node.name() | |
| if name.startswith("OUT_"): | |
| name = name[4:] | |
| out_ctx = hou.node("/out") | |
| rop = out_ctx.createNode("geometry", name) | |
| if position is not None: | |
| rop.setPosition(position) | |
| else: | |
| rop.moveToGoodPosition() | |
| parent = node.parent() | |
| file_node = parent.createNode("file") | |
| for out_node in node.outputs(): | |
| for i, in_node in enumerate(out_node.inputs()): | |
| if in_node == node: | |
| out_node.setInput(i, file_node) | |
| rop.parm("soppath").set(node.path()) | |
| rop_output = '`chs("{}/sopoutput")`'.format(rop.path()) | |
| rop.parm("postrender").set("opupdate()") | |
| rop.parm("lpostrender").set("hscript") | |
| file_node.parm("file").set(rop_output) | |
| file_node.parm("missingframe").set(1) | |
| file_node.setInput(0, node) | |
| file_node.setPosition(node.position() + hou.Vector2(0, -1)) | |
| return rop, file_node | |
| def sync_reference_parms(node): | |
| """Refresh spare NodeReference parms for nodes that depend on node.""" | |
| group = node.parmTemplateGroup() | |
| for entry in list(group.entries()): | |
| if entry.name().startswith(PARM_PREFIX): | |
| group.remove(entry) | |
| node.setParmTemplateGroup(group) | |
| for i, dep_node in enumerate(node.dependents()): | |
| parm_name = "{}{}".format(PARM_PREFIX, i) | |
| parm = hou.StringParmTemplate( | |
| parm_name, | |
| "", | |
| 1, | |
| string_type=hou.stringParmType.NodeReference, | |
| ) | |
| node.addSpareParmTuple(parm) | |
| node.parm(parm_name).set(dep_node.path()) | |
| def run(): | |
| sop_nodes = [ | |
| n for n in hou.selectedNodes() | |
| if n.type().category() == hou.sopNodeTypeCategory() | |
| ] | |
| if not sop_nodes: | |
| hou.ui.displayMessage( | |
| "Select one or more SOP nodes.", | |
| severity=hou.severityType.Message, | |
| ) | |
| return | |
| created = [] | |
| last_rop_pos = None | |
| with hou.undos.group("SOP to ROP"): | |
| for node in sop_nodes: | |
| position = None | |
| if last_rop_pos is not None: | |
| position = last_rop_pos + hou.Vector2(0, -1) | |
| rop, file_node = create_geometry_rop(node, position=position) | |
| last_rop_pos = rop.position() | |
| sync_reference_parms(node) | |
| created.append((rop, file_node)) | |
| if not created: | |
| return | |
| if len(created) == 1: | |
| rop, file_node = created[0] | |
| msg = 'Created "{}" and "{}"'.format(rop.path(), file_node.path()) | |
| else: | |
| msg = "Created {} geometry ROP(s).".format(len(created)) | |
| hou.ui.setStatusMessage(msg, severity=hou.severityType.ImportantMessage) | |
| last_rop = created[-1][0] | |
| desktop = hou.ui.curDesktop() | |
| pane = desktop.paneTabOfType(hou.paneTabType.NetworkEditor) if desktop else None | |
| if pane is None: | |
| pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor) | |
| if pane is not None: | |
| pane.setPwd(hou.node("/out")) | |
| pane.setCurrentNode(last_rop) | |
| hdefereval.executeDeferred(run) | |
| ]]></scriptCode> | |
| </scriptItem> | |
| <scriptItem id="h.pane.wsheet.sop_to_lop"> | |
| <label>SOP to LOP</label> | |
| <expression><![CDATA[ | |
| return any( | |
| n.type().category() == hou.sopNodeTypeCategory() | |
| for n in hou.selectedNodes() | |
| ) | |
| ]]></expression> | |
| <scriptCode><![CDATA[ | |
| import hou | |
| import hdefereval | |
| PARM_PREFIX = "_reference" | |
| def sanitize_node_name(name): | |
| # Houdini node names are fairly permissive, but spaces can cause confusion. | |
| return name.replace(" ", "_") | |
| def create_sop_import(sop_node, position=None): | |
| stage = hou.node("/stage") | |
| if stage is None: | |
| raise RuntimeError('Cannot find "/stage".') | |
| base = sop_node.name() | |
| if base.startswith("OUT_"): | |
| base = base[4:] | |
| base = sanitize_node_name(base) | |
| desired_name = "sopimport_{}".format(base) | |
| name = desired_name | |
| if stage.node(name) is not None: | |
| i = 1 | |
| while stage.node("{}_{}".format(name, i)) is not None: | |
| i += 1 | |
| name = "{}_{}".format(name, i) | |
| sop_import = stage.createNode("sopimport", name) | |
| sop_import.parm("soppath").set(sop_node.path()) | |
| if position is not None: | |
| sop_import.setPosition(position) | |
| else: | |
| sop_import.moveToGoodPosition() | |
| # Mirror the original shelf tool behavior: | |
| # update _reference* spare parms for nodes that depend on the SOP. | |
| group = sop_node.parmTemplateGroup() | |
| for entry in list(group.entries()): | |
| if entry.name().startswith(PARM_PREFIX): | |
| group.remove(entry) | |
| sop_node.setParmTemplateGroup(group) | |
| for i, dep_node in enumerate(sop_node.dependents()): | |
| parm_name = "{}{}".format(PARM_PREFIX, i) | |
| parm = hou.StringParmTemplate( | |
| parm_name, | |
| "", | |
| 1, | |
| string_type=hou.stringParmType.NodeReference, | |
| ) | |
| sop_node.addSpareParmTuple(parm) | |
| sop_node.parm(parm_name).set(dep_node.path()) | |
| return sop_import | |
| def run(): | |
| sop_nodes = [ | |
| n for n in hou.selectedNodes() | |
| if n.type().category() == hou.sopNodeTypeCategory() | |
| ] | |
| if not sop_nodes: | |
| hou.ui.displayMessage( | |
| "Select one or more SOP nodes.", | |
| severity=hou.severityType.Message, | |
| ) | |
| return | |
| stage = hou.node("/stage") | |
| if stage is None: | |
| hou.ui.displayMessage( | |
| "Cannot find /stage.", | |
| severity=hou.severityType.Error, | |
| ) | |
| return | |
| created = [] | |
| errors = [] | |
| last_position = None | |
| with hou.undos.group("SOP to LOP"): | |
| for node in sop_nodes: | |
| try: | |
| position = None | |
| if last_position is not None: | |
| position = last_position + hou.Vector2(0, -1) | |
| sop_import = create_sop_import(node, position=position) | |
| last_position = sop_import.position() | |
| created.append(sop_import) | |
| except Exception as e: | |
| errors.append((node.path(), str(e))) | |
| if created: | |
| last_import = created[-1] | |
| if len(created) == 1: | |
| hou.ui.setStatusMessage( | |
| 'Created "{}".'.format(last_import.path()), | |
| severity=hou.severityType.ImportantMessage, | |
| ) | |
| else: | |
| hou.ui.setStatusMessage( | |
| "Created {} SOP import(s).".format(len(created)), | |
| severity=hou.severityType.ImportantMessage, | |
| ) | |
| # Switch the network editor to /stage so the user lands in LOP context. | |
| desktop = hou.ui.curDesktop() | |
| pane = desktop.paneTabOfType(hou.paneTabType.NetworkEditor) if desktop else None | |
| if pane is None: | |
| pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor) | |
| if pane is not None: | |
| pane.setPwd(stage) | |
| pane.setCurrentNode(last_import) | |
| pane.homeToSelection() | |
| if errors: | |
| max_show = 5 | |
| lines = [ | |
| "SOP to LOP completed with errors:" | |
| ] | |
| for p, err in errors[:max_show]: | |
| lines.append("- {}: {}".format(p, err)) | |
| if len(errors) > max_show: | |
| lines.append("- ... ({} more)".format(len(errors) - max_show)) | |
| hou.ui.displayMessage("\n".join(lines), severity=hou.severityType.Error) | |
| hdefereval.executeDeferred(run) | |
| ]]></scriptCode> | |
| </scriptItem> | |
| <separatorItem/> | |
| <scriptItem id="h.pane.wsheet.karma_fetch"> | |
| <label>Karma Fetch</label> | |
| <expression><![CDATA[ | |
| return any( | |
| n.type().category() == hou.ropNodeTypeCategory() | |
| and n.type().name() == "karma" | |
| for n in hou.selectedNodes() | |
| ) | |
| ]]></expression> | |
| <scriptCode><![CDATA[ | |
| import hou | |
| import hdefereval | |
| TRANGE_LABELS = [ | |
| "Render Current Frame", | |
| "Render Frame Range", | |
| "Render Frame Range Only(Strict)", | |
| ] | |
| FETCH_COLOR = hou.Color(0.976, 0.78, 0.263) | |
| def add_fetch_parms(node): | |
| group = node.parmTemplateGroup() | |
| parm_trange = hou.MenuParmTemplate( | |
| "trange", | |
| "Valid Frame Range", | |
| ["off", "normal", "on"], | |
| menu_labels=TRANGE_LABELS, | |
| default_value=0, | |
| ) | |
| parm_f = hou.FloatParmTemplate( | |
| "f", | |
| "Start/End/Inc", | |
| 3, | |
| disable_when="{trange == off}", | |
| naming_scheme=hou.parmNamingScheme.Base1, | |
| ) | |
| parm_output = hou.StringParmTemplate( | |
| "ropoutput", | |
| "Output File", | |
| 1, | |
| string_type=hou.stringParmType.FileReference, | |
| ) | |
| group.insertAfter("source", parm_output) | |
| group.insertAfter("source", parm_f) | |
| group.insertAfter("source", parm_trange) | |
| node.setParmTemplateGroup(group) | |
| def find_promoted_parm(karma_node, name): | |
| """Return a parm only from the Karma ROP's own interface (not locked internals).""" | |
| return karma_node.parm(name) | |
| def find_promoted_parm_tuple(karma_node, name): | |
| return karma_node.parmTuple(name) | |
| def safe_set_expression(parm, expression): | |
| if parm is None: | |
| return False, "missing" | |
| if parm.isLocked(): | |
| return False, "locked" | |
| try: | |
| parm.setExpression(expression) | |
| return True, None | |
| except hou.PermissionError: | |
| return False, "permission" | |
| except hou.OperationFailed as err: | |
| return False, str(err) | |
| def safe_set(parm, value): | |
| if parm is None: | |
| return False, "missing" | |
| if parm.isLocked(): | |
| return False, "locked" | |
| try: | |
| parm.set(value) | |
| return True, None | |
| except hou.PermissionError: | |
| return False, "permission" | |
| except hou.OperationFailed as err: | |
| return False, str(err) | |
| def link_karma_to_fetch(karma_node, fetch_path): | |
| """Link promoted Karma ROP parms to the fetch ROP.""" | |
| linked = [] | |
| missing = [] | |
| denied = [] | |
| def ch(suffix): | |
| return 'ch("{}")'.format(fetch_path + "/" + suffix) | |
| def chs(suffix): | |
| return 'chs("{}")'.format(fetch_path + "/" + suffix) | |
| def record(name, ok, reason): | |
| if ok: | |
| linked.append(name) | |
| elif reason == "missing": | |
| missing.append(name) | |
| else: | |
| denied.append("{} ({})".format(name, reason)) | |
| trange = find_promoted_parm(karma_node, "trange") | |
| ok, reason = safe_set_expression(trange, ch("trange")) | |
| record("trange", ok, reason) | |
| f1 = find_promoted_parm(karma_node, "f1") | |
| f2 = find_promoted_parm(karma_node, "f2") | |
| f3 = find_promoted_parm(karma_node, "f3") | |
| if f1 is not None and f2 is not None and f3 is not None: | |
| ok1, r1 = safe_set_expression(f1, ch("f1")) | |
| ok2, r2 = safe_set_expression(f2, ch("f2")) | |
| ok3, r3 = safe_set_expression(f3, ch("f3")) | |
| if ok1 and ok2 and ok3: | |
| linked.extend(["f1", "f2", "f3"]) | |
| else: | |
| for name, ok, reason in ( | |
| ("f1", ok1, r1), | |
| ("f2", ok2, r2), | |
| ("f3", ok3, r3), | |
| ): | |
| record(name, ok, reason) | |
| else: | |
| f_tuple = find_promoted_parm_tuple(karma_node, "f") | |
| if f_tuple is not None and len(f_tuple) >= 3: | |
| ok1, r1 = safe_set_expression(f_tuple[0], ch("f1")) | |
| ok2, r2 = safe_set_expression(f_tuple[1], ch("f2")) | |
| ok3, r3 = safe_set_expression(f_tuple[2], ch("f3")) | |
| if ok1 and ok2 and ok3: | |
| linked.append("f") | |
| else: | |
| for name, ok, reason in ( | |
| ("f1", ok1, r1), | |
| ("f2", ok2, r2), | |
| ("f3", ok3, r3), | |
| ): | |
| record(name, ok, reason) | |
| else: | |
| missing.append("f1/f2/f3") | |
| output_linked = False | |
| for name in ( | |
| "outputimage", | |
| "picture", | |
| "vm_picture", | |
| "outputfile", | |
| "soho_outputimage", | |
| ): | |
| output_parm = find_promoted_parm(karma_node, name) | |
| if output_parm is None: | |
| continue | |
| ok, reason = safe_set_expression(output_parm, chs("ropoutput")) | |
| if ok: | |
| linked.append(name) | |
| output_linked = True | |
| break | |
| denied.append("{} ({})".format(name, reason)) | |
| if not output_linked and not any( | |
| item.startswith("output") or item.startswith("picture") | |
| for item in denied | |
| ): | |
| missing.append("outputimage") | |
| postrender = find_promoted_parm(karma_node, "postrender") | |
| ok, reason = safe_set(postrender, "opupdate()") | |
| record("postrender", ok, reason) | |
| return linked, missing, denied | |
| def create_karma_fetch(karma_node): | |
| out_ctx = hou.node("/out") | |
| fetch = out_ctx.createNode("fetch", "karma_{}".format(karma_node.name())) | |
| fetch.setColor(FETCH_COLOR) | |
| add_fetch_parms(fetch) | |
| fetch.parm("f1").setExpression("$FSTART") | |
| fetch.parm("f2").setExpression("$FEND") | |
| fetch.parm("f3").set(1) | |
| fetch.parm("source").set(karma_node.path()) | |
| fetch.parm("ropoutput").set("$HIP/render/$OS.$F4.exr") | |
| fetch.setPosition(karma_node.position() + hou.Vector2(0, -1)) | |
| fetch_path = fetch.path() | |
| linked, missing, denied = link_karma_to_fetch(karma_node, fetch_path) | |
| return fetch, missing, denied | |
| def run(): | |
| karma_nodes = [ | |
| n for n in hou.selectedNodes() | |
| if n.type().category() == hou.ropNodeTypeCategory() | |
| and n.type().name() == "karma" | |
| ] | |
| if not karma_nodes: | |
| hou.ui.displayMessage( | |
| "Select one or more Karma ROP nodes.", | |
| severity=hou.severityType.Message, | |
| ) | |
| return | |
| created = [] | |
| link_warnings = [] | |
| with hou.undos.group("Karma Fetch"): | |
| for node in karma_nodes: | |
| fetch, missing, denied = create_karma_fetch(node) | |
| created.append(fetch) | |
| if missing or denied: | |
| link_warnings.append((node.path(), missing, denied)) | |
| if not created: | |
| return | |
| if len(created) == 1: | |
| msg = 'Created "{}".'.format(created[0].path()) | |
| else: | |
| msg = "Created {} fetch ROP(s).".format(len(created)) | |
| hou.ui.setStatusMessage(msg, severity=hou.severityType.ImportantMessage) | |
| last_fetch = created[-1] | |
| desktop = hou.ui.curDesktop() | |
| pane = desktop.paneTabOfType(hou.paneTabType.NetworkEditor) if desktop else None | |
| if pane is None: | |
| pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor) | |
| if pane is not None: | |
| pane.setPwd(hou.node("/out")) | |
| pane.setCurrentNode(last_fetch) | |
| if link_warnings: | |
| max_show = 5 | |
| lines = [ | |
| "Karma Fetch created fetch node(s), but some Karma parms could not be linked:" | |
| ] | |
| for path, missing, denied in link_warnings[:max_show]: | |
| parts = [] | |
| if missing: | |
| parts.append("not found: {}".format(", ".join(missing))) | |
| if denied: | |
| parts.append("denied: {}".format(", ".join(denied))) | |
| lines.append("- {} ({})".format(path, "; ".join(parts))) | |
| if len(link_warnings) > max_show: | |
| lines.append("- ... ({} more)".format(len(link_warnings) - max_show)) | |
| lines.append("") | |
| lines.append( | |
| "Edit frame range and output on the fetch ROP, then render the fetch node." | |
| ) | |
| hou.ui.displayMessage("\n".join(lines), severity=hou.severityType.Warning) | |
| hdefereval.executeDeferred(run) | |
| ]]></scriptCode> | |
| </scriptItem> | |
| <scriptItem id="h.pane.wsheet.usd_fetch"> | |
| <label>USD Fetch</label> | |
| <expression><![CDATA[ | |
| return any( | |
| n.type().category() == hou.sopNodeTypeCategory() | |
| for n in hou.selectedNodes() | |
| ) | |
| ]]></expression> | |
| <scriptCode><![CDATA[ | |
| import hou | |
| import hdefereval | |
| TRANGE_LABELS = [ | |
| "Render Current Frame", | |
| "Render Frame Range", | |
| "Render Frame Range Only(Strict)", | |
| ] | |
| USD_FETCH_COLOR = hou.Color(0.584, 0.776, 1) | |
| def sanitize_node_name(name): | |
| if name.startswith("OUT_"): | |
| name = name[4:] | |
| return name.replace(" ", "_") | |
| def unique_node_name(parent, base_name): | |
| name = base_name | |
| if parent.node(name) is not None: | |
| i = 1 | |
| while parent.node("{}_{}".format(base_name, i)) is not None: | |
| i += 1 | |
| name = "{}_{}".format(base_name, i) | |
| return name | |
| def add_usd_fetch_parms(node): | |
| group = node.parmTemplateGroup() | |
| parm_ref = hou.StringParmTemplate( | |
| "ref_node", | |
| "reference node", | |
| 1, | |
| string_type=hou.stringParmType.NodeReference, | |
| ) | |
| parm_trange = hou.MenuParmTemplate( | |
| "trange", | |
| "Valid Frame Range", | |
| ["off", "normal", "on"], | |
| menu_labels=TRANGE_LABELS, | |
| default_value=0, | |
| ) | |
| parm_f = hou.FloatParmTemplate( | |
| "f", | |
| "Start/End/Inc", | |
| 3, | |
| disable_when="{trange == off}", | |
| naming_scheme=hou.parmNamingScheme.Base1, | |
| ) | |
| parm_output = hou.StringParmTemplate( | |
| "ropoutput", | |
| "Output File", | |
| 1, | |
| string_type=hou.stringParmType.FileReference, | |
| ) | |
| parm_fileperframe = hou.ToggleParmTemplate( | |
| "fileperframe", | |
| "Separate File Per Frame", | |
| ) | |
| group.insertAfter("source", parm_fileperframe) | |
| group.insertAfter("source", parm_output) | |
| group.insertAfter("source", parm_f) | |
| group.insertAfter("source", parm_trange) | |
| group.insertAfter("source", parm_ref) | |
| node.setParmTemplateGroup(group) | |
| def safe_set_expression(parm, expression): | |
| if parm is None: | |
| return False, "missing" | |
| if parm.isLocked(): | |
| return False, "locked" | |
| try: | |
| parm.setExpression(expression) | |
| return True, None | |
| except hou.PermissionError: | |
| return False, "permission" | |
| except hou.OperationFailed as err: | |
| return False, str(err) | |
| def safe_set(parm, value): | |
| if parm is None: | |
| return False, "missing" | |
| if parm.isLocked(): | |
| return False, "locked" | |
| try: | |
| parm.set(value) | |
| return True, None | |
| except hou.PermissionError: | |
| return False, "permission" | |
| except hou.OperationFailed as err: | |
| return False, str(err) | |
| def usd_fetch_source_path(usdexport): | |
| for rel_path in ("lopnet_EXPORT/USD_OUT", "USD_OUT", "lopnet/USD_OUT"): | |
| child = usdexport.node(rel_path) | |
| if child is not None: | |
| return child.path() | |
| return usdexport.path() + "/lopnet_EXPORT/USD_OUT" | |
| def rewire_to_usdexport(sop_node, usdexport, skip_nodes=()): | |
| skip = set(skip_nodes) | |
| skip.add(usdexport) | |
| for out_node in sop_node.outputs(): | |
| if out_node in skip: | |
| continue | |
| for i, in_node in enumerate(out_node.inputs()): | |
| if in_node == sop_node: | |
| out_node.setInput(i, usdexport) | |
| def link_usdexport_to_fetch(usdexport, fetch_path): | |
| linked = [] | |
| missing = [] | |
| denied = [] | |
| def ch(suffix): | |
| return 'ch("{}")'.format(fetch_path + "/" + suffix) | |
| def chs(suffix): | |
| return 'chs("{}")'.format(fetch_path + "/" + suffix) | |
| def record(name, ok, reason): | |
| if ok: | |
| linked.append(name) | |
| elif reason == "missing": | |
| missing.append(name) | |
| else: | |
| denied.append("{} ({})".format(name, reason)) | |
| for name, expr in ( | |
| ("trange", ch("trange")), | |
| ("f1", ch("f1")), | |
| ("f2", ch("f2")), | |
| ("f3", ch("f3")), | |
| ("lopoutput", chs("ropoutput")), | |
| ("fileperframe", ch("fileperframe")), | |
| ): | |
| ok, reason = safe_set_expression(usdexport.parm(name), expr) | |
| record(name, ok, reason) | |
| ok, reason = safe_set(usdexport.parm("postrender"), "opupdate()") | |
| record("postrender", ok, reason) | |
| return linked, missing, denied | |
| def create_usd_fetch(sop_node, fetch_position=None, ref_position=None): | |
| out_ctx = hou.node("/out") | |
| stage = hou.node("/stage") | |
| if out_ctx is None: | |
| raise RuntimeError('Cannot find "/out".') | |
| name = sanitize_node_name(sop_node.name()) | |
| parent = sop_node.parent() | |
| usdexport = parent.createNode("usdexport", unique_node_name(parent, name)) | |
| rewire_to_usdexport(sop_node, usdexport) | |
| usdexport.setInput(0, sop_node) | |
| usdexport.setPosition(sop_node.position() + hou.Vector2(0, -1)) | |
| fetch = out_ctx.createNode("fetch", unique_node_name(out_ctx, name)) | |
| fetch.setColor(USD_FETCH_COLOR) | |
| add_usd_fetch_parms(fetch) | |
| fetch.parm("f1").setExpression("$FSTART") | |
| fetch.parm("f2").setExpression("$FEND") | |
| fetch.parm("f3").set(1) | |
| fetch.parm("source").set(usd_fetch_source_path(usdexport)) | |
| fetch.parm("ref_node").set(usdexport.path()) | |
| fetch.parm("fileperframe").set(0) | |
| fetch.parm("ropoutput").set("$CACHE/`$OS`/v001/`$OS`.usd") | |
| if fetch_position is not None: | |
| fetch.setPosition(fetch_position) | |
| else: | |
| fetch.moveToGoodPosition() | |
| reference = None | |
| if stage is not None: | |
| reference = stage.createNode( | |
| "reference", | |
| unique_node_name(stage, name), | |
| ) | |
| reference.parm("filepath1").set( | |
| '`chs("{}/ropoutput")`'.format(fetch.path()) | |
| ) | |
| if ref_position is not None: | |
| reference.setPosition(ref_position) | |
| else: | |
| reference.moveToGoodPosition() | |
| linked, missing, denied = link_usdexport_to_fetch(usdexport, fetch.path()) | |
| safe_set(usdexport.parm("enable_attribs"), 1) | |
| safe_set(usdexport.parm("enable_subsetgroups"), 1) | |
| safe_set(usdexport.parm("subsetgroups"), "*") | |
| usdimport = parent.createNode("usdimport") | |
| usdimport.parm("filepath1").setExpression( | |
| 'chs("{}/ropoutput")'.format(fetch.path()) | |
| ) | |
| usdimport.setPosition(usdexport.position() + hou.Vector2(0, -1)) | |
| return { | |
| "fetch": fetch, | |
| "reference": reference, | |
| "usdexport": usdexport, | |
| "usdimport": usdimport, | |
| "linked": linked, | |
| "missing": missing, | |
| "denied": denied, | |
| } | |
| def run(): | |
| sop_nodes = [ | |
| n for n in hou.selectedNodes() | |
| if n.type().category() == hou.sopNodeTypeCategory() | |
| ] | |
| if not sop_nodes: | |
| hou.ui.displayMessage( | |
| "Select one or more SOP nodes.", | |
| severity=hou.severityType.Message, | |
| ) | |
| return | |
| created = [] | |
| link_warnings = [] | |
| errors = [] | |
| last_fetch_pos = None | |
| last_ref_pos = None | |
| with hou.undos.group("USD Fetch"): | |
| for node in sop_nodes: | |
| try: | |
| fetch_position = None | |
| if last_fetch_pos is not None: | |
| fetch_position = last_fetch_pos + hou.Vector2(0, -1) | |
| ref_position = None | |
| if last_ref_pos is not None: | |
| ref_position = last_ref_pos + hou.Vector2(0, -1) | |
| result = create_usd_fetch( | |
| node, | |
| fetch_position=fetch_position, | |
| ref_position=ref_position, | |
| ) | |
| last_fetch_pos = result["fetch"].position() | |
| if result["reference"] is not None: | |
| last_ref_pos = result["reference"].position() | |
| created.append(result) | |
| if result["missing"] or result["denied"]: | |
| link_warnings.append( | |
| (node.path(), result["missing"], result["denied"]) | |
| ) | |
| except Exception as err: | |
| errors.append((node.path(), str(err))) | |
| if not created: | |
| if errors: | |
| hou.ui.displayMessage( | |
| "\n".join("- {}: {}".format(p, e) for p, e in errors[:5]), | |
| severity=hou.severityType.Error, | |
| ) | |
| return | |
| last = created[-1] | |
| if len(created) == 1: | |
| msg = 'Created "{}" and related nodes.'.format(last["fetch"].path()) | |
| else: | |
| msg = "Created {} USD fetch setup(s).".format(len(created)) | |
| hou.ui.setStatusMessage(msg, severity=hou.severityType.ImportantMessage) | |
| desktop = hou.ui.curDesktop() | |
| pane = desktop.paneTabOfType(hou.paneTabType.NetworkEditor) if desktop else None | |
| if pane is None: | |
| pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor) | |
| if pane is not None: | |
| pane.setPwd(hou.node("/out")) | |
| pane.setCurrentNode(last["fetch"]) | |
| if link_warnings: | |
| max_show = 5 | |
| lines = [ | |
| "USD Fetch created nodes, but some usdexport parms could not be linked:" | |
| ] | |
| for path, missing, denied in link_warnings[:max_show]: | |
| parts = [] | |
| if missing: | |
| parts.append("not found: {}".format(", ".join(missing))) | |
| if denied: | |
| parts.append("denied: {}".format(", ".join(denied))) | |
| lines.append("- {} ({})".format(path, "; ".join(parts))) | |
| if len(link_warnings) > max_show: | |
| lines.append("- ... ({} more)".format(len(link_warnings) - max_show)) | |
| hou.ui.displayMessage("\n".join(lines), severity=hou.severityType.Warning) | |
| if errors: | |
| max_show = 5 | |
| lines = ["USD Fetch completed with errors:"] | |
| for path, err in errors[:max_show]: | |
| lines.append("- {}: {}".format(path, err)) | |
| if len(errors) > max_show: | |
| lines.append("- ... ({} more)".format(len(errors) - max_show)) | |
| hou.ui.displayMessage("\n".join(lines), severity=hou.severityType.Error) | |
| hdefereval.executeDeferred(run) | |
| ]]></scriptCode> | |
| </scriptItem> | |
| </menu> | |
| </menuDocument> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment