2025-07-01
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
|
||||
bl_info = {
|
||||
"name": "Find Missing Textures - FMT",
|
||||
"author": "GilaDDD",
|
||||
"version": (3, 5),
|
||||
"blender": (2, 80, 0),
|
||||
"location": "Properties > Scene tab > Find Missing Textures",
|
||||
"description": "Powerful tool that simplifies managing missing textures. Quickly locate broken image paths and get detailed reports of the objects, materials, and nodes using them.",
|
||||
"doc_url": "https://blendermarket.com/products/report-missing-textures--fmt/docs",
|
||||
"category": "Material"}
|
||||
|
||||
from . import ops_fmt, panels_fmt
|
||||
import bpy
|
||||
from bpy.props import IntProperty, CollectionProperty, PointerProperty, BoolProperty
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("texture.find_missing_textures", text ="Report Missing Textures (full report)")
|
||||
|
||||
# def warnning_before_render(self, *args):
|
||||
# bpy.ops.texture.find_missing_textures()
|
||||
|
||||
classes = (
|
||||
ops_fmt.TEXTURE_OT_find_missing_textures,
|
||||
ops_fmt.TEXTURE_OT_find_in_folder,
|
||||
ops_fmt.TEXTURE_OT_remove_image,
|
||||
ops_fmt.TEXTURE_OT_search_bars,
|
||||
panels_fmt.FMT_PT_Panel,
|
||||
panels_fmt.ListItem,
|
||||
panels_fmt.List_View,
|
||||
panels_fmt.SearchBars,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.Scene.fmt_list = CollectionProperty(type = panels_fmt.ListItem)
|
||||
bpy.types.Scene.fmt_search_bars = CollectionProperty(type = panels_fmt.SearchBars)
|
||||
bpy.types.Scene.fmt_list_pointer = PointerProperty(type=panels_fmt.ListItem)
|
||||
bpy.types.Scene.fmt_list_index = IntProperty(name = "", default = 0)
|
||||
bpy.types.Scene.fmt_extension_replace = BoolProperty(name = "replace_ext", default = 0)
|
||||
bpy.types.Scene.fmt_select_bad_obj = BoolProperty(name = "select bad objects", default = 1)
|
||||
bpy.types.TOPBAR_MT_file_external_data.append(draw)
|
||||
# bpy.app.handlers.render_init.append(warnning_before_render)
|
||||
|
||||
|
||||
def unregister():
|
||||
# bpy.app.handlers.render_init.remove(warnning_before_render)
|
||||
del bpy.types.Scene.fmt_list
|
||||
del bpy.types.Scene.fmt_search_bars
|
||||
del bpy.types.Scene.fmt_list_pointer
|
||||
del bpy.types.Scene.fmt_list_index
|
||||
del bpy.types.Scene.fmt_extension_replace
|
||||
bpy.types.TOPBAR_MT_file_external_data.remove(draw)
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -0,0 +1,156 @@
|
||||
import bpy
|
||||
from os import path, walk
|
||||
from pathlib import Path
|
||||
|
||||
class TEXTURE_OT_search_bars(bpy.types.Operator):
|
||||
"""Add or remove search bars"""
|
||||
bl_idname = "texture.bars"
|
||||
bl_label = "Add search bar"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_region_type = 'UI'
|
||||
|
||||
add : bpy.props.StringProperty(default="")
|
||||
|
||||
def execute(self, context):
|
||||
if self.add[0] == "T":
|
||||
context.scene.fmt_search_bars.add()
|
||||
elif self.add[0] == "F":
|
||||
context.scene.fmt_search_bars.remove(int(self.add[1:]))
|
||||
return {'FINISHED'}
|
||||
|
||||
class TEXTURE_OT_remove_image(bpy.types.Operator):
|
||||
"""Delete the image from the file"""
|
||||
bl_idname = "texture.remove_image"
|
||||
bl_label = "Delete image"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_region_type = 'UI'
|
||||
|
||||
def execute(self, context):
|
||||
if len(bpy.context.scene.fmt_list) > 0:
|
||||
image_name = bpy.context.scene.fmt_list[bpy.context.scene.fmt_list_index].img_name
|
||||
bpy.context.scene.fmt_list.remove(bpy.context.scene.fmt_list_index)
|
||||
bpy.data.images.remove(bpy.data.images[image_name])
|
||||
return {'FINISHED'}
|
||||
|
||||
class TEXTURE_OT_find_in_folder(bpy.types.Operator):
|
||||
"""Find your missing textures in this folder and subfolders"""
|
||||
bl_idname = "texture.folder_find"
|
||||
bl_label = "Find in folder"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_region_type = 'UI'
|
||||
|
||||
def execute(self, context):
|
||||
scene = context.scene
|
||||
files_dir_list = []
|
||||
try:
|
||||
for search_path in scene.fmt_search_bars:
|
||||
if search_path.bars:
|
||||
for dirpath, dirnames, filenames in walk(bpy.path.abspath(search_path.bars)):
|
||||
files_dir_list.append([dirpath, filenames])
|
||||
except FileNotFoundError:
|
||||
print(f"Error - Bad file path {search_path.bars}")
|
||||
return {'FINISHED'}
|
||||
for missing_img in scene.fmt_list:
|
||||
img_file_name = bpy.path.basename(bpy.data.images[missing_img.img_name].filepath)
|
||||
for imgs in files_dir_list:
|
||||
if img_file_name in imgs[1]:
|
||||
bpy.data.images[missing_img.img_name].filepath = path.join(imgs[0], img_file_name)
|
||||
elif bpy.context.scene.fmt_extension_replace:
|
||||
self.search_different_extension(missing_img, img_file_name, imgs)
|
||||
|
||||
bpy.ops.texture.find_missing_textures()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def search_different_extension(self, missing_img, img_file_name, imgs):
|
||||
base_img_name = path.splitext(img_file_name)[0]
|
||||
for img_in_folders in imgs[1]:
|
||||
if base_img_name == path.splitext(img_in_folders)[0]:
|
||||
bpy.data.images[missing_img.img_name].filepath = path.join(imgs[0], img_in_folders)
|
||||
|
||||
class TEXTURE_OT_find_missing_textures(bpy.types.Operator):
|
||||
"""Search for misssing textures"""
|
||||
bl_idname = "texture.find_missing_textures"
|
||||
bl_label = "Search missing textures"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_region_type = 'UI'
|
||||
|
||||
def execute(self, context):
|
||||
bpy.context.scene.fmt_list.clear()
|
||||
self.search_for_missing_texture()
|
||||
self.report_full_list()
|
||||
if len(bpy.context.scene.fmt_list) > 0:
|
||||
self.report({'WARNING'}, "Some texture are missing!")
|
||||
return {'FINISHED'}
|
||||
|
||||
def search_for_missing_texture(self):
|
||||
if len(bpy.context.scene.fmt_search_bars) == 0:
|
||||
bpy.context.scene.fmt_search_bars.add()
|
||||
for img in bpy.data.images:
|
||||
if img.name == "Render Result" or img.name == "Viewer Node":
|
||||
continue
|
||||
if hasattr(img, 'has_data'):
|
||||
img.size[0] # Need to do this to refresh the data at the first time
|
||||
if img.has_data == False:
|
||||
self.add_texture_to_missing_list(img)
|
||||
elif not img.packed_file:
|
||||
if not Path(bpy.path.abspath(img.filepath)).is_file():
|
||||
self.add_texture_to_missing_list(img)
|
||||
|
||||
def report_full_list(self): # Get list of bad materials
|
||||
mat_list = []
|
||||
self.report({'INFO'}, f"------------------------------------------")
|
||||
self.report({'INFO'}, f"Missing material report:")
|
||||
self.report({'INFO'}, f"(Material name >> Node name (Node label) >> Image name)")
|
||||
self.report({'INFO'}, f"(Image path)")
|
||||
for mat in bpy.data.materials:
|
||||
if not mat.name == "Dots Stroke" and hasattr(mat.node_tree, 'nodes'):
|
||||
for node in mat.node_tree.nodes:
|
||||
if node.type == 'TEX_IMAGE':
|
||||
if hasattr(node.image, 'has_data'):
|
||||
if not node.image.has_data:
|
||||
if node.label == "":
|
||||
node_label = node.image.name
|
||||
else:
|
||||
node_label = node.label
|
||||
if not mat.name in mat_list:
|
||||
mat_list.append(mat.name)
|
||||
self.report({'WARNING'}, f"{mat.name} >> {node.name} ({node_label}) >> {node.image.name}")
|
||||
self.report({'WARNING'}, f"{node.image.filepath}")
|
||||
if not mat_list:
|
||||
self.report({'INFO'}, f"Good work! All the shaders are good!")
|
||||
else:
|
||||
self.search_object_with_bad_mat(mat_list)
|
||||
|
||||
def search_object_with_bad_mat(self, mat_list):
|
||||
self.deselect_all_objs()
|
||||
self.report({'INFO'}, f"------------------------------------------")
|
||||
self.report({'INFO'}, f"List of objects with missing textures:")
|
||||
if len(bpy.data.scenes) == 1:
|
||||
self.report({'INFO'}, f"Object name >> Material name")
|
||||
else:
|
||||
self.report({'INFO'}, f"Scene name >> Object name >> Material name")
|
||||
for scene in bpy.data.scenes:
|
||||
for obj in scene.objects:
|
||||
if obj.type == "MESH":
|
||||
if len(obj.material_slots.items()) > 0:
|
||||
for miss_mat in mat_list:
|
||||
if miss_mat in obj.material_slots:
|
||||
if len(bpy.data.scenes) == 1:
|
||||
if bpy.context.scene.fmt_select_bad_obj:
|
||||
obj.select_set(True)
|
||||
self.report({'WARNING'}, f"{obj.name} >> {miss_mat}")
|
||||
else:
|
||||
self.report({'WARNING'}, f"{scene.name} >> {obj.name} >> {miss_mat}")
|
||||
|
||||
def deselect_all_objs(self):
|
||||
if bpy.context.scene.fmt_select_bad_obj:
|
||||
for obj in bpy.context.selected_objects:
|
||||
obj.select_set(False)
|
||||
|
||||
|
||||
def add_texture_to_missing_list(self, img):
|
||||
img.reload()
|
||||
new_image_list = bpy.context.scene.fmt_list.add()
|
||||
new_image_list.img_name = img.name
|
||||
new_image_list.img_path = img.filepath
|
||||
@@ -0,0 +1,79 @@
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
from bpy.types import PropertyGroup, UIList, Panel
|
||||
|
||||
class FMT_PT_Panel(Panel):
|
||||
"""Find missing texture panel"""
|
||||
bl_label = "Find missing textures"
|
||||
bl_idname = "FMT_PT_Panel"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
|
||||
bl_category = "FMT"
|
||||
bl_icon = "OUTLINER_OB_HAIR"
|
||||
|
||||
def invoke(self, context, event):
|
||||
bpy.context.scene.fmt_search_bars.add()
|
||||
|
||||
def draw(self, context):
|
||||
scene = context.scene
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.label(text=f"Bad images list: ({len(scene.fmt_list)} images)", icon ="LIBRARY_DATA_BROKEN")
|
||||
row = layout.row()
|
||||
row.template_list("List_View", "", scene, "fmt_list", scene, "fmt_list_index")
|
||||
row = layout.row()
|
||||
row.prop(bpy.context.scene, "fmt_select_bad_obj", text = "Select objects with bad textures")
|
||||
row = layout.row()
|
||||
|
||||
row.scale_x = .3
|
||||
row.operator("texture.remove_image", text = 'Delete selected image', icon ="TRASH")
|
||||
row.scale_x = .7
|
||||
row.operator("texture.find_missing_textures", icon ="FILE_REFRESH")
|
||||
row = layout.row()
|
||||
if len(bpy.context.scene.fmt_list) > 0 and len(scene.fmt_search_bars) > 0:
|
||||
|
||||
row.prop(bpy.context.scene, "fmt_extension_replace", text = "Search for images with different extension")
|
||||
row = layout.row()
|
||||
for bar_index in range (len(scene.fmt_search_bars)):
|
||||
row = layout.row()
|
||||
row.prop(scene.fmt_search_bars[bar_index], "bars", text ="Search path")
|
||||
row.operator("texture.bars", text = "", icon ="REMOVE").add = 'F'+ str(bar_index)
|
||||
row.operator("texture.bars", text = "", icon ="ADD").add = 'T'
|
||||
row = layout.row()
|
||||
row.operator("texture.folder_find", text = "Find textures in this folders", icon ="VIEWZOOM")
|
||||
|
||||
|
||||
class List_View(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data,
|
||||
active_propname, index):
|
||||
custom_icon = 'OBJECT_DATAMODE'
|
||||
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||
path_scale = 0.7
|
||||
layout.scale_x = 1 - path_scale
|
||||
layout.label(text=item.img_name)
|
||||
layout.scale_x = path_scale
|
||||
if not bpy.data.images[item.img_name].has_data:
|
||||
layout.prop(bpy.data.images[item.img_name], "filepath", text = "",icon="LIBRARY_DATA_BROKEN")
|
||||
else:
|
||||
layout.prop(bpy.data.images[item.img_name], "filepath", text = "",icon="IMAGE_DATA")
|
||||
|
||||
|
||||
elif self.layout_type in {'GRID'}:
|
||||
layout.alignment = 'CENTER'
|
||||
layout.label(text="", icon = custom_icon)
|
||||
|
||||
class ListItem(PropertyGroup):
|
||||
"""Group of properties representing an item in the list."""
|
||||
img_name: StringProperty(
|
||||
name="Name",
|
||||
description="Name of the image",
|
||||
default="Non")
|
||||
|
||||
class SearchBars(PropertyGroup):
|
||||
bars: StringProperty(
|
||||
name="Searching folder",
|
||||
subtype="DIR_PATH",
|
||||
description="Searching path for the images (folder and sub folders)",
|
||||
default="//")
|
||||
Reference in New Issue
Block a user