Files
2026-03-17 15:34:28 -06:00

468 lines
18 KiB
Python

# Copyright (C) 2021 Victor Soupday
# This file is part of CC/iC Blender Tools <https://github.com/soupday/cc_blender_tools>
#
# CC/iC Blender Tools 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 3 of the License, or
# (at your option) any later version.
#
# CC/iC Blender Tools 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 CC/iC Blender Tools. If not, see <https://www.gnu.org/licenses/>.
import os
import bpy
from . import colorspace, nodeutils, params, lib, utils, vars
IMAGE_FORMATS = {
"PNG": ".png",
"JPEG": ".jpg",
"BMP": ".bmp",
"TARGA": ".tga",
"JPEG2000": "jp2",
"IRIS": ".rgb",
"TARGA_RAW": ".tga",
"CINEON": ".cin",
"DPX": ".dpx",
"OPEN_EXR_MULTILAYER": ".exr",
"OPEN_EXR": ".exr",
"HDR": ".hdr",
"TIFF": ".tif",
"WEBP": ".webp",
}
def check_max_size(image):
prefs = vars.prefs()
width = image.size[0]
height = image.size[1]
if width > prefs.max_texture_size or height > prefs.max_texture_size:
image.scale(min(width, prefs.max_texture_size), min(height, prefs.max_texture_size))
# load an image from a file, but try to find it in the existing images first
def load_image(filename, color_space, processed_images = None, reuse_existing = True):
i: bpy.types.Image = None
# TODO: should the de-duplication only consider images brough in from the import.
# (but then the rebuild won't work...)
# or only consider images with the characters folder as a common path...
if reuse_existing:
for i in bpy.data.images:
if i.type == "IMAGE" and i.filepath != "":
if os.path.normpath(bpy.path.abspath(i.filepath)) == os.path.normpath(os.path.abspath(filename)):
utils.log_info("Using existing image: " + i.filepath)
found = False
image_md5 = None
image_path = bpy.path.abspath(i.filepath)
if processed_images is not None and os.path.exists(image_path):
image_md5 = utils.md5sum(image_path)
for p in processed_images:
if p[0] == image_md5:
utils.log_info("Skipping duplicate existing image, reusing: " + p[1].filepath)
i = p[1]
found = True
if (i.depth == 32 or i.depth == 128) and i.alpha_mode != "CHANNEL_PACKED":
i.alpha_mode = "CHANNEL_PACKED"
if processed_images is not None and i and image_md5 and not found:
processed_images.append([image_md5, i])
if not i.is_dirty:
utils.log_detail(f"Reloading image: {i.name}")
try:
i.reload()
except:
utils.log_detail(f"Unable to reload image: {i.name}")
else:
utils.log_info(f"Image {i.name} has been modified, keeping in-memory image.")
colorspace.set_image_color_space(i, color_space)
return i
try:
image_md5 = None
if processed_images is not None and os.path.exists(filename):
image_md5 = utils.md5sum(filename)
for p in processed_images:
if p[0] == image_md5 and utils.image_exists(p[1]):
utils.log_info("Skipping duplicate image, reusing existing: " + p[1].filepath)
return p[1]
utils.log_info("Loading new image: " + filename)
image = bpy.data.images.load(filename)
colorspace.set_image_color_space(image, color_space)
if (image.depth == 32 or image.depth == 128):
image.alpha_mode = "CHANNEL_PACKED"
#check_max_size(image)
if processed_images is not None and image and image_md5:
processed_images.append([image_md5, image])
return image
except Exception as e:
utils.log_error("Unable to load image: " + filename, e)
return None
## Search the directory for an image filename that contains the search substring
def find_image_file(base_dir, dirs, mat, texture_type):
suffix_list = get_image_type_suffix_list(texture_type)
material_name = utils.strip_name(mat.name).lower()
last = ""
for dir in dirs:
if dir:
# if the texture folder does not exist, (e.g. files have been moved)
# remap the relative path to the current blend file directory to try and find the images there
if not os.path.exists(dir):
dir = utils.local_repath(dir, base_dir)
dir = os.path.normpath(dir)
if dir and os.path.exists(dir):
if last != dir:
last = dir
for suffix in suffix_list:
search = f"{material_name}_{suffix}"
file = find_file_by_name(dir, search)
if file:
return file
return None
def find_file_by_name(search_dir, search):
"""Find the file by the name (without extension)."""
search = search.lower()
if os.path.exists(search_dir):
files = os.listdir(search_dir)
for f in files:
dir, file = os.path.split(f)
name, ext = os.path.splitext(file)
name = name.lower()
if name == search:
return os.path.join(search_dir, f)
return None
def is_image_type_srgb(texture_type):
for tex in params.TEXTURE_TYPES:
if tex[0] == texture_type:
return tex[2]
return False
def get_image_type_suffix_list(texture_type):
for tex in params.TEXTURE_TYPES:
if tex[0] == texture_type:
return tex[3]
return []
def get_image_type_json_id(texture_type):
for tex in params.TEXTURE_TYPES:
if tex[0] == texture_type:
return tex[1]
return None
def get_image_type_lib_name(texture_type):
for tex in params.TEXTURE_TYPES:
if tex[0] == texture_type and len(tex) > 5:
return tex[5]
return None
def get_image_type_size_group(texture_type):
for tex in params.TEXTURE_TYPES:
if tex[0] == texture_type:
return tex[4]
return None
def is_library_tex(texture_type):
if get_image_type_lib_name(texture_type):
return True
return False
def search_image_in_material_dirs(chr_cache, mat_cache, mat, texture_type):
return find_image_file(chr_cache.get_import_dir(), [mat_cache.get_tex_dir(chr_cache), chr_cache.get_tex_dir()], mat, texture_type)
def get_max_sized_width_height(width, height, max_size):
if width > max_size or height > max_size:
if width > height:
width = max_size
height = int(height * max_size / width)
elif height > width:
height = max_size
width = int(width * max_size / height)
else:
width = max_size
height = max_size
return width, height
def apply_max_size(image: bpy.types.Image, texture_type):
prefs = vars.prefs()
if prefs.use_max_tex_size:
size_group = get_image_type_size_group(texture_type)
max_size = int(prefs.size_max_tex_default)
if size_group == "DETAIL":
max_size = int(prefs.size_max_tex_detail)
elif size_group == "MINIMAL":
max_size = int(prefs.size_max_tex_minimal)
width = image.size[0]
height = image.size[0]
if width > max_size or height > max_size:
if width > height:
width = max_size
height = int(height * max_size / width)
elif height > width:
height = max_size
width = int(width * max_size / height)
else:
width = max_size
height = max_size
utils.log_info(f"resizing image: {image.name} (size_group) to {width} x {height}")
image.scale(width, height)
def find_material_image(mat, texture_type, processed_images = None, tex_json = None, mat_json = None):
"""Try to find the texture for a material input by searching for the material name
appended with the possible suffixes e.g. Vest_diffuse or Hair_roughness
"""
props = vars.props()
mat_cache = props.get_material_cache(mat)
chr_cache = props.get_character_cache(None, mat)
image_file = None
color_space = "Non-Color"
if is_image_type_srgb(texture_type):
color_space = "sRGB"
# temp weight maps in the cache override weight maps on disk
if texture_type == "WEIGHTMAP" and mat_cache.temp_weight_map is not None:
utils.log_info(f"Using material cache user weightmap: {mat_cache.temp_weight_map.name}")
return mat_cache.temp_weight_map
# try to find as library image
lib_name = get_image_type_lib_name(texture_type)
if lib_name:
image = lib.get_image(lib_name)
colorspace.set_image_color_space(image, color_space)
if image:
apply_max_size(image, texture_type)
return image
# try to find the image in the json data first:
if tex_json:
tex_path: str = utils.fix_texture_rel_path(tex_json["Texture Path"])
is_tex_path_relative = not os.path.isabs(tex_path)
if tex_path:
if is_tex_path_relative:
image_file = os.path.normpath(os.path.join(chr_cache.get_import_dir(), tex_path))
else:
image_file = os.path.normpath(tex_path)
# try to load image path directly
if os.path.exists(image_file):
image = load_image(image_file, color_space, processed_images)
if image:
apply_max_size(image, texture_type)
return image
# try remapping the image path relative to the local directory
if is_tex_path_relative:
image_file = utils.local_path(tex_path)
if image_file and os.path.exists(image_file):
image = load_image(image_file, color_space, processed_images)
if image:
apply_max_size(image, texture_type)
return image
# try to find the image in the texture_mappings (all embedded images should be here)
for tex_mapping in mat_cache.texture_mappings:
if tex_mapping:
if texture_type == tex_mapping.texture_type:
if tex_mapping.image:
image = tex_mapping.image
apply_max_size(image, texture_type)
return image
utils.log_error(f"{texture_type} - json image path not found: {tex_path}")
return None
# if there is a mat_json but no texture json, then there is no texture to use
# (so don't look for one as it could find the wrong one i.e. fbm files with duplicated names)
#elif mat_json
#
# utils.log_warn(f"No {texture_type} json data found!")
# return None
# with no Json data, try to locate the images in the texture folders:
else:
# try to find the image in the texture_mappings (all embedded images should be here)
if mat_cache:
for tex_mapping in mat_cache.texture_mappings:
if tex_mapping:
if texture_type == tex_mapping.texture_type:
if tex_mapping.image:
utils.log_info(f"Using embedded image: {tex_mapping.image.name}")
image = tex_mapping.image
apply_max_size(image, texture_type)
return image
image_file = search_image_in_material_dirs(chr_cache, mat_cache, mat, texture_type)
if image_file:
image = load_image(image_file, color_space, processed_images)
if image:
apply_max_size(image, texture_type)
return image
# then try to find the image in the texture_mappings (all embedded images should be here)
for tex_mapping in mat_cache.texture_mappings:
if tex_mapping:
if texture_type == tex_mapping.texture_type:
if tex_mapping.image:
image = tex_mapping.image
apply_max_size(image, texture_type)
return image
elif tex_mapping.texture_path is not None and tex_mapping.texture_path != "":
image = load_image(tex_mapping.texture_path, color_space, processed_images)
if image:
apply_max_size(image, texture_type)
return image
return None
def get_material_tex_dir(chr_cache, obj, mat):
"""Returns the *relative* path to the texture folder for this material.
"""
props = vars.props()
if chr_cache.is_import_type("FBX"):
object_name = utils.strip_name(obj.name)
mesh_name = utils.strip_name(obj.data.name)
material_name = utils.strip_name(mat.name)
# non .fbm textures are stored in two possible locations:
# /textures/character_name/object_name/mesh_name/material_name
# or /textures/character_name/character_name/mesh_name/material_name
rel_object = os.path.join("textures", chr_cache.get_character_id(), object_name, mesh_name, material_name)
path_object = os.path.join(chr_cache.get_import_dir(), rel_object)
rel_character = os.path.join("textures", chr_cache.get_character_id(), chr_cache.get_character_id(), mesh_name, material_name)
path_character = os.path.join(chr_cache.get_import_dir(), rel_character)
if os.path.exists(path_object):
return rel_object
elif os.path.exists(path_character):
return rel_character
else:
return os.path.join(chr_cache.get_character_id() + ".fbm")
elif chr_cache.is_import_type("OBJ"):
return chr_cache.get_character_id()
def get_material_tex_dirs(chr_cache, obj, mat):
mat_dir = os.path.normpath(os.path.join(chr_cache.get_import_dir(), get_material_tex_dir(chr_cache, obj, mat)))
return [chr_cache.get_tex_dir(), mat_dir]
def find_texture_folder_in_objects(objects):
for obj in objects:
if obj.type == "MESH":
for mat in obj.data.materials:
if mat.node_tree:
nodes = mat.node_tree.nodes
for node in nodes:
if node.type == "TEX_IMAGE":
image = node.image
if image.filepath:
file_path = bpy.path.abspath(image.filepath)
folder = os.path.dirname(file_path)
if folder:
return folder
return None
def get_custom_image(image_name, size, alpha=False, data=True, float=False, path="", unique=False):
# find the image by name
image = None
if unique:
image_name = utils.unique_image_name(image_name)
else:
if image_name in bpy.data.images:
image = bpy.data.images[image_name]
if image.size[0] != size or image.size[1] != size:
bpy.data.images.remove(image)
image = None
utils.log_info(f"Deleting Custom image: {image_name}, wrong size.")
else:
utils.log_info(f"Reusing Custom image: {image_name}")
# or create the bake image
if not image:
utils.log_info(f"Creating new Custom image: {image_name} {size}x{size}")
image = bpy.data.images.new(image_name, size, size, alpha=alpha, is_data=data, float_buffer=float)
if float:
image.use_half_precision = False
if path:
image.filepath_raw = path
image.save()
return image
def save_scene_image(image : bpy.types.Image, file_path, file_format = 'PNG', color_depth = '8'):
"""To reload properly, the image must be pre-saved with image.filepath_raw = ... and image.save()"""
scene = bpy.data.scenes.new("RL_Save_Image_Settings_Scene")
settings = scene.render.image_settings
settings.color_depth = color_depth
settings.file_format = file_format
settings.color_mode = 'RGB' if image.depth == 24 else 'RGBA'
if not file_path and image.filepath:
file_path = bpy.path.abspath(image.filepath)
image.save_render(filepath = file_path, scene = scene)
if image.filepath:
image.reload()
bpy.data.scenes.remove(scene)
def make_new_image(name, width, height, format, dir, data, has_alpha, channel_packed):
img = bpy.data.images.new(name, width, height, alpha=has_alpha, is_data=data)
img.pixels[0] = 0
if has_alpha:
img.alpha_mode = "STRAIGHT" if not channel_packed else "CHANNEL_PACKED"
return save_image_to_format_dir(img, format, dir, name)
def save_image_to_format_dir(img, format, dir, name):
if format in IMAGE_FORMATS:
ext = IMAGE_FORMATS[format]
else:
format = "PNG"
ext = ".png"
img.file_format = format
full_dir = os.path.normpath(dir)
full_path = os.path.normpath(os.path.join(full_dir, name + ext))
utils.log_info(f" Path: {full_path}")
os.makedirs(full_dir, exist_ok=True)
img.filepath_raw = full_path
img.save()
return img