0

I have a folder with 750 texture folders, within each texture folder there are four files: jpg, png, exr, and x2. I can import these textures manually by going to the shader tab and pressing Ctrl+Shift+T with Node Wrangler enabled and then select all the files within a given texture folder. My goal with this post is to automate this so that I don't have to manually do this for all 750 texture folders.

Here is the code I have so far:

import bpy
import os

FOLDER_PATH = "/Users/torrinleonard/Desktop/Texture_Importer/Textures_Folder" MATERIAL_OBJECT = "Material_Object" # The name of an object used to only add materials to the scene and nothing else.

for texture_folder in os.listdir(FOLDER_PATH): if texture_folder != '.DS_Store': # Make sure Blender is not importing MacOs .DS_Store files # Get folder path for each texture texture_path = os.path.join(FOLDER_PATH, texture_folder)

    # Create new material slot in material list and create new material:
    material_object = bpy.data.objects[MATERIAL_OBJECT]  # Set active object to variable
    material = bpy.data.materials.new(name=texture_folder)  # Set new material to variable
    material_object.data.materials.append(material)  # Add material to the object

    bpy.context.area.type = 'NODE_EDITOR'
    bpy.context.area.ui_type = 'ShaderNodeTree'

    # Prepare node setup of material:
    material.use_nodes = True
    material.node_tree.nodes.get("Principled BSDF").select = True  # Set nodes in material node tree to active

    textures = os.listdir(texture_path)
    first_jpg = ""
    files = []

    for texture in textures:
        if texture != '.DS_Store':  # Make sure Blender is not importing MacOs .DS_Store files
            if texture.endswith(".jpg"):
                first_jpg = texture
            files.append(
                {
                    "name": texture,
                    "name": texture
                }
            )

    bpy.ops.node.nw_add_textures_for_principled(
        filepath=f"//Textures_Folder/metal_plate/{first_jpg}",
        directory=os.path.join(FOLDER_PATH, texture_folder),
        files=files
    )

It's pretty self explanatory but here is the recap: We have an object in our scene, "Material_Object", for each texture located in our Texture folder, we add a new material to that object's material list, and create a new material. This creates a Principled BSDF node in the Shader Editor.

When running this script inside Blender, the materials are created, the names are set, but for some reason the files are not applied to the Principled BSDF node as if I were to do it manually:

enter image description here

Why is this happening?

I believe it is due to the way Blender is selecting the Principled BSDF node. In the console it prints it successfully Selected that node:

enter image description here

However going to that materials node group we can see it is highlighted in orange, not white, which is the selection type we need to activate Ctrl+Shift+T, or in this case, bpy.ops.node.nw_add_textures_for_principled with Python. REMEMBER: This was selected with the above function material.node_tree.nodes.get("Principled BSDF").select = True:

enter image description here

To contrast that, here is what the selection looks like when you do it manually:

enter image description here

Below I've highlighted the code for nw_add_textures_for_principled in Node Wranglers, it appears to be looking for nodes.active:

enter image description here

I have spent hours trying to find out how to get this hidden 'active' state on a node with python, but with no luck. I've poured through the documentation and node.select is the only thing close to what I can find, but that's not selecting the Principled BSDF like you would do it manually to import textures with Ctrl+Shift+T.

My question: How do I apply this hidden 'active' state to a node so that I can import images to a materials Principled BSDF node using nw_add_textures_for_principled from Node Wrangler?

Is this the only way to bulk import textures? Am I missing some much better solution?

I'm using Blender 3.1.2 on MacOS 11.6.7 Big Sur to code/test this and Blender 3.2 on Windows 10 to execute it if that matters

Torrin worx
  • 318
  • 1
  • 13
  • 1
    material.node_tree.nodes.active = material.node_tree.nodes.get("Principled BSDF") ? https://docs.blender.org/api/current/bpy.types.Nodes.html#bpy.types.Nodes.active active is a property of the node tree's nodes construct, it's not tied to each and every node (Every node can be selected, but only one node at most can be active per tree at any given time) – Gorgious Jun 21 '22 at 13:57
  • @Gorgious, that seems to be selecting the BSDF node correctly, however for some reason it changes the context away from the NODE_EDITOR/ShaderNodeTree:

    RuntimeError: Operator bpy.ops.node.nw_add_textures_for_principled.poll() failed, context is incorrect

    – Torrin worx Jun 21 '22 at 14:13
  • @Torrinworx The operator needs to be executed in the shader editor, which your script does not do. You will either have to supply a custom context or alternatively write your own script to connect the textures with the Principled BSDF. You can take a look at Node Wrangler's implemention in 3.2\scripts\addons\node_wrangler.py NWAddPrincipledSetup. – Robert Gützkow Jun 21 '22 at 14:18
  • The strange thing is, when I add print(f"AREA TYPE: {bpy.context.area.ui_type}") before the node wrangler line, the console prints the area as AREA TYPE: ShaderNodeTree – Torrin worx Jun 21 '22 at 14:18
  • @RobertGützkow Could you elaborate on what you mean by "needs to be executed in the shader editor" and "supply a custom context"? Do you mean running the script in a panel inside the Shading tab?

    Is this what you mean when you say "connect the textures with the Principled BSDF": https://blender.stackexchange.com/questions/239854/using-python-to-batch-link-image-texture-alpha-to-principled-bsdf

    Sorry for the newbie questions, I'm a python guy at heart

    – Torrin worx Jun 21 '22 at 14:27
  • @Torrinworx Contex override the new way (3.2): https://docs.blender.org/api/current/bpy.types.Context.html#bpy.types.Context.temp_override or the old way: https://docs.blender.org/api/3.1/bpy.ops.html#overriding-context The Node Wrangler operator's poll method requires that the script runs in the shader editor so that certain UI elements etc. are present. This is not the case when just running the script from the script editor or as an add-on, it has to be explicitly set. Yes, that is what I meant by connecting the textures. You could do this yourself without relying on node wrangler. – Robert Gützkow Jun 21 '22 at 14:34
  • @RobertGützkow, ok thank you for the explanation and links, I'm going to try this method out and post back here if it works – Torrin worx Jun 21 '22 at 14:59
  • 1
    Also as a suggestion you could try and locate the part of the addon which creates the setup in the node wrangler addon and copy / paste it inside of your script, you'll even be able to tweak it to your liking and won't need to rely on operator overrides. – Gorgious Jun 21 '22 at 15:28
  • @Torrinworx hi did u try the solution? – Harry McKenzie Sep 07 '22 at 15:33

1 Answers1

2

I had a similar issue here, and finally got it resolved. The important but unintuitive thing is to set:

areas[0].spaces.active.node_tree = material.node_tree

This is the script that should work for you. Before you run it: make sure you test it first with a folder with only a few textures, maybe 2 or 3, because it takes a while for blender python to process.

import bpy
import os

FOLDER_PATH = "/Users/torrinleonard/Desktop/Texture_Importer/Textures_Folder/" MATERIAL_OBJECT = "Material_Object"

#-----------------------------------------------------------------------------

Starting in 3.2 context overrides are deprecated in favor of temp_override

https://docs.blender.org/api/3.2/bpy.types.Context.html#bpy.types.Context.temp_override

They are scheduled to be removed in 3.3

def use_temp_override(): ''' Determine whether Blender is 3.2 or newer and requires the temp_override function, or is older and requires the context override dictionary ''' version = bpy.app.version major = version[0] minor = version[1] if major < 3 or (major == 3 and minor < 2): return False return True

for texture_folder in os.listdir(FOLDER_PATH): if texture_folder == '.DS_Store': # Make sure Blender is not importing MacOs .DS_Store files continue

texture_path = os.path.join(FOLDER_PATH, texture_folder)
object = material_object = bpy.data.objects[MATERIAL_OBJECT]
material = bpy.data.materials.new(name=texture_folder)
object.data.materials.append(material)

object.active_material_index = len(object.material_slots) - 1

material.use_nodes = True
tree = material.node_tree
nodes = tree.nodes

material.node_tree.nodes['Material Output'].select = False
material.node_tree.nodes['Principled BSDF'].select = True

material.node_tree.nodes.active = material.node_tree.nodes.get(&quot;Principled BSDF&quot;)

bpy.context.area.type = 'NODE_EDITOR'
bpy.context.area.ui_type = 'ShaderNodeTree'

textures = os.listdir(texture_path)
files = []

for texture in textures:
    if texture == '.DS_Store':
        continue

    files.append(
        {
            &quot;name&quot;: texture,
            &quot;name&quot;: texture
        }
    )

    filepath = texture_path + '/'
    directory = texture_path + '/'
    relative_path = True

    win = bpy.context.window
    scr = win.screen
    areas  = [area for area in scr.areas if area.type == 'NODE_EDITOR']
    areas[0].spaces.active.node_tree = material.node_tree
    regions = [region for region in areas[0].regions if region.type == 'WINDOW']

    if use_temp_override():
        with bpy.context.temp_override(window=win, area=areas[0], region=regions[0], screen=scr):
            bpy.ops.node.nw_add_textures_for_principled(
                filepath=filepath,
                directory=directory,
                files=files,
                relative_path=relative_path
            )
    else:
        override = {
            'window': win,
            'screen': scr,
            'area': areas[0],
            'region': regions[0],
        }

        bpy.ops.node.nw_add_textures_for_principled(
            override,
            filepath=filepath,
            directory=directory,
            files=files,
            relative_path=relative_path
        )

L0Lock
  • 15,965
  • 1
  • 20
  • 44
Harry McKenzie
  • 10,995
  • 8
  • 23
  • 51
  • Could you maybe add just a few words explaining shortly what makes things work please? Just to make this answer reach more people :) – L0Lock Sep 17 '22 at 15:59
  • 1
    @L0Lock sorry i didn't give more details. the thing is the important piece is this unintuitive thing you have to set areas[0].spaces.active.node_tree = material.node_tree. i always run into this same problem wondering why things don't work because of this missing line. – Harry McKenzie Jan 30 '24 at 04:58
  • https://projects.blender.org/blender/blender/issues/99484 – Harry McKenzie Jan 31 '24 at 11:57