Creating materials using ops or data?

Objective: script to add a number of materials, assign colours using hex codes and be able to tweak afterwards in the Shader Editor using nodes.

Progress: Thanks to others (links in the file) I’ve got as far as creating materials and assigning colours from hex codes, as well as setting roughness and metallic values.

Problem: If I want to tweak a material afterwards by enabling nodes in the Shader Editor, all the values are lost immediately. I would like to add a normal map and be able to fine tune later, so this is why I am interested in trying to incorporate the BSDF shader from the outset.

I was kind of hoping that when a material was created that the Principled BSDF was there by default, as is the case when a new material is created using bpy.ops.materials.new() (either by clicking ‘New Material’ or typing that into the console).

If I try

bpy.data.materials["yellow"].node_tree.nodes["Principled BSDF"].inputs[0].default_value = (0.8, 0, 0, 1)

in the console (or script) then I get the error:: AttributeError: ‘NoneType’ object has no attribute ‘nodes’.

Similarly, if I try

mat_1.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (0.8, 0, 0, 1) 
#mat_1 is defined in the code below

then I get the same error. I presume because it is looking for the BSDF shader and nodes are not switched on. If I turn on nodes and re-run the code I get the same error, so I’m not sure.

This morning I thought I had got the solution:

mat_1 = bpy.ops.materials.new()
bpy.data.materials[0].name = "yellow"

Nope! This does work in the console but not as a script.

Disclaimer: I’m clearly not a coder and more like a bull in a china shop.

The code so far;

experiment 170522.blend (4.4 MB)

You can add a material directly to the relevant data container without using bpy.ops

Link to the docs for bpy.data.materials.new
Link to the docs for bpy.types.Material.use_nodes

import bpy

mat_1 = bpy.data.materials.new("yellow")
mat_1.use_nodes = True

It has the benefits ot not needing a context and you can set its name directly in the creation statement.

3 Likes

Looks like @Gorgious beat me to it already. :smile:
Been working on the script for this topic. Finally got the script to work, if you’re interested.

image

import bpy

hex_val1 = 0xFDF8E2
hex_val2 = 0xFFDDD3
hex_val3 = 0xF3BF3B
hex_val4 = 0x5EA9EB
hex_val5 = 0x9ACDE0
hex_val6 = 0xCBE1EF

metallic_all = 1.0
roughness_all = 0.312

mat_1 = bpy.data.materials.new("yellow")
mat_1.use_nodes = True
mat_2 = bpy.data.materials.new("pink")
mat_2.use_nodes = True
mat_3 = bpy.data.materials.new("beige")
mat_3.use_nodes = True
mat_4 = bpy.data.materials.new("turq")
mat_4.use_nodes = True
mat_5 = bpy.data.materials.new("lightblue")
mat_5.use_nodes = True
mat_6 = bpy.data.materials.new("sky")
mat_6.use_nodes = True

def srgb_to_linearrgb(c):
    if   c < 0:       return 0
    elif c < 0.04045: return c/12.92
    else:             return ((c+0.055)/1.055)**2.4

def hex_to_rgb(h,alpha=1):
    r = (h & 0xff0000) >> 16
    g = (h & 0x00ff00) >> 8
    b = (h & 0x0000ff)
    return tuple([srgb_to_linearrgb(c/0xff) for c in (r,g,b)] + [alpha])

bpy.data.materials[str(mat_1.name)].node_tree.nodes["Principled BSDF"].inputs[0].default_value = hex_to_rgb(hex_val1)
bpy.data.materials[str(mat_2.name)].node_tree.nodes["Principled BSDF"].inputs[0].default_value = hex_to_rgb(hex_val2)
bpy.data.materials[str(mat_3.name)].node_tree.nodes["Principled BSDF"].inputs[0].default_value = hex_to_rgb(hex_val3)
bpy.data.materials[str(mat_4.name)].node_tree.nodes["Principled BSDF"].inputs[0].default_value = hex_to_rgb(hex_val4)
bpy.data.materials[str(mat_5.name)].node_tree.nodes["Principled BSDF"].inputs[0].default_value = hex_to_rgb(hex_val5)
bpy.data.materials[str(mat_6.name)].node_tree.nodes["Principled BSDF"].inputs[0].default_value = hex_to_rgb(hex_val6)

bpy.data.materials[str(mat_1.name)].node_tree.nodes["Principled BSDF"].inputs[6].default_value = metallic_all
bpy.data.materials[str(mat_2.name)].node_tree.nodes["Principled BSDF"].inputs[6].default_value = metallic_all 
bpy.data.materials[str(mat_3.name)].node_tree.nodes["Principled BSDF"].inputs[6].default_value = metallic_all 
bpy.data.materials[str(mat_4.name)].node_tree.nodes["Principled BSDF"].inputs[6].default_value = metallic_all 
bpy.data.materials[str(mat_5.name)].node_tree.nodes["Principled BSDF"].inputs[6].default_value = metallic_all 
bpy.data.materials[str(mat_6.name)].node_tree.nodes["Principled BSDF"].inputs[6].default_value = metallic_all 

bpy.data.materials[str(mat_1.name)].node_tree.nodes["Principled BSDF"].inputs[9].default_value = roughness_all
bpy.data.materials[str(mat_2.name)].node_tree.nodes["Principled BSDF"].inputs[9].default_value = roughness_all
bpy.data.materials[str(mat_3.name)].node_tree.nodes["Principled BSDF"].inputs[9].default_value = roughness_all
bpy.data.materials[str(mat_4.name)].node_tree.nodes["Principled BSDF"].inputs[9].default_value = roughness_all
bpy.data.materials[str(mat_5.name)].node_tree.nodes["Principled BSDF"].inputs[9].default_value = roughness_all
bpy.data.materials[str(mat_6.name)].node_tree.nodes["Principled BSDF"].inputs[9].default_value = roughness_all
1 Like

That’s great - thank you to both of you. I really, really appreciate it.

One thing I did just discover (when formulating my subsequent question!), which makes the code a little easier for me to follow is that “bpy.data.materials[str(mat_1.name)]” can be replaced with just “mat_1”. So the line simplifies slightly to

mat_1.node_tree.nodes["Principled BSDF"].inputs[0].default_value = hex_to_rgb(hex_val1)

Anyway: this may seem a daft question to someone experienced, or esoteric to others (like me!), but why is the above line not equal to

      mat_1.diffuse_color = hex_to_rgb(hex_val1)

?
My shaky reasoning is that mat_1 is the data container and that the data is the same …
I feel the answer may help me, and hopefully others, avoid blind alleys moving forward.

Good catch. The reason I had left it that way is because I was working on a string replacement system that would switch the names numbers for example mat_% with the appropriate numbers. It’s still a work-in-progress on my end though.

diffuse_color does work and it is shorter. However it requires a blend refresh after applying the changes, otherwise nothing will visually appear to having changed. That is why I preferred the other longer method, because it doesn’t require a refresh, unlike the other method - I’m not exactly sure why. I think it has something to do with constant values.

bpy.types.diffuse_color is a remnant of the older blender internal render engine and it currently only dictates the color of the object in the workbench engine (solid mode). Unless you uncheck “Use Nodes” in the material properties. This is equivalent to this property
image

A PBR material can have a wide variety of sources for its color, so you have to dive into the node tree to get the node you’re interested in. In simpler shaders, there is only one principled BSDF, but it’s possible to construct shaders with a very high number of shaders. Hence the complicated syntax.

With mat_1.node_tree.nodes["Principled BSDF"].inputs[0].default_value you’re accessing this property.

image

If you open up the shader editor, this is the same thing as this

image

BTW I think what you’re looking for is

for i in range(6):
    exec(f"mat_{i + 1}.node_tree.nodes['Principled BSDF'].inputs[0].default_value = hex_to_rgb(hex_val{i + 1})")
    exec(f"mat_{i + 1}.node_tree.nodes['Principled BSDF'].inputs[6].default_value = metallic_all")
    exec(f"mat_{i + 1}.node_tree.nodes['Principled BSDF'].inputs[9].default_value = roughness_all")

Mandatory warning about using exec https://stackoverflow.com/questions/1933451/why-should-exec-and-eval-be-avoided

1 Like

Ace! Thank you for the comprehensive answer. And yes - that is EXACTLY what I have been trying to do for an hour or so. I’ve not come across that syntax before. I’ve just read up on f strings and I’m happy! Many thanks :slight_smile:

I’m glad you sorted it out !
In order to avoid using f strings and exec which are generally smelly signs of insufficient code design, I suggest you use lists or tuples instead of named variables.

eg

import bpy

hex_vals = (0xFDF8E2, 0xFFDDD3, 0xF3BF3B, 0x5EA9EB, 0x9ACDE0, 0xCBE1EF)
mat_names = ("yellow", "pink", "beige", "turq", "lightblue", "sky")

metallic_all = 1.0
roughness_all = 0.312

def srgb_to_linearrgb(c):
    if   c < 0:       return 0
    elif c < 0.04045: return c/12.92
    else:             return ((c+0.055)/1.055)**2.4

def hex_to_rgb(h,alpha=1):
    r = (h & 0xff0000) >> 16
    g = (h & 0x00ff00) >> 8
    b = (h & 0x0000ff)
    return tuple([srgb_to_linearrgb(c/0xff) for c in (r,g,b)] + [alpha])

for mat_name, hex_val in zip(mat_names, hex_vals):
    mat = bpy.data.materials.new(mat_name)
    mat.use_nodes = True
    mat.node_tree.nodes["Principled BSDF"].inputs[0].default_value = hex_to_rgb(hex_val)
    mat.node_tree.nodes["Principled BSDF"].inputs[6].default_value = metallic_all
    mat.node_tree.nodes["Principled BSDF"].inputs[9].default_value = roughness_all

Link to zip docs

3 Likes

When I did a bit of reading about exec(), one site described it as dangerous. It was pretty cool though - I’d spent ages trying to concatenate various strings and variables and force it to do something and then you gave me that. However, you’ve now made it even more elegant. And less ‘dangerous’, I suppose?! Huge thanks - I’m learning tons.

Yeah it’s pretty exhillirating to experiment with the power of executing any arbitrary piece of code ! Outside of the fact that it’s a go-to entry point for adversary code injection (which you shouldn’t have to worry about if you don’t intend to share your script just yet), as a newcomer it obfuscates the actual code that is run inside your script and disrupts the flow of your program. Take it as an exercise to see how you can replace it with code that don’t rely on it !

Cheers :slight_smile: