Controlling Material Properties In Blender With Python Scripting

Accessing Materials in Blender’s Python API

The Blender Python API allows scripts to access and modify materials applied to objects and meshes in a Blender scene. To get existing materials, the API provides methods like bpy.context.object.active_material and bpy.data.meshes['Mesh'].materials. These return Blender material datablocks that store properties like color, textures, and shader setups.

New materials can also be created fully in Python with bpy.data.materials.new() and then assigned to objects with bpy.context.object.active_material = material. Multiple materials can be assigned per object to mix together in the rendered result.

Getting Existing Materials from Objects and Meshes

The most common way to access existing materials in a Blender scene is first get the active object with bpy.context.object, then access its first material slot with .active_material. This returns the Blender Material datablock for that slot, which stores all properties of the material.

import bpy

object = bpy.context.object
mat = object.active_material

# Print name of material 
print(mat.name)  

Objects in Blender can have multiple materials assigned, accessed with object.material_slots. So we can loop through all slots and materials on an object:

for slot in object.material_slots:
    material = slot.material
    print(material.name)

For mesh objects specifically, we can also get the material list directly from the Mesh datablock with bpy.data.meshes['Mesh'].materials. This references the same materials, but avoids going through the object:

# Get mesh datablock
mesh = bpy.data.meshes['Cube']  

# Print all material names on this mesh
for material in mesh.materials:
    print(material.name)

Now that we have material datablocks from objects and meshes, we can start modifying properties and textures covered in the next sections.

Creating New Materials Programmatically

Brand new materials can be created directly in Python scripts with bpy.data.materials.new(name). This will return a new blank Material datablock that can then be edited.

Here is an example creating a new material and setting its first color property:

# Create new material 
new_mat = bpy.data.materials.new(name="MyMaterial")

# Set diffuse color
new_mat.diffuse_color = (1.0, 0.3, 0.1, 1.0) 

# Assign to object
obj = bpy.context.object
obj.active_material = new_mat

This creates a red-orange solid color material, and assigns it to the active object in the Blender scene. Many more properties can be set like this to control shaders, textures, physics settings, and animation.

For materials that will be re-used across multiple objects, it is best to create the datablock on its own, then reference it from object material slots only as needed. This allows modifying the material in one place, with changes propagating everywhere it is assigned.

Modifying Material Properties

The main power of scripting materials in Blender comes from dynamically modifying properties like color, shaders, and texture maps. This allows materials that change procedurally, react to simulations, or animate over time.

Changing Basic Properties Like Color and Roughness

The most basic material properties control the fixed color, shininess and roughness. For solid colors, the main property is the diffuse_color, defined as an RGB vector. Common colors can be set by name with shortcuts like .diffuse_color = 'BLUE' or assigned directly from color picker hex values.

# Set solid blue color
mat.diffuse_color = (0.1, 0.3, 0.8)

# Make fully white
mat.diffuse_color = 'WHITE'  

# Set red-orange from colorpicker
mat.diffuse_color = '#FF7F00'  

The metallic and roughness properties control how shiny/glossy and rough/blurry reflections appear. Lower roughness gives sharper reflections and mirror gloss on metals.

# Make material rough like concrete
mat.roughness = 0.5
mat.metallic = 0.1

# Make very smooth & reflective metal  
mat.roughness = 0.05 
mat.metallic = 1.0  

Scripting these gives controlled base values before adding more complex layers.

Setting up Complex Shader Nodes with Scripting

While colors and textures can be assigned directly, advanced material effects require building shader node graphs. The NodeTree datablock accessed via mat.node_tree contains all shader nodes, linking their inputs and outputs together.

Nodes are created with node_tree.nodes.new(type), positioned manually, then connected by named socket. For example, mapping a noise texture into the base color input:

nodes = mat.node_tree.nodes

# Create texture node
tex = nodes.new(type='ShaderNodeTexNoise')  
tex.inputs[2].default_value = 5.0 # Scale

# Map color output into base color input
mat.node_tree.links.new(
    tex.outputs['Color'],  
    nodes['Principled BSDF'].inputs['Base Color']
)

All inputs and outputs use predefined names like ‘Color’ making it possible connect nodes without needing to reference indices. With a node-based material, textures, colors, and other settings stack together to give the final look.

For complex materials with many nodes, it is best to create groups that encapsulate details. The group can expose key properties, while keeping the node graph organized.

Texturing and Mapping

Textures add detail and variation to materials in Blender. Procedural textures create patterns algorithmically, while image textures sample photos and pictures. UV mapping projects 2D textures onto a 3D model’s surface.

Assigning Image Textures to Materials

Image textures represent one of the most common types of textures, where an external image is mapped onto an object’s polygons. In Python, we first load the image as an bpy.data.images datablock. This references the image pixel data stored somewhere on disk or in Blender’s packed textures.

A shader Texture Node can then sample this image. Common texture nodes include:

  • ShaderNodeTexImage – General color/alpha sampling
  • ShaderNodeTexEnvironment – HDRi background / reflection

Here is an example loading an image file and assigning to a shader node:

import bpy

# Load image datablock
image = bpy.data.images.load(filepath="texture.png")

# Create texture node
tex = mat.node_tree.nodes.new('ShaderNodeTexImage')  
tex.image = image

# Link color output to material  
mat.node_tree.links.new(
    tex.outputs['Color'],
    mat.node_tree.nodes['Principled BSDF'].inputs['Base Color']
)

Now the image colors will be mapped as the base color for shading.

Controlling UV Mappings and Coordinates

UV unwrapping projects mesh polygons onto 2D coordinates, determining how image textures align to surfaces. Each vertex stores a UV coordinate saying where it maps on the image.

UV layers are accessed via bpy.data.meshes['Mesh'].uv_layers. Each layer stores per-vertex UV coordinates that textures sample from. New UV maps can be created and assigned in Python as needed.

By scripting UV layers, we can:

  • Program procedural UV unwrappings
  • Animate UVs moving over time
  • Switch between UV maps for texture variation

Here we animate the UV offset to scroll an image texture:


import bpy

# Get first image texture node
tex = mat.node_tree.nodes['Image Texture']  

# Animate offset 
def scroll_texture(scene):
    tex.inputs['Vector'].default_value[0] += 0.01

bpy.app.handlers.frame_change_pre.append(scroll_texture)  

This adds a handler to shift UVs every frame, making the image texture scroll continuously.

Many texture effects can be achieved through creative UV manipulation.

Animating Material Properties

Animation takes material scripting to the next level, allowing property changes over time, reactionary behaviors, and interactive controls.

Changing Colors, Values, and Textures over Time

Animation requires setting values to change across frames. For colors, common effects include:

  • Pulsing glows – Cycling HSV with sine math
  • Color shifts – Animating RGB channels independently
  • Blinking lights – Toggling alpha on/off

Here is a pulsating lava material, cycling the hue:


import bpy
import math

# Animate function
def lava_pulse(scene):
    
    mat = bpy.data.materials['Lava']
    
    # Cycle Hue 0-1 over 12 frames 
    hue = (math.sin(scene.frame_current / 12.0 * math.pi) + 1.0) / 2.0
        
    mat.node_tree.nodes['Emission'].inputs[0].default_value[:3] = (hue, 0.4, 1.0)
 
# Register handler    
bpy.app.handlers.frame_change_pre.append(lava_pulse)   

Keyframes could also be inserted automatically each frame to capture property changes.

Reacting to Events by Altering Materials

Materials can also react to events like object collisions, proximity sensors, game damage, etc. For example, changing color when hits occur:


# Listen for attack hitting
def on_attack(scene): 
   obj.active_material.diffuse_color = 'RED'
   
# Reset after a few seconds   
def reset_color(scene):
   obj.active_material.diffuse_color = 'GRAY'
   
# Run handlers   
bpy.app.handlers.frame_change_pre.append(on_attack)
bpy.app.handlers.frame_change_post.append(reset_color)

Now the material flashes red for a few frames whenever an attack event happens.

Python scripting allows very custom behaviors by altering materials dynamically.

Practical Examples

Some examples of scripting materials in real productions:

Creating a Procedurally Generated Terrain Material

Terrain often uses procedurally generated texture patterns to add variation. Here is an example layered material with two texture nodes mixing together:


# Create terrain material
mat = bpy.data.materials.new(name="Terrain")

# Add base color texture 
tex1 = mat.node_tree.nodes.new('ShaderNodeTexNoise')
tex1.inputs[2].default_value = 40.0 # Scale
mat.node_tree.links.new(tex1.outputs['Color'], nodes['Mix'].inputs[1])

# Add slope dirt texture
tex2 = mat.node_tree.nodes.new('ShaderNodeTexMusgrave') 
tex2.inputs[1].default_value = 6.0 # Scale  
mat.node_tree.links.new(tex2.outputs['Color'], nodes['Mix'].inputs[2])
 
# Mix textures together based on height  
math = mat.node_tree.nodes.new('ShaderNodeMath')
math.operation = 'MULTIPLY'
math.inputs[0].default_value = 0.2 # Blend factor
mat.node_tree.links.new(math.outputs[0], nodes['Mix'].inputs['Fac'])

Now the material mixes together procedural noise and musgrave textures, with increasing dirt in steep areas.

Fading Object Colors When Damaged in a Game

As objects take damage in games, materials can visually react to the events. Here the color shifts toward red based on damage percentage:


def apply_damage(obj, percent):
    
    # Get material 
    mat = obj.active_material
    
    # Shift color toward red
    factor = percent / 100.0
    color = [1.0 - factor, factor, 0.1, 1.0] 
    
    mat.diffuse_color = color 
    
# Call when damage event occurs   
apply_damage(obj, health_percent) 

Now objects will start fading into red as they take damage during gameplay.

Cycling Through Multiple Labeled Image Textures

Multiple images can be blended via Mix nodes, labeling each with a parameter. Scripting then allows crossfading between named textures:


# Define images 
concrete = bpy.data.images['Concrete.jpg'] 
tiles = bpy.data.images['Tiles.jpg']
wood = bpy.data.images['WoodPlanks.jpg']

# Mix node labels  
nodes['Mix1'].inputs[1].name = 'concrete'
nodes['Mix2'].inputs[1].name = 'tiles'  
nodes['Mix3'].inputs[1].name = 'wood'

# Set current blend factor 
nodes['Mix1'].inputs['Fac'].default_value = 1.0 # Show concrete

Now cycling the Fac values on Mix nodes will crossfade between labeled textures.

Leave a Reply

Your email address will not be published. Required fields are marked *