DynamicなHDAを実現する方法について

TrsNium
44 min readDec 19, 2023

こちらはHoudini Advent Calendar 20日目の記事です。他にもたくさんの投稿がありますので、是非ご覧ください!

背景

DynamicなHDAとは…?の前に、何故それが必要になったかの背景について説明をします。現在私は街並みの背景製作に焦点を当て、Houdiniで仕組み作りをしています。現在は、敷地の形状から様々な家を生成できる状態です。

皆さんは家をHoudiniでモデリングするとして何を考えますか?私の場合は、以下のようなダイアグラムを想像します。敷地から、どのような要素が関連して存在しているのかを考えます。最小単位に切り分けてモデリングをすることで、シンプルに物事をとらえ、仕組みを作ることが可能だからです。

以降、壁や屋根をドメイン、その子にあるドアや窓等の意匠はサブドメインと呼びます。

何も考えずにHoudiniで家をモデリングをしHDAにすると、1個のHDAの中にドメインやサブドメインの意匠を生成するためのノードが大量にできます。HDAに含まれるノードの量が多い場合、メンテナンスコストが増えることや、新しい意匠を付け加えることが難しくなります。この問題は、昨年のHoudini Advent Calendarで紹介したプロシージャル社寺建築で発生していました。この問題を解決するために、DynamicなHDAを作りメンテナンスや新しい意匠を作成しやすい環境を目指しました。

flowchart TD
A[敷地]
A --> B[家]
B --> BA[屋根]
BA --> BAA[瓦]
BA --> BAB[雨樋]
BA --> BAC[軒先]
B --> BB[壁]
BB --> BBE[真壁]
BBE --> BBEA[下見板張]
BBE --> BBEB[簓子下見板張]
BB --> BBF[大壁]
BBF --> BBFA[ノーマルマップ]
BB --> BBA[ドア]
BBA --> BBAB[押戸]
BBA --> BBAC[引き戸]
BBA --> BBAA[屋根] --> BA
BB --> BBB[窓]
BBB --> BBBD[雨戸]
BBB --> BBBA[屋根] --> BA
BBB --> BBBB[柵]
BBB --> BBBC[バルコニー]
BB --> BBC[室外機]
BB --> BBD[排気口]
A --> C[門]
A --> D[塀]
A --> F[庭]
A --> E[カーポート]

DynamicなHDAとは

DynamicなHDAは特定のドメインを形成するために、複数のサブドメイン(HDA)から構成されたHDAです。DynamicなHDAの操作感から、内部の仕組みへと深堀をしていきます。

DynamicなHDAの操作

操作は以下のGIFのようになります。例として窓の形状生成をおこなっています。GIFの冒頭でReloadボタンを押しています。この時に、HDA内部の処理が変わりHDAのパラメーターインタフェースが書き換わっています。次に、窓の形や凹凸、柵などの形状をパラメーターインターフェース経由で変更をしています。GIFでは窓しか操作していませんが、ドアや屋根等も同じような操作感です。

DynamicなHDAの内部

上部で述べた通りDynamicなHDAの実態は、複数のサブドメインを形成するHDAを束ねたものです。では、どのようにDynamicに束ねるのかについて説明をしています。

まず(窓を生成する部分の)HDAの内部がどのようになっているかを見ていきます。HDAの内部は以下の写真のようになっています。メッシュを操作するノードが殆どないことと、左部分のノードグラフが途中で途切れているのがわかるかと思います。この途切れた部分は、窓の形状を生成するHDAや屋根を生成するHDAに後程自動的に繋がれます。下部の下部の画像はノードが自動配置され繋がれたものになります。さらに下部の下部の画像は自動配置されるノードの一覧です。

途中で途切れたノードグラフ。途切れた部分は後で自動的にノードが配置され繋がれる。
ノードが自動配置された後のノードグラフ
自動配置されるHDA達

次にHDA内部にノードを自動的に配置したりパラメーターインターフェースを変更する方法について説明します。

HDAにはEvent HandlerにPythonやHScriptを書ける機能があります。Event Handlerではノードのライフサイクル等にコードを仕込めます。この機能を使い、ノードが作成された時点でHDA内に新しくHDAを配置し、パラメーターインターフェースを書き換えています。

ノード作成時のEvent Handlerは以下のようになっています。node.allowEditingOfContents()をすることにより、HDA内部に新しくノードを配置したり、パラメーターインターフェースを編集可能できるようになります(UIでいうと、HDAの鍵を外す作業にあたります)。次にhdamoduleからsetup関数を呼んでいます。実際の処理はhdamodule内にあります。

node = kwargs['node']
node.allowEditingOfContents()

node.setColor(hou.Color((0.29, 0.565, 0.886)))
node.setUserData("nodeshape", 'burst')

modules = node.hdaModule()
modules.setUp(node)

hou.ui.triggerUpdate()

hdamodule内は以下のコードブロックの通りです(長いですね…少しお付き合いください…)。

上からParameterRenamerクラスとLinkedChannelSetterクラスがあるのがわかると思います。ParameterRenamerクラスはHDAのパラメーターインターフェースを編集する際にparameter名が衝突するのを防ぐため、パラメータにprefixを自動で設定するものになります。LinkedChannelSetterはHDAのパラメータと動的に配置されたパラメーターをリンクするために使います。LinkedChannelSetterは少し制限があり、n(1<n)重のmultiparmblockのパラメータやーRampParameterは扱うことができません。disconnect_output関数は指定したノードのアウトプットを切ります。getNodeTypes関数はNodeTypeの一覧を取得しています。get*NodeTypeNames関数たちは特定の名前を持つノードの一覧を取得しています。

次にsetup関数についてです。処理の流れは以下の通りです。

  • 自動で配置するノード名を上部で定義した関数から読み出す。
  • HDAのparmTemplateGroupを取得する
  • Splitノードに指定するための、GroupパラメータをparmTemplateGroupに追加する
  • HDA選択用のMenuParmTemplateを追加
  • 自動配置するHDAのinputとoutputを取得する
  • HDAを自動配置し、inputとoutputを接続する
  • 配置したHDAのパラメータを取得後renameし、parmTemplateGroupに追加する
  • parmTemplateGroupに追加したパラメーターと配置したHDAのパラメータを接続する
  • hdaのparmTemplateGroupを変更を加えたparmTemplateGroupで更新
  • foreachやsplitノードにに値を設定する。

補足になりますが、parmTemplateGroupはパラメーターインターフェースをPythonで更新する際につかうクラスです。こちらに変更を加えノードのparmTemplateGroupを更新をするAPIを呼ぶことでパラメーターインターフェースに変更を加えることができます。また、他の屋根やドア等の処理もコードの長さは違いますが、本質的にやってることは同じです。

次にclean関数です。この関数では配置したHDAの削除や繋いだ線を元に戻す作業を行っています。

最後にreload関数です。これはcleanreloadを呼びます。

from typing import List, Tuple

class ParameterRenamer:
def __init__(self, prefix: str = '', suffix: str = ''):
self.prefix = prefix
self.suffix = suffix

def _renameParm(
self,
parmTemplate: hou.ParmTemplate,
) -> hou.ParmTemplate:
renamed_parm = self.prefix + parmTemplate.name() + self.suffix
self.renamed_parms[parmTemplate.name()] = renamed_parm
parmTemplate.setName(renamed_parm)
return parmTemplate

def _setConditions(
self,
parmTemplate: hou.ParmTemplate,
) -> hou.ParmTemplate:
for cond_type, cond in parmTemplate.conditionals().items():
for old_parm_name in self.renamed_parms.keys():
if old_parm_name in cond:
new_cond = cond.replace(old_parm_name, self.renamed_parms[old_parm_name])
parmTemplate.setConditional(cond_type,new_cond)
return parmTemplate

def _executeParamsInFolderParmTemplate(
self,
folderParmTemplate: hou.FolderParmTemplate,
method,
apply_folder=False,
) -> hou.FolderParmTemplate:
folderParmTemplate = method(folderParmTemplate)
parmTemplates: Tuple[hou.ParmTemplate] = folderParmTemplate.parmTemplates()
# Delete parameters in folder
folderParmTemplate.setParmTemplates(())

for parmTemplate in parmTemplates:
if isinstance(parmTemplate, hou.FolderParmTemplate):
parmTemplate = self._executeParamsInFolderParmTemplate(parmTemplate, method, apply_folder)
else:
parmTemplate = method(parmTemplate)
folderParmTemplate.addParmTemplate(parmTemplate)
return folderParmTemplate

def __call__(
self,
parms: Tuple[hou.ParmTemplate],
) -> Tuple[hou.ParmTemplate]:
self.renamed_parms = {}
parameters: List[hou.ParmTemplate] = []
for parm in parms:
if isinstance(parm, hou.FolderParmTemplate):
parameters.append(self._executeParamsInFolderParmTemplate(parm, self._renameParm))
else:
parameters.append(self._renameParm(parm))

# update hidden and disable condition
result_paramters: List[hou.ParmTemplate] = []
for parm in parameters:
if isinstance(parm, hou.FolderParmTemplate):
result_paramters.append(self._executeParamsInFolderParmTemplate(parm, self._setConditions, True))
else:
result_paramters.append(self._setConditions(parm))
return tuple(result_paramters)


class LinkedChannelSetter:
def __init__(
self,
source_node: hou.Node,
target_node: hou.Node,
source_parm_prefix: str = '',
source_parm_suffix: str = '',
):
self.source_parm_prefix = source_parm_prefix
self.source_parm_suffix = source_parm_suffix
self.source_node = source_node
self.target_node = target_node

def _getSchemeSuffixs(self, scheme: hou.parmNamingScheme, num_component: int) -> Tuple[str]:
if scheme == hou.parmNamingScheme.Base1:
return [str(i+1) for i in range(num_component)]
elif scheme == hou.parmNamingScheme.XYZW:
return ["x", "y", "z", "w"][:num_component]
elif scheme == hou.parmNamingScheme.XYWH:
return ["x", "y", "w", "h"][:num_component]
elif scheme == hou.parmNamingScheme.RGBA:
return ["r", "g", "b", "a"][:num_component]
elif scheme == hou.parmNamingScheme.MinMax:
return ["min", "max"][:num_component]
elif scheme == hou.parmNamingScheme.MaxMin:
return ["max", "min"][:num_component]
elif scheme == hou.parmNamingScheme.StartEnd:
return ["start", "end"][:num_component]
elif scheme == hou.parmNamingScheme.BeginEnd:
return ["begin", "end"][:num_component]
else:
raise ValueError(f"not support {scheme}")

def _getNumComponent(self, parmTemplate: hou.ParmTemplate):
try:
return parmTemplate.numComponents()
except Exception:
return 0

def _isMultiParmblock(self, parm: hou.ParmTemplate) -> bool:
if isinstance(parm, hou.FolderParmTemplate) and parm.folderType() \
in (hou.folderType.MultiparmBlock,
hou.folderType.ScrollingMultiparmBlock,
hou.folderType.TabbedMultiparmBlock):
return True
return False

def _executeOpMultiParmBlock(
self,
parmTemplate: hou.ParmTemplate
) -> None:
source_parm_name = f"{self.source_parm_prefix}{parmTemplate.name()}{self.source_parm_suffix}"
command = f"opmultiparm {self.target_node.path()} \"{parmTemplate.name()}\" \"{self.target_node.relativePathTo(self.source_node)}/{source_parm_name}\""
hou.hscript(command)

def _setLinkedMultiParmBlock(
self,
parmTemplate: hou.ParmTemplate,
) ->None:
parmTemplates = parmTemplate.parmTemplates()
for parmTemplate in parmTemplates:
if isinstance(parmTemplate, hou.FolderParmTemplate):
if self._isMultiParmblock(parmTemplate):
self._executeOpMultiParmBlock(parmTemplate)
self._setLinkedMultiParmBlock(parmTemplate)
else:
self._executeOpMultiParmBlock(parmTemplate)

def _setLinkedChannel(
self,
parmTemplate: hou.ParmTemplate,
) -> None:
source_parm_name = f"{self.source_parm_prefix}{parmTemplate.name()}{self.source_parm_suffix}"
source_parm = self.source_node.parm(source_parm_name)
if source_parm:
target_parm = self.target_node.parm(parmTemplate.name())
target_parm.set(source_parm)
elif 1 < self._getNumComponent(parmTemplate):
scheme_suffixs = self._getSchemeSuffixs(parmTemplate.namingScheme(), parmTemplate.numComponents())
source_parm_names = [f"{source_parm_name}{suffix}" for suffix in scheme_suffixs]
target_parm_names = [f"{parmTemplate.name()}{suffix}" for suffix in scheme_suffixs]
for src_parm_name, tgt_parm_name in zip(source_parm_names,target_parm_names):
source_parm = self.source_node.parm(src_parm_name)
target_parm = self.target_node.parm(tgt_parm_name)
if source_parm and target_parm:
target_parm.set(source_parm)

def _setLinkedChannelInFolderParmTemplate(
self,
folderParmTemplate: hou.FolderParmTemplate,
) -> None:
parmTemplates: Tuple[hou.ParmTemplate] = folderParmTemplate.parmTemplates()
for parmTemplate in parmTemplates:
if isinstance(parmTemplate, hou.FolderParmTemplate):
if self._isMultiParmblock(parmTemplate):
self._setLinkedChannel(parmTemplate)
self._setLinkedMultiParmBlock(parmTemplate)
else:
self._setLinkedChannelInFolderParmTemplate(parmTemplate)
else:
self._setLinkedChannel(parmTemplate)

def __call__(
self,
) -> None:
group = self.target_node.parmTemplateGroup()
parmTemplates = group.entries()
for parm in parmTemplates:
if isinstance(parm, hou.FolderParmTemplate):
if self._isMultiParmblock(parm):
self._setLinkedChannel(parm)
self._setLinkedMultiParmBlock(parm)
else:
self._setLinkedChannelInFolderParmTemplate(parm)
else:
self._setLinkedChannel(parm)

def disconnect_output(node):
output_connections = node.outputConnections()
for connection in output_connections:
output_node = connection.outputNode()
output_node.setInput(connection.inputIndex(), None)

def getNodeTypes():
# Get the list of available node categories
node_categories = hou.nodeTypeCategories()

# Loop through each category and get the node types
all_node_types = []
for category in node_categories.values():
node_types = category.nodeTypes().values()
all_node_types.extend(node_types)
return all_node_types

def getWindowPreprocessorNodeTypeNames():
shape_nodes_type_names = []
for node_type in getNodeTypes():
name = node_type.name().lower()
if "jph_wall_detail_window_preprocessor" in name:
shape_nodes_type_names.append(node_type.name())
return shape_nodes_type_names

def getWindowShapeNodeTypeNames():
shape_nodes_type_names = []
for node_type in getNodeTypes():
name = node_type.name().lower()
if "jph_wall_detail_window_shape" in name:
shape_nodes_type_names.append(node_type.name())
return shape_nodes_type_names

def getWindowUtilNodeTypeNames():
shape_nodes_type_names = []
for node_type in getNodeTypes():
name = node_type.name().lower()
if "jph_wall_detail_window_util" in name:
shape_nodes_type_names.append(node_type.name())
return shape_nodes_type_names

GROUP_MENU_SCRIPT="""
node = kwargs.get('node').inputs()[0]

names = []
geo = node.geometry()
groups = geo.primGroups()

for g in groups:
names.append(g.name())
names.append(g.name())

return names
"""

def setUp(node: hou.Node) -> None:
preprocessor_nodes_type_names = getWindowPreprocessorNodeTypeNames()
shape_nodes_type_names = getWindowShapeNodeTypeNames()
util_nodes_type_names = getWindowUtilNodeTypeNames()
split_node = hou.node(node.path()+"/split1")
# Get ParmTemplateGroup
group = node.parmTemplateGroup()
input_folder = hou.FolderParmTemplate(
f"input_configure", f"Input",
folder_type=hou.folderType.Collapsible)
wall_base_group_parm = hou.StringParmTemplate('wall_base_group','Wall Base Group', 1, default_value=('WALL_BASE',), script_callback_language=hou.scriptLanguage.Python)
wall_base_group_parm.setItemGeneratorScript(GROUP_MENU_SCRIPT)
wall_base_group_parm.setMenuType(hou.menuType.StringToggle)
input_folder.addParmTemplate(wall_base_group_parm)
group.append(input_folder)

group_parm = split_node.parmTemplateGroup().find('group')
group_parm.setItemGeneratorScript(GROUP_MENU_SCRIPT)
group_parm.setItemGeneratorScriptLanguage(hou.scriptLanguage.Python)
group.append(group_parm)
group.append(hou.SeparatorParmTemplate("separator1"))

# Preprocessor Option
preprocessor_menu = hou.MenuParmTemplate('preprocessor_selector', 'Preprocessor',
menu_items=[str(i) for i in range(len(preprocessor_nodes_type_names))],
menu_labels=preprocessor_nodes_type_names)
group.append(preprocessor_menu)
preprocessor_output_node1 = hou.node(node.path()+"/PREPROCESSOR_OUTPUT1")
preprocessor_output_node1.parm("input").setExpression(f"ch(\"../preprocessor_selector\")")
preprocessor_output_node2 = hou.node(node.path()+"/PREPROCESSOR_OUTPUT2")
preprocessor_output_node2.parm("input").setExpression(f"ch(\"../preprocessor_selector\")")
preprocessor_output_node3 = hou.node(node.path()+"/PREPROCESSOR_OUTPUT3")
preprocessor_output_node3.parm("input").setExpression(f"ch(\"../preprocessor_selector\")")

created_nodes = []
preprocessor_input_node1 = hou.node(node.path()+"/PREPROCESSOR_INPUT1")
preprocessor_input_node2 = hou.node(node.path()+"/PREPROCESSOR_INPUT2")
for idx, node_type_name in enumerate(preprocessor_nodes_type_names):
# Put Preprocess Node
preprocessor_node = node.createNode(node_type_name, node_type_name)
created_nodes.append(preprocessor_node)
preprocessor_node.setDisplayFlag(True)

# Connect Input
preprocessor_node.setInput(0, preprocessor_input_node1)
preprocessor_node.setInput(1, preprocessor_input_node2)

# Connect Output
preprocessor_output_node1.setInput(idx, preprocessor_node, 0)
preprocessor_output_node2.setInput(idx, preprocessor_node, 1)
preprocessor_output_node3.setInput(idx, preprocessor_node, 2)

# Promote parameter to subnet interface
preprocessor_node_group = preprocessor_node.parmTemplateGroup()
preprocessor_configures_folder = hou.FolderParmTemplate(
f"{node_type_name}_configure", f"{node_type_name}",
folder_type=hou.folderType.Simple)
preprocessor_configures_folder.setConditional(
hou.parmCondType.HideWhen, '{ preprocessor_selector != '+str(idx)+' }')

preprocessor_node_params = preprocessor_node_group.entries()
renamed_preprocessor_node_params = ParameterRenamer(
prefix=node_type_name, suffix='')(preprocessor_node_params)
preprocessor_configures_folder.setParmTemplates(renamed_preprocessor_node_params)
group.append(preprocessor_configures_folder)

group.append(hou.SeparatorParmTemplate("separator3", tags={'sidefx::layout_height': 'large'}))

shape_menu = hou.MenuParmTemplate('shape_selector', 'Shape',
menu_items=[str(i) for i in range(len(shape_nodes_type_names))],
menu_labels=shape_nodes_type_names)
group.append(shape_menu)

shape_output_node = hou.node(node.path()+"/WINDOW_SHAPE_OUTPUT")
shape_output_node.parm("input").setExpression(f"ch(\"../shape_selector\")")

shape_input_node = hou.node(node.path()+"/WINDOW_SHAPE_INPUT")
for idx, node_type_name in enumerate(shape_nodes_type_names):
# Put Base Shape Node
shape_node = node.createNode(node_type_name, node_type_name)
created_nodes.append(shape_node)
shape_node.setDisplayFlag(True)

# Connect Input
shape_node.setInput(0, shape_input_node)

# Connect Output
shape_output_node.setInput(idx, shape_node, 0)

# Promote parameter to subnet interface
shape_node_group = shape_node.parmTemplateGroup()
shape_configures_folder = hou.FolderParmTemplate(
f"{node_type_name}_configure", f"{node_type_name}",
folder_type=hou.folderType.Simple)
shape_configures_folder.setConditional(
hou.parmCondType.HideWhen, '{ shape_selector != '+str(idx)+' }')

shape_node_params = shape_node_group.entries()
renamed_shape_node_params = ParameterRenamer(
prefix=node_type_name, suffix='')(shape_node_params)
shape_configures_folder.setParmTemplates(renamed_shape_node_params)
group.append(shape_configures_folder)

group.append(hou.SeparatorParmTemplate("separator2"))
multiple_window_util_config_folder = hou.FolderParmTemplate('window_util_configures', 'Utils',
folder_type=hou.folderType.TabbedMultiparmBlock)
util_menu = hou.MenuParmTemplate('window_util_selector#', 'Util',
menu_items=[str(i) for i in range(len(util_nodes_type_names))],
menu_labels=util_nodes_type_names)
multiple_window_util_config_folder.addParmTemplate(util_menu)

util_output_node = hou.node(node.path()+"/WINDOW_UTIL_OUTPUT")
util_output_node.parm("input").setExpression(f"ch(strcat(\"../window_util_selector\", ftoa(detail(\"../foreach_count2\", \"iteration\", 0)+1)))")

util_input_node = hou.node(node.path()+"/WINDOW_UTIL_INPUT")
for idx, node_type_name in enumerate(util_nodes_type_names):
# Put Util Node
util_node = node.createNode(node_type_name, node_type_name)
util_node.setDisplayFlag(True)

# Connect Input
util_node.setInput(0, util_input_node)

# Connect Output
util_output_node.setInput(idx, util_node, 0)

# Promote parameter to subnet interface
util_node_group = util_node.parmTemplateGroup()
util_configures_folder = hou.FolderParmTemplate(
f"{node_type_name}_configure#", f"{node_type_name}",
folder_type=hou.folderType.Simple)
util_configures_folder.setConditional(
hou.parmCondType.HideWhen, '{ window_util_selector# != '+str(idx)+' }')

util_node_params = util_node_group.entries()
renamed_util_node_params = ParameterRenamer(
prefix=node_type_name, suffix='#')(util_node_params)

for parm in renamed_util_node_params:
util_configures_folder.addParmTemplate(parm)
multiple_window_util_config_folder.addParmTemplate(util_configures_folder)

# Reference parameter from subnet interface
for parm in util_node.parms():
parmTemplate = parm.parmTemplate()
if isinstance(parmTemplate, hou.StringParmTemplate):
util_node.parm(parm.name()).set(
f"`chs(strcat(\"../{node_type_name}{parm.name()}\", ftoa(detail(\"../foreach_count2\", \"iteration\", 0)+1)))`")
elif not isinstance(parmTemplate, hou.FolderSetParmTemplate):
util_node.parm(parm.name()).setExpression(
f"ch(strcat(\"../{node_type_name}{parm.name()}\", ftoa(detail(\"../foreach_count2\", \"iteration\", 0)+1)))")

group.append(multiple_window_util_config_folder)
node.setParmTemplateGroup(group)

# Set Expression
for created_node in created_nodes:
name = created_node.name()
LinkedChannelSetter(node, created_node, f"{name}")()

# Create a reference for foreach block iteration
foreawch_block = hou.node(node.path()+"/foreach_end2")
source_parm = node.parm("window_util_configures")
target_parm = foreawch_block.parm("iterations")
reference_string = "ch('{}')".format(source_parm.path())
target_parm.setExpression(reference_string, language=hou.exprLanguage.Hscript)

split_node.parm("group").setExpression("chs(\"../group\")")
wall_base_split = hou.node(node.path()+"/WALL_BASE_SPLIT")
wall_base_split.parm("group").set('`chs("../wall_base_group")`', language=hou.exprLanguage.Hscript)

def clean() -> None:
newnode = hou.pwd()
newnode = newnode.path()
node = hou.node(newnode)

# Get ParmTemplateGroup
group = node.parmTemplateGroup()
parms = group.entries()
delete_parms = [parm.name() for parm in parms if parm.name() not in ('clean', 'reload')]
try:
for parm in delete_parms:
group.remove(parm)
node.setParmTemplateGroup(group)
except hou.OperationFailed:
pass

# Remove Expressions
wall_base_split = hou.node(node.path()+"/WALL_BASE_SPLIT")
wall_base_split.parm("group").deleteAllKeyframes()
wall_base_split.parm("group").set("")
preprocessor_output_node1 = hou.node(node.path()+"/PREPROCESSOR_OUTPUT1")
preprocessor_output_node1.parm("input").deleteAllKeyframes()
preprocessor_output_node2 = hou.node(node.path()+"/PREPROCESSOR_OUTPUT2")
preprocessor_output_node2.parm("input").deleteAllKeyframes()
preprocessor_output_node3 = hou.node(node.path()+"/PREPROCESSOR_OUTPUT3")
preprocessor_output_node3.parm("input").deleteAllKeyframes()
shape_output_node = hou.node(node.path()+"/WINDOW_SHAPE_OUTPUT")
shape_output_node.parm("input").deleteAllKeyframes()
util_output_node = hou.node(node.path()+"/WINDOW_UTIL_OUTPUT")
util_output_node.parm("input").deleteAllKeyframes()
hou.node(node.path()+"/foreach_end2").parm("iterations").deleteAllKeyframes()
hou.node(node.path()+"/split1").parm("group").deleteAllKeyframes()

# Disconnect Output
shape_input_node = hou.node(node.path()+"/WINDOW_SHAPE_INPUT")
disconnect_output(shape_input_node)
util_input_node = hou.node(node.path()+"/WINDOW_UTIL_INPUT")
disconnect_output(util_input_node)
preprocessor_input_node1 = hou.node(node.path()+"/PREPROCESSOR_INPUT1")
disconnect_output(preprocessor_input_node1)
preprocessor_input_node2 = hou.node(node.path()+"/PREPROCESSOR_INPUT2")
disconnect_output(preprocessor_input_node2)

shape_nodes_type_names = getWindowShapeNodeTypeNames()
util_nodes_type_names = getWindowUtilNodeTypeNames()
preprocessor_nodes_type_names = getWindowPreprocessorNodeTypeNames()
for node_type_name in (shape_nodes_type_names+util_nodes_type_names+preprocessor_nodes_type_names):
shape_node = hou.node(node.path()+f"/{node_type_name}")
try:
shape_node.destroy()
except Exception:
pass

def reload() -> None:
newnode = hou.pwd()
newnode = newnode.path()
node = hou.node(newnode)

clean()
setUp(node)

ドメインをまたいだHDAの繋ぎこみ

ここまでは、ドメイン(HDA)の中にサブドメイン的な処理を自動的に追加する方法を紹介しました。このセクションでは、ドメイン(HDA)通しの繋ぎこみについて紹介します。

ドメイン(HDA)通しはメッシュに書き込まれているGroupもしくはアウトプットの分岐を使い、どの部分に処理を加えるかを決めています。(アトリビュートではなくグループを使用する理由を考えていましたが、うまく言語化できませんでした。感覚的にはグループ情報があればアトリビュートの値を計算できるが、その逆は難しいという感じです。)

また、ドメインを跨ぐようなGroup名は用語集としてNotionで管理しています。決まったGroup名や新しく追加されたGroupを選択し、処理を加えいます。

グループを使い形状生成をする例1
グループを使い形状生成をする例2

課題と今後の展望

今回紹介したやり方でHDAを大量に作っており、現在90個を超えました。このままのやり方でどれくらいスケールするのか分かっていません。またスケールさせることでHDAの管理にどの程度コストを考えないといけないのか、まだ判断できておりません。

家の生成に関しては、4か月ほど週4,5日、2,3時間程度、作業時間を取りここまでこれました。思い描いていた最短ルートで対象を生成し、ある程度のクオリティを出せていると感じています。また、昨年のプロシージャル社寺建築の課題感(ノード内が複雑でメンテナンスコストがきつい、スケールしない)を払拭できました。今後街へとスケールするために、ベイクや生成処理の並列化を考えていきたいと思っています。

以上で本記事は終わります。最後まで閲覧ありがとうございました!

--

--

0 Followers

Software Engineer(Data Engineer/MLOps/DevOps) / Houdini Hobbyist