1 # SPDX-FileCopyrightText: 2019-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
8 This is something I've been wanting to have for a while, a way to know
9 certain info about your scene. A way to "debug" it, especially when
10 working in production with other teams, this came in very handy.
12 Being mostly a lighting guy myself, I needed two main features to start with:
14 * List Cycles Material using X shader
15 Where X is any shader type you want. It will display (and print on console)
16 a list of all the materials containing the shader you specified above.
17 Good for finding out if there's any Meshlight (Emission) material hidden,
18 or if there are many glossy shaders making things noisy.
19 A current limitation is that it doesn't look inside node groups (yet,
20 working on it!). It works since 0.8.8!
22 Under the "Scene Debug" panel in Scene properties.
25 This is an UI List of Lights in the scene(s).
26 It allows you to quickly see how many lights you have, select them by
27 clicking on their name, see their type (icon), samples number (if using
28 Branched Path Tracing), size, and change their visibility.
32 # TODO: module cleanup! maybe break it up in a package
33 # dicts instead of if, elif, else all over the place.
34 # helper functions instead of everything on the execute method.
35 # str.format() + dicts instead of inline % op all over the place.
36 # remove/manage debug print calls.
37 # avoid duplicate code/patterns through helper functions.
41 from amaranth
import utils
42 from bpy
.types
import (
48 from bpy
.props
import (
57 # default string used in the List Users for Datablock section menus
58 USER_X_NAME_EMPTY
= "Data Block not selected/existing"
61 class AMTH_store_data():
62 # used by: AMTH_SCENE_OT_list_users_for_x operator
64 'OBJECT_DATA': [], # Store Objects with Material
65 'MATERIAL': [], # Materials (Node tree)
68 'TEXTURE': [], # Textures (Psys, Brushes)
69 'MODIFIER': [], # Modifiers
70 'MESH_DATA': [], # Vertex Colors
71 'OUTLINER_OB_CAMERA': [], # Background Images in Cameras
72 'OUTLINER_OB_EMPTY': [], # Empty type Image
73 'NODETREE': [], # Compositor
75 libraries
= [] # Libraries x type
77 # used by: AMTH_SCENE_OT_list_missing_material_slots operator
78 obj_mat_slots
= [] # Missing material slots
79 obj_mat_slots_lib
= [] # Libraries with missing material slots
81 # used by: AMTH_SCENE_OT_cycles_shader_list_nodes operator
82 mat_shaders
= [] # Materials that use a specific shader
84 # used by : AMTH_SCENE_OT_list_missing_node_links operator
85 count_groups
= 0 # Missing node groups count
86 count_images
= 0 # Missing node images
87 count_image_node_unlinked
= 0 # Unlinked Image nodes
90 def call_update_datablock_type(self
, context
):
92 # Note: this is pretty weak, but updates the operator enum selection
93 bpy
.ops
.scene
.amth_list_users_for_x_type(list_type_select
='0')
99 scene
= bpy
.types
.Scene
101 scene
.amaranth_lighterscorner_list_meshlights
= BoolProperty(
103 name
="List Meshlights",
104 description
="Include light emitting meshes on the list"
106 amth_datablock_types
= (
107 ("IMAGE_DATA", "Image", "Image Datablocks", 0),
108 ("MATERIAL", "Material", "Material Datablocks", 1),
109 ("GROUP_VCOL", "Vertex Colors", "Vertex Color Layers", 2),
111 scene
.amth_datablock_types
= EnumProperty(
112 items
=amth_datablock_types
,
114 description
="Datablock Type",
116 update
=call_update_datablock_type
,
117 options
={"SKIP_SAVE"}
119 if utils
.cycles_exists():
120 cycles_shader_node_types
= (
121 ("BSDF_DIFFUSE", "Diffuse BSDF", "", 0),
122 ("BSDF_GLOSSY", "Glossy BSDF", "", 1),
123 ("BSDF_TRANSPARENT", "Transparent BSDF", "", 2),
124 ("BSDF_REFRACTION", "Refraction BSDF", "", 3),
125 ("BSDF_GLASS", "Glass BSDF", "", 4),
126 ("BSDF_TRANSLUCENT", "Translucent BSDF", "", 5),
127 ("BSDF_ANISOTROPIC", "Anisotropic BSDF", "", 6),
128 ("BSDF_VELVET", "Velvet BSDF", "", 7),
129 ("BSDF_TOON", "Toon BSDF", "", 8),
130 ("SUBSURFACE_SCATTERING", "Subsurface Scattering", "", 9),
131 ("EMISSION", "Emission", "", 10),
132 ("BSDF_HAIR", "Hair BSDF", "", 11),
133 ("BACKGROUND", "Background", "", 12),
134 ("AMBIENT_OCCLUSION", "Ambient Occlusion", "", 13),
135 ("HOLDOUT", "Holdout", "", 14),
136 ("VOLUME_ABSORPTION", "Volume Absorption", "", 15),
137 ("VOLUME_SCATTER", "Volume Scatter", "", 16),
138 ("MIX_SHADER", "Mix Shader", "", 17),
139 ("ADD_SHADER", "Add Shader", "", 18),
140 ('BSDF_PRINCIPLED', 'Principled BSDF', "", 19),
142 scene
.amaranth_cycles_node_types
= EnumProperty(
143 items
=cycles_shader_node_types
,
150 "amaranth_cycles_node_types",
151 "amaranth_lighterscorner_list_meshlights",
153 wm
= bpy
.context
.window_manager
159 def print_with_count_list(text
="", send_list
=[]):
161 print("\n* {}\n".format(text
))
163 print("List is empty, no items to display")
166 for i
, entry
in enumerate(send_list
):
167 print('{:02d}. {}'.format(i
+ 1, send_list
[i
]))
171 def print_grammar(line
="", single
="", multi
="", cond
=[]):
172 phrase
= single
if len(cond
) == 1 else multi
173 print("\n* {} {}:\n".format(line
, phrase
))
176 def reset_global_storage(what
="NONE"):
181 for user
in AMTH_store_data
.users
:
182 AMTH_store_data
.users
[user
] = []
183 AMTH_store_data
.libraries
= []
185 elif what
== "MAT_SLOTS":
186 AMTH_store_data
.obj_mat_slots
[:] = []
187 AMTH_store_data
.obj_mat_slots_lib
[:] = []
189 elif what
== "NODE_LINK":
190 AMTH_store_data
.obj_mat_slots
[:] = []
191 AMTH_store_data
.count_groups
= 0
192 AMTH_store_data
.count_images
= 0
193 AMTH_store_data
.count_image_node_unlinked
= 0
195 elif what
== "SHADER":
196 AMTH_store_data
.mat_shaders
[:] = []
199 class AMTH_SCENE_OT_cycles_shader_list_nodes(Operator
):
200 """List Cycles materials containing a specific shader"""
201 bl_idname
= "scene.cycles_list_nodes"
202 bl_label
= "List Materials"
205 def poll(cls
, context
):
206 return utils
.cycles_exists() and utils
.cycles_active(context
)
208 def execute(self
, context
):
209 node_type
= context
.scene
.amaranth_cycles_node_types
211 shaders_roughness
= ("BSDF_GLOSSY", "BSDF_DIFFUSE", "BSDF_GLASS")
213 reset_global_storage("SHADER")
215 print("\n=== Cycles Shader Type: {} === \n".format(node_type
))
217 for ma
in bpy
.data
.materials
:
221 nodes
= ma
.node_tree
.nodes
222 print_unconnected
= (
223 "Note: \nOutput from \"{}\" node in material \"{}\" "
224 "not connected\n".format(node_type
, ma
.name
)
228 if no
.type == node_type
:
229 for ou
in no
.outputs
:
232 if no
.type in shaders_roughness
:
233 roughness
= "R: {:.4f}".format(
234 no
.inputs
["Roughness"].default_value
240 print(print_unconnected
)
242 if ma
.name
not in AMTH_store_data
.mat_shaders
:
243 AMTH_store_data
.mat_shaders
.append(
245 ("[L] " if ma
.library
else "",
248 "[F]" if ma
.use_fake_user
else "",
250 roughness
if roughness
else "",
251 " * Output not connected" if not connected
else "")
253 elif no
.type == "GROUP":
255 for nog
in no
.node_tree
.nodes
:
256 if nog
.type == node_type
:
257 for ou
in nog
.outputs
:
260 if nog
.type in shaders_roughness
:
261 roughness
= "R: {:.4f}".format(
262 nog
.inputs
["Roughness"].default_value
268 print(print_unconnected
)
270 if ma
.name
not in AMTH_store_data
.mat_shaders
:
271 AMTH_store_data
.mat_shaders
.append(
272 '%s%s%s [%s] %s%s%s' %
273 ("[L] " if ma
.library
else "",
274 "Node Group: %s%s -> " %
275 ("[L] " if no
.node_tree
.library
else "",
279 "[F]" if ma
.use_fake_user
else "",
281 roughness
if roughness
else "",
282 " * Output not connected" if not connected
else "")
284 AMTH_store_data
.mat_shaders
= sorted(list(set(AMTH_store_data
.mat_shaders
)))
286 message
= "No materials with nodes type {} found".format(node_type
)
287 if len(AMTH_store_data
.mat_shaders
) > 0:
288 message
= "A total of {} {} using {} found".format(
289 len(AMTH_store_data
.mat_shaders
),
290 "material" if len(AMTH_store_data
.mat_shaders
) == 1 else "materials",
292 print_with_count_list(send_list
=AMTH_store_data
.mat_shaders
)
294 self
.report({'INFO'}, message
)
295 AMTH_store_data
.mat_shaders
= sorted(list(set(AMTH_store_data
.mat_shaders
)))
300 class AMTH_SCENE_OT_amaranth_object_select(Operator
):
302 bl_idname
= "scene.amaranth_object_select"
303 bl_label
= "Select Object"
305 object_name
: StringProperty()
307 def execute(self
, context
):
308 if not (self
.object_name
and self
.object_name
in bpy
.data
.objects
):
309 self
.report({'WARNING'},
310 "Object with the given name could not be found. Operation Cancelled")
313 obj
= bpy
.data
.objects
[self
.object_name
]
315 bpy
.ops
.object.select_all(action
="DESELECT")
317 context
.view_layer
.objects
.active
= obj
322 class AMTH_SCENE_OT_list_missing_node_links(Operator
):
323 """Print a list of missing node links"""
324 bl_idname
= "scene.list_missing_node_links"
325 bl_label
= "List Missing Node Links"
327 def execute(self
, context
):
330 image_nodes_unlinked
= []
333 reset_global_storage(what
="NODE_LINK")
335 for ma
in bpy
.data
.materials
:
339 for no
in ma
.node_tree
.nodes
:
340 if no
.type == "GROUP":
342 AMTH_store_data
.count_groups
+= 1
346 for ob
in bpy
.data
.objects
:
347 if ob
.material_slots
and ma
.name
in ob
.material_slots
:
348 users_ngroup
.append("%s%s%s" % (
349 "[L] " if ob
.library
else "",
350 "[F] " if ob
.use_fake_user
else "",
353 missing_groups
.append(
354 "MA: %s%s%s [%s]%s%s%s\n" %
355 ("[L] " if ma
.library
else "",
356 "[F] " if ma
.use_fake_user
else "",
359 " *** No users *** " if ma
.users
== 0 else "",
361 ma
.library
.filepath
if ma
.library
else "",
363 ", ".join(users_ngroup
) if users_ngroup
else "")
366 libraries
.append(ma
.library
.filepath
)
368 if no
.type == "TEX_IMAGE":
370 outputs_empty
= not no
.outputs
["Color"].is_linked
and \
371 not no
.outputs
["Alpha"].is_linked
374 image_path_exists
= os
.path
.exists(
377 library
=no
.image
.library
)
380 if outputs_empty
or not no
.image
or not image_path_exists
:
384 for ob
in bpy
.data
.objects
:
385 if ob
.material_slots
and ma
.name
in ob
.material_slots
:
386 users_images
.append("%s%s%s" % (
387 "[L] " if ob
.library
else "",
388 "[F] " if ob
.use_fake_user
else "",
392 AMTH_store_data
.count_image_node_unlinked
+= 1
394 image_nodes_unlinked
.append(
395 "%s%s%s%s%s [%s]%s%s%s%s%s\n" %
399 "[L] " if ma
.library
else "",
400 "[F] " if ma
.use_fake_user
else "",
403 " *** No users *** " if ma
.users
== 0 else "",
405 ma
.library
.filepath
if ma
.library
else "",
407 no
.image
.name
if no
.image
else "",
409 no
.image
.filepath
if no
.image
and no
.image
.filepath
else "",
411 ', '.join(users_images
) if users_images
else ""))
413 if not no
.image
or not image_path_exists
:
414 AMTH_store_data
.count_images
+= 1
416 missing_images
.append(
417 "MA: %s%s%s [%s]%s%s%s%s%s\n" %
418 ("[L] " if ma
.library
else "",
419 "[F] " if ma
.use_fake_user
else "",
422 " *** No users *** " if ma
.users
== 0 else "",
424 ma
.library
.filepath
if ma
.library
else "",
426 no
.image
.name
if no
.image
else "",
428 no
.image
.filepath
if no
.image
and no
.image
.filepath
else "",
430 ', '.join(users_images
) if users_images
else ""))
433 libraries
.append(ma
.library
.filepath
)
435 # Remove duplicates and sort
436 missing_groups
= sorted(list(set(missing_groups
)))
437 missing_images
= sorted(list(set(missing_images
)))
438 image_nodes_unlinked
= sorted(list(set(image_nodes_unlinked
)))
439 libraries
= sorted(list(set(libraries
)))
442 "\n\n== %s missing image %s, %s missing node %s and %s image %s unlinked ==" %
443 ("No" if AMTH_store_data
.count_images
== 0 else str(
444 AMTH_store_data
.count_images
),
445 "node" if AMTH_store_data
.count_images
== 1 else "nodes",
446 "no" if AMTH_store_data
.count_groups
== 0 else str(
447 AMTH_store_data
.count_groups
),
448 "group" if AMTH_store_data
.count_groups
== 1 else "groups",
449 "no" if AMTH_store_data
.count_image_node_unlinked
== 0 else str(
450 AMTH_store_data
.count_image_node_unlinked
),
451 "node" if AMTH_store_data
.count_groups
== 1 else "nodes")
453 # List Missing Node Groups
455 print_with_count_list("Missing Node Group Links", missing_groups
)
457 # List Missing Image Nodes
459 print_with_count_list("Missing Image Nodes Link", missing_images
)
461 # List Image Nodes with its outputs unlinked
462 if image_nodes_unlinked
:
463 print_with_count_list("Image Nodes Unlinked", image_nodes_unlinked
)
465 if missing_groups
or missing_images
or image_nodes_unlinked
:
467 print_grammar("That's bad, run check", "this library", "these libraries", libraries
)
468 print_with_count_list(send_list
=libraries
)
470 self
.report({"INFO"}, "Yay! No missing node links")
472 if missing_groups
and missing_images
:
475 "%d missing image %s and %d missing node %s found" %
476 (AMTH_store_data
.count_images
,
477 "node" if AMTH_store_data
.count_images
== 1 else "nodes",
478 AMTH_store_data
.count_groups
,
479 "group" if AMTH_store_data
.count_groups
== 1 else "groups")
485 class AMTH_SCENE_OT_list_missing_material_slots(Operator
):
486 """List objects with empty material slots"""
487 bl_idname
= "scene.list_missing_material_slots"
488 bl_label
= "List Empty Material Slots"
490 def execute(self
, context
):
491 reset_global_storage("MAT_SLOTS")
493 for ob
in bpy
.data
.objects
:
494 for ma
in ob
.material_slots
:
496 AMTH_store_data
.obj_mat_slots
.append('{}{}'.format(
497 '[L] ' if ob
.library
else '', ob
.name
))
499 AMTH_store_data
.obj_mat_slots_lib
.append(ob
.library
.filepath
)
501 AMTH_store_data
.obj_mat_slots
= sorted(list(set(AMTH_store_data
.obj_mat_slots
)))
502 AMTH_store_data
.obj_mat_slots_lib
= sorted(list(set(AMTH_store_data
.obj_mat_slots_lib
)))
504 if len(AMTH_store_data
.obj_mat_slots
) == 0:
505 self
.report({"INFO"},
506 "No objects with empty material slots found")
510 "\n* A total of {} {} with empty material slots was found \n".format(
511 len(AMTH_store_data
.obj_mat_slots
),
512 "object" if len(AMTH_store_data
.obj_mat_slots
) == 1 else "objects")
514 print_with_count_list(send_list
=AMTH_store_data
.obj_mat_slots
)
516 if AMTH_store_data
.obj_mat_slots_lib
:
517 print_grammar("Check", "this library", "these libraries",
518 AMTH_store_data
.obj_mat_slots_lib
520 print_with_count_list(send_list
=AMTH_store_data
.obj_mat_slots_lib
)
525 class AMTH_SCENE_OT_list_users_for_x_type(Operator
):
526 bl_idname
= "scene.amth_list_users_for_x_type"
528 bl_description
= "Select Datablock Name"
533 data_block
= bpy
.context
.scene
.amth_datablock_types
535 if data_block
== 'IMAGE_DATA':
536 for im
in bpy
.data
.images
:
537 if im
.name
not in {'Render Result', 'Viewer Node'}:
540 elif data_block
== 'MATERIAL':
541 where
= bpy
.data
.materials
543 elif data_block
== 'GROUP_VCOL':
544 for ob
in bpy
.data
.objects
:
545 if ob
.type == 'MESH':
546 for v
in ob
.data
.vertex_colors
:
547 if v
and v
not in where
:
549 where
= list(set(where
))
553 def avail(self
, context
):
554 datablock_type
= bpy
.context
.scene
.amth_datablock_types
555 where
= AMTH_SCENE_OT_list_users_for_x_type
.fill_where()
556 items
= [(str(i
), x
.name
, x
.name
, datablock_type
, i
) for i
, x
in enumerate(where
)]
557 items
= sorted(list(set(items
)))
559 items
= [('0', USER_X_NAME_EMPTY
, USER_X_NAME_EMPTY
, "INFO", 0)]
562 list_type_select
: EnumProperty(
565 options
={"SKIP_SAVE"}
569 def poll(cls
, context
):
570 return bpy
.context
.scene
.amth_datablock_types
572 def execute(self
, context
):
573 where
= self
.fill_where()
574 bpy
.context
.scene
.amth_list_users_for_x_name
= \
575 where
[int(self
.list_type_select
)].name
if where
else USER_X_NAME_EMPTY
580 class AMTH_SCENE_OT_list_users_for_x(Operator
):
581 """List users for a particular datablock"""
582 bl_idname
= "scene.amth_list_users_for_x"
583 bl_label
= "List Users for Datablock"
585 name
: StringProperty()
587 def execute(self
, context
):
589 x
= self
.name
if self
.name
else context
.scene
.amth_list_users_for_x_name
591 if USER_X_NAME_EMPTY
in x
:
592 self
.report({'INFO'},
593 "Please select a DataBlock name first. Operation Cancelled")
596 dtype
= context
.scene
.amth_datablock_types
598 reset_global_storage("XTYPE")
601 if dtype
== 'IMAGE_DATA':
603 for ma
in d
.materials
:
605 if utils
.cycles_exists():
606 if ma
and ma
.node_tree
and ma
.node_tree
.nodes
:
609 for nd
in ma
.node_tree
.nodes
:
610 if nd
and nd
.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}:
613 if nd
and nd
.type == 'GROUP':
614 if nd
.node_tree
and nd
.node_tree
.nodes
:
615 for ng
in nd
.node_tree
.nodes
:
616 if ng
.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}:
620 if no
.image
and no
.image
.name
== x
:
624 if ma
.name
in ob
.material_slots
:
625 objects
.append(ob
.name
)
632 name
= '"{0}" {1}{2}'.format(
634 'in object: {0}'.format(objects
) if objects
else ' (unassigned)',
635 '' if links
else ' (unconnected)')
637 if name
not in AMTH_store_data
.users
['MATERIAL']:
638 AMTH_store_data
.users
['MATERIAL'].append(name
)
643 if utils
.cycles_exists():
644 if la
and la
.node_tree
and la
.node_tree
.nodes
:
645 for no
in la
.node_tree
.nodes
:
647 no
.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \
648 no
.image
and no
.image
.name
== x
:
649 if la
.name
not in AMTH_store_data
.users
['LIGHT']:
650 AMTH_store_data
.users
['LIGHT'].append(la
.name
)
655 if utils
.cycles_exists():
656 if wo
and wo
.node_tree
and wo
.node_tree
.nodes
:
657 for no
in wo
.node_tree
.nodes
:
659 no
.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \
660 no
.image
and no
.image
.name
== x
:
661 if wo
.name
not in AMTH_store_data
.users
['WORLD']:
662 AMTH_store_data
.users
['WORLD'].append(wo
.name
)
665 for te
in d
.textures
:
666 if te
and te
.type == 'IMAGE' and te
.image
:
670 name
not in AMTH_store_data
.users
['TEXTURE']:
671 AMTH_store_data
.users
['TEXTURE'].append(te
.name
)
673 # Check Modifiers in Objects
675 for mo
in ob
.modifiers
:
676 if mo
.type in {'UV_PROJECT'}:
679 if mo
and image
and image
.name
== x
:
680 name
= '"{0}" modifier in {1}'.format(mo
.name
, ob
.name
)
681 if name
not in AMTH_store_data
.users
['MODIFIER']:
682 AMTH_store_data
.users
['MODIFIER'].append(name
)
684 # Check Background Images in Cameras
686 if ob
and ob
.type == 'CAMERA' and ob
.data
.background_images
:
687 for bg
in ob
.data
.background_images
:
690 if bg
and image
and image
.name
== x
:
691 name
= 'Used as background for Camera "{0}"'\
693 if name
not in AMTH_store_data
.users
['OUTLINER_OB_CAMERA']:
694 AMTH_store_data
.users
['OUTLINER_OB_CAMERA'].append(name
)
696 # Check Empties type Image
698 if ob
and ob
.type == 'EMPTY' and ob
.image_user
:
699 if ob
.image_user
.id_data
.data
:
700 image
= ob
.image_user
.id_data
.data
702 if image
and image
.name
== x
:
703 name
= 'Used in Empty "{0}"'\
705 if name
not in AMTH_store_data
.users
['OUTLINER_OB_EMPTY']:
706 AMTH_store_data
.users
['OUTLINER_OB_EMPTY'].append(name
)
708 # Check the Compositor
710 if sce
.node_tree
and sce
.node_tree
.nodes
:
712 for nd
in sce
.node_tree
.nodes
:
713 if nd
.type == 'IMAGE':
715 elif nd
.type == 'GROUP':
716 if nd
.node_tree
and nd
.node_tree
.nodes
:
717 for ng
in nd
.node_tree
.nodes
:
718 if ng
.type == 'IMAGE':
722 if no
.image
and no
.image
.name
== x
:
729 name
= 'Node {0} in Compositor (Scene "{1}"){2}'.format(
732 '' if links
else ' (unconnected)')
734 if name
not in AMTH_store_data
.users
['NODETREE']:
735 AMTH_store_data
.users
['NODETREE'].append(name
)
737 if dtype
== 'MATERIAL':
738 # Check Materials - Note: build an object_check list as only strings are stored
739 object_check
= [d
.objects
[names
] for names
in AMTH_store_data
.users
['OBJECT_DATA'] if
742 for ma
in ob
.material_slots
:
744 if ma
not in object_check
:
745 AMTH_store_data
.users
['OBJECT_DATA'].append(ob
.name
)
748 AMTH_store_data
.libraries
.append(ob
.library
.filepath
)
750 elif dtype
== 'GROUP_VCOL':
751 # Check VCOL in Meshes
752 for ob
in bpy
.data
.objects
:
753 if ob
.type == 'MESH':
754 for v
in ob
.data
.vertex_colors
:
756 name
= '{0}'.format(ob
.name
)
758 if name
not in AMTH_store_data
.users
['MESH_DATA']:
759 AMTH_store_data
.users
['MESH_DATA'].append(name
)
760 # Check VCOL in Materials
761 for ma
in d
.materials
:
763 if utils
.cycles_exists():
764 if ma
and ma
.node_tree
and ma
.node_tree
.nodes
:
765 for no
in ma
.node_tree
.nodes
:
766 if no
and no
.type in {'ATTRIBUTE'}:
767 if no
.attribute_name
== x
:
771 if ma
.name
in ob
.material_slots
:
772 objects
.append(ob
.name
)
775 name
= '{0} in object: {1}'.format(ma
.name
, objects
)
777 name
= '{0} (unassigned)'.format(ma
.name
)
779 if name
not in AMTH_store_data
.users
['MATERIAL']:
780 AMTH_store_data
.users
['MATERIAL'].append(name
)
782 AMTH_store_data
.libraries
= sorted(list(set(AMTH_store_data
.libraries
)))
786 for t
in AMTH_store_data
.users
:
787 if AMTH_store_data
.users
[t
]:
789 print('\n== {0} {1} use {2} "{3}" ==\n'.format(
790 len(AMTH_store_data
.users
[t
]),
794 for p
in AMTH_store_data
.users
[t
]:
795 print(' {0}'.format(p
))
797 if AMTH_store_data
.libraries
:
798 print_grammar("Check", "this library", "these libraries",
799 AMTH_store_data
.libraries
801 print_with_count_list(send_list
=AMTH_store_data
.libraries
)
804 self
.report({'INFO'}, "No users for {}".format(x
))
809 class AMTH_SCENE_OT_list_users_debug_clear(Operator
):
810 """Clear the list below"""
811 bl_idname
= "scene.amth_list_users_debug_clear"
812 bl_label
= "Clear Debug Panel lists"
814 what
: StringProperty(
820 def execute(self
, context
):
821 reset_global_storage(self
.what
)
826 class AMTH_SCENE_OT_blender_instance_open(Operator
):
827 """Open in a new Blender instance"""
828 bl_idname
= "scene.blender_instance_open"
829 bl_label
= "Open Blender Instance"
831 filepath
: StringProperty()
833 def execute(self
, context
):
835 filepath
= os
.path
.normpath(bpy
.path
.abspath(self
.filepath
))
839 subprocess
.Popen([bpy
.app
.binary_path
, filepath
])
841 print("Error opening a new Blender instance")
843 traceback
.print_exc()
848 class AMTH_SCENE_OT_Collection_List_Refresh(Operator
):
849 bl_idname
= "scene.amaranth_lighters_corner_refresh"
851 bl_description
= ("Generate/Refresh the Lists\n"
852 "Use to generate/refresh the list or after changes to Data")
853 bl_options
= {"REGISTER", "INTERNAL"}
855 what
: StringProperty(default
="NONE")
857 def execute(self
, context
):
858 message
= "No changes applied"
860 if self
.what
== "LIGHTS":
861 fill_ligters_corner_props(context
, refresh
=True)
863 found_lights
= len(context
.window_manager
.amth_lighters_state
.keys())
864 message
= "No Lights in the Data" if found_lights
== 0 else \
865 "Generated list for {} found light(s)".format(found_lights
)
867 elif self
.what
== "IMAGES":
868 fill_missing_images_props(context
, refresh
=True)
870 found_images
= len(context
.window_manager
.amth_missing_images_state
.keys())
871 message
= "Great! No missing Images" if found_images
== 0 else \
872 "Missing {} image(s) in the Data".format(found_images
)
874 self
.report({'INFO'}, message
)
879 class AMTH_SCENE_PT_scene_debug(Panel
):
881 bl_label
= "Scene Debug"
882 bl_space_type
= "PROPERTIES"
883 bl_region_type
= "WINDOW"
885 bl_options
= {"DEFAULT_CLOSED"}
887 def draw_header(self
, context
):
889 layout
.label(text
="", icon
="RADIOBUT_ON")
891 def draw_label(self
, layout
, body_text
, single
, multi
, lists
, ico
="BLANK1"):
893 text
="{} {} {}".format(
894 str(len(lists
)), body_text
,
895 single
if len(lists
) == 1 else multi
),
899 def draw_miss_link(self
, layout
, text1
, single
, multi
, text2
, count
, ico
="BLANK1"):
901 text
="{} {} {} {}".format(
903 single
if count
== 1 else multi
, text2
),
907 def draw(self
, context
):
909 scene
= context
.scene
911 has_images
= len(bpy
.data
.images
)
912 engine
= scene
.render
.engine
914 # List Missing Images
916 split
= box
.split(factor
=0.8, align
=True)
920 subrow
= split
.row(align
=True)
921 subrow
.alignment
= "RIGHT"
922 subrow
.operator(AMTH_SCENE_OT_Collection_List_Refresh
.bl_idname
,
923 text
="", icon
="FILE_REFRESH").what
= "IMAGES"
924 image_state
= context
.window_manager
.amth_missing_images_state
927 text
="{} Image Blocks present in the Data".format(has_images
),
930 if len(image_state
.keys()) > 0:
932 'AMTH_UL_MissingImages_UI',
933 'amth_collection_index_prop',
934 context
.window_manager
,
935 'amth_missing_images_state',
936 context
.window_manager
.amth_collection_index_prop
,
941 row
.label(text
="No images loaded yet", icon
="RIGHTARROW_THIN")
943 # List Cycles Materials by Shader
944 if utils
.cycles_exists() and engine
== "CYCLES":
947 col
= split
.column(align
=True)
948 col
.prop(scene
, "amaranth_cycles_node_types",
951 row
= split
.row(align
=True)
952 row
.operator(AMTH_SCENE_OT_cycles_shader_list_nodes
.bl_idname
,
954 text
="List Materials Using Shader")
955 if len(AMTH_store_data
.mat_shaders
) != 0:
957 AMTH_SCENE_OT_list_users_debug_clear
.bl_idname
,
958 icon
="X", text
="").what
= "SHADER"
961 if len(AMTH_store_data
.mat_shaders
) != 0:
962 col
= box
.column(align
=True)
963 self
.draw_label(col
, "found", "material", "materials",
964 AMTH_store_data
.mat_shaders
, "INFO"
966 for i
, mat
in enumerate(AMTH_store_data
.mat_shaders
):
968 text
="{}".format(AMTH_store_data
.mat_shaders
[i
]), icon
="MATERIAL"
971 # List Missing Node Trees
973 row
= box
.row(align
=True)
975 col
= split
.column(align
=True)
977 split
= col
.split(align
=True)
978 split
.label(text
="Node Links")
979 row
= split
.row(align
=True)
980 row
.operator(AMTH_SCENE_OT_list_missing_node_links
.bl_idname
,
983 if AMTH_store_data
.count_groups
!= 0 or \
984 AMTH_store_data
.count_images
!= 0 or \
985 AMTH_store_data
.count_image_node_unlinked
!= 0:
988 AMTH_SCENE_OT_list_users_debug_clear
.bl_idname
,
989 icon
="X", text
="").what
= "NODE_LINK"
990 col
.label(text
="Warning! Check Console", icon
="ERROR")
992 if AMTH_store_data
.count_groups
!= 0:
993 self
.draw_miss_link(col
, "node", "group", "groups", "missing link",
994 AMTH_store_data
.count_groups
, "NODE_TREE"
996 if AMTH_store_data
.count_images
!= 0:
997 self
.draw_miss_link(col
, "image", "node", "nodes", "missing link",
998 AMTH_store_data
.count_images
, "IMAGE_DATA"
1000 if AMTH_store_data
.count_image_node_unlinked
!= 0:
1001 self
.draw_miss_link(col
, "image", "node", "nodes", "with no output connected",
1002 AMTH_store_data
.count_image_node_unlinked
, "NODE"
1005 # List Empty Materials Slots
1008 col
= split
.column(align
=True)
1009 col
.label(text
="Material Slots")
1011 row
= split
.row(align
=True)
1012 row
.operator(AMTH_SCENE_OT_list_missing_material_slots
.bl_idname
,
1014 text
="List Empty Materials Slots"
1016 if len(AMTH_store_data
.obj_mat_slots
) != 0:
1018 AMTH_SCENE_OT_list_users_debug_clear
.bl_idname
,
1019 icon
="X", text
="").what
= "MAT_SLOTS"
1022 col
= box
.column(align
=True)
1023 self
.draw_label(col
, "found empty material slot", "object", "objects",
1024 AMTH_store_data
.obj_mat_slots
, "INFO"
1026 for entry
, obs
in enumerate(AMTH_store_data
.obj_mat_slots
):
1028 row
.alignment
= "LEFT"
1030 text
="{}".format(AMTH_store_data
.obj_mat_slots
[entry
]),
1033 if AMTH_store_data
.obj_mat_slots_lib
:
1035 col
.label("Check {}:".format(
1037 len(AMTH_store_data
.obj_mat_slots_lib
) == 1 else
1040 for ilib
, libs
in enumerate(AMTH_store_data
.obj_mat_slots_lib
):
1041 row
= col
.row(align
=True)
1042 row
.alignment
= "LEFT"
1044 AMTH_SCENE_OT_blender_instance_open
.bl_idname
,
1045 text
=AMTH_store_data
.obj_mat_slots_lib
[ilib
],
1047 emboss
=False).filepath
= AMTH_store_data
.obj_mat_slots_lib
[ilib
]
1050 row
= box
.row(align
=True)
1051 row
.label(text
="List Users for Datablock")
1053 col
= box
.column(align
=True)
1055 row
= split
.row(align
=True)
1057 scene
, "amth_datablock_types",
1058 icon
=scene
.amth_datablock_types
,
1061 row
.operator_menu_enum(
1062 "scene.amth_list_users_for_x_type",
1064 text
=scene
.amth_list_users_for_x_name
1067 row
= split
.row(align
=True)
1068 row
.enabled
= True if USER_X_NAME_EMPTY
not in scene
.amth_list_users_for_x_name
else False
1070 AMTH_SCENE_OT_list_users_for_x
.bl_idname
,
1071 icon
="COLLAPSEMENU").name
= scene
.amth_list_users_for_x_name
1073 if any(val
for val
in AMTH_store_data
.users
.values()):
1074 col
= box
.column(align
=True)
1076 for t
in AMTH_store_data
.users
:
1078 for ma
in AMTH_store_data
.users
[t
]:
1079 subrow
= col
.row(align
=True)
1080 subrow
.alignment
= "LEFT"
1082 if t
== 'OBJECT_DATA':
1083 text_lib
= " [L] " if \
1084 ma
in bpy
.data
.objects
and bpy
.data
.objects
[ma
].library
else ""
1086 AMTH_SCENE_OT_amaranth_object_select
.bl_idname
,
1087 text
="{} {}{}".format(text_lib
, ma
,
1088 "" if ma
in context
.scene
.objects
else " [Not in Scene]"),
1090 emboss
=False).object_name
= ma
1092 subrow
.label(text
=ma
, icon
=t
)
1094 AMTH_SCENE_OT_list_users_debug_clear
.bl_idname
,
1095 icon
="X", text
="").what
= "XTYPE"
1097 if AMTH_store_data
.libraries
:
1101 col
.label("Check {}:".format(
1103 len(AMTH_store_data
.libraries
) == 1 else
1106 for libs
in AMTH_store_data
.libraries
:
1108 row
= col
.row(align
=True)
1109 row
.alignment
= "LEFT"
1111 AMTH_SCENE_OT_blender_instance_open
.bl_idname
,
1112 text
=AMTH_store_data
.libraries
[count_lib
- 1],
1114 emboss
=False).filepath
= AMTH_store_data
.libraries
[count_lib
- 1]
1117 class AMTH_PT_LightersCorner(Panel
):
1118 """The Lighters Panel"""
1119 bl_label
= "Lighter's Corner"
1120 bl_idname
= "AMTH_SCENE_PT_lighters_corner"
1121 bl_space_type
= 'PROPERTIES'
1122 bl_region_type
= 'WINDOW'
1123 bl_context
= "scene"
1124 bl_options
= {"DEFAULT_CLOSED"}
1126 def draw_header(self
, context
):
1127 layout
= self
.layout
1128 layout
.label(text
="", icon
="LIGHT_SUN")
1130 def draw(self
, context
):
1131 layout
= self
.layout
1132 state_props
= len(context
.window_manager
.amth_lighters_state
)
1133 engine
= context
.scene
.render
.engine
1135 row
= box
.row(align
=True)
1137 if utils
.cycles_exists():
1138 row
.prop(context
.scene
, "amaranth_lighterscorner_list_meshlights")
1140 subrow
= row
.row(align
=True)
1141 subrow
.alignment
= "RIGHT"
1142 subrow
.operator(AMTH_SCENE_OT_Collection_List_Refresh
.bl_idname
,
1143 text
="", icon
="FILE_REFRESH").what
= "LIGHTS"
1147 message
= "Please Refresh" if len(bpy
.data
.lights
) > 0 else "No Lights in Data"
1148 row
.label(text
=message
, icon
="INFO")
1150 row
= box
.row(align
=True)
1151 split
= row
.split(factor
=0.5, align
=True)
1152 col
= split
.column(align
=True)
1154 col
.label(text
="Name/Library link")
1156 if engine
in ["CYCLES"]:
1158 splita
= split
.split(factor
=splits
, align
=True)
1160 if utils
.cycles_exists() and engine
== "CYCLES":
1161 col
= splita
.column(align
=True)
1162 col
.label(text
="Size")
1164 cols
= row
.row(align
=True)
1165 cols
.alignment
= "RIGHT"
1166 cols
.label(text
="{}Render Visibility/Selection".format(
1167 "Rays /" if utils
.cycles_exists() else "")
1170 'AMTH_UL_LightersCorner_UI',
1171 'amth_collection_index_prop',
1172 context
.window_manager
,
1173 'amth_lighters_state',
1174 context
.window_manager
.amth_collection_index_prop
,
1180 class AMTH_UL_MissingImages_UI(UIList
):
1182 def draw_item(self
, context
, layout
, data
, item
, icon
, active_data
, active_propname
):
1183 text_lib
= item
.text_lib
1184 has_filepath
= item
.has_filepath
1185 is_library
= item
.is_library
1187 split
= layout
.split(factor
=0.4)
1188 row
= split
.row(align
=True)
1189 row
.alignment
= "LEFT"
1190 row
.label(text
=text_lib
, icon
="IMAGE_DATA")
1191 image
= bpy
.data
.images
.get(item
.name
, None)
1193 subrow
= split
.row(align
=True)
1194 splitp
= subrow
.split(factor
=0.8, align
=True).row(align
=True)
1195 splitp
.alignment
= "LEFT"
1196 row_lib
= subrow
.row(align
=True)
1197 row_lib
.alignment
= "RIGHT"
1199 splitp
.label(text
="Image is not available", icon
="ERROR")
1201 splitp
.label(text
=has_filepath
, icon
="LIBRARY_DATA_DIRECT")
1204 AMTH_SCENE_OT_blender_instance_open
.bl_idname
,
1206 emboss
=False, icon
="LINK_BLEND").filepath
= is_library
1209 class AMTH_UL_LightersCorner_UI(UIList
):
1211 def draw_item(self
, context
, layout
, data
, item
, icon
, active_data
, active_propname
):
1212 icon_type
= item
.icon_type
1213 engine
= context
.scene
.render
.engine
1214 text_lib
= item
.text_lib
1215 is_library
= item
.is_library
1217 split
= layout
.split(factor
=0.35)
1218 row
= split
.row(align
=True)
1219 row
.alignment
= "LEFT"
1220 row
.label(text
=text_lib
, icon
=icon_type
)
1221 ob
= bpy
.data
.objects
.get(item
.name
, None)
1223 row
.label(text
="Object is not available", icon
="ERROR")
1227 AMTH_SCENE_OT_blender_instance_open
.bl_idname
,
1229 emboss
=False, icon
="LINK_BLEND").filepath
= is_library
1231 rows
= split
.row(align
=True)
1233 splitlamp
= rows
.split(factor
=splits
, align
=True)
1234 splitlampc
= splitlamp
.row(align
=True)
1235 splitlampd
= rows
.row(align
=True)
1236 splitlampd
.alignment
= "RIGHT"
1238 if utils
.cycles_exists() and engine
== "CYCLES":
1239 if "LIGHT" in icon_type
:
1240 clamp
= ob
.data
.cycles
1242 if lamp
.type in ["POINT", "SUN", "SPOT"]:
1243 splitlampc
.label(text
="{:.2f}".format(lamp
.shadow_soft_size
))
1244 elif lamp
.type == "HEMI":
1245 splitlampc
.label(text
="N/A")
1246 elif lamp
.type == "AREA" and lamp
.shape
== "RECTANGLE":
1248 text
="{:.2f} x {:.2f}".format(lamp
.size
, lamp
.size_y
)
1251 splitlampc
.label(text
="{:.2f}".format(lamp
.size
))
1252 if utils
.cycles_exists():
1253 splitlampd
.prop(ob
, "visible_camera", text
="")
1254 splitlampd
.prop(ob
, "visible_diffuse", text
="")
1255 splitlampd
.prop(ob
, "visible_glossy", text
="")
1256 splitlampd
.prop(ob
, "visible_shadow", text
="")
1257 splitlampd
.separator()
1258 splitlampd
.prop(ob
, "hide_viewport", text
="", emboss
=False)
1259 splitlampd
.prop(ob
, "hide_render", text
="", emboss
=False)
1260 splitlampd
.operator(
1261 AMTH_SCENE_OT_amaranth_object_select
.bl_idname
,
1263 emboss
=False, icon
="RESTRICT_SELECT_OFF").object_name
= item
.name
1266 def fill_missing_images_props(context
, refresh
=False):
1267 image_state
= context
.window_manager
.amth_missing_images_state
1269 for key
in image_state
.keys():
1270 index
= image_state
.find(key
)
1272 image_state
.remove(index
)
1274 for im
in bpy
.data
.images
:
1275 if im
.type not in ("UV_TEST", "RENDER_RESULT", "COMPOSITING"):
1276 if not im
.packed_file
and \
1277 not os
.path
.exists(bpy
.path
.abspath(im
.filepath
, library
=im
.library
)):
1278 text_l
= "{}{} [{}]{}".format("[L] " if im
.library
else "", im
.name
,
1279 im
.users
, " [F]" if im
.use_fake_user
else "")
1280 prop
= image_state
.add()
1282 prop
.text_lib
= text_l
1283 prop
.has_filepath
= im
.filepath
if im
.filepath
else "No Filepath"
1284 prop
.is_library
= im
.library
.filepath
if im
.library
else ""
1287 def fill_ligters_corner_props(context
, refresh
=False):
1288 light_state
= context
.window_manager
.amth_lighters_state
1289 list_meshlights
= context
.scene
.amaranth_lighterscorner_list_meshlights
1291 for key
in light_state
.keys():
1292 index
= light_state
.find(key
)
1294 light_state
.remove(index
)
1296 for ob
in bpy
.data
.objects
:
1297 if ob
.name
not in light_state
.keys() or refresh
:
1298 is_light
= ob
.type == "LIGHT"
1299 is_emission
= True if utils
.cycles_is_emission(
1300 context
, ob
) and list_meshlights
else False
1302 if is_light
or is_emission
:
1303 icons
= "LIGHT_%s" % ob
.data
.type if is_light
else "MESH_GRID"
1304 text_l
= "{} {}{}".format(" [L] " if ob
.library
else "", ob
.name
,
1305 "" if ob
.name
in context
.scene
.objects
else " [Not in Scene]")
1306 prop
= light_state
.add()
1308 prop
.icon_type
= icons
1309 prop
.text_lib
= text_l
1310 prop
.is_library
= ob
.library
.filepath
if ob
.library
else ""
1313 class AMTH_LightersCornerStateProp(PropertyGroup
):
1314 icon_type
: StringProperty()
1315 text_lib
: StringProperty()
1316 is_library
: StringProperty()
1319 class AMTH_MissingImagesStateProp(PropertyGroup
):
1320 text_lib
: StringProperty()
1321 has_filepath
: StringProperty()
1322 is_library
: StringProperty()
1325 class AMTH_LightersCollectionIndexProp(PropertyGroup
):
1329 index_image
: IntProperty(
1335 AMTH_SCENE_PT_scene_debug
,
1336 AMTH_SCENE_OT_list_users_debug_clear
,
1337 AMTH_SCENE_OT_blender_instance_open
,
1338 AMTH_SCENE_OT_amaranth_object_select
,
1339 AMTH_SCENE_OT_list_missing_node_links
,
1340 AMTH_SCENE_OT_list_missing_material_slots
,
1341 AMTH_SCENE_OT_cycles_shader_list_nodes
,
1342 AMTH_SCENE_OT_list_users_for_x
,
1343 AMTH_SCENE_OT_list_users_for_x_type
,
1344 AMTH_SCENE_OT_Collection_List_Refresh
,
1345 AMTH_LightersCornerStateProp
,
1346 AMTH_LightersCollectionIndexProp
,
1347 AMTH_MissingImagesStateProp
,
1348 AMTH_PT_LightersCorner
,
1349 AMTH_UL_LightersCorner_UI
,
1350 AMTH_UL_MissingImages_UI
,
1358 bpy
.utils
.register_class(cls
)
1360 bpy
.types
.Scene
.amth_list_users_for_x_name
= StringProperty(
1361 default
="Select DataBlock Name",
1363 description
=USER_X_NAME_EMPTY
,
1364 options
={"SKIP_SAVE"}
1366 bpy
.types
.WindowManager
.amth_collection_index_prop
= PointerProperty(
1367 type=AMTH_LightersCollectionIndexProp
1369 bpy
.types
.WindowManager
.amth_lighters_state
= CollectionProperty(
1370 type=AMTH_LightersCornerStateProp
1372 bpy
.types
.WindowManager
.amth_missing_images_state
= CollectionProperty(
1373 type=AMTH_MissingImagesStateProp
1381 bpy
.utils
.unregister_class(cls
)
1383 del bpy
.types
.Scene
.amth_list_users_for_x_name
1384 del bpy
.types
.WindowManager
.amth_collection_index_prop
1385 del bpy
.types
.WindowManager
.amth_lighters_state
1386 del bpy
.types
.WindowManager
.amth_missing_images_state