2
$\begingroup$

I have a default cube ‘Cube’ which has 1 default material (active_material_index = 0). I’m adding another material dynamically via script that adds a Principled BSDF texture setup (CTRL+SHIFT+T) for this second material at active_material_index = 1 using bpy.ops.node.nw_add_textures_for_principled() as in the code below. The weird thing is that if i use active_material_index = 0 or any index for a material that already exists and have its Principled BSDF node already selected, the script actually works and properly adds the texture setup. But it doesn't work if you dynamically create the material and dynamically select the Principled BSDF node (white outline). It looks like something asynchronous is happening where the creation of the material is not ready yet when nw_add_textures_for_principled executes. Any idea? If the problem was the context, then how was it possible to work for the existing material?

enter image description here

import bpy

mat = bpy.data.materials.new(name='dynamically_created')
mat.use_nodes = True

bpy.data.objects['Cube'].data.materials.append(mat)
bpy.context.object.active_material_index = 1

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

m = bpy.context.object.active_material
m.node_tree.nodes['Material Output'].select = False
m.node_tree.nodes['Principled BSDF'].select = True
m.node_tree.nodes.active = m.node_tree.nodes.get("Principled BSDF")

r = bpy.ops.node.nw_add_textures_for_principled(filepath="E:\\ZZZZ\\Projects\\blender\\textures\\metal\\Metal021_1K-JPG\\", directory="E:\\ZZZZ\\Projects\\blender\\textures\\metal\\Metal021_1K-JPG\\", files=[ {"name":"Metal021_1K_Color.jpg", "name":"Metal021_1K_Color.jpg"}, {"name":"Metal021_1K_Displacement.jpg", "name":"Metal021_1K_Displacement.jpg"}, {"name":"Metal021_1K_Metalness.jpg", "name":"Metal021_1K_Metalness.jpg"}, {"name":"Metal021_1K_NormalDX.jpg", "name":"Metal021_1K_NormalDX.jpg"}, {"name":"Metal021_1K_NormalGL.jpg", "name":"Metal021_1K_NormalGL.jpg"}, {"name":"Metal021_1K_Roughness.jpg", "name":"Metal021_1K_Roughness.jpg"}, {"name":"Metal021_PREVIEW.jpg", "name":"Metal021_PREVIEW.jpg"}], relative_path=True)
print("result ==== ", r) #{'CANCELLED'}

UPDATE w/ Marty Fouts' Answer:

Thanks to Marty Fouts the context usage is properly implemented now but:

When I manually select the Principled BSDF node of material at index 0 (aka the already-existing material), the script does add the texture node setup but to material of index 0:

enter image description here

When NO Principled BSDF shader node is selected and i want the script to select this node from material index 1 (aka the dynamically generated material) it selects the node but unfortunately does NOT add the texture setup as I would have expected. The result is:

enter image description here

Hence the issue as described in the question title of this thread still persists

Here's the new complete script (make sure to use the default cube that already has 1 material on slot index 0):

import bpy

object = bpy.context.active_object
material = bpy.data.materials.new(name='dynamically_created')

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("Principled BSDF")

filepath="E:\\ZZZZ\\Projects\\blender\\textures\\metal\\Metal021_1K-JPG\\"
directory="E:\\ZZZZ\\Projects\\blender\\textures\\metal\\Metal021_1K-JPG\\"
files=[
    {"name":"Metal021_1K-JPG.usda", "name":"Metal021_1K-JPG.usda"},
    {"name":"Metal021_1K-JPG.usdc", "name":"Metal021_1K-JPG.usdc"},
    {"name":"Metal021_1K_Color.jpg", "name":"Metal021_1K_Color.jpg"},
    {"name":"Metal021_1K_Displacement.jpg", "name":"Metal021_1K_Displacement.jpg"},
    {"name":"Metal021_1K_Metalness.jpg", "name":"Metal021_1K_Metalness.jpg"},
    {"name":"Metal021_1K_NormalDX.jpg", "name":"Metal021_1K_NormalDX.jpg"},
    {"name":"Metal021_1K_NormalGL.jpg", "name":"Metal021_1K_NormalGL.jpg"},
    {"name":"Metal021_1K_Roughness.jpg", "name":"Metal021_1K_Roughness.jpg"},
    {"name":"Metal021_PREVIEW.jpg", "name":"Metal021_PREVIEW.jpg"},
]
relative_path=True


#-----------------------------------------------------------------------------
#
# 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
    else:
        return True
        
win = bpy.context.window
scr = win.screen
areas  = [area for area in scr.areas if area.type == 'NODE_EDITOR']
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
    )

$\endgroup$
14
  • $\begingroup$ If you check the console, you will see that it complains about no folder being selected Info: No Folder Selected. Do you want to set the folder programmatically or let the user pick (display the file browser)? $\endgroup$
    – Robert Gützkow
    Jun 24, 2022 at 7:11
  • $\begingroup$ hi. not in my case. no errors. i tried running it now by removing all materials and it runs without errors where console is saying Matched Textures etc and result {"FINISHED"} but no textures were added. I want to set it automatically. I have a list of folders through which i will iterate each folder to one material. $\endgroup$ Jun 24, 2022 at 7:17
  • $\begingroup$ it actually is inconsistent. sometimes its FINISHED sometimes CANCELLED. but it never adds the textures unless i run bpy.ops.node.nw_add_textures_for_principled() on an already created material. $\endgroup$ Jun 24, 2022 at 7:23
  • 1
    $\begingroup$ @HarryMcKenzie I've confirmed that's the problem. If I run it interactively in the Python window then the node editor that is visible switches to the dynamic material and node wrangler operates on that material. But if you run it in the script, the switch isn't made until after the node wrangler routine runs. I'm out of time for today but maybe you can figure out how to make the window switch to the new material. $\endgroup$ Jul 1, 2022 at 4:26
  • 1
    $\begingroup$ Let us continue this discussion in chat. $\endgroup$ Jul 1, 2022 at 13:32

2 Answers 2

3
+100
$\begingroup$

Your problem starts with the fact that bpy.context.area is meant to be read-only. The randomness of behavior stems from that.

You need to establish a context override to use the node wrangler function. But that means you need to deal with context override dictionaries being deprecated in 3.2 and scheduled to be removed in 3.3. Here's a way to deal with that:

#-----------------------------------------------------------------------------
#
# 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
    else:
        return True
        
win = bpy.context.window
scr = win.screen
areas3d = [area for area in scr.areas if area.type == 'VIEW_3D']
regions = [region for region in areas3d[0].regions if region.type == 'WINDOW']


if use_temp_override():
    with bpy.context.temp_override(window=win, area=areas3d[0], region=regions[0],
        screen=scr):
        print("place your bpy.ops call here, IE bpy.ops.view3d.camera_to_view()")
else:
    override = {
        'window': win,
        'screen': scr,
        'area': areas3d[0],
        'region': regions[0],
    }
    print("use an override here, IE bpy.ops.view3d.camera_to_view(override)")

but you're not overriding a 3d view, but rather the shader editor, so you'll need to adjust area and region.

EDIT: Your second problem is that you have to set the object's active_material_index to the index of the newly added material. the Node Wrangler ops operate on the active material. After you've appended the new material you need to add

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

after

object.data.materials.append(material)

This works because the new material is appended into the last material slot.

Second Edit: As per an exchange described in a comment in the answer, you also need to set the space.active.node_tree for the appropriate Node Editor.

Also, there is a typo in the if statement in the latest version of your script in your question. in the with statement region[0] should be regions[0]. I think that came from my original answer. I've fixed all the spellings in the answer.

Putting it all together, tested with an example from my own collection of textures, but with the names changed:

import bpy

object = bpy.context.active_object
material = bpy.data.materials.new(name='dynamically_created')

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("Principled BSDF")

filepath= "CHANGE ME"
directory="CHANGE ME"
files=[
    {"name":"CHANGE ME", "name":"CHANGE ME"},
] 
relative_path=True


#-----------------------------------------------------------------------------
#
# 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
    else:
        return True
        
win = bpy.context.window
scr = win.screen
areas  = [area for area in scr.areas if area.type == 'NODE_EDITOR']
regions = [region for region in areas[0].regions if region.type == 'WINDOW']

# Omar Emara informed Harry McKenzie that in order to solve the problem of this script
# working when pasted into the console but not working when run
# "You just need to assign the node tree to the node tree space type because the
# operator uses that."  Harry McKenize posted an answer showing the exact
# method.  I've adapted it to this code's flow.

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

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
    )

You would, of course, have to change the "CHANGE ME" strings and have at least one entry per texture from the directory.

$\endgroup$
8
  • $\begingroup$ hey sorry for the late response. thank u very much for ur help and time :) $\endgroup$ Jun 28, 2022 at 15:11
  • $\begingroup$ hi i just got the time to test it, and while the script works with the override functionality, it unfortunately still does not solve the problem described in the title. The function will not add the texture nodes if the material is dynamically created. It only works on the second run with the already created material. $\endgroup$ Jun 30, 2022 at 1:48
  • $\begingroup$ maybe i have to subscribe to an event that gets triggered when the material is successfully created and ready to be manipulated? and within this onUpdateComplete i can execute the texture setup function. $\endgroup$ Jul 2, 2022 at 10:24
  • $\begingroup$ Maybe, but usually when things work interactively but not in a script its because the interpreter implicitly is flushing state but the script isn't. In this case it seems like the NW operator is working with a stale value of active_material_index. I've read the NW code but I can't see a problem, so I suspect that there's a flush of some sort needed. $\endgroup$ Jul 2, 2022 at 17:16
  • 1
    $\begingroup$ @HarryMcKenzie I appreciate the gesture. Thank you. Looks like you've gotten a solution. Congratulations, even though I don't understand why setting spaces.active.node_tree works in a script but isn't needed in the console. $\endgroup$ Jul 6, 2022 at 15:14
1
$\begingroup$

I have finally gotten the last piece of the puzzle thanks to Omar Emara

You just need to assign the node tree to the node tree space type because the operator uses that.

areas  = [area for area in scr.areas if area.type == 'NODE_EDITOR']
areas[0].spaces.active.node_tree = material.node_tree
$\endgroup$
2

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .