Merge branch 'blender-v4.0-release'
[blender-addons.git] / materials_utils / functions.py
blob2bc290453108e6a8e79dbe28d1e13b547cf3cd9e
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
6 from math import radians, degrees
8 # -----------------------------------------------------------------------------
9 # utility functions
11 def mu_assign_material_slots(object, material_list):
12 """Given an object and a list of material names removes all material slots from the object
13 adds new ones for each material in the material list, adds the materials to the slots as well."""
15 scene = bpy.context.scene
16 active_object = bpy.context.active_object
17 bpy.context.view_layer.objects.active = object
19 for s in object.material_slots:
20 bpy.ops.object.material_slot_remove()
22 # re-add them and assign material
23 i = 0
24 for mat in material_list:
25 material = bpy.data.materials[mat]
26 object.data.materials.append(material)
27 i += 1
29 # restore active object:
30 bpy.context.view_layer.objects.active = active_object
32 def mu_assign_to_data(object, material, index, edit_mode, all = True):
33 """Assign the material to the object data (polygons/splines)"""
35 if object.type == 'MESH':
36 # now assign the material to the mesh
37 mesh = object.data
38 if all:
39 for poly in mesh.polygons:
40 poly.material_index = index
41 else:
42 for poly in mesh.polygons:
43 if poly.select:
44 poly.material_index = index
46 mesh.update()
48 elif object.type in {'CURVE', 'SURFACE', 'TEXT'}:
49 bpy.ops.object.mode_set(mode = 'EDIT') # This only works in Edit mode
51 # If operator was run in Object mode
52 if not edit_mode:
53 # Select everything in Edit mode
54 bpy.ops.curve.select_all(action = 'SELECT')
56 bpy.ops.object.material_slot_assign() # Assign material of the current slot to selection
58 if not edit_mode:
59 bpy.ops.object.mode_set(mode = 'OBJECT')
61 def mu_new_material_name(material):
62 for mat in bpy.data.materials:
63 name = mat.name
65 if (name == material):
66 try:
67 base, suffix = name.rsplit('.', 1)
69 # trigger the exception
70 num = int(suffix, 10)
71 material = base + "." + '%03d' % (num + 1)
72 except ValueError:
73 material = material + ".001"
75 return material
78 def mu_clear_materials(object):
79 #obj.data.materials.clear()
81 for mat in object.material_slots:
82 bpy.ops.object.material_slot_remove()
85 def mu_assign_material(self, material_name = "Default", override_type = 'APPEND_MATERIAL', link_override = 'KEEP'):
86 """Assign the defined material to selected polygons/objects"""
88 # get active object so we can restore it later
89 active_object = bpy.context.active_object
91 edit_mode = False
92 all_polygons = True
93 if (not active_object is None) and active_object.mode == 'EDIT':
94 edit_mode = True
95 all_polygons = False
96 bpy.ops.object.mode_set()
98 # check if material exists, if it doesn't then create it
99 found = False
100 for material in bpy.data.materials:
101 if material.name == material_name:
102 target = material
103 found = True
104 break
106 if not found:
107 target = bpy.data.materials.new(mu_new_material_name(material_name))
108 target.use_nodes = True # When do we not want nodes today?
111 index = 0
112 objects = bpy.context.selected_editable_objects
114 for obj in objects:
115 # Apparently selected_editable_objects includes objects as cameras etc
116 if not obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
117 continue
119 # set the active object to our object
120 scene = bpy.context.scene
121 bpy.context.view_layer.objects.active = obj
123 if link_override == 'KEEP':
124 if len(obj.material_slots) > 0:
125 link = obj.material_slots[0].link
126 else:
127 link = 'DATA'
128 else:
129 link = link_override
131 # If we should override all current material slots
132 if override_type == 'OVERRIDE_ALL' or obj.type == 'META':
134 # If there's more than one slot, Clear out all the material slots
135 if len(obj.material_slots) > 1:
136 mu_clear_materials(obj)
138 # If there's no slots left/never was one, add a slot
139 if len(obj.material_slots) == 0:
140 bpy.ops.object.material_slot_add()
142 # Assign the material to that slot
143 obj.material_slots[0].link = link
144 obj.material_slots[0].material = target
146 if obj.type == 'META':
147 self.report({'INFO'}, "Meta balls only support one material, all other materials overridden!")
149 # If we should override each material slot
150 elif override_type == 'OVERRIDE_SLOTS':
151 i = 0
152 # go through each slot
153 for material in obj.material_slots:
154 # assign the target material to current slot
155 if not link_override == 'KEEP':
156 obj.material_slots[i].link = link
157 obj.material_slots[i].material = target
158 i += 1
160 elif override_type == 'OVERRIDE_CURRENT':
161 active_slot = obj.active_material_index
163 if len(obj.material_slots) == 0:
164 self.report({'INFO'}, 'No material slots found! A material slot was added!')
165 bpy.ops.object.material_slot_add()
167 obj.material_slots[active_slot].material = target
169 # if we should keep the material slots and just append the selected material (if not already assigned)
170 elif override_type == 'APPEND_MATERIAL':
171 found = False
172 i = 0
173 material_slots = obj.material_slots
175 if (obj.data.users > 1) and (len(material_slots) >= 1 and material_slots[0].link == 'OBJECT'):
176 self.report({'WARNING'}, 'Append material is not recommended for linked duplicates! ' +
177 'Unwanted results might happen!')
179 # check material slots for material_name materia
180 for material in material_slots:
181 if material.name == material_name:
182 found = True
183 index = i
185 # make slot active
186 obj.active_material_index = i
187 break
188 i += 1
190 if not found:
191 # In Edit mode, or if there's not a slot, append the assigned material
192 # If we're overriding, there's currently no materials at all, so after this there will be 1
193 # If not, this adds another slot with the assigned material
195 index = len(obj.material_slots)
196 bpy.ops.object.material_slot_add()
197 obj.material_slots[index].link = link
198 obj.material_slots[index].material = target
199 obj.active_material_index = index
201 mu_assign_to_data(obj, target, index, edit_mode, all_polygons)
203 # We shouldn't risk unsetting the active object
204 if not active_object is None:
205 # restore the active object
206 bpy.context.view_layer.objects.active = active_object
208 if edit_mode:
209 bpy.ops.object.mode_set(mode='EDIT')
211 return {'FINISHED'}
214 def mu_select_by_material_name(self, find_material_name, extend_selection = False, internal = False):
215 """Searches through all objects, or the polygons/curves of the current object
216 to find and select objects/data with the desired material"""
218 # in object mode selects all objects with material find_material_name
219 # in edit mode selects all polygons with material find_material_name
221 find_material = bpy.data.materials.get(find_material_name)
223 if find_material is None:
224 self.report({'INFO'}, "The material " + find_material_name + " doesn't exists!")
225 return {'CANCELLED'} if not internal else -1
227 # check for edit_mode
228 edit_mode = False
229 found_material = False
231 scene = bpy.context.scene
233 # set selection mode to polygons
234 scene.tool_settings.mesh_select_mode = False, False, True
236 active_object = bpy.context.active_object
238 if (not active_object is None) and (active_object.mode == 'EDIT'):
239 edit_mode = True
241 if not edit_mode:
242 objects = bpy.context.visible_objects
244 for obj in objects:
245 if obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
246 mat_slots = obj.material_slots
247 for material in mat_slots:
248 if material.material == find_material:
249 obj.select_set(state = True)
251 found_material = True
253 # the active object may not have the material!
254 # set it to one that does!
255 bpy.context.view_layer.objects.active = obj
256 break
257 else:
258 if not extend_selection:
259 obj.select_set(state=False)
261 #deselect non-meshes
262 elif not extend_selection:
263 obj.select_set(state=False)
265 if not found_material:
266 if not internal:
267 self.report({'INFO'}, "No objects found with the material " +
268 find_material_name + "!")
269 return {'FINISHED'} if not internal else 0
271 else:
272 # it's edit_mode, so select the polygons
274 if active_object.type == 'MESH':
275 # if not extending the selection, deselect all first
276 # (Without this, edges/faces were still selected
277 # while the faces were deselected)
278 if not extend_selection:
279 bpy.ops.mesh.select_all(action = 'DESELECT')
281 objects = bpy.context.selected_editable_objects
283 for obj in objects:
284 bpy.context.view_layer.objects.active = obj
286 if obj.type == 'MESH':
287 bpy.ops.object.mode_set()
289 mat_slots = obj.material_slots
291 # same material can be on multiple slots
292 slot_indeces = []
293 i = 0
294 for material in mat_slots:
295 if material.material == find_material:
296 slot_indeces.append(i)
297 i += 1
299 mesh = obj.data
301 for poly in mesh.polygons:
302 if poly.material_index in slot_indeces:
303 poly.select = True
304 found_material = True
305 elif not extend_selection:
306 poly.select = False
308 mesh.update()
310 bpy.ops.object.mode_set(mode = 'EDIT')
313 elif obj.type in {'CURVE', 'SURFACE'}:
314 # For Curve objects, there can only be one material per spline
315 # and thus each spline is linked to one material slot.
316 # So to not have to care for different data structures
317 # for different curve types, we use the material slots
318 # and the built in selection methods
319 # (Technically, this should work for meshes as well)
321 mat_slots = obj.material_slots
323 i = 0
324 for material in mat_slots:
325 bpy.context.active_object.active_material_index = i
327 if material.material == find_material:
328 bpy.ops.object.material_slot_select()
329 found_material = True
330 elif not extend_selection:
331 bpy.ops.object.material_slot_deselect()
333 i += 1
335 elif not internal:
336 # Some object types are not supported
337 # mostly because don't really support selecting by material (like Font/Text objects)
338 # or that they don't support multiple materials/are just "weird" (i.e. Meta balls)
339 self.report({'WARNING'}, "The type '" +
340 obj.type +
341 "' isn't supported in Edit mode by Material Utilities!")
342 #return {'CANCELLED'}
344 bpy.context.view_layer.objects.active = active_object
346 if (not found_material) and (not internal):
347 self.report({'INFO'}, "Material " + find_material_name + " isn't assigned to anything!")
349 return {'FINISHED'} if not internal else 1
352 def mu_copy_material_to_others(self):
353 """Copy the material to of the current object to the other seleceted all_objects"""
354 # Currently uses the built-in method
355 # This could be extended to work in edit mode as well
357 #active_object = context.active_object
359 bpy.ops.object.material_slot_copy()
361 return {'FINISHED'}
364 def mu_cleanmatslots(self, affect):
365 """Clean the material slots of the seleceted objects"""
367 # check for edit mode
368 edit_mode = False
369 active_object = bpy.context.active_object
370 if active_object.mode == 'EDIT':
371 edit_mode = True
372 bpy.ops.object.mode_set()
374 objects = []
376 if affect == 'ACTIVE':
377 objects = [active_object]
378 elif affect == 'SELECTED':
379 objects = bpy.context.selected_editable_objects
380 elif affect == 'SCENE':
381 objects = bpy.context.scene.objects
382 else: # affect == 'ALL'
383 objects = bpy.data.objects
385 for obj in objects:
386 used_mat_index = [] # we'll store used materials indices here
387 assigned_materials = []
388 material_list = []
389 material_names = []
391 materials = obj.material_slots.keys()
393 if obj.type == 'MESH':
394 # check the polygons on the mesh to build a list of used materials
395 mesh = obj.data
397 for poly in mesh.polygons:
398 # get the material index for this face...
399 material_index = poly.material_index
401 if material_index >= len(materials):
402 poly.select = True
403 self.report({'ERROR'},
404 "A poly with an invalid material was found, this should not happen! Canceling!")
405 return {'CANCELLED'}
407 # indices will be lost: Store face mat use by name
408 current_mat = materials[material_index]
409 assigned_materials.append(current_mat)
411 # check if index is already listed as used or not
412 found = False
413 for mat in used_mat_index:
414 if mat == material_index:
415 found = True
417 if not found:
418 # add this index to the list
419 used_mat_index.append(material_index)
421 # re-assign the used materials to the mesh and leave out the unused
422 for u in used_mat_index:
423 material_list.append(materials[u])
424 # we'll need a list of names to get the face indices...
425 material_names.append(materials[u])
427 mu_assign_material_slots(obj, material_list)
429 # restore face indices:
430 i = 0
431 for poly in mesh.polygons:
432 material_index = material_names.index(assigned_materials[i])
433 poly.material_index = material_index
434 i += 1
436 elif obj.type in {'CURVE', 'SURFACE'}:
438 splines = obj.data.splines
440 for spline in splines:
441 # Get the material index of this spline
442 material_index = spline.material_index
444 # indices will be last: Store material use by name
445 current_mat = materials[material_index]
446 assigned_materials.append(current_mat)
448 # check if indek is already listed as used or not
449 found = False
450 for mat in used_mat_index:
451 if mat == material_index:
452 found = True
454 if not found:
455 # add this index to the list
456 used_mat_index.append(material_index)
458 # re-assigned the used materials to the curve and leave out the unused
459 for u in used_mat_index:
460 material_list.append(materials[u])
461 # we'll need a list of names to get the face indices
462 material_names.append(materials[u])
464 mu_assign_material_slots(obj, material_list)
466 # restore spline indices
467 i = 0
468 for spline in splines:
469 material_index = material_names.index(assigned_materials[i])
470 spline.material_index = material_index
471 i += 1
473 else:
474 # Some object types are not supported
475 self.report({'WARNING'},
476 "The type '" + obj.type + "' isn't currently supported " +
477 "for Material slots cleaning by Material Utilities!")
479 if edit_mode:
480 bpy.ops.object.mode_set(mode='EDIT')
482 return {'FINISHED'}
484 def mu_remove_material(self, for_active_object = False):
485 """Remove the active material slot from selected object(s)"""
487 if for_active_object:
488 bpy.ops.object.material_slot_remove()
489 else:
490 last_active = bpy.context.active_object
491 objects = bpy.context.selected_editable_objects
493 for obj in objects:
494 bpy.context.view_layer.objects.active = obj
495 bpy.ops.object.material_slot_remove()
497 bpy.context.view_layer.objects.active = last_active
499 return {'FINISHED'}
501 def mu_remove_all_materials(self, for_active_object = False):
502 """Remove all material slots from selected object(s)"""
504 if for_active_object:
505 obj = bpy.context.active_object
507 # Clear out the material slots
508 obj.data.materials.clear()
510 else:
511 last_active = bpy.context.active_object
512 objects = bpy.context.selected_editable_objects
514 for obj in objects:
515 obj.data.materials.clear()
517 bpy.context.view_layer.objects.active = last_active
519 return {'FINISHED'}
522 def mu_replace_material(material_a, material_b, all_objects=False, update_selection=False):
523 """Replace one material with another material"""
525 # material_a is the name of original material
526 # material_b is the name of the material to replace it with
527 # 'all' will replace throughout the blend file
529 mat_org = bpy.data.materials.get(material_a)
530 mat_rep = bpy.data.materials.get(material_b)
532 if mat_org != mat_rep and None not in (mat_org, mat_rep):
533 # Store active object
534 scn = bpy.context.scene
536 if all_objects:
537 objs = bpy.data.objects
538 else:
539 objs = bpy.context.selected_editable_objects
541 for obj in objs:
542 if obj.type == 'MESH':
543 match = False
545 for mat in obj.material_slots:
546 if mat.material == mat_org:
547 mat.material = mat_rep
549 # Indicate which objects were affected
550 if update_selection:
551 obj.select_set(state = True)
552 match = True
554 if update_selection and not match:
555 obj.select_set(state = False)
557 return {'FINISHED'}
560 def mu_set_fake_user(self, fake_user, materials):
561 """Set the fake user flag for the objects material"""
563 if materials == 'ALL':
564 mats = (mat for mat in bpy.data.materials if mat.library is None)
565 elif materials == 'UNUSED':
566 mats = (mat for mat in bpy.data.materials if mat.library is None and mat.users == 0)
567 else:
568 mats = []
569 if materials == 'ACTIVE':
570 objs = [bpy.context.active_object]
571 elif materials == 'SELECTED':
572 objs = bpy.context.selected_objects
573 elif materials == 'SCENE':
574 objs = bpy.context.scene.objects
575 else: # materials == 'USED'
576 objs = bpy.data.objects
577 # Maybe check for users > 0 instead?
579 mats = (mat for ob in objs
580 if hasattr(ob.data, "materials")
581 for mat in ob.data.materials
582 if mat.library is None)
584 if fake_user == 'TOGGLE':
585 done_mats = []
586 for mat in mats:
587 if not mat.name in done_mats:
588 mat.use_fake_user = not mat.use_fake_user
589 done_mats.append(mat.name)
590 else:
591 fake_user_val = fake_user == 'ON'
592 for mat in mats:
593 mat.use_fake_user = fake_user_val
595 for area in bpy.context.screen.areas:
596 if area.type in ('PROPERTIES', 'NODE_EDITOR'):
597 area.tag_redraw()
599 return {'FINISHED'}
602 def mu_change_material_link(self, link, affect, override_data_material = False):
603 """Change what the materials are linked to (Object or Data), while keeping materials assigned"""
605 objects = []
607 if affect == "ACTIVE":
608 objects = [bpy.context.active_object]
609 elif affect == "SELECTED":
610 objects = bpy.context.selected_objects
611 elif affect == "SCENE":
612 objects = bpy.context.scene.objects
613 elif affect == "ALL":
614 objects = bpy.data.objects
616 for object in objects:
617 index = 0
618 for slot in object.material_slots:
619 present_material = slot.material
621 if link == 'TOGGLE':
622 slot.link = ('DATA' if slot.link == 'OBJECT' else 'OBJECT')
623 else:
624 slot.link = link
626 if slot.link == 'OBJECT':
627 override_data_material = True
628 elif slot.material is None:
629 override_data_material = True
630 elif not override_data_material:
631 self.report({'INFO'},
632 'The object Data for object ' + object.name_full + ' already had a material assigned ' +
633 'to slot #' + str(index) + ' (' + slot.material.name + '), it was not overridden!')
635 if override_data_material:
636 slot.material = present_material
638 index = index + 1
640 return {'FINISHED'}
642 def mu_join_objects(self, materials):
643 """Join objects together based on their material"""
645 for material in materials:
646 mu_select_by_material_name(self, material, False, True)
648 bpy.ops.object.join()
650 return {'FINISHED'}
652 def mu_set_auto_smooth(self, angle, affect, set_smooth_shading):
653 """Set Auto smooth values for selected objects"""
654 # Inspired by colkai
656 objects = []
657 objects_affected = 0
659 if affect == "ACTIVE":
660 objects = [bpy.context.active_object]
661 elif affect == "SELECTED":
662 objects = bpy.context.selected_editable_objects
663 elif affect == "SCENE":
664 objects = bpy.context.scene.objects
665 elif affect == "ALL":
666 objects = bpy.data.objects
668 if len(objects) == 0:
669 self.report({'WARNING'}, 'No objects available to set Auto Smooth on')
670 return {'CANCELLED'}
672 for object in objects:
673 if object.type == "MESH":
674 if set_smooth_shading:
675 for poly in object.data.polygons:
676 poly.use_smooth = True
678 #bpy.ops.object.shade_smooth()
680 object.data.set_sharp_from_angle(angle=angle) # 35 degrees as radians
682 objects_affected += 1
684 self.report({'INFO'}, 'Auto smooth angle set to %.0f° on %d of %d objects' %
685 (degrees(angle), objects_affected, len(objects)))
687 return {'FINISHED'}