Featured

Unreal Engine – Cloth Solver

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.

The tweet which inspired me!

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
  • Move solver to GPU with a compute shader
  • Optional GPU scene depth collision

Unreal Shader for Substance Designer

Grab this from my git repo – github.com/Calvinatorr/SubstanceShaderUE
*Install instructions included in the readme

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.

What this provides:

  • ACES filmic tonemapping curve
  • (optional) directional light
  • (optional) IBL toggle
  • IBL samples slider (default changed from 16 to 64)
  • BRDFs more accurate to Unreal (Fresnel Schlick approximation, GGX Smith) – light source calculation & IBL
Options added in the material properties menu

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.

Substance Designer vanilla (left), my shader (middle), Unreal Engine 4 viewport

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).

Load FBX sop

github.com/Calvinatorr/CommercialHDAs/blob/master/FBX_sops.hda
To install just point your houdini.env file to the hda/folder of your hdas, or use the install hda feature.

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.

For example this subway train has a load of submeshes & material IDs which you can see in this hiearchy
The Load FBX sop will build the objmerge networks in these subnets and output them (including groups and attributes), pretty simple
The actual network looks like this.. and unfortunately you can’t just use a single objmerge to combine all these nodes
And for the unaware – you can define Editable Nodes in your HDA. This means even when the HDA is locked you can change the state of those nodes!
Thanks Luiz Kruel for pointing me to this, I felt pretty dumb when I realised this is of course what they are for!
Subsequently, the user only has to see this – simple! If they want to reload the geometry, they can just call pressButton() on the button param.
I’ve even exposed the collision net as guide geo – neat!
sop level access & parameter panel
Can be embedded into other HDAs just like a file sop

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!