Site icon Solus Mundi

Houdini Node Generation

I’m grinding on a personal project that will require a lot of 3D assets, meshes that will be textured and exported to a game engine. In this post I’m sharing a python script that can be put into a Houdini shelf tool and executed to give a starting point, without having to memorize all the different parameters that need to be set.

Here’s it is, I’ll explain more after the code block:

# --- Modules

import datetime # Console timestamping

# --- Variables

# Vertical node graph spacing
vertOffset = hou.Vector2(0, -1.05)
vertOffsetSum = hou.Vector2(0, 0)

# Stores node object references, paths
assetNodeRefs = [] # Stores node object references
assetNodePaths = [] # Stores full tree paths of nodes

# --- Functions

# Utility that spits out all the parameters for a given node
def dumpParms(myNodePath):
tempNodeParms = hou.node(myNodePath).parms()

# Iterate and print them out
for idx, param in enumerate(tempNodeParms):
if idx == 0: # Print node path once
print("For node: " + myNodePath)
tempValue = param.eval()
tempName = param.name()
print("Name: " + tempName + " [ " + str(tempValue) + " ]") # Some need to be cast str

return

def makeNode(myPath, myType):
# Debug
print("Making node at: " + myPath + " Type: " + myType)
# Assign node reference
tempNodeRef = hou.node(myPath).createNode(myType)
# Keeping track of node object references in makeDefaultAsset function
#spawnedNodes.append(tempNodeRef) # Original - Deprecated
# Get created node path
tempNodePath = tempNodeRef.path()

return tempNodeRef, tempNodePath

# I don't like the X, Y offset for automatic layout, just want a Y offset for created nodes
def adjPosition(myNodeRef, myOffset): # Takes node reference, applies Y offset
# Global var for sum
global vertOffsetSum
# Get current node position
tempNodePos = myNodeRef.position() # [X, Y]
# Apply Y offset
offsetNodePos = hou.Vector2(0, (tempNodePos[1] + vertOffsetSum[1]))
# update offset sum
vertOffsetSum = (0, (vertOffsetSum[1] + myOffset[1]))
# Apply cumulative offset position to node
myNodeRef.setPosition(vertOffsetSum)

return offsetNodePos

# Takes two node object references
def wireNodes(nodeFromRef, nodeToRef): # Assumes first input index, first output index
# Make connection
nodeFromRef.setInput(0, nodeToRef, 0)

return

def getNodeRef(myNodeName): # Takes spawned node name, returns object reference if matched
tempNodeObj = None
for nodePath in assetNodePaths: # Using global list of stored spawned node paths
if hou.node(nodePath).name() == myNodeName: # Check node name - matches?
tempNodeObj = hou.node(nodePath) # Then get node object reference

return tempNodeObj

# First element is top "root" node of the structure - for more complex topologies, you'll have
# to make changes to this code, including my assumptions about created node names
# Nodes are created in the order listed, left to right, using node type names

assetNodeList = ['geo', 'box', 'groupcreate', 'xform', 'normal', 'uvunwrap', 'attribcreate',
'merge', 'uvlayout', 'material', 'groupcreate', 'output', 'rop_fbx']

# Parameter Settings Keys/List - Access syntax nodeParamsList[0]['nodename'][0]['paramkey']
# Yes, this assumes that the node name is the first ever created (nodename1), and for my uses
# it will be - this would require more thorough checking if that assumption isn't true
#
# Hovering your mouse cursor over a parameter in the Houdini node details pane provides the
# referenced parameter name in its pop-up, which is used below - or dump a node's parameters
# using my dumpParms(yournodetreepath) helper function I've provided

nodeParamsList = [{
'attribcreate1' : [
{'name1':'path'}
],
'uvlayout1' : [
{'correctareas' : 1}, {'axisalignislands' : 2}, {'scaling' : 1}, {'scale' : 1},
{'rotstep' : 0}, {'packbetween' : 0}, {'packincavities' : 1}, {'padding' : 1}, {'paddingboundary' : 1},
{'expandpadding' : 0}, {'targettype' : 1}, {'usedefaultudimtarget' : 1}, {'defaultudimtarget' : 1001},
{'tilesizex' : 1}, {'tilesizey' : 1}, {'numcolumns' : 10}, {'startingudim' : 1001}, {'stackislands' : 0}
],
'group2' : [
{'groupname' : 'rendered_collision_geo_ucx'}
],
'rop_fbx1' : [
{'sopoutput' : 'Mesh_AddPathChangeThisName.fbx'}, {'mkpath' : 1}, {'buildfrompath' : 1}, {'pathattrib' : 'path'},
{'exportkind' : 0}, {'sdkversion' : ' '}, {'vcformat' : 0}, {'invisobj' : 0}, {'axissystem' : 0},
{'convertaxis' : 0}, {'convertunits' : 1}, {'detectconstpointobjs' : 1}, {'exportendeffectors' : 0},
{'computesmoothinggroups' : 1}
]
}]

shaderParamsList = [{
'principledshader1' : [
{'basecolorr' : 1.0}, {'basecolorg' : 1.0}, {'basecolorb' : 1.0}, {'albedomult' : 1.0},
{'basecolor_usePointColor' : 0}, {'basecolor_usePackedColor' : 0}, {'rough' : 1.0}, {'metallic' : 1.0},
{'reflect' : 1.0}, {'baseBumpAndNormal_enable' : 1}, {'baseNormal_vectorSpace' : 'uvtangent'}
]
}]

# Note that on the ROP FBX node converting units is disabled
standinParamsList = [{
'polyreduce1' : [
{'percentage' : 50}
],
'rop_fbx2' : [
{'sopoutput' : 'Mesh_AddPathChangeScaleProxyName.fbx'}, {'mkpath' : 1}, {'buildfrompath' : 1}, {'pathattrib' : 'path'},
{'exportkind' : 0}, {'sdkversion' : ' '}, {'vcformat' : 0}, {'invisobj' : 0}, {'axissystem' : 0},
{'convertaxis' : 0}, {'convertunits' : 0}, {'detectconstpointobjs' : 1}, {'exportendeffectors' : 0},
{'computesmoothinggroups' : 1}
]
}]

def makeDefaultAsset(): # Make nodes based on node list of types
objRootPath = '/obj' # Root path for first geo node
matRootPath = '/mat' # Root path for material principle shader nodes
childPath = ''
# Create root, then child nodes
for idx, nodeType in enumerate(assetNodeList):
if idx == 0: # Root node?
tempNodeRef, tempNodePath = makeNode(objRootPath, assetNodeList[idx])
assetNodeRefs.append(tempNodeRef)
assetNodePaths.append(tempNodePath)
childPath = tempNodePath # Assign root path
else: # Child of root node, use root node path
tempNodeRef, tempNodePath = makeNode(childPath, assetNodeList[idx])
if idx > 1: # Start wiring nodes when we're at second child node inside root
# This assumes input index 0, from first output
tempNodeRef.setInput(0, assetNodeRefs[idx-1], 0)

assetNodeRefs.append(tempNodeRef)
assetNodePaths.append(tempNodePath)

# Iterate Nodes Parameter List and set parameters accordingly
for nodeName in nodeParamsList[0]:
# Get node object reference for spawned node name
tempObjRef = getNodeRef(nodeName)
# If we have an object reference, set parameter(s)
if tempObjRef is not None:
# Iterate through parameters and set them
for idx, setting in enumerate(nodeParamsList[0][nodeName]):
# Debug
#print("For node name: " + nodeName + " Setting " + str(idx) + " is: " + str(nodeParamsList[0][nodeName][idx]))
tempObjRef.setParms(nodeParamsList[0][nodeName][idx])

# Now adjust positions of all nodes
for nodeRef in assetNodeRefs:
adjPosition(nodeRef, vertOffset)

# Create Principle Shader Node in /mat context
shaderNodeRef, shaderNodePath = makeNode(matRootPath, 'principledshader')
# May not make sense to set all parameters here - but I have the full list archived in the project folder
# Set node name
shaderNodeRef.setName("mat_changethisname")
# Additional setup parameters
for idx, setting in enumerate(shaderParamsList[0]['principledshader1']):
# Debug
#print("For principled shader - Setting " + str(idx) + " is: " + str(shaderParamsList[0]['principledshader1'][idx]))
shaderNodeRef.setParms(shaderParamsList[0]['principledshader1'][idx])
# Assign to 'material1' '/obj' node object ref index [9] using 'materialpath1' parameter
assetNodeRefs[9].setParms({'shop_materialpath1' : '/mat/mat_changethisname'})

# Create two more nodes for 'stand in' objects used as scale proxies when constructing scenes/levels
polyReduceRef, polyReducePath = makeNode(childPath, 'polyreduce') # Reduce polygons
rop_fbx2Ref, rop_fbx2Path = makeNode(childPath, 'rop_fbx') # Another ROP fbx output
# Set polyreduce params, fbx params
for idx, setting in enumerate(standinParamsList[0]['polyreduce1']):
# Debug
#print("For node name: polyreduce1 " + " Setting " + str(idx) + " is: " + str(standinParamsList[0]['polyreduce1'][idx]))
polyReduceRef.setParms(standinParamsList[0]['polyreduce1'][idx])

for idx, setting in enumerate(standinParamsList[0]['rop_fbx2']):
# Debug
#print("For node name: rop_fbx2 " + " Setting " + str(idx) + " is: " + str(standinParamsList[0]['rop_fbx2'][idx]))
rop_fbx2Ref.setParms(standinParamsList[0]['rop_fbx2'][idx])
# Connect polyreduce1 to output of material1, connect rop_fbx2 to output of polyreduce1
wireNodes(polyReduceRef, assetNodeRefs[9]) # From node, To node - check function for labeling consistency
wireNodes(rop_fbx2Ref, polyReduceRef)

# Custom positioning using X and Y offset for these two nodes - using standInOffset:
collNodeRef = hou.node('/obj/geo1/group2') # Get group2/collision mesh node X, Y position [0, -11.55]
standInVertOffset = collNodeRef.position()
polyNodePos = hou.Vector2(-3.0, standInVertOffset[1])
rop_fbx2Pos = hou.Vector2(-3.0, (standInVertOffset[1] + (vertOffset[1] * 2)))
# Set positions
polyReduceRef.setPosition(polyNodePos)
rop_fbx2Ref.setPosition(rop_fbx2Pos)

return

# --- Main Exec

# Clear console a bit
print('\n' * 4)

# Timestamp Banner
timeStamp = datetime.datetime.now()
print("\n ----------[ TallTim - Default Asset Node Generator Exec: " + str(timeStamp) + " ]---------- \n")

# Generate Default Geometry Asset for Unreal Engine Export As FBX, With Collision Mesh
makeDefaultAsset()

#dumpParms('/obj/geo1/uvlayout1) # Get parameters

# set names by <nodeRef>.setName('myName')

Here’s the result, a generated network of nodes that takes less than a second:

I’ll step through why each node is there, including why I have two FBX output nodes – which may seem confusing at first, but it will make sense, I promise.

Keep in mind that this is in the path or context of Houdini’s ‘/obj’ level – while it is entirely possible to make other assets with this automatically, my first use was to create a ‘/obj/geo’ node with all its sub-nodes so I could start modeling something right away.

From the top down (I’m omitting numbers for most of them since Houdini puts a ‘1’ after the first instance of a node.):

  1. Box – This is a ‘primitive’ type in Houdini, which just creates a 6-sided cube. I’ll typically replace this with other things, curves, swept extrusions, whatever – the box is just there as a stand-in.
  2. Group – I like keeping things orderly, so for multi-mesh parts I will make group names for them which makes it easier to refer to if I need to do any specific actions on them later.
  3. Transform – Not absolutely necessary, but may be needed to place the object on the ‘ground’ construction plane.
  4. Normal – After the polygons have been created, I find it useful to have normals applied, since later UV mapping and texturing works much better if everything is uniform.
  5. UV Unwrap – This prepares the mesh for a later step when it comes to texture mapping.
  6. Attribute Create – This allows me to create a ‘path’ value that tells the FBX exporter my object consists of multiple meshes, very handy when using Substance Painter, since you can then easily mask and select individual parts.
  7. I just realized that my code example doesn’t set these parameters entirely (always something, there’s a lot of moving parts) – but the “Class” setting needs to be “Primitive” and “Type” needs to be “String”. Once this is set, you can type in something like: “intro_basic_monitor/monitor_frame” — where the first part is the ‘root’ model name, and the latter is the part name. Really helps later down the line.
  8. Merge – This is where you’d combine all of your parts using the nodes described so far. I left this in because I rarely make anything that is just one single part.
  9. UV Layout – Here’s where the meat of setting up texture mapping happens. I’m using UDIMs, a method to spread high resolution textures over a larger texture space, but this would still be necessary if you were using regular texturing methods. Setting parameters here automatically really saves time.
  10. Material – This node assigns your texture, which lives under the ‘/mat’ context – yes, this script automagically created a material shader for you too. You’ll have to rename the material and such, but helps to have it set up already.
  11. You’ll notice that there’s a branch ‘split’ here – and I’ll explain briefly why. The ‘rop_fbx1’ node is my high-resolution output mesh. The ‘rop_fbx2’ node is used for ‘proxies’ that I create so I can assemble a large scene/level in Houdini without copying their associated node netorks, polyreduced and referencing a FBX file. This keeps overhead low and allows me to work on a new asset for a scene without using up a lot of CPU/GPU to do it. May not matter if you have a beast of a rig, but for me I know my scenes will have a lot of things in them, so I’m getting ahead of that now.
  12. The next two nodes are related to Unreal Engine and collision meshes used in the physics engine.
  13. Group2 – The name ‘rendered_collision_geo_ucx’ tells UE that it should create a collision mesh that matches the following node.
  14. Output – This node is how UE understands the object geometry and allows it to create a collision mesh on import. You can customize these, but I haven’t attempted that yet.
  15. rop_fbx1 – As I described above, this node saves a high-resolution mesh to a path specified, with the proper parameters. You’ll have to specify the path yourself, its set to some dummy value here.

Another note about this python script – the ‘assetNodeList’ variable assumes that the FIRST node is the ‘root’ under the ‘/obj’ context. The rest of the nodes are children of this ‘root’ node. If you wanted to make a different asset using this, you’d have to change how I detect/handle the root node type, but its totally doable with a few small alterations.

That’s it for now, quite a long post. I’ll post more as I get time.

Exit mobile version