Jump to content

[REDSHIFT]FBX Import Helper

Recommended Posts

I created a short python script to help out with the excessive nodes that are presented when importing detailed FBX files. My example is a vehicle with 278 object nodes referencing 37 materials inside the materials subnet generated by the import.

This script scans the materials subnet and creates a new object level geo for each material it finds. Inside this new geo node it creates an ObjectMerge node and populates the node with all object references to the FBX material. It assigns the new material to this new geo node that points to /shop instead of the FBX materials subnet. Then it reviews the FBX materials and creates a new Redshift material for each FBX material detected. It scans the FBX Surface node and extracts a few parameters like diffuse color, specular etc...

The net result is that I only have 37 nodes to manage instead of 278 after running the script. Also my nodes have Redshift placeholder materials assigned so I can get right to rendering.



Add this code to a new shelf button and adjust the paths, at the bottom of the script, to point to your FBX subnet. The texture path is not really used at this time.

# Scan a FBX subnet for materials.
# Create a geo object with an object merge for each object that references the material.
# Create a place holder Redshift material by reviewing the FBX materials in the subnet.
# Atom  08-22-2018
#	10-14-2018

import hou, os, re

def returnValidHoudiniNodeName(passedItem):
    # Thanks to Graham on OdForce for this function!
    # Replace any illegal characters for node names here.
    return re.sub("[^0-9a-zA-Z\.]+", "_", passedItem)
def createRedshiftImageMapMaterial(passedSHOP, passedImageFilePath, passedName, passedDiffuse=[0,0,0], passedSpecular=[0,0,0], passedWeight=0.1, passedRoughness=0.23, passedIOR=1.0, passedOpacity=1.0):
    #print "->%s [%s] [%s]" % (passedSHOP, passedImageFilePath, passedName)
    rs_vop = hou.node(passedSHOP).createNode("redshift_vopnet",passedName)
    if rs_vop != None:
        rs_output = hou.node("%s/%s/redshift_material1" % (passedSHOP, passedName))  # Detect the default closure node that should be created by the redshift_vopnet.
        if rs_output != None:
            # Create.
            rs_mat = rs_vop.createNode("redshift::Material","rs_Mat")
            if rs_mat != None:
                # Set passed values.
                if passedIOR ==0:
                    # A zero based IOR means activate mirror mode for the reflection section.
                rs_tex = rs_vop.createNode("redshift::TextureSampler",returnValidHoudiniNodeName("rs_Tex_%s" % passedName))
                if rs_tex != None:
                    # Wire
                        can_continue = True
                        can_continue = False
                    if can_continue:
                        if passedImageFilePath.find("NOT_DETECTED")==-1:
                            # Only plug in texture if the texture map was specified.
                            rs_mat.setInput(0,rs_tex)                       # input #0 is diffuse color.
                        extension = os.path.splitext(passedImageFilePath)[1]
                        files_with_alphas = [".png",".PNG",".tga",".TGA",".tif",".TIF",".tiff",".TIFF",".exr",".EXR"]
                        if extension in files_with_alphas:
                            # Place a sprite after the rsMaterial to implment opacity support.
                            rs_sprite = rs_vop.createNode("redshift::Sprite",returnValidHoudiniNodeName("rs_Sprite_%s" % passedName))
                            if rs_sprite != None:
                                rs_sprite.parm("tex0").set(passedImageFilePath)    # set the filename to the texture.
                                #rs_mat.setInput(46,rs_tex)                  # input #46 is opacity color (i.e. alpha).

                        rs_tex.parm("tex0").set(passedImageFilePath)    # set the filename to the texture.
                        # Remove luminosity from texture using a color corrector.
                        rs_cc = rs_vop.createNode("redshift::RSColorCorrection",returnValidHoudiniNodeName("rs_CC_%s" % passedName))
                        if rs_cc != None:
                            # Add a slight bump using the greyscale value of the diffuse texture.
                            rs_bump = rs_vop.createNode("redshift::BumpMap",returnValidHoudiniNodeName("rs_Bump_%s" % passedName))
                            if rs_bump != None:
                                rs_bump.parm("scale").set(0.25)          # Hard coded, feel free to adjust.
                        # Layout.
                    print "problem creating redshift::TextureSampler node."
                print "problem creating redshift::Material node."
            print "problem detecting redshift_material1 automatic closure."
        print "problem creating redshift vop net?"

def childrenOfNode(node, filter):
    # Return nodes of type matching the filter (i.e. geo etc...).
    result = []
    if node != None:
        for n in node.children():
            t = str(n.type())
            if t != None:
                for filter_item in filter:
                    if (t.find(filter_item) != -1):
                        # Filter nodes based upon passed list of strings.
                        result.append((n.name(), t))
                    result += childrenOfNode(n, filter)
    return result
def groupByFBXMaterials(node_path, rewrite_original=False):
    lst_geo_objs = []
    lst_fbx_mats = []
    s = ""
    material_nodes = childrenOfNode(hou.node("%s/materials" % node_path),["Shop material"]) #Other valid filters are Sop, Object, cam.
    for (name, type) in material_nodes:
        node_candidate = "%s/%s" % ("%s/materials" % node_path, name)
        n = hou.node(node_candidate)
        if n !=None:

    object_nodes = childrenOfNode(hou.node(node_path),["Object geo"]) #Other valid filters are Sop, Object, cam.
    for (name, type) in object_nodes:
        node_candidate = "%s/%s" % (node_path, name)
        n = hou.node(node_candidate)
        if n !=None:

    # Make an object geo node for each material detected.
    # Inside the object will reside an object merge to fetch in each object that references the material.
    root = hou.node("/obj")
    if root != None:
        for mat in lst_fbx_mats:
            mat_name = os.path.basename(mat)
            shader_name = "rs_%s" % mat_name
            geo_name = "geo_%s" % mat_name
            node_geo = root.createNode("geo", geo_name)
            if node_geo:
                # Delete the default File node that is automatically created as well.
                if (len(node_geo.children())) > 0:
                    n = node_geo.children()[0]
                    if n:
                node_geo.parm("shop_materialpath").set("/shop/%s" % shader_name)
                node_obm = node_geo.createNode("object_merge","object_merge1")
                if node_obm != None:
                    p = node_obm.parm("objpath1")
                    all_obj = ""
                    for obj in lst_geo_objs:
                        temp_node = hou.node(obj)
                        if temp_node != None:
                            smp = temp_node.parm("shop_materialpath").eval()
                            if smp.find(mat_name) != -1:
                                all_obj += "%s " % obj
            # Make a place holder Redshift material by reviewing the FBX material.
            opacity = 1.0
            ior = 1.025
            reflection_weight = 0.1
            reflection_roughness = 0.23
            diffuse_color = [0,0,0]
            specular_color = [0,0,0]
            # Typically the FBX Surface Shader is the second node created in the FBX materials subnet.
            n = hou.node(mat).children()[1]
            if n != None:
                r = n.parm("Cdr").eval()
                g = n.parm("Cdg").eval()
                b = n.parm("Cdb").eval()
                diffuse_color = [r,g,b]
                sm = n.parm("specular_mult").eval()
                if sm > 1.0: sm = 1.0
                reflection_weight = 1.0-sm
                if (sm==0) and (n.parm("Car").eval()+n.parm("Cdr").eval()==2):
                    # Mirrors should use another Fresnel type.
                r = n.parm("Csr").eval()
                g = n.parm("Csg").eval()
                b = n.parm("Csb").eval()
                specular_color = [r,g,b]
                opacity = n.parm("opacity_mult").eval()
                reflection_roughness = n.parm("shininess").eval()*0.01  
                em = n.parm("emission_mult").eval()
                if em > 0:
                    # We should create an rsIncandescent shader, using this color, instead.
                    r = n.parm("Cer").eval()
                    g = n.parm("Ceg").eval()
                    b = n.parm("Ceb").eval()
                # Try to fetch the diffuse image map, if any.
                tex_map = n.parm("map1").rawValue()
                if len(tex_map) > 0:
                    tex_map = "%s/%s" % (texture_path,"NOT_DETECTED")
                createRedshiftImageMapMaterial("/shop", tex_map, shader_name, diffuse_color, specular_color, reflection_weight, reflection_roughness, ior, opacity)
        if rewrite_original:
            # Re-write the original object node's material reference to point to the Redshift material.
            for obj in lst_geo_objs:
                node_geo = hou.node(obj)
                if node_geo:
                    m = node_geo.parm("shop_materialpath").eval()
                    if len(m):
                        mat_name = os.path.basename(m)
                        shader_name = "/shop/rs_%s" % mat_name
                        # To do this right, we need to add a material node to the end of the network and populate it with the shop_materialpath value.
                        node_display = node_geo.displayNode()
                        if node_display != None:
                            node_mat = node_geo.createNode("material","material1")  # Create new node.
                            if node_mat != None:
                                node_mat.setInput(0,node_display)                   # Wire it into the network.
                                node_mat.setDisplayFlag(True)                       # Move the display flag to the new node.
                                node_mat.setRenderFlag(True)                        # Move the render flag to the new node.
# Program starts here.
texture_path = '/media/banedesh/Storage/Documents/Models/Ford/Ford_F-150_Raptor_2017_crewcab_fbx'   #Not really used yet.
fbx_subnet_path = "/obj/Container_Ship_Generic_FBX"
groupByFBXMaterials(fbx_subnet_path, True)



Edited by Atom

Share this post

Link to post
Share on other sites

I added a new feature to the script, which can be disabled or enabled by passing a True/False flag. This new feature allows you to re-write the original object nodes, inside the FBX subnet, with the new Redshift materials. So if you want to work with the original nodes, but assign them all new materials, try this feature.

Here is the result from running the script. The FBX materials are translated into Redshift equivalents. The script tries to guess mirror types and assigns an alternate fresnel mode in that case. For the headlights, I did manually choose the Glass preset inside the generated rsMaterial.



Edited by Atom

Share this post

Link to post
Share on other sites

Hey Atom,

I like your python script...

I tried to implement to read the fbx_subnet_path from selection but with no luck.

Here is what I tried:

sel = hou.selectedNodes()
Mypath = sel[0].path
texture_path = '/media/banedesh/Storage/Documents/Models/Ford/Ford_F-150_Raptor_2017_crewcab_fbx'   #Not really used yet.
fbx_subnet_path = str(Mypath)
groupByFBXMaterials(fbx_subnet_path, True)

Share this post

Link to post
Share on other sites

Try print MyPath to see what you are passing? Maybe you have a trailing slash or something...

This code will only work with a single fbx subnet supplied.

Edited by Atom

Share this post

Link to post
Share on other sites

I tried that and it seems the path is correct.

If I uses the printed path in quotes - like you did in your script - everythings works!

Here is what I get when I print Mypath:

<bound method ObjNode.path of <hou.ObjNode of type subnet at /obj/SlopeHouse_FBX>>

Share this post

Link to post
Share on other sites

Oh yeah, Houdini treats all parameters as functions so try...

Mypath = sel[0].path()


Share this post

Link to post
Share on other sites

Great it works ....

What is the difference between  .path() and .path?

Share this post

Link to post
Share on other sites

.path fetches the code based object while .path() fetches the result of invoking the code based object. My guess is it basically runs the __INIT__.

Share this post

Link to post
Share on other sites

Hey Atom!

Thanks so much for this script. It's been a help and step forward to the community.

I tried it but i'm not sure if I'm doing it correctly. I'm using Houdini. I imported a cg model that I downloaded from cg trader in fbx format. Houdini and mantra render the materials on the model perfectly but redshift is making me jump through hoops. 

I created a new shelf tool with your script and it moved the geo nodes within the fbx file in the obj level but it only moved the geo at the top of the ship and the geo nodes are missing their materials. When I try to render it either crashes or there is a render error and it doesn't render. 

I contacted Redshift and they said that 'Redshift only can work with its own VOP shader nodes, so you are going to need to convert the materials. Of course, you can use the texture maps provided with the models, but using the Redshift texture sampler VOP node to apply them.' 

Any ideas what I can do to get redshift to read and render my cg model's geo and materials?


Screenshot from 2018-10-10 20-13-54.png

Screenshot from 2018-10-10 20-13-42.png

Screenshot from 2018-10-10 20-13-27.png

Share this post

Link to post
Share on other sites

With the new re-write material feature, you can throw away all those top level OBJ files unless you have an additional use for them.

Dive inside the FBX subnet and then inside one of the geometry objects. Did the script add a new material node to the end of the network? If so, is the material name on that node pointing to a Redshift material?

The script does not detect subnets inside subnets. It assumes all objects in the FBX reside at the top level of the master FBX subnet.

Can you post the FBX or HIP?

Edited by Atom

Share this post

Link to post
Share on other sites

Yah I ran the script and it did correctly add a new material node to each geometry object that seems to have a name that points to a Redshift material. 

But half of the geometry in the viewport is missing the materials and I can't get any of it to render. 

Am I doing something wrong? Any help would be greatly appreciated. 





Here is the file. 


Share this post

Link to post
Share on other sites

The viewport is another issue and is not addressed by this script. This script is supposed to help with rendering and setting up the Redshift materials automatically.

I looked over the scene and noticed that a lot of the FBX materials are all colored grey. So the script had nothing to detect and makes all materials the same color. That might be what you are experiencing when you mention you can't get any of it to render, a white render.

I also noticed that there are maps associated with the FBX materials. I altered the code to examine the texture filepath for map1 of the FBX shader. If the length of that field is non-zero then I fetch that value and pass it along to the Redshift texture sampler node. This ends up with relative texturing in place. Something like...$HIP/Downloads/Flags_diff.png


This means you need to create a Downloads folder inside your HIP folder and copy all the texture maps for the FBX into that folder. Then Redshift will detect and display them in the render.

If you are going to try the new script, try it in a clean scene. Import your FBX then run the script.


Edited by Atom

Share this post

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now