My modest PBR Texture importer in 3 clicks

Hi,

I’ve been playing around with PBR textures and wanted to make my own script (for learning purpose) in order to easily create a principled bsdf material in few clicks instead of having to do each manually for all materials with blender 2.79b

That script will create a new panel in the Tools tab.

image

How to :

  1. execute the script (will create the panel in tools).

  2. You have to choose the folder where are located the texture (coming from substance but code can be adapted to other file naming scheme).

  3. Put the prefix of filenames (here it’s “MeetMat_2_Cameras_01”)

  4. Put the extension file (here it’s PNG)

  5. don’t forget to select an object and click on the “Create Material and assign to object”

  6. normally if everything goes well you have a new material assigned to your object and you are ready to render.

Normally your texture folder should look this.

image

Note :

  • Ambiant occlusion map are optional (the shader nodes will be create and linked correctly if AO is used or not
  • Emission map are optional also and the shader will take that into account or not.
  • Normal map should i think in OpenGL mode not DirectX

PS : The code is not bullet proof, so look in the console if you see something strange.
PS2 : I’ll port that code to 2.8 and will not maintain version for 2.79b

Here’s the script :

# @Warnotte Renaud 2018-2019
import bpy

def createImageTexture(material, path) : 
    try:
        img = bpy.data.images.load(path)
    except:
        raise NameError("Cannot load image %s" % path)
    print("Loaded = "+path);
    node = material.node_tree.nodes.new("ShaderNodeTexImage")
    node.image = img
    return node

def createPrincipledBSDFMaterial(folder, prefix, extension) :

    # Test if material exists
    # Create a new material
    material = bpy.data.materials.new(name=prefix)
    material.use_nodes = True

    # Remove default
    # TODO : this is giving shit with 2.80
    material.node_tree.nodes.remove(material.node_tree.nodes.get('Diffuse BSDF'))
    material_output = material.node_tree.nodes.get('Material Output')
    material_output.location = (500,0)

    nodeBsdfPrincipled = material.node_tree.nodes.new('ShaderNodeBsdfPrincipled')
    nodeBsdfPrincipled.location = (300,0)

    # link BSDFPrincipled shader to material
    material.node_tree.links.new(nodeBsdfPrincipled.outputs[0], material_output.inputs[0])

    plane = bpy.context.active_object
    plane.active_material = material

    offsetX_tex = -400
    offsetY_tex = 340
    offsetYInc = 0

    # Load all texture an connect them

    ## BASE COLOR
    try:
        path_BaseColor =  folder+prefix+'_'+'BaseColor.'+extension
        BaseColor_node = createImageTexture(material, path_BaseColor)
    except:
        print("!!! %s texture not found" % path_BaseColor)
    else:
        BaseColor_node.location = (offsetX_tex, offsetY_tex-offsetYInc)

        # make links
        # y'a aussi un remove si une AO est detectée attention
        material.node_tree.links.new(BaseColor_node.outputs["Color"], nodeBsdfPrincipled.inputs["Base Color"])
        offsetYInc+=300
  
    ## AMBIANT OCCLUSION
    try:
        path_AO =  folder+prefix+'_'+'AmbientOcclusion.'+extension
        AO_node = createImageTexture(material, path_AO)
    except:
        print("!!! %s texture not found" % path_AO)
    else:
        AO_node.location = (offsetX_tex, offsetY_tex-offsetYInc)
     
        nodeMathAO = material.node_tree.nodes.new('ShaderNodeMixRGB')
        nodeMathAO.location = (offsetX_tex+200,offsetY_tex-offsetYInc)
        nodeMathAO.blend_type = 'MIX'
        nodeMathAO.inputs[1].default_value=(0,0,0,1)

        # make links
        material.node_tree.links.new(AO_node.outputs["Color"], nodeMathAO.inputs[0])
        material.node_tree.links.new(nodeMathAO.outputs["Color"], nodeBsdfPrincipled.inputs["Base Color"])

        # PAS BESOIN Remove link from ColorTexture to Principled
        #material.node_tree.links.delete(BaseColor_node.outputs["Color"], nodeBsdfPrincipled.inputs["Base Color"])
        material.node_tree.links.new(BaseColor_node.outputs["Color"], nodeMathAO.inputs[2])

        offsetYInc+=300
        
		## METALLIC COLOR
    try:
        path_Metallic =  folder+prefix+'_'+'Metallic.'+extension
        Mettalic_node = createImageTexture(material, path_Metallic)
    except:
        print("!!! %s texture not found" % path_Metallic)
    else:
        Mettalic_node.location = (offsetX_tex, offsetY_tex-offsetYInc)
        Mettalic_node.color_space = 'NONE'
        # make links
        material.node_tree.links.new(Mettalic_node.outputs["Color"], nodeBsdfPrincipled.inputs["Metallic"])
        offsetYInc+=300

    ## ROUGHNESS
    try:

        path_Roughness =  folder+prefix+'_'+'Roughness.'+extension
        Roughness_node = createImageTexture(material, path_Roughness)
    except:
        print("!!! %s texture not found" % path_Roughness)
    else:
        Roughness_node.location = (offsetX_tex, offsetY_tex-offsetYInc)
        Roughness_node.color_space = 'NONE'

        # make links
        material.node_tree.links.new(Roughness_node.outputs["Color"], nodeBsdfPrincipled.inputs["Roughness"])
        offsetYInc+=300


    ## NORMAL
    try:
        path_Normal =  folder+prefix+'_'+'Normal.'+extension
        Normal_node = createImageTexture(material, path_Normal)
    except:
        print("!!! %s texture not found" % path_Normal)
    else:
        Normal_node.location = (offsetX_tex-200, offsetY_tex-offsetYInc)
        Normal_node.color_space = 'NONE'

        nodeNormalMap = material.node_tree.nodes.new('ShaderNodeNormalMap')
        nodeNormalMap .location = (offsetX_tex,offsetY_tex-offsetYInc)

        # make links
        #Bug, should be 16 ? ou c'est normal je ne sais pas...
        material.node_tree.links.new(Normal_node.outputs["Color"], nodeNormalMap.inputs["Color"])
        
        # blender 2.8 ? a regler cette histoire.
        #material.node_tree.links.new(nodeNormalMap.outputs["Normal"],nodeBsdfPrincipled.inputs["Normal"])
        material.node_tree.links.new(nodeNormalMap.outputs["Normal"], nodeBsdfPrincipled.inputs["Tangent"])

        offsetYInc+=300

    ## HEIGHT
    try:
        path_Height =  folder+prefix+'_'+'Height.'+extension
        Height_node = createImageTexture(material, path_Height)
    except:
        print("!!! %s texture not found" % path_Height)
    else:
        Height_node.location = (offsetX_tex, offsetY_tex-offsetYInc)

        nodeMath = material.node_tree.nodes.new('ShaderNodeMath')
        nodeMath.location = (offsetX_tex+200,offsetY_tex-offsetYInc)
        nodeMath.operation = 'MULTIPLY'

        # make links
        # Relie Texture height -> node Math -> Material output height
        material.node_tree.links.new(Height_node.outputs["Color"], nodeMath.inputs[0])
        material.node_tree.links.new(nodeMath.outputs["Value"], material_output.inputs["Displacement"])

        # TODO : Displacement Vector dans la version 2.80 et pas l'autre
        offsetYInc+=300

    ## EMISSION
    try:
        path_Emission =  folder+prefix+'_'+'Emission.'+extension
        Emission_texturenode = createImageTexture(material, path_Emission)
    except:
        print("!!! %s texture not found" % path_Emission)
    else:
        Emission_texturenode.location = (offsetX_tex, offsetY_tex-offsetYInc)
        
        # TODO : Not 100 % sure it's the good workflow
        nodeEmission = material.node_tree.nodes.new('ShaderNodeEmission')
        nodeEmission.location = (offsetX_tex+200,offsetY_tex-offsetYInc)
        
        nodeShaderMix = material.node_tree.nodes.new('ShaderNodeAddShader')
        nodeShaderMix.location = (offsetX_tex+400,offsetY_tex-offsetYInc)
        
        # make links
        material.node_tree.links.new(Emission_texturenode.outputs["Color"], nodeEmission.inputs["Color"])
        material.node_tree.links.new(nodeEmission.outputs["Emission"], nodeShaderMix.inputs[0])
        material.node_tree.links.new(nodeBsdfPrincipled.outputs["BSDF"], nodeShaderMix.inputs[1])
        material.node_tree.links.new(nodeShaderMix.outputs["Shader"], material_output.inputs[0])
        
        offsetYInc+=300



##### GUI

from bpy.props import (StringProperty,
                       PointerProperty,
                       )
from bpy.types import (Panel,
                       Operator,
                       AddonPreferences,
                       PropertyGroup,
                       )

# ------------------------------------------------------------------------
#    ui
# ------------------------------------------------------------------------

class MySettings(PropertyGroup):

    path = StringProperty(
        name="",
        description="Path to Directory",
        default="",
        maxlen=1024,
        subtype='DIR_PATH')
        
    prefix = StringProperty(
        name="prefix",
        default="TOTO_")
        
    extension = StringProperty(
        name="extension",
        default="png")


class ExcuteMyScript(bpy.types.Operator):
    """Create Material and assign to object"""
    bl_idname = "object.simple_operator"
    bl_label = "Create Material and assign to object"

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        scn = context.scene
        print (scn.wax_importtexture_tool.path)
        print("Starting !");
        #createPrincipledBSDFMaterial("F:\\Modeles_3D\\SubstancePRBTool\\material1\\", "MeetMat_2_Cameras_01_Head", "png")
        createPrincipledBSDFMaterial(scn.wax_importtexture_tool.path, scn.wax_importtexture_tool.prefix, scn.wax_importtexture_tool.extension)
        print("Finished!");
        return {'FINISHED'}


class OBJECT_PT_waxprincipled_panel(Panel):
    bl_idname = "OBJECT_PT_waxprincipled_panel"
    bl_label = "Wax principled material creator"
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_category = "Tools"
    bl_context = "objectmode"

    def draw(self, context):
        layout = self.layout
        scn = context.scene
        col = layout.column(align=True)
        col.label(text="Directory of textures")
        col.prop(scn.wax_importtexture_tool, "path", text="")
        col.label(text="Prefix of textures")
        col.prop(scn.wax_importtexture_tool, "prefix", text="")
        col.label(text="Files extension")
        col.prop(scn.wax_importtexture_tool, "extension", text="")

        row = layout.row()
        row.operator("object.simple_operator")
        
        # print the path to the console
        
# ------------------------------------------------------------------------
#    register and unregister functions
# ------------------------------------------------------------------------

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.wax_importtexture_tool = PointerProperty(type=MySettings)

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.wax_importtexture_tool

if __name__ == "__main__":
    register()
3 Likes

I know you said you did this to learn but I wanted to make sure you also knew that Node Wrangler(included in Blender) already does this with Ctrl+Shift+T. Seems to be a lot of people recreating this wheel right now.

1 Like

I know node wrangler but very little bit.

But I don’t think node wrangler create all the texture node, shader node, and connect everything together automatically to create a new material, isn’t it ? :slight_smile:

HI,

first, congratulations for your addon …:+1:

But I don’t think node wrangler create all the texture node, shader node, and connect everything together automatically to create a new material, isn’t it ?

Yes actually NodeWrangler already does everything you want.
In the Node Editor: (For 2.79 or 2.80 version)

  • select the Principled Shader and CTRL + Shift + T
  • select textures to export and click [Principled Texture Setup]

I should have a look at this. Thanks for that advices both of you :wink:

1 Like

What the parameters in brackets mean?

material_output.location = (500,0)
...
nodeBsdfPrincipled.location = (300,0)

It’s a Python tuple. It’s a data structure for things that need to be represented with multiple elements but it’s not an array because they have an order, you can’t change them and you can have the same value multiple times.

Also, you bumped an extremely old thread to ask a question that would have been better suited for new thread in Python Support.

2 Likes