I spent the weekend writing a cloth solver for Unreal Engine 4 after being inspired by the cloth placement tool I saw for Source 2’s Hammer editor.
Currently it runs on the CPU & will generate its own primitive scene proxy to calculate the mesh on the render thread from the particle simulation. It also collides with the scene in real time – works a lot better than I was expecting!
I am contemplating releasing this as a plugin for the marketplace, until then here are my plans:
Improve stability (ensure assert conditions won’t be hit as much as possible)
Double-check performance & provide CPU profiling stats akin to the CableComponent plugin
The aim of this tool is to provide a more accurate representation with the Unreal Engine 4 viewport by providing a pixel shader which uses BRDFs closer/pulled from Unreal’s shaders, as well ACES tonemapping.
IBL samples slider (default changed from 16 to 64)
BRDFs more accurate to Unreal (Fresnel Schlick approximation, GGX Smith) – light source calculation & IBL
Comparison
I compared the same input parameters against Substance Designer vanilla, my shader, and the UE4 viewport. Those being albedo=(.75,.75,.75), roughness = .25, and metallic=1.
The results I got still aren’t 1-1 as there’s a lot of complex stuff going on Unreal, especially with an adjustable log curve for the tonemapping, but generally I feel this is good enough to at least have a better idea of what your material will look like across packages.
What Next?
I’m continuing to review the BRDFs and colour transform as I use the shader myself and will update the git repo as I go.
I’ve already investigated doing the same for Substance Painter, but unfortunately Painter’s renderer is more complicated (seemingly deferred), compared to Designer which seems to be done in forward. This means in Designer I can touch the final pixel colour as I wish (there is some still some post effects which can be enabled after this in the pipeline), which means I can directly control the shading BRDFs.
Painter seemingly provides shaders more as material shaders, to provide a way to program the inputs into an already existing shading model which I can’t touch (think Unreal’s material graph).
I developed this tool because the current import FBX sop isn’t very procedural as it’s only accessed through a menu (though you can call it from Python, which is what I am doing essentially).
Said tool generates an objnet consisting of a series of nodes which represents the hierarchy of the FBX – this can be a pain to work with though, especially compared to the File sop which just pulls in geo as is.
This tool just wraps up that tool in a sop context and pulls all references in to the local HDA level, and builds the subsequent networks to output and organsie the geometry.
How it works
As this tool is just a sop wrapper which calls the FBX API already exposed, it should be maintainable going forward as all the heavy lifting is already done.
This is also useful as it builds the necessary matnets, transforms, hiearchies etc which I can utilise – I just move this whole objnet to an editable objnet inside my HDA context. My tool then builds 2 objmerge networks to pull in the geo from the objnet.
All the work in this tool is in the Python module attached to the HDA, where the LoadFBX() method is called by a callback on the load button callback, and the text field callback.
I’m doing this with this very simple line to be able to call my methods from a Py module on the current node context we’re executing from.
hou.phm().LoadFBX()
And this is the Python module. Edit 06/01/2020: Updated with import transform functionality, vertex colour preservation (prevent mismatch of Cd attribute), added ClearGeometry() method, & added suite of import options
import os, sys
def ClearNetwork(network):
for child in network.children():
child.destroy()
def SetupMergeNetwork(network):
result = {"network":network}
ClearNetwork(network)
result["merge"] = network.createNode("merge", "merge")
result["merge"].setDisplayFlag(True)
result["merge"].setInput(0, result["network"].indirectInputs()[0])
return result
def ClearGeometry():
with hou.undos.group("Clearing geometry loaded from '" + hou.parm("file").eval() + "'"):
ClearNetwork(hou.node("IMPORT"))
ClearNetwork(hou.node("GEO_MERGE"))
ClearNetwork(hou.node("COLLISION_MERGE"))
def LoadFBX():
# Get file path
file = hou.parm("file").eval() # Evaluate path
file = os.path.normpath(file)
if not os.path.exists(file):
raise hou.NodeError("File doesn't exist")
return
# Find import objnet (editable)
objnet = hou.node("IMPORT")
ClearNetwork(objnet)
# Import FBX
#hou.hscript("fbximport {}".format(file))
materialMode = hou.fbxMaterialMode.FBXShaderNodes if hou.parm("materialMode").eval()==1 else hou.fbxMaterialMode.VopNetworks
compatabilityMode = hou.fbxCompatibilityMode.FBXStandard if hou.parm("compatabilityMode").eval()==1 else hou.fbxCompatibilityMode.Maya
rawFbx = hou.hipFile.importFBX(file,
import_animation=hou.parm("importAnimation").eval(),
import_joints_and_skin=hou.parm("importJointsAndSkin").eval(),
resample_animation=hou.parm("resampleAnimation").eval(),
resample_interval=hou.parm("resampleInterval").eval(),
import_materials=hou.parm("importMaterials").eval(),
material_mode=materialMode,
compatibility_mode=compatabilityMode)
fbx = rawFbx[0].copyTo(objnet)
rawFbx[0].destroy()
# Link import transform
fbx.parm("xOrd").set(hou.parm("xOrd"))
fbx.parm("rOrd").set(hou.parm("rOrd"))
fbx.parm("tx").set(hou.parm("tx"))
fbx.parm("ty").set(hou.parm("ty"))
fbx.parm("tz").set(hou.parm("tz"))
fbx.parm("rx").setExpression('ch("../../rx") + if(ch("../../axis")==1, 90, 0)', language=hou.exprLanguage.Hscript) # Link scale
fbx.parm("ry").set(hou.parm("ry"))
fbx.parm("rz").set(hou.parm("rz"))
fbx.parm("sx").set(hou.parm("sx"))
fbx.parm("sy").set(hou.parm("sy"))
fbx.parm("sz").set(hou.parm("sz"))
fbx.parm("px").set(hou.parm("px"))
fbx.parm("py").set(hou.parm("py"))
fbx.parm("pz").set(hou.parm("pz"))
fbx.parm("prx").set(hou.parm("prx"))
fbx.parm("pry").set(hou.parm("pry"))
fbx.parm("prz").set(hou.parm("prz"))
fbx.parm("scale").setExpression('ch("../../importScale") * ch("../../scale")', language=hou.exprLanguage.Hscript) # Link scale
# Build merge networks
# Clear networks first
geo = SetupMergeNetwork(hou.node("GEO_MERGE"))
collision = SetupMergeNetwork(hou.node("COLLISION_MERGE"))
# Generate objmerge nodes
for child in fbx.children():
if not child.type().name() == "geo":
continue # Skip this iteration
isCollider = any(s in child.name() for s in {"UBX", "UCP", "USP", "UCX"})
mergeNet = collision if isCollider else geo
objMerge = mergeNet["network"].createNode("object_merge", child.name()) # Create objmerge node
# Set-up parameters
objMerge.parm("xformtype").set("local")
objMerge.parm("createprimstring").set(True)
objMerge.parm("pathattrib").set("path")
objMerge.parm("objpath1").set(mergeNet["merge"].relativePathTo(child))
objMerge.parm("createprimgroups").set(True)
objMerge.parm("primgroupprefix").set(objMerge.name() + "_node")
objMerge.parm("suffixfirstgroup").set(False)
mergeNet["merge"].setInput(len(mergeNet["merge"].inputConnections()), objMerge) # Connect to merge
objMerge.moveToGoodPosition()
geo["merge"].moveToGoodPosition()
collision["merge"].moveToGoodPosition()
# Select this node to bring parameter panel back
hou.pwd().setSelected(True, True, True)
What’s next?
FBX Export sop of course!
An FBX rop exists of course, but I would like to simplify the process of building a hiearchy from an objnet level, to a sop level and be data-driven to make proceduralism easier.
I am still thinking of my approach to this, but at the simplest level I believe it’d be at least useful to include a collision pin and allow my sop to build the relevant hiearchy and node names, and perhaps some helper sops to categorise collisions.. we will see!