1 # This program is free software; you can redistribute it and/or
2 # modify it under the terms of the GNU General Public License
3 # as published by the Free Software Foundation; either version 2
4 # of the License, or (at your option) any later version.
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU General Public License for more details.
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software Foundation,
13 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 This is something I've been wanting to have for a while, a way to know
18 certain info about your scene. A way to "debug" it, especially when
19 working in production with other teams, this came in very handy.
21 Being mostly a lighting guy myself, I needed two main features to start with:
23 * List Cycles Material using X shader
24 Where X is any shader type you want. It will display (and print on console)
25 a list of all the materials containing the shader you specified above.
26 Good for finding out if there's any Meshlight (Emission) material hidden,
27 or if there are many glossy shaders making things noisy.
28 A current limitation is that it doesn't look inside node groups (yet,
29 working on it!). It works since 0.8.8!
31 Under the "Scene Debug" panel in Scene properties.
34 This is an UI List of Lights in the scene(s).
35 It allows you to quickly see how many lights you have, select them by
36 clicking on their name, see their type (icon), samples number (if using
37 Branched Path Tracing), size, and change their visibility.
41 # TODO: module cleanup! maybe break it up in a package
42 # dicts instead of if, elif, else all over the place.
43 # helper functions instead of everything on the execute method.
44 # str.format() + dicts instead of inline % op all over the place.
45 # remove/manage debug print calls.
46 # avoid duplicate code/patterns through helper functions.
50 from amaranth
import utils
51 from bpy
.types
import (
57 from bpy
.props
import (
66 # default string used in the List Users for Datablock section menus
67 USER_X_NAME_EMPTY
= "Data Block not selected/existing"
70 class AMTH_store_data():
71 # used by: AMTH_SCENE_OT_list_users_for_x operator
73 'OBJECT_DATA': [], # Store Objects with Material
74 'MATERIAL': [], # Materials (Node tree)
77 'TEXTURE': [], # Textures (Psys, Brushes)
78 'MODIFIER': [], # Modifiers
79 'MESH_DATA': [], # Vertex Colors
80 'VIEW3D': [], # Background Images
81 'NODETREE': [], # Compositor
83 libraries
= [] # Libraries x type
85 # used by: AMTH_SCENE_OT_list_missing_material_slots operator
86 obj_mat_slots
= [] # Missing material slots
87 obj_mat_slots_lib
= [] # Libraries with missing material slots
89 # used by: AMTH_SCENE_OT_cycles_shader_list_nodes operator
90 mat_shaders
= [] # Materials that use a specific shader
92 # used by : AMTH_SCENE_OT_list_missing_node_links operator
93 count_groups
= 0 # Missing node groups count
94 count_images
= 0 # Missing node images
95 count_image_node_unlinked
= 0 # Unlinked Image nodes
98 def call_update_datablock_type(self
, context
):
100 # Note: this is pretty weak, but updates the operator enum selection
101 bpy
.ops
.scene
.amth_list_users_for_x_type(list_type_select
='0')
107 scene
= bpy
.types
.Scene
109 scene
.amaranth_lighterscorner_list_meshlights
= BoolProperty(
111 name
="List Meshlights",
112 description
="Include light emitting meshes on the list"
114 amth_datablock_types
= (
115 ("IMAGE_DATA", "Image", "Image Datablocks", 0),
116 ("MATERIAL", "Material", "Material Datablocks", 1),
117 ("GROUP_VCOL", "Vertex Colors", "Vertex Color Layers", 2),
119 scene
.amth_datablock_types
= EnumProperty(
120 items
=amth_datablock_types
,
122 description
="Datablock Type",
124 update
=call_update_datablock_type
,
125 options
={"SKIP_SAVE"}
127 if utils
.cycles_exists():
128 cycles_shader_node_types
= (
129 ("BSDF_DIFFUSE", "Diffuse BSDF", "", 0),
130 ("BSDF_GLOSSY", "Glossy BSDF", "", 1),
131 ("BSDF_TRANSPARENT", "Transparent BSDF", "", 2),
132 ("BSDF_REFRACTION", "Refraction BSDF", "", 3),
133 ("BSDF_GLASS", "Glass BSDF", "", 4),
134 ("BSDF_TRANSLUCENT", "Translucent BSDF", "", 5),
135 ("BSDF_ANISOTROPIC", "Anisotropic BSDF", "", 6),
136 ("BSDF_VELVET", "Velvet BSDF", "", 7),
137 ("BSDF_TOON", "Toon BSDF", "", 8),
138 ("SUBSURFACE_SCATTERING", "Subsurface Scattering", "", 9),
139 ("EMISSION", "Emission", "", 10),
140 ("BSDF_HAIR", "Hair BSDF", "", 11),
141 ("BACKGROUND", "Background", "", 12),
142 ("AMBIENT_OCCLUSION", "Ambient Occlusion", "", 13),
143 ("HOLDOUT", "Holdout", "", 14),
144 ("VOLUME_ABSORPTION", "Volume Absorption", "", 15),
145 ("VOLUME_SCATTER", "Volume Scatter", "", 16),
146 ("MIX_SHADER", "Mix Shader", "", 17),
147 ("ADD_SHADER", "Add Shader", "", 18),
148 ('BSDF_PRINCIPLED', 'Principled BSDF', "", 19),
150 scene
.amaranth_cycles_node_types
= EnumProperty(
151 items
=cycles_shader_node_types
,
158 "amaranth_cycles_node_types",
159 "amaranth_lighterscorner_list_meshlights",
161 wm
= bpy
.context
.window_manager
167 def print_with_count_list(text
="", send_list
=[]):
169 print("\n* {}\n".format(text
))
171 print("List is empty, no items to display")
174 for i
, entry
in enumerate(send_list
):
175 print('{:02d}. {}'.format(i
+ 1, send_list
[i
]))
179 def print_grammar(line
="", single
="", multi
="", cond
=[]):
180 phrase
= single
if len(cond
) == 1 else multi
181 print("\n* {} {}:\n".format(line
, phrase
))
184 def reset_global_storage(what
="NONE"):
189 for user
in AMTH_store_data
.users
:
190 AMTH_store_data
.users
[user
] = []
191 AMTH_store_data
.libraries
= []
193 elif what
== "MAT_SLOTS":
194 AMTH_store_data
.obj_mat_slots
[:] = []
195 AMTH_store_data
.obj_mat_slots_lib
[:] = []
197 elif what
== "NODE_LINK":
198 AMTH_store_data
.obj_mat_slots
[:] = []
199 AMTH_store_data
.count_groups
= 0
200 AMTH_store_data
.count_images
= 0
201 AMTH_store_data
.count_image_node_unlinked
= 0
203 elif what
== "SHADER":
204 AMTH_store_data
.mat_shaders
[:] = []
207 class AMTH_SCENE_OT_cycles_shader_list_nodes(Operator
):
208 """List Cycles materials containing a specific shader"""
209 bl_idname
= "scene.cycles_list_nodes"
210 bl_label
= "List Materials"
213 def poll(cls
, context
):
214 return utils
.cycles_exists() and utils
.cycles_active(context
)
216 def execute(self
, context
):
217 node_type
= context
.scene
.amaranth_cycles_node_types
219 shaders_roughness
= ("BSDF_GLOSSY", "BSDF_DIFFUSE", "BSDF_GLASS")
221 reset_global_storage("SHADER")
223 print("\n=== Cycles Shader Type: {} === \n".format(node_type
))
225 for ma
in bpy
.data
.materials
:
229 nodes
= ma
.node_tree
.nodes
230 print_unconnected
= (
231 "Note: \nOutput from \"{}\" node in material \"{}\" "
232 "not connected\n".format(node_type
, ma
.name
)
236 if no
.type == node_type
:
237 for ou
in no
.outputs
:
240 if no
.type in shaders_roughness
:
241 roughness
= "R: {:.4f}".format(
242 no
.inputs
["Roughness"].default_value
248 print(print_unconnected
)
250 if ma
.name
not in AMTH_store_data
.mat_shaders
:
251 AMTH_store_data
.mat_shaders
.append(
253 ("[L] " if ma
.library
else "",
256 "[F]" if ma
.use_fake_user
else "",
258 roughness
if roughness
else "",
259 " * Output not connected" if not connected
else "")
261 elif no
.type == "GROUP":
263 for nog
in no
.node_tree
.nodes
:
264 if nog
.type == node_type
:
265 for ou
in nog
.outputs
:
268 if nog
.type in shaders_roughness
:
269 roughness
= "R: {:.4f}".format(
270 nog
.inputs
["Roughness"].default_value
276 print(print_unconnected
)
278 if ma
.name
not in AMTH_store_data
.mat_shaders
:
279 AMTH_store_data
.mat_shaders
.append(
280 '%s%s%s [%s] %s%s%s' %
281 ("[L] " if ma
.library
else "",
282 "Node Group: %s%s -> " %
283 ("[L] " if no
.node_tree
.library
else "",
287 "[F]" if ma
.use_fake_user
else "",
289 roughness
if roughness
else "",
290 " * Output not connected" if not connected
else "")
292 AMTH_store_data
.mat_shaders
= sorted(list(set(AMTH_store_data
.mat_shaders
)))
294 message
= "No materials with nodes type {} found".format(node_type
)
295 if len(AMTH_store_data
.mat_shaders
) > 0:
296 message
= "A total of {} {} using {} found".format(
297 len(AMTH_store_data
.mat_shaders
),
298 "material" if len(AMTH_store_data
.mat_shaders
) == 1 else "materials",
300 print_with_count_list(send_list
=AMTH_store_data
.mat_shaders
)
302 self
.report({'INFO'}, message
)
303 AMTH_store_data
.mat_shaders
= sorted(list(set(AMTH_store_data
.mat_shaders
)))
308 class AMTH_SCENE_OT_amaranth_object_select(Operator
):
310 bl_idname
= "scene.amaranth_object_select"
311 bl_label
= "Select Object"
313 object_name
: StringProperty()
315 def execute(self
, context
):
316 if not (self
.object_name
and self
.object_name
in bpy
.data
.objects
):
317 self
.report({'WARNING'},
318 "Object with the given name could not be found. Operation Cancelled")
321 obj
= bpy
.data
.objects
[self
.object_name
]
323 bpy
.ops
.object.select_all(action
="DESELECT")
325 context
.view_layer
.objects
.active
= obj
330 class AMTH_SCENE_OT_list_missing_node_links(Operator
):
331 """Print a list of missing node links"""
332 bl_idname
= "scene.list_missing_node_links"
333 bl_label
= "List Missing Node Links"
335 def execute(self
, context
):
338 image_nodes_unlinked
= []
341 reset_global_storage(what
="NODE_LINK")
343 for ma
in bpy
.data
.materials
:
347 for no
in ma
.node_tree
.nodes
:
348 if no
.type == "GROUP":
350 AMTH_store_data
.count_groups
+= 1
354 for ob
in bpy
.data
.objects
:
355 if ob
.material_slots
and ma
.name
in ob
.material_slots
:
356 users_ngroup
.append("%s%s%s" % (
357 "[L] " if ob
.library
else "",
358 "[F] " if ob
.use_fake_user
else "",
361 missing_groups
.append(
362 "MA: %s%s%s [%s]%s%s%s\n" %
363 ("[L] " if ma
.library
else "",
364 "[F] " if ma
.use_fake_user
else "",
367 " *** No users *** " if ma
.users
== 0 else "",
369 ma
.library
.filepath
if ma
.library
else "",
371 ", ".join(users_ngroup
) if users_ngroup
else "")
374 libraries
.append(ma
.library
.filepath
)
376 if no
.type == "TEX_IMAGE":
378 outputs_empty
= not no
.outputs
["Color"].is_linked
and \
379 not no
.outputs
["Alpha"].is_linked
382 image_path_exists
= os
.path
.exists(
385 library
=no
.image
.library
)
388 if outputs_empty
or not no
.image
or not image_path_exists
:
392 for ob
in bpy
.data
.objects
:
393 if ob
.material_slots
and ma
.name
in ob
.material_slots
:
394 users_images
.append("%s%s%s" % (
395 "[L] " if ob
.library
else "",
396 "[F] " if ob
.use_fake_user
else "",
400 AMTH_store_data
.count_image_node_unlinked
+= 1
402 image_nodes_unlinked
.append(
403 "%s%s%s%s%s [%s]%s%s%s%s%s\n" %
407 "[L] " if ma
.library
else "",
408 "[F] " if ma
.use_fake_user
else "",
411 " *** No users *** " if ma
.users
== 0 else "",
413 ma
.library
.filepath
if ma
.library
else "",
415 no
.image
.name
if no
.image
else "",
417 no
.image
.filepath
if no
.image
and no
.image
.filepath
else "",
419 ', '.join(users_images
) if users_images
else ""))
421 if not no
.image
or not image_path_exists
:
422 AMTH_store_data
.count_images
+= 1
424 missing_images
.append(
425 "MA: %s%s%s [%s]%s%s%s%s%s\n" %
426 ("[L] " if ma
.library
else "",
427 "[F] " if ma
.use_fake_user
else "",
430 " *** No users *** " if ma
.users
== 0 else "",
432 ma
.library
.filepath
if ma
.library
else "",
434 no
.image
.name
if no
.image
else "",
436 no
.image
.filepath
if no
.image
and no
.image
.filepath
else "",
438 ', '.join(users_images
) if users_images
else ""))
441 libraries
.append(ma
.library
.filepath
)
443 # Remove duplicates and sort
444 missing_groups
= sorted(list(set(missing_groups
)))
445 missing_images
= sorted(list(set(missing_images
)))
446 image_nodes_unlinked
= sorted(list(set(image_nodes_unlinked
)))
447 libraries
= sorted(list(set(libraries
)))
450 "\n\n== %s missing image %s, %s missing node %s and %s image %s unlinked ==" %
451 ("No" if AMTH_store_data
.count_images
== 0 else str(
452 AMTH_store_data
.count_images
),
453 "node" if AMTH_store_data
.count_images
== 1 else "nodes",
454 "no" if AMTH_store_data
.count_groups
== 0 else str(
455 AMTH_store_data
.count_groups
),
456 "group" if AMTH_store_data
.count_groups
== 1 else "groups",
457 "no" if AMTH_store_data
.count_image_node_unlinked
== 0 else str(
458 AMTH_store_data
.count_image_node_unlinked
),
459 "node" if AMTH_store_data
.count_groups
== 1 else "nodes")
461 # List Missing Node Groups
463 print_with_count_list("Missing Node Group Links", missing_groups
)
465 # List Missing Image Nodes
467 print_with_count_list("Missing Image Nodes Link", missing_images
)
469 # List Image Nodes with its outputs unlinked
470 if image_nodes_unlinked
:
471 print_with_count_list("Image Nodes Unlinked", image_nodes_unlinked
)
473 if missing_groups
or missing_images
or image_nodes_unlinked
:
475 print_grammar("That's bad, run check", "this library", "these libraries", libraries
)
476 print_with_count_list(send_list
=libraries
)
478 self
.report({"INFO"}, "Yay! No missing node links")
480 if missing_groups
and missing_images
:
483 "%d missing image %s and %d missing node %s found" %
484 (AMTH_store_data
.count_images
,
485 "node" if AMTH_store_data
.count_images
== 1 else "nodes",
486 AMTH_store_data
.count_groups
,
487 "group" if AMTH_store_data
.count_groups
== 1 else "groups")
493 class AMTH_SCENE_OT_list_missing_material_slots(Operator
):
494 """List objects with empty material slots"""
495 bl_idname
= "scene.list_missing_material_slots"
496 bl_label
= "List Empty Material Slots"
498 def execute(self
, context
):
499 reset_global_storage("MAT_SLOTS")
501 for ob
in bpy
.data
.objects
:
502 for ma
in ob
.material_slots
:
504 AMTH_store_data
.obj_mat_slots
.append('{}{}'.format(
505 '[L] ' if ob
.library
else '', ob
.name
))
507 AMTH_store_data
.obj_mat_slots_lib
.append(ob
.library
.filepath
)
509 AMTH_store_data
.obj_mat_slots
= sorted(list(set(AMTH_store_data
.obj_mat_slots
)))
510 AMTH_store_data
.obj_mat_slots_lib
= sorted(list(set(AMTH_store_data
.obj_mat_slots_lib
)))
512 if len(AMTH_store_data
.obj_mat_slots
) == 0:
513 self
.report({"INFO"},
514 "No objects with empty material slots found")
518 "\n* A total of {} {} with empty material slots was found \n".format(
519 len(AMTH_store_data
.obj_mat_slots
),
520 "object" if len(AMTH_store_data
.obj_mat_slots
) == 1 else "objects")
522 print_with_count_list(send_list
=AMTH_store_data
.obj_mat_slots
)
524 if AMTH_store_data
.obj_mat_slots_lib
:
525 print_grammar("Check", "this library", "these libraries",
526 AMTH_store_data
.obj_mat_slots_lib
528 print_with_count_list(send_list
=AMTH_store_data
.obj_mat_slots_lib
)
533 class AMTH_SCENE_OT_list_users_for_x_type(Operator
):
534 bl_idname
= "scene.amth_list_users_for_x_type"
536 bl_description
= "Select Datablock Name"
541 data_block
= bpy
.context
.scene
.amth_datablock_types
543 if data_block
== 'IMAGE_DATA':
544 for im
in bpy
.data
.images
:
545 if im
.name
not in {'Render Result', 'Viewer Node'}:
548 elif data_block
== 'MATERIAL':
549 where
= bpy
.data
.materials
551 elif data_block
== 'GROUP_VCOL':
552 for ob
in bpy
.data
.objects
:
553 if ob
.type == 'MESH':
554 for v
in ob
.data
.vertex_colors
:
555 if v
and v
not in where
:
557 where
= list(set(where
))
561 def avail(self
, context
):
562 datablock_type
= bpy
.context
.scene
.amth_datablock_types
563 where
= AMTH_SCENE_OT_list_users_for_x_type
.fill_where()
564 items
= [(str(i
), x
.name
, x
.name
, datablock_type
, i
) for i
, x
in enumerate(where
)]
565 items
= sorted(list(set(items
)))
567 items
= [('0', USER_X_NAME_EMPTY
, USER_X_NAME_EMPTY
, "INFO", 0)]
570 list_type_select
: EnumProperty(
573 options
={"SKIP_SAVE"}
577 def poll(cls
, context
):
578 return bpy
.context
.scene
.amth_datablock_types
580 def execute(self
, context
):
581 where
= self
.fill_where()
582 bpy
.context
.scene
.amth_list_users_for_x_name
= \
583 where
[int(self
.list_type_select
)].name
if where
else USER_X_NAME_EMPTY
588 class AMTH_SCENE_OT_list_users_for_x(Operator
):
589 """List users for a particular datablock"""
590 bl_idname
= "scene.amth_list_users_for_x"
591 bl_label
= "List Users for Datablock"
593 name
: StringProperty()
595 def execute(self
, context
):
597 x
= self
.name
if self
.name
else context
.scene
.amth_list_users_for_x_name
599 if USER_X_NAME_EMPTY
in x
:
600 self
.report({'INFO'},
601 "Please select a DataBlock name first. Operation Cancelled")
604 dtype
= context
.scene
.amth_datablock_types
606 reset_global_storage("XTYPE")
609 if dtype
== 'IMAGE_DATA':
611 for ma
in d
.materials
:
613 if utils
.cycles_exists():
614 if ma
and ma
.node_tree
and ma
.node_tree
.nodes
:
617 for nd
in ma
.node_tree
.nodes
:
618 if nd
and nd
.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}:
621 if nd
and nd
.type == 'GROUP':
622 if nd
.node_tree
and nd
.node_tree
.nodes
:
623 for ng
in nd
.node_tree
.nodes
:
624 if ng
.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}:
628 if no
.image
and no
.image
.name
== x
:
632 if ma
.name
in ob
.material_slots
:
633 objects
.append(ob
.name
)
640 name
= '"{0}" {1}{2}'.format(
642 'in object: {0}'.format(objects
) if objects
else ' (unassigned)',
643 '' if links
else ' (unconnected)')
645 if name
not in AMTH_store_data
.users
['MATERIAL']:
646 AMTH_store_data
.users
['MATERIAL'].append(name
)
650 if utils
.cycles_exists():
651 if la
and la
.node_tree
and la
.node_tree
.nodes
:
652 for no
in la
.node_tree
.nodes
:
654 no
.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \
655 no
.image
and no
.image
.name
== x
:
656 if la
.name
not in AMTH_store_data
.users
['LIGHT']:
657 AMTH_store_data
.users
['LIGHT'].append(la
.name
)
661 if utils
.cycles_exists():
662 if wo
and wo
.node_tree
and wo
.node_tree
.nodes
:
663 for no
in wo
.node_tree
.nodes
:
665 no
.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \
666 no
.image
and no
.image
.name
== x
:
667 if wo
.name
not in AMTH_store_data
.users
['WORLD']:
668 AMTH_store_data
.users
['WORLD'].append(wo
.name
)
670 for te
in d
.textures
:
671 if te
and te
.type == 'IMAGE' and te
.image
:
675 name
not in AMTH_store_data
.users
['TEXTURE']:
676 AMTH_store_data
.users
['TEXTURE'].append(te
.name
)
677 # Check Modifiers in Objects
679 for mo
in ob
.modifiers
:
680 if mo
.type in {'UV_PROJECT'}:
683 if mo
and image
and image
.name
== x
:
684 name
= '"{0}" modifier in {1}'.format(mo
.name
, ob
.name
)
685 if name
not in AMTH_store_data
.users
['MODIFIER']:
686 AMTH_store_data
.users
['MODIFIER'].append(name
)
687 # Check Background Images in Viewports
688 for scr
in d
.screens
:
690 if ar
.type == 'VIEW_3D':
692 ar
.spaces
.active
and \
693 ar
.spaces
.active
.background_images
:
694 for bg
in ar
.spaces
.active
.background_images
:
697 if bg
and image
and image
.name
== x
:
698 name
= 'Background for 3D Viewport in Screen "{0}"'\
700 if name
not in AMTH_store_data
.users
['VIEW3D']:
701 AMTH_store_data
.users
['VIEW3D'].append(name
)
702 # Check the Compositor
704 if sce
.node_tree
and sce
.node_tree
.nodes
:
706 for nd
in sce
.node_tree
.nodes
:
707 if nd
.type == 'IMAGE':
709 elif nd
.type == 'GROUP':
710 if nd
.node_tree
and nd
.node_tree
.nodes
:
711 for ng
in nd
.node_tree
.nodes
:
712 if ng
.type == 'IMAGE':
716 if no
.image
and no
.image
.name
== x
:
723 name
= 'Node {0} in Compositor (Scene "{1}"){2}'.format(
726 '' if links
else ' (unconnected)')
728 if name
not in AMTH_store_data
.users
['NODETREE']:
729 AMTH_store_data
.users
['NODETREE'].append(name
)
731 if dtype
== 'MATERIAL':
732 # Check Materials - Note: build an object_check list as only strings are stored
733 object_check
= [d
.objects
[names
] for names
in AMTH_store_data
.users
['OBJECT_DATA'] if
736 for ma
in ob
.material_slots
:
738 if ma
not in object_check
:
739 AMTH_store_data
.users
['OBJECT_DATA'].append(ob
.name
)
742 AMTH_store_data
.libraries
.append(ob
.library
.filepath
)
744 elif dtype
== 'GROUP_VCOL':
745 # Check VCOL in Meshes
746 for ob
in bpy
.data
.objects
:
747 if ob
.type == 'MESH':
748 for v
in ob
.data
.vertex_colors
:
750 name
= '{0}'.format(ob
.name
)
752 if name
not in AMTH_store_data
.users
['MESH_DATA']:
753 AMTH_store_data
.users
['MESH_DATA'].append(name
)
754 # Check VCOL in Materials
755 for ma
in d
.materials
:
757 if utils
.cycles_exists():
758 if ma
and ma
.node_tree
and ma
.node_tree
.nodes
:
759 for no
in ma
.node_tree
.nodes
:
760 if no
and no
.type in {'ATTRIBUTE'}:
761 if no
.attribute_name
== x
:
765 if ma
.name
in ob
.material_slots
:
766 objects
.append(ob
.name
)
769 name
= '{0} in object: {1}'.format(ma
.name
, objects
)
771 name
= '{0} (unassigned)'.format(ma
.name
)
773 if name
not in AMTH_store_data
.users
['MATERIAL']:
774 AMTH_store_data
.users
['MATERIAL'].append(name
)
776 AMTH_store_data
.libraries
= sorted(list(set(AMTH_store_data
.libraries
)))
780 for t
in AMTH_store_data
.users
:
781 if AMTH_store_data
.users
[t
]:
783 print('\n== {0} {1} use {2} "{3}" ==\n'.format(
784 len(AMTH_store_data
.users
[t
]),
788 for p
in AMTH_store_data
.users
[t
]:
789 print(' {0}'.format(p
))
791 if AMTH_store_data
.libraries
:
792 print_grammar("Check", "this library", "these libraries",
793 AMTH_store_data
.libraries
795 print_with_count_list(send_list
=AMTH_store_data
.libraries
)
798 self
.report({'INFO'}, "No users for {}".format(x
))
803 class AMTH_SCENE_OT_list_users_debug_clear(Operator
):
804 """Clear the list bellow"""
805 bl_idname
= "scene.amth_list_users_debug_clear"
806 bl_label
= "Clear Debug Panel lists"
808 what
: StringProperty(
814 def execute(self
, context
):
815 reset_global_storage(self
.what
)
820 class AMTH_SCENE_OT_blender_instance_open(Operator
):
821 """Open in a new Blender instance"""
822 bl_idname
= "scene.blender_instance_open"
823 bl_label
= "Open Blender Instance"
825 filepath
: StringProperty()
827 def execute(self
, context
):
829 filepath
= os
.path
.normpath(bpy
.path
.abspath(self
.filepath
))
833 subprocess
.Popen([bpy
.app
.binary_path
, filepath
])
835 print("Error opening a new Blender instance")
837 traceback
.print_exc()
842 class AMTH_SCENE_OT_Collection_List_Refresh(Operator
):
843 bl_idname
= "scene.amaranth_lighters_corner_refresh"
845 bl_description
= ("Generate/Refresh the Lists\n"
846 "Use to generate/refresh the list or after changes to Data")
847 bl_options
= {"REGISTER", "INTERNAL"}
849 what
: StringProperty(default
="NONE")
851 def execute(self
, context
):
852 message
= "No changes applied"
854 if self
.what
== "LIGHTS":
855 fill_ligters_corner_props(context
, refresh
=True)
857 found_lights
= len(context
.window_manager
.amth_lighters_state
.keys())
858 message
= "No Lights in the Data" if found_lights
== 0 else \
859 "Generated list for {} found light(s)".format(found_lights
)
861 elif self
.what
== "IMAGES":
862 fill_missing_images_props(context
, refresh
=True)
864 found_images
= len(context
.window_manager
.amth_missing_images_state
.keys())
865 message
= "Great! No missing Images" if found_images
== 0 else \
866 "Missing {} image(s) in the Data".format(found_images
)
868 self
.report({'INFO'}, message
)
873 class AMTH_SCENE_PT_scene_debug(Panel
):
875 bl_label
= "Scene Debug"
876 bl_space_type
= "PROPERTIES"
877 bl_region_type
= "WINDOW"
879 bl_options
= {"DEFAULT_CLOSED"}
881 def draw_header(self
, context
):
883 layout
.label(text
="", icon
="RADIOBUT_ON")
885 def draw_label(self
, layout
, body_text
, single
, multi
, lists
, ico
="BLANK1"):
887 text
="{} {} {}".format(
888 str(len(lists
)), body_text
,
889 single
if len(lists
) == 1 else multi
),
893 def draw_miss_link(self
, layout
, text1
, single
, multi
, text2
, count
, ico
="BLANK1"):
895 text
="{} {} {} {}".format(
897 single
if count
== 1 else multi
, text2
),
901 def draw(self
, context
):
903 scene
= context
.scene
905 has_images
= len(bpy
.data
.images
)
906 engine
= scene
.render
.engine
908 # List Missing Images
910 split
= box
.split(factor
=0.8, align
=True)
914 subrow
= split
.row(align
=True)
915 subrow
.alignment
= "RIGHT"
916 subrow
.operator(AMTH_SCENE_OT_Collection_List_Refresh
.bl_idname
,
917 text
="", icon
="FILE_REFRESH").what
= "IMAGES"
918 image_state
= context
.window_manager
.amth_missing_images_state
921 text
="{} Image Blocks present in the Data".format(has_images
),
924 if len(image_state
.keys()) > 0:
926 'AMTH_UL_MissingImages_UI',
927 'amth_collection_index_prop',
928 context
.window_manager
,
929 'amth_missing_images_state',
930 context
.window_manager
.amth_collection_index_prop
,
935 row
.label(text
="No images loaded yet", icon
="RIGHTARROW_THIN")
937 # List Cycles Materials by Shader
938 if utils
.cycles_exists() and engine
== "CYCLES":
941 col
= split
.column(align
=True)
942 col
.prop(scene
, "amaranth_cycles_node_types",
945 row
= split
.row(align
=True)
946 row
.operator(AMTH_SCENE_OT_cycles_shader_list_nodes
.bl_idname
,
948 text
="List Materials Using Shader")
949 if len(AMTH_store_data
.mat_shaders
) != 0:
951 AMTH_SCENE_OT_list_users_debug_clear
.bl_idname
,
952 icon
="X", text
="").what
= "SHADER"
955 if len(AMTH_store_data
.mat_shaders
) != 0:
956 col
= box
.column(align
=True)
957 self
.draw_label(col
, "found", "material", "materials",
958 AMTH_store_data
.mat_shaders
, "INFO"
960 for i
, mat
in enumerate(AMTH_store_data
.mat_shaders
):
962 text
="{}".format(AMTH_store_data
.mat_shaders
[i
]), icon
="MATERIAL"
965 # List Missing Node Trees
967 row
= box
.row(align
=True)
969 col
= split
.column(align
=True)
971 split
= col
.split(align
=True)
972 split
.label(text
="Node Links")
973 row
= split
.row(align
=True)
974 row
.operator(AMTH_SCENE_OT_list_missing_node_links
.bl_idname
,
977 if AMTH_store_data
.count_groups
!= 0 or \
978 AMTH_store_data
.count_images
!= 0 or \
979 AMTH_store_data
.count_image_node_unlinked
!= 0:
982 AMTH_SCENE_OT_list_users_debug_clear
.bl_idname
,
983 icon
="X", text
="").what
= "NODE_LINK"
984 col
.label(text
="Warning! Check Console", icon
="ERROR")
986 if AMTH_store_data
.count_groups
!= 0:
987 self
.draw_miss_link(col
, "node", "group", "groups", "missing link",
988 AMTH_store_data
.count_groups
, "NODE_TREE"
990 if AMTH_store_data
.count_images
!= 0:
991 self
.draw_miss_link(col
, "image", "node", "nodes", "missing link",
992 AMTH_store_data
.count_images
, "IMAGE_DATA"
994 if AMTH_store_data
.count_image_node_unlinked
!= 0:
995 self
.draw_miss_link(col
, "image", "node", "nodes", "with no output conected",
996 AMTH_store_data
.count_image_node_unlinked
, "NODE"
999 # List Empty Materials Slots
1002 col
= split
.column(align
=True)
1003 col
.label(text
="Material Slots")
1005 row
= split
.row(align
=True)
1006 row
.operator(AMTH_SCENE_OT_list_missing_material_slots
.bl_idname
,
1008 text
="List Empty Materials Slots"
1010 if len(AMTH_store_data
.obj_mat_slots
) != 0:
1012 AMTH_SCENE_OT_list_users_debug_clear
.bl_idname
,
1013 icon
="X", text
="").what
= "MAT_SLOTS"
1016 col
= box
.column(align
=True)
1017 self
.draw_label(col
, "found empty material slot", "object", "objects",
1018 AMTH_store_data
.obj_mat_slots
, "INFO"
1020 for entry
, obs
in enumerate(AMTH_store_data
.obj_mat_slots
):
1022 row
.alignment
= "LEFT"
1024 text
="{}".format(AMTH_store_data
.obj_mat_slots
[entry
]),
1027 if AMTH_store_data
.obj_mat_slots_lib
:
1029 col
.label("Check {}:".format(
1031 len(AMTH_store_data
.obj_mat_slots_lib
) == 1 else
1034 for ilib
, libs
in enumerate(AMTH_store_data
.obj_mat_slots_lib
):
1035 row
= col
.row(align
=True)
1036 row
.alignment
= "LEFT"
1038 AMTH_SCENE_OT_blender_instance_open
.bl_idname
,
1039 text
=AMTH_store_data
.obj_mat_slots_lib
[ilib
],
1041 emboss
=False).filepath
= AMTH_store_data
.obj_mat_slots_lib
[ilib
]
1044 row
= box
.row(align
=True)
1045 row
.label(text
="List Users for Datablock")
1047 col
= box
.column(align
=True)
1049 row
= split
.row(align
=True)
1051 scene
, "amth_datablock_types",
1052 icon
=scene
.amth_datablock_types
,
1055 row
.operator_menu_enum(
1056 "scene.amth_list_users_for_x_type",
1058 text
=scene
.amth_list_users_for_x_name
1061 row
= split
.row(align
=True)
1062 row
.enabled
= True if USER_X_NAME_EMPTY
not in scene
.amth_list_users_for_x_name
else False
1064 AMTH_SCENE_OT_list_users_for_x
.bl_idname
,
1065 icon
="COLLAPSEMENU").name
= scene
.amth_list_users_for_x_name
1067 if any(val
for val
in AMTH_store_data
.users
.values()):
1068 col
= box
.column(align
=True)
1070 for t
in AMTH_store_data
.users
:
1072 for ma
in AMTH_store_data
.users
[t
]:
1073 subrow
= col
.row(align
=True)
1074 subrow
.alignment
= "LEFT"
1076 if t
== 'OBJECT_DATA':
1077 text_lib
= " [L] " if \
1078 ma
in bpy
.data
.objects
and bpy
.data
.objects
[ma
].library
else ""
1080 AMTH_SCENE_OT_amaranth_object_select
.bl_idname
,
1081 text
="{} {}{}".format(text_lib
, ma
,
1082 "" if ma
in context
.scene
.objects
else " [Not in Scene]"),
1084 emboss
=False).object_name
= ma
1086 subrow
.label(text
=ma
, icon
=t
)
1088 AMTH_SCENE_OT_list_users_debug_clear
.bl_idname
,
1089 icon
="X", text
="").what
= "XTYPE"
1091 if AMTH_store_data
.libraries
:
1095 col
.label("Check {}:".format(
1097 len(AMTH_store_data
.libraries
) == 1 else
1100 for libs
in AMTH_store_data
.libraries
:
1102 row
= col
.row(align
=True)
1103 row
.alignment
= "LEFT"
1105 AMTH_SCENE_OT_blender_instance_open
.bl_idname
,
1106 text
=AMTH_store_data
.libraries
[count_lib
- 1],
1108 emboss
=False).filepath
= AMTH_store_data
.libraries
[count_lib
- 1]
1111 class AMTH_PT_LightersCorner(Panel
):
1112 """The Lighters Panel"""
1113 bl_label
= "Lighter's Corner"
1114 bl_idname
= "AMTH_SCENE_PT_lighters_corner"
1115 bl_space_type
= 'PROPERTIES'
1116 bl_region_type
= 'WINDOW'
1117 bl_context
= "scene"
1118 bl_options
= {"DEFAULT_CLOSED"}
1120 def draw_header(self
, context
):
1121 layout
= self
.layout
1122 layout
.label(text
="", icon
="LIGHT_SUN")
1124 def draw(self
, context
):
1125 layout
= self
.layout
1126 state_props
= len(context
.window_manager
.amth_lighters_state
)
1127 engine
= context
.scene
.render
.engine
1129 row
= box
.row(align
=True)
1131 if utils
.cycles_exists():
1132 row
.prop(context
.scene
, "amaranth_lighterscorner_list_meshlights")
1134 subrow
= row
.row(align
=True)
1135 subrow
.alignment
= "RIGHT"
1136 subrow
.operator(AMTH_SCENE_OT_Collection_List_Refresh
.bl_idname
,
1137 text
="", icon
="FILE_REFRESH").what
= "LIGHTS"
1141 message
= "Please Refresh" if len(bpy
.data
.lights
) > 0 else "No Lights in Data"
1142 row
.label(text
=message
, icon
="INFO")
1144 row
= box
.row(align
=True)
1145 split
= row
.split(factor
=0.5, align
=True)
1146 col
= split
.column(align
=True)
1148 col
.label(text
="Name/Library link")
1150 if engine
in ["CYCLES", "BLENDER_RENDER"]:
1151 splits
= 0.6 if engine
== "BLENDER_RENDER" else 0.4
1152 splita
= split
.split(factor
=splits
, align
=True)
1153 col
= splita
.column(align
=True)
1154 col
.alignment
= "LEFT"
1155 col
.label(text
="Samples")
1157 if utils
.cycles_exists() and engine
== "CYCLES":
1158 col
= splita
.column(align
=True)
1159 col
.label(text
="Size")
1161 cols
= row
.row(align
=True)
1162 cols
.alignment
= "RIGHT"
1163 cols
.label(text
="{}Render Visibility/Selection".format(
1164 "Rays /" if utils
.cycles_exists() else "")
1167 'AMTH_UL_LightersCorner_UI',
1168 'amth_collection_index_prop',
1169 context
.window_manager
,
1170 'amth_lighters_state',
1171 context
.window_manager
.amth_collection_index_prop
,
1177 class AMTH_UL_MissingImages_UI(UIList
):
1179 def draw_item(self
, context
, layout
, data
, item
, icon
, active_data
, active_propname
):
1180 text_lib
= item
.text_lib
1181 has_filepath
= item
.has_filepath
1182 is_library
= item
.is_library
1184 split
= layout
.split(factor
=0.4)
1185 row
= split
.row(align
=True)
1186 row
.alignment
= "LEFT"
1187 row
.label(text
=text_lib
, icon
="IMAGE_DATA")
1188 image
= bpy
.data
.images
.get(item
.name
, None)
1190 subrow
= split
.row(align
=True)
1191 splitp
= subrow
.split(factor
=0.8, align
=True).row(align
=True)
1192 splitp
.alignment
= "LEFT"
1193 row_lib
= subrow
.row(align
=True)
1194 row_lib
.alignment
= "RIGHT"
1196 splitp
.label(text
="Image is not available", icon
="ERROR")
1198 splitp
.label(text
=has_filepath
, icon
="LIBRARY_DATA_DIRECT")
1201 AMTH_SCENE_OT_blender_instance_open
.bl_idname
,
1203 emboss
=False, icon
="LINK_BLEND").filepath
= is_library
1206 class AMTH_UL_LightersCorner_UI(UIList
):
1208 def draw_item(self
, context
, layout
, data
, item
, icon
, active_data
, active_propname
):
1209 icon_type
= item
.icon_type
1210 engine
= context
.scene
.render
.engine
1211 text_lib
= item
.text_lib
1212 is_library
= item
.is_library
1214 split
= layout
.split(factor
=0.35)
1215 row
= split
.row(align
=True)
1216 row
.alignment
= "LEFT"
1217 row
.label(text
=text_lib
, icon
=icon_type
)
1218 ob
= bpy
.data
.objects
.get(item
.name
, None)
1220 row
.label(text
="Object is not available", icon
="ERROR")
1224 AMTH_SCENE_OT_blender_instance_open
.bl_idname
,
1226 emboss
=False, icon
="LINK_BLEND").filepath
= is_library
1228 rows
= split
.row(align
=True)
1229 splits
= 0.9 if engine
== "BLENDER_RENDER" else 0.4
1230 splitlamp
= rows
.split(factor
=splits
, align
=True)
1231 splitlampb
= splitlamp
.row(align
=True)
1232 splitlampc
= splitlamp
.row(align
=True)
1233 splitlampd
= rows
.row(align
=True)
1234 splitlampd
.alignment
= "RIGHT"
1236 if utils
.cycles_exists() and engine
== "CYCLES":
1237 if "LIGHT" in icon_type
:
1238 clamp
= ob
.data
.cycles
1239 if context
.scene
.cycles
.progressive
== "BRANCHED_PATH":
1240 splitlampb
.prop(clamp
, "samples", text
="")
1241 if context
.scene
.cycles
.progressive
== "PATH":
1242 splitlampb
.label(text
="N/A")
1244 if lamp
.type in ["POINT", "SUN", "SPOT"]:
1245 splitlampc
.label(text
="{:.2f}".format(lamp
.shadow_soft_size
))
1246 elif lamp
.type == "HEMI":
1247 splitlampc
.label(text
="N/A")
1248 elif lamp
.type == "AREA" and lamp
.shape
== "RECTANGLE":
1250 text
="{:.2f} x {:.2f}".format(lamp
.size
, lamp
.size_y
)
1253 splitlampc
.label(text
="{:.2f}".format(lamp
.size
))
1255 splitlampb
.label(text
="N/A")
1256 if engine
== "BLENDER_RENDER":
1257 if "LIGHT" in icon_type
:
1259 if lamp
.type == "HEMI":
1260 splitlampb
.label(text
="Not Available")
1261 elif lamp
.type == "AREA" and lamp
.shadow_method
== "RAY_SHADOW":
1262 splitlampb
.prop(lamp
, "shadow_ray_samples_x", text
="X")
1263 if lamp
.shape
== "RECTANGLE":
1264 splitlampb
.prop(lamp
, "shadow_ray_samples_y", text
="Y")
1265 elif lamp
.shadow_method
== "RAY_SHADOW":
1266 splitlampb
.prop(lamp
, "shadow_ray_samples", text
="Ray Samples")
1267 elif lamp
.shadow_method
== "BUFFER_SHADOW":
1268 splitlampb
.prop(lamp
, "shadow_buffer_samples", text
="Buffer Samples")
1270 splitlampb
.label(text
="No Shadow")
1272 splitlampb
.label(text
="N/A")
1273 if utils
.cycles_exists():
1274 visibility
= ob
.cycles_visibility
1275 splitlampd
.prop(visibility
, "camera", text
="")
1276 splitlampd
.prop(visibility
, "diffuse", text
="")
1277 splitlampd
.prop(visibility
, "glossy", text
="")
1278 splitlampd
.prop(visibility
, "shadow", text
="")
1279 splitlampd
.separator()
1280 splitlampd
.prop(ob
, "hide", text
="", emboss
=False)
1281 splitlampd
.prop(ob
, "hide_render", text
="", emboss
=False)
1282 splitlampd
.operator(
1283 AMTH_SCENE_OT_amaranth_object_select
.bl_idname
,
1285 emboss
=False, icon
="RESTRICT_SELECT_OFF").object_name
= item
.name
1288 def fill_missing_images_props(context
, refresh
=False):
1289 image_state
= context
.window_manager
.amth_missing_images_state
1291 for key
in image_state
.keys():
1292 index
= image_state
.find(key
)
1294 image_state
.remove(index
)
1296 for im
in bpy
.data
.images
:
1297 if im
.type not in ("UV_TEST", "RENDER_RESULT", "COMPOSITING"):
1298 if not im
.packed_file
and \
1299 not os
.path
.exists(bpy
.path
.abspath(im
.filepath
, library
=im
.library
)):
1300 text_l
= "{}{} [{}]{}".format("[L] " if im
.library
else "", im
.name
,
1301 im
.users
, " [F]" if im
.use_fake_user
else "")
1302 prop
= image_state
.add()
1304 prop
.text_lib
= text_l
1305 prop
.has_filepath
= im
.filepath
if im
.filepath
else "No Filepath"
1306 prop
.is_library
= im
.library
.filepath
if im
.library
else ""
1309 def fill_ligters_corner_props(context
, refresh
=False):
1310 light_state
= context
.window_manager
.amth_lighters_state
1311 list_meshlights
= context
.scene
.amaranth_lighterscorner_list_meshlights
1313 for key
in light_state
.keys():
1314 index
= light_state
.find(key
)
1316 light_state
.remove(index
)
1318 for ob
in bpy
.data
.objects
:
1319 if ob
.name
not in light_state
.keys() or refresh
:
1320 is_light
= ob
.type == "LIGHT"
1321 is_emission
= True if utils
.cycles_is_emission(
1322 context
, ob
) and list_meshlights
else False
1324 if is_light
or is_emission
:
1325 icons
= "LIGHT_%s" % ob
.data
.type if is_light
else "MESH_GRID"
1326 text_l
= "{} {}{}".format(" [L] " if ob
.library
else "", ob
.name
,
1327 "" if ob
.name
in context
.scene
.objects
else " [Not in Scene]")
1328 prop
= light_state
.add()
1330 prop
.icon_type
= icons
1331 prop
.text_lib
= text_l
1332 prop
.is_library
= ob
.library
.filepath
if ob
.library
else ""
1335 class AMTH_LightersCornerStateProp(PropertyGroup
):
1336 icon_type
: StringProperty()
1337 text_lib
: StringProperty()
1338 is_library
: StringProperty()
1341 class AMTH_MissingImagesStateProp(PropertyGroup
):
1342 text_lib
: StringProperty()
1343 has_filepath
: StringProperty()
1344 is_library
: StringProperty()
1347 class AMTH_LightersCollectionIndexProp(PropertyGroup
):
1351 index_image
: IntProperty(
1357 AMTH_SCENE_PT_scene_debug
,
1358 AMTH_SCENE_OT_list_users_debug_clear
,
1359 AMTH_SCENE_OT_blender_instance_open
,
1360 AMTH_SCENE_OT_amaranth_object_select
,
1361 AMTH_SCENE_OT_list_missing_node_links
,
1362 AMTH_SCENE_OT_list_missing_material_slots
,
1363 AMTH_SCENE_OT_cycles_shader_list_nodes
,
1364 AMTH_SCENE_OT_list_users_for_x
,
1365 AMTH_SCENE_OT_list_users_for_x_type
,
1366 AMTH_SCENE_OT_Collection_List_Refresh
,
1367 AMTH_LightersCornerStateProp
,
1368 AMTH_LightersCollectionIndexProp
,
1369 AMTH_MissingImagesStateProp
,
1370 AMTH_PT_LightersCorner
,
1371 AMTH_UL_LightersCorner_UI
,
1372 AMTH_UL_MissingImages_UI
,
1380 bpy
.utils
.register_class(cls
)
1382 bpy
.types
.Scene
.amth_list_users_for_x_name
= StringProperty(
1383 default
="Select DataBlock Name",
1385 description
=USER_X_NAME_EMPTY
,
1386 options
={"SKIP_SAVE"}
1388 bpy
.types
.WindowManager
.amth_collection_index_prop
= PointerProperty(
1389 type=AMTH_LightersCollectionIndexProp
1391 bpy
.types
.WindowManager
.amth_lighters_state
= CollectionProperty(
1392 type=AMTH_LightersCornerStateProp
1394 bpy
.types
.WindowManager
.amth_missing_images_state
= CollectionProperty(
1395 type=AMTH_MissingImagesStateProp
1403 bpy
.utils
.unregister_class(cls
)
1405 del bpy
.types
.Scene
.amth_list_users_for_x_name
1406 del bpy
.types
.WindowManager
.amth_collection_index_prop
1407 del bpy
.types
.WindowManager
.amth_lighters_state
1408 del bpy
.types
.WindowManager
.amth_missing_images_state