1
$\begingroup$

Let us just say you have a 100x100 cubes 2d grid and each cube will be assigned its own individual material. How can you do this? My code below worked initially when I created all the cubes as individual objects but it took a very very long time to create that many cubes. I was then told I can speed up this process by just copying over the mesh from the first cube to help create the next cube. This speeded up the process but when I tried to run my coloring functions all the cubes now shares the same color material. I assume this is because the new cubes inherited it's material from the creation of the first cube. I think its has something to do with the last else statement below but I am not sure.

How can I keep the speed gain from copying cube mesh to create cubes without inheriting the initial material color from the first cube as well? I asked this question in the Blender official chat a whiles ago and was told to use MaterialSlot(bpy_struct) and I have no idea how it works.

Full relevant code

class Create(Animation):

    def __init__(self, collection_name, H, W, y_axis):

        self.collection_name = collection_name
        self.H = H
        self.W = W
        self.master_collection = bpy.context.scene.collection
        self.myCol = bpy.data.collections.new(self.collection_name) # Automatically create the correct layer
        bpy.context.scene.collection.children.link(self.myCol)


    def create_block(self, x_axis, y_axis, z_axis):
        """
        1) Takes the Height and Width and create a 3D wall in Blender
        2) Center it in the middle, assign it to the given collection
        3) Translate corresponding pixel values to the correct mat color

        Args:
            H -- The height(int)
            W -- The width(int)
            collection -- The collection this block belongs too(str)
        """  

        if self.collection_name == "input_image": # try mycol flag

            self.x = x_axis
            self.y = y_axis
            self.z = z_axis

        elif self.collection_name == "filters":

            self.x = x_axis + 69
            self.y = y_axis
            self.z = z_axis + 69    

        pixel_idx = 0 # corresponding pixel value index from the tensor

        bpy.ops.mesh.primitive_cube_add(size=2, view_align=False, enter_editmode=False, location=(self.x, self.y, self.z)) #create initial cube

        cube = bpy.context.scene.objects[0] # remove the duplicate cube from initbpy.ops


        for height in range(self.H): # z-axis

            for width in range(self.W): # # x-axis



                new_cube = bpy.data.objects.new(self.collection_name + "cube", cube.data) #cube.data = mesh struct

                new_cube.location = (self.x, self.y, self.z)

                self.myCol.objects.link(new_cube)

                #self.master_collection.objects.unlink(new_cube)

                self.x += 3

            if self.collection_name == "filters":
                self.x = x_axis + 69
            elif self.collection_name == "input_image":
                self.x = x_axis

            self.z += 3


        bpy.ops.object.delete()   

    def fill_pixel_value(self, tensor_img):

        self.image_flatten = tensor_img

        self.obs = bpy.context.scene.objects


        for i, o in enumerate(self.obs):

            name = o.name #grab the cube's name

            mat = bpy.data.materials.get("Material")

            if mat is None:

                mat = bpy.data.materials.new( name + "_Material" )

                #Change the mat color to the corresponding image_flatten tensor values
                # This used to work but now the first cube I made has the dominate material.

                mat.diffuse_color = ( self.image_flatten[i], self.image_flatten[i], self.image_flatten[i], 1 ) #RGBA

                # Assign it to object
            if o.data.materials:

                # assign to 1st material slot

                o.data.materials[0] = mat

            else:
                # no slots

                o.data.materials.append(mat)
$\endgroup$
1
$\begingroup$

One mesh, object linked materials.

You are trying to make same linked mesh have different materials on a different object. Using object linked materials can use only the one mesh, and N x N materials and objects.

import bpy
context = bpy.context
scene = context.scene
count = 32

bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0))
ob = context.object
dx, dy = ob.dimensions.xy
bpy.ops.object.material_slot_add()
ob.material_slots[0].link = 'OBJECT'

mat = bpy.data.materials.new("Mat")
ob.material_slots[0].material = mat
for i in range(1, count * count):
    o = ob.copy()
    o.material_slots[0].material = mat.copy()
    # set some mat props
    o.location = (dx * (i % count), dy * (i // count), 0)
    scene.collection.objects.link(o)

One material could be used here instead by mapping texture coordinates via the object.


Method 2: 1 material, N x N objects and meshes.

Grid, UVMap image texture, edge split, solidify, separate by loose parts

Using the same methods as outlined to you here https://blender.stackexchange.com/a/131765/15543

Material is an image texture mapped to the UV. This way we only have one material. The UV can be mapped to 1 pixel squares.

enter image description here Result of script. Image is 128x128, grid was split 32 x 32

enter image description here So each block has a top with 4x4 pixels.

import bpy
context = bpy.context

bpy.ops.mesh.primitive_grid_add(
        size=10,
        x_subdivisions=33,
        y_subdivisions=33,
        calc_uvs=True)

ob = context.object
me = ob.data
me.materials.append(bpy.data.materials["Material"])
sfy = ob.modifiers.new(name="Solidify", type='SOLIDIFY')
sfy.thickness = 1 / 3
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.edge_split()
bpy.ops.mesh.separate(type='LOOSE')
bpy.ops.object.mode_set()

bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')

Creating 128 x 128 objects and as many materials too is going to be slooooooooooooooooow.

Can be optimized for speed somewhat. Not sure it's worth it for zero feedback.


Method 3: 1 object, 1 mesh, N x N materials.

Single cube, applied array modifiers, materials assigned to each part

Similarly to this answer https://blender.stackexchange.com/a/61254/15543 can create an array mesh very quickly.

After the array modifiers are applied the faces are still ordered such that each 6 belongs to a cube. A new material is added for each and assigned. The row and column of the parts can be obtained using integer div and modulus and hence can be mapped to pixel colour.

For example sake I've only made a 32 x 32 array. Note the offset is at 1.5, change to 1.0 to have touching cubes.

To make each a single object, separate by loose parts as above. This is the rate-determining-step. As one mesh, individual parts can be animated via hooks, armature, shapekeys.

import bpy
from bpy import context
# add a cube
bpy.ops.mesh.primitive_cube_add()
ob = context.active_object
me = ob.data
# add two array modifiers
count = 32
arrays = {"x": (1.5, 0, 0),
          "y": (0, 1.5, 0)}

for axis, displace in arrays.items():          
    mod = ob.modifiers.new(axis, 'ARRAY')
    mod.count = count
    mod.relative_offset_displace = displace
    bpy.ops.object.modifier_apply(modifier=mod.name)

def chunks(l, n):
    for i in range(0, len(l), n):
        yield l[i:i+n]

material_index = 0
for cube in chunks(me.polygons, 6):
    mat = bpy.data.materials.new("Mat%d" % material_index)

    mat.diffuse_color[0] = material_index / 100
    me.materials.append(mat)
    for f in cube:

        f.material_index = material_index
    material_index += 1

Alternatively could use a UV map here also, for 1 object, 1 mesh (with NxN parts) and 1 material.

$\endgroup$
  • $\begingroup$ Thank you for all these wonderful pieces of information. I did manage to figure out why its not working and it's exactly like you said. I was trying to apply a material to an object's mesh which was the same cube mesh, instead, I should of created to material under the object itself. Thanks again. $\endgroup$ – Inkplay_ Mar 22 at 18:49
  • $\begingroup$ Follow up question, I am currently trying to integrate the first method you showed me. I want to loop through all my OBJECTS after I create them. I first want to create an empty material slot but when I tried to use bpy.ops.object.material_slot_add() I get RuntimeError: Operator bpy.ops.object.material_slot_add.poll() failed, context is incorrect? I tried to google a lower level version of it but no dice. $\endgroup$ – Inkplay_ Mar 23 at 2:16
  • $\begingroup$ i) Set object to active first, ii) override context, or iii) add a 'DATA' slot by appending a material to mesh, then change the slot link to 'OBJECT' and remove material from mesh. Code above copies object with one object linked material slot so you only have to do it once. $\endgroup$ – batFINGER Mar 23 at 3:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.