Skip to content

Assigning a random material to each face on a mesh

January 30, 2013

For the last two days, I’ve been working on a different script than the Ivy Growth Animator that’s been at the center of my attention recently.

Now that the leaves are also animated, I wanted to create a user interface to enable users to interact with the script’s parameters, and activate it with a convenient button. But since I knew exactly nothing about creating UI elements via python, I had to start with something more rudimentary and basic.

Blender Cookie was a great help yet again, with their nice little tut about creating a random material assigner for the active object. This tut demonstrates UI element creation nicely and wrapps it with an interesting script that I wanted to know how to write anyway.

But after I finished watching and reproduced the outcome, I was curious how quickly I could make a derivative of this script and create a random material assigner for FACES. Assigning a random material to the selected object is nice and could certainly be useful, but I frequently need to assign random materials for faces, for instance to create variations in the leaves created by the Ivy Generator or SaplingTree add-ons.

Tree leaves before and after randomization with the script

Tree_Comparison

So, after some fiddling around (especially with regard to the interface, as the rest of the code was simpler and the techniques more familiar to me), here it is! You can download and install it like any other addon, and here’s a short video explaining how to use it:

If you’re not interested in python and scripting in Blender, you can stop reading this post here. If you are, the next part of this post is a sort of a “making of” article.

“Making of” or something

So, after I started coding, it didn’t take long before I was all giddy, because the first attempt I made at this seemed to work well and good. Until later…

BI who?

So, this is what I initially wrote to assign randomly picked materials to mesh object faces:

import bpy, random

obj = bpy.context.object                  # Reference to selected object
all_materials   = obj.data.materials      # All the materials of the selected object
no_of_materials = len(all_materials) # Number of materials on the sel. object

# Choose the index of a random material from the list
randomly_chosen_material_index = random.randint(0, no_of_materials - 1)

obj.data.update( calc_tessface = True )                     # Calculate faces data
for face in obj.tessfaces:                                  # Iterate over all faces
face.material_index = randomly_chosen_material_index    # Assign random index to current face

Simple right? I didn’t even have to mess around with face indices with the old mesh interface and get them all garbled. Yay!

Only it turned out to work only with the Blender Internal rendering engine.

Yup. Extremely annoying stuff, I couldn’t get this code to randomly assign cycles materials per face even after several major changes. It kept changing all the faces together or to do nothing at all, while working smoothly with BI materials. Gladly, I found another solution.

BMESH to the rescue

I started sniffing around the Bmesh API documentation when I saw some comments on BlenderArtists hinting that it makes selecting faces much less annoying and cumbersome. But the documentation is so basic and lacks examples that I didn’t make sense of it and my few meager attempts at using it failed miserably.

But after my new script refused to work in cycles, I resumed my efforts to research bmesh and found a wonderful post that explained all I needed to know.

It showed how to select faces easily, which I merrily tried out and was happy to discover that it actually works! Though it was nice to learn, for the material assigning script I didn’t even need to select faces, I just needed  a reliable interface to the face data which would work in cycles as well. And the bmesh module got the job done beautifully:

Selecting faces:

import bpy, bmesh

bpy.ops.object.mode_set(mode = 'EDIT')  # Go to edit mode to create bmesh
ob = bpy.context.object                 # Reference to selected object

bm = bmesh.from_edit_mesh(ob.data)      # Create bmesh object from object mesh

bm.select_mode = {'FACE'}               # Go to face selection mode
bm.faces[0].select_set(True)            # Select   face 0
bm.faces[1].select_set(False)           # Deselect face 1
ob.data.update()                        # Update these changes to the original mesh

Change a face’s material index via bmesh:


bpy.ops.object.mode_set(mode = 'EDIT')          # Go to edit mode to create bmesh
ob = bpy.context.object                         # Reference to selected object

bm = bmesh.from_edit_mesh(ob.data)              # Create bmesh object from object mesh

for face in bm.faces:        # Iterate over all of the object's faces
face.material_index = random.randint(0, no_of_materials - 1)  # Assing random material to face

ob.data.update()                            # Update the mesh from the bmesh data
bpy.ops.object.mode_set(mode = 'OBJECT')    # Return to object mode</pre>

Almost as simple as the direct (BI only) approach, but consistently works regardless of what rendering engine you’re using.

Writing a user interface (basics)

The Blender Cookie tut shows how to create a basic panel with a button and two fields which will be used for changing the randomization seed, and for filtering materials by name. The problem was that when you run the script, the execute button and the two fields are created in different panels which can be quite far apart from each other.

I wanted to have everything in one panel, but I liked the way that in the original tutorial, the panel and the operator (button) are separated into two classes. More tidy and orderly. Peeking about in some existing add-ons, also made it clear that this is probably the standard.

But since I knew nothing about creating interface elements, it took me quite a while to figure out how to integrate between functions and properties contained in two different classes, and how to draw everything in one panel. Here’s some of what I’ve learned (disclaimer: this is rather new to me, so I might be oversimplifying or inaccurate. Read the API docs to be sure).

Panels

Panels are classes that enable you to draw user interface elements such as buttons, fields, list boxes, etc. These interface elements must be associated with operators and properties, in order to actually do something except being pretty. I’ll explain what operators and properties are later.

class panel_name(bpy.types.Panel):
    bl_idname      = "myPanel"                # The panel's ID
    bl_label       = "Extremely cool panel"   # Label (title) - appears at the top of the panel
    bl_space_type  = 'VIEW_3D'                # In what window to place the panel
    bl_region_type = 'TOOLS'                  # In what part of the window to place it
    bl_context     = 'objectmode'             # The context in which the panel is visible

Each panel has a “draw” function, where you, well, draw the user interface elements:

def draw( self, context):  # Draw panel UI elements #
    layout = self.layout   # Reference to panel layout object

props = context.scene.my_object  # Create reference to the class that contains the properties we want to access

    layout.operator(       # Create a button associated with an operator
       "someKindOf.littleOperator")
    col = layout.column()  # Create a column (which will draw included elements above each other)
    col.label(text="This is a simple non-interactive text label")

    box = layout.box()                   # Draw a box
    col2 = box.column(align=True)        # Create a column inside the box
    col2.prop( props, "some_property"  ) # create a field for assigning values to a property

It’s important to note that the panel creates the appropriate interface element according the the type of property you created (and buttons for operators). If you’re using a string property, it creates a textbox. If it’s an integer, it creates a nice little slider with increment arrows on both sides, etc.

When you create a property interface element, you need to assign two mandatory parameters: the first one is the object that contains the property you want to access, and the second is the actual property’s name. For instance:

obj = bpy.context.object  # Reference to selected object in scene
col.prop( obj, "name" )   # Access the object's name via the property

Operators and Properties

An operator (also a class) can perform actions, and contain properties. Properties are containers of data which belong to objects (classes), whether existing objects (scenes, meshes, materials, etc) or new classes that you write yourself (including operators).

Operators are usually associated with panel buttons, and by definition they have several default functions (or “methods”), such as “execute” (which runs automatically when the operator is first called, for instance via a button click on the panel) and “check” (which runs automatically when one of the operator’s properties changes). Functions defined inside operators accept two standard parameters, “self” and “context”.

“self” is a reference to the whole operator object, including its properties and other methods.
“context” is the same as bpy.context, and it gives access to blender’s context scene, (selected) objects, etc.

class myLittleOperator( bpy.types.Operator ):
""" Assign a random material to the each of the selected objects """ # Tooltip
bl_idname      = "object.rand_mat_assigner"        # Unique identifier for buttons and menus to reference
bl_label       = "The coolest op on the block"     # display name
bl_description = "I do all kinds of cool stuff, oh yeah baby!"
bl_options     = {'REGISTER', 'UNDO' }             # Enable undo

myNum = bpy.props.IntProperty(name="myNum")        # an Integer property
myStr = bpy.props.StringProperty(                  # A string property
name="myStr",
default="")                                    # Default value is an empty string

def execute( self, context ):    # Execute function runs when the operator is called
# Do all kinds of stuff when the button or menu command is pressed
return {'FINISHED'}         # Standard return value that lets blender know all is well

def check( self, context ):  # This function runs when properties change
# Do all kinds of other stuff when properties change
return {'FINISHED'}

def myFunction( self, context ):
# And you've guessed it, this function does all kinds of stuff too

To be continued

Since this has become a rather sizable post, I’ll continue describing user interface coding in a later post. Meanwhile, I’ll be leaving you a bonus script, which is simpler and more straightforward than the face material assigner. The one below is directly based on Patrick Boelens’ BlenderCookie tutorial which I mentioned previously, with one difference, it assigns random materials from the complete list of materials in the scene to all selected objects.

Enjoy!

Source code for the selected objects random material assigner script (not the face material assigner which can be downloaded here):

</pre>
bl_info = {
"name"     : "Random Material Assigner for Objects",
"category" : "Object",
"author"   : "Tamir Lousky",
"version"  : "1.0"
}

import bpy
import random

class random_mat_panel(bpy.types.Panel):
bl_idname      = "randomObjMatPanel"
bl_label       = "Random Material Assigner for the selected objects"
bl_space_type  = 'VIEW_3D'
bl_region_type = 'TOOLS'

def draw( self, context) :
layout = self.layout
layout.operator("object.rand_mat_assigner")

class rand_mat_assigner( bpy.types.Operator ):
""" Assign a random material to the each of the selected objects """ # Tooltip
bl_idname      = "object.rand_mat_assigner"                          # Unique identifier for buttons and menus to reference
bl_label       = "Assign Random Materials to selected objects"       # display name
bl_description = "This script assigns random materials out of \
a pool of materials which share a name prefix, \
to a each of the selected objects"
bl_options     = {'REGISTER', 'UNDO' }                               # Enable undo

seed   = bpy.props.IntProperty(name="seed")      # randomization seed
prefix = bpy.props.StringProperty(name="prefix", default="") # a field in the panel to choose the prefix name

def execute( self, context):
self.randomize()
return {'FINISHED'}

def check( self, context ):  # This function runs when properties change
random.seed(self.seed)   # Set the randomization seed
self.randomize()
return {'FINISHED'}

def randomize( self ):
random.seed(self.seed)   # Set the randomization seed

filtered_materials = []
if self.prefix != "":                           # If the user entered a prefix
for material in bpy.data.materials:         # Iterate over all materials
if self.prefix in material.name:        # Look for materials with the prefix
filtered_materials.append(material) # And filter them in
else:
filtered_materials = bpy.data.materials     # If there's no prefix, use all materials

no_of_materials = len(filtered_materials)

for obj in bpy.context.selected_objects:        # Iterate over all selected objects
random_index    = random.randint(0, no_of_materials - 1)    # Choose a random (index) number
randomly_chosen_material = filtered_materials[random_index] # Reference the corresponding material
obj.active_material = randomly_chosen_material              # Then assign this material to the current object

return {'FINISHED'}

# This registers the class and panel when the script starts
bpy.utils.register_class(rand_mat_assigner)
bpy.utils.register_class(random_mat_panel)

Advertisements
16 Comments
  1. eisklotz permalink

    Thats great and so simple.

  2. Thank you boy ! For the next version, can you give attribution option; by loose part ? Thank you again, nice work !

  3. Flex permalink

    nearly perfect –
    If it has the possibility to “weight” some materials it will be perfect.
    or instead better filter options with “and” “or” operators,

    but hey – this add on is just great!

  4. Thank you kindly!

  5. @IK: Cheers dude 😀 Yeah some other people also asked for a “by loose part” option, and I’m certain to try getting that done in a future release.

    @Flex: Thanks! Combining materials by vertex group weight will be tricky, as that means creating new materials and combining them in various ways which change between rendering engines, so I’m not sure if I wanna go there just yet. But as for having more than one name field and logical operators to choose from, that sounds fairly straightforward and I might add that in a future release.

    @Marco and @eisklotz: thanks people 🙂

  6. Seems that your cool add-on doesn`t work in 2.74 (

    • Hi Dan,
      Sorry for the delay, just fixed the bug in the script – it now works in 2.74. Enjoy!

      • Renan permalink

        I couldn’t download the py script, mail me please… =)

      • Hi Renan,
        Please download the file from this link.

        After you open the link go to the file menu in your browser and choose “save as”, then save in your computer and continue installation as instructed from there.

  7. The last script with “random per object” i copy/pasted this and sort of got it to work. It only works 1 time in blender 2.76. After that i need to delete all materials on the meshes and redo it if i want a different seed.

    The downlaod link is broken so i cant check if i did all the intends correct in the script

    • Hi Rombout,
      Thanks for notifying me about this, I’ll have a look at it as soon as I can.
      Cheers

      • Hi Bio,

        I was wondering if you had the time yet to have a look at this script. Im in sort of a need to apply 2 different materials on leave meshes. If i could get the material per loose part to it would be great.

Trackbacks & Pingbacks

  1. First studio adventure « BioBlogical – BioLogic's 3D & Design blog
  2. Random material Assigner V2 – assign by vertex groups or loose parts! | BioBlogical - BioLogic's 3D & Design blog
  3. Nice Forest landscape or 17 billion faces in Blender | Eisklotz
  4. Material variation : AZVAvisuals

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: