From 58d5458432c1a4e237e530b79c3cb9024f3d0d1c Mon Sep 17 00:00:00 2001 From: matyalatte Date: Sun, 15 Jan 2023 11:06:47 +0900 Subject: [PATCH] refined codes, added comments --- .gitignore | 1 + addons/blender_dds_addon/directx/texconv.py | 76 ++++++++++----------- addons/blender_dds_addon/ui/bpy_util.py | 4 ++ addons/blender_dds_addon/ui/export_dds.py | 11 ++- addons/blender_dds_addon/ui/import_dds.py | 44 +++++++----- 5 files changed, 79 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index 02e3211..cb3e2de 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ htmlcov !tests/cube.dds *.tga *.png +*.jpg *.hdr *.xcf diff --git a/addons/blender_dds_addon/directx/texconv.py b/addons/blender_dds_addon/directx/texconv.py index 775b2af..37c9e10 100644 --- a/addons/blender_dds_addon/directx/texconv.py +++ b/addons/blender_dds_addon/directx/texconv.py @@ -33,8 +33,6 @@ def load_dll(self, dll_path=None): dll_path = os.path.join(dirname, dll_name) dll_path2 = os.path.join(os.path.dirname(dirname), dll_name) # allow ../texconv.dll - dll_path = os.path.abspath(dll_path) - if not os.path.exists(dll_path): if os.path.exists(dll_path2): dll_path = dll_path2 @@ -43,28 +41,6 @@ def load_dll(self, dll_path=None): self.dll = ctypes.cdll.LoadLibrary(dll_path) - def texconv(self, file, args, out=None, verbose=True, allow_slow_codec=False): - """Run texconv.""" - if out is not None and isinstance(out, str): - args += ['-o', out] - else: - out = '.' - - if out not in ['.', ''] and not os.path.exists(out): - util.mkdir(out) - - args += ["-y"] - args += [os.path.normpath(file)] - - args_p = [ctypes.c_wchar_p(arg) for arg in args] - args_p = (ctypes.c_wchar_p*len(args_p))(*args_p) - err_buf = ctypes.create_unicode_buffer(512) - result = self.dll.texconv(len(args), args_p, verbose, False, allow_slow_codec, err_buf, 512) - if result != 0: - raise RuntimeError(err_buf.value) - - return out - def convert_to_tga(self, file, out=None, cubemap_layout="h-cross", invert_normals=False, verbose=True): """Convert dds to tga.""" dds_header = DDSHeader.read_from_file(file) @@ -98,14 +74,14 @@ def convert_to_tga(self, file, out=None, cubemap_layout="h-cross", invert_normal if invert_normals: args += ['-inverty'] + out = self.__texconv(file, args, out=out, verbose=verbose) + + name = os.path.join(out, os.path.basename(file)) + name = ".".join(name.split(".")[:-1] + [fmt]) + if dds_header.is_cube(): - name = os.path.join(out, os.path.basename(file)) - name = ".".join(name.split(".")[:-1] + [fmt]) - self.cube_to_image(file, name, args, cubemap_layout=cubemap_layout, verbose=verbose) - else: - out = self.texconv(file, args, out=out, verbose=verbose) - name = os.path.join(out, os.path.basename(file)) - name = '.'.join(name.split('.')[:-1] + [fmt]) + self.__cube_to_image(file, name, args, cubemap_layout=cubemap_layout, verbose=verbose) + return name def convert_to_dds(self, file, dds_fmt, out=None, @@ -145,27 +121,49 @@ def convert_to_dds(self, file, dds_fmt, out=None, temp_args = ['-f', 'rgba'] with tempfile.TemporaryDirectory() as temp_dir: temp_dds = os.path.join(temp_dir, base_name) - self.image_to_cube(file, temp_dds, temp_args, cubemap_layout=cubemap_layout, verbose=verbose) - out = self.texconv(temp_dds, args, out=out, verbose=verbose, allow_slow_codec=allow_slow_codec) + self.__image_to_cube(file, temp_dds, temp_args, cubemap_layout=cubemap_layout, verbose=verbose) + out = self.__texconv(temp_dds, args, out=out, verbose=verbose, allow_slow_codec=allow_slow_codec) else: - out = self.texconv(file, args, out=out, verbose=verbose, allow_slow_codec=allow_slow_codec) + out = self.__texconv(file, args, out=out, verbose=verbose, allow_slow_codec=allow_slow_codec) name = os.path.join(out, base_name) return name - def cube_to_image(self, file, new_file, args, cubemap_layout="h-cross", verbose=True): + def __texconv(self, file, args, out=None, verbose=True, allow_slow_codec=False): + """Run texconv.""" + if out is not None and isinstance(out, str): + args += ['-o', out] + else: + out = '.' + + if out not in ['.', ''] and not os.path.exists(out): + util.mkdir(out) + + args += ["-y"] + args += [os.path.normpath(file)] + + args_p = [ctypes.c_wchar_p(arg) for arg in args] + args_p = (ctypes.c_wchar_p*len(args_p))(*args_p) + err_buf = ctypes.create_unicode_buffer(512) + result = self.dll.texconv(len(args), args_p, verbose, False, allow_slow_codec, err_buf, 512) + if result != 0: + raise RuntimeError(err_buf.value) + + return out + + def __cube_to_image(self, file, new_file, args, cubemap_layout="h-cross", verbose=True): """Genarate an image from a cubemap with texassemble.""" if cubemap_layout.endswith("-fnz"): cubemap_layout = cubemap_layout[:-4] args = [cubemap_layout] + args - self.texassemble(file, new_file, args, verbose=verbose) + self.__texassemble(file, new_file, args, verbose=verbose) - def image_to_cube(self, file, new_file, args, cubemap_layout="h-cross", verbose=True): + def __image_to_cube(self, file, new_file, args, cubemap_layout="h-cross", verbose=True): """Generate a cubemap from an image with texassemble.""" cmd = "cube-from-" + cubemap_layout[0] + cubemap_layout[2] args = [cmd] + args - self.texassemble(file, new_file, args, verbose=verbose) + self.__texassemble(file, new_file, args, verbose=verbose) - def texassemble(self, file, new_file, args, verbose=True): + def __texassemble(self, file, new_file, args, verbose=True): """Run texassemble.""" out = os.path.dirname(new_file) if out not in ['.', ''] and not os.path.exists(out): diff --git a/addons/blender_dds_addon/ui/bpy_util.py b/addons/blender_dds_addon/ui/bpy_util.py index 17af2c2..c8469cc 100644 --- a/addons/blender_dds_addon/ui/bpy_util.py +++ b/addons/blender_dds_addon/ui/bpy_util.py @@ -2,6 +2,10 @@ import bpy +def flush_stdout(): + print("", end="", flush=True) + + def dds_properties_exist(): return hasattr(bpy.types.Image, "dds_props") diff --git a/addons/blender_dds_addon/ui/export_dds.py b/addons/blender_dds_addon/ui/export_dds.py index ec7dd49..b5e05d1 100644 --- a/addons/blender_dds_addon/ui/export_dds.py +++ b/addons/blender_dds_addon/ui/export_dds.py @@ -14,7 +14,7 @@ from ..directx.dds import is_hdr from ..directx.texconv import Texconv -from .bpy_util import save_texture, dds_properties_exist, get_selected_tex +from .bpy_util import save_texture, dds_properties_exist, get_selected_tex, flush_stdout def save_dds(tex, file, dds_fmt, invert_normals=False, no_mip=False, @@ -135,12 +135,14 @@ def export_as_dds(context, tex, file): dds_options = context.scene.dds_options if dds_properties_exist(): + # Use image's properties props = tex.dds_props dxgi = props.dxgi_format is_cube = props.is_cube cubemap_layout = props.cubemap_layout no_mip = props.no_mip else: + # Use scene's properties dxgi = dds_options.dxgi_format is_cube = dds_options.export_as_cubemap cubemap_layout = dds_options.cubemap_layout @@ -170,11 +172,13 @@ def put_export_options(context, layout): class DDS_OT_export_base(Operator): + """Base class for export operators.""" + def draw(self, context): """Draw options for file picker.""" layout = self.layout layout.use_property_split = False - layout.use_property_decorate = False # No animation. + layout.use_property_decorate = False put_export_options(context, layout) def execute_base(self, context, file=None, directory=None, is_dir=False): @@ -182,6 +186,7 @@ def execute_base(self, context, file=None, directory=None, is_dir=False): start_time = time.time() if is_dir: + # For DDS_OT_export_all count = 0 for tex in bpy.data.images: if dds_properties_exist() and tex.dds_props.dxgi_format == "NONE": @@ -191,8 +196,10 @@ def execute_base(self, context, file=None, directory=None, is_dir=False): name += ".dds" file = os.path.join(directory, name) export_as_dds(context, tex, file) + flush_stdout() count += 1 else: + # For DDS_OT_export_dds tex = get_selected_tex(context) export_as_dds(context, tex, file) count = 1 diff --git a/addons/blender_dds_addon/ui/import_dds.py b/addons/blender_dds_addon/ui/import_dds.py index ae1d380..892a03f 100644 --- a/addons/blender_dds_addon/ui/import_dds.py +++ b/addons/blender_dds_addon/ui/import_dds.py @@ -14,7 +14,7 @@ from ..directx.dds import DDSHeader from ..directx.texconv import Texconv -from .bpy_util import get_image_editor_space, load_texture, dds_properties_exist +from .bpy_util import get_image_editor_space, load_texture, dds_properties_exist, flush_stdout from .custom_properties import DDS_FMT_NAMES @@ -35,12 +35,13 @@ def load_dds(file, invert_normals=False, cubemap_layout='h-cross', texconv=None) with tempfile.TemporaryDirectory() as temp_dir: temp = os.path.join(temp_dir, os.path.basename(file)) shutil.copyfile(file, temp) + + # Convert dds to tga if texconv is None: texconv = Texconv() - temp_tga = texconv.convert_to_tga(temp, out=temp_dir, cubemap_layout=cubemap_layout, invert_normals=invert_normals) - if temp_tga is None: # if texconv doesn't exist + if temp_tga is None: raise RuntimeError('Failed to convert texture.') # Check dxgi_format @@ -50,33 +51,38 @@ def load_dds(file, invert_normals=False, cubemap_layout='h-cross', texconv=None) else: color_space = 'Non-Color' + # Load tga file tex = load_texture(temp_tga, name=os.path.basename(temp_tga)[:-4], color_space=color_space) - dxgi = dds_header.get_format_as_str() if dds_properties_exist(): + # Update custom properties props = tex.dds_props + dxgi = dds_header.get_format_as_str() if dxgi in DDS_FMT_NAMES: - props.dxgi_format = dds_header.get_format_as_str() + props.dxgi_format = dxgi props.no_mip = dds_header.mipmap_num <= 1 props.is_cube = dds_header.is_cube() if props.is_cube: props.cubemap_layout = cubemap_layout + if cubemap_layout.endswith("-fnz"): + # Flip -z face for cubemaps + w, h = tex.size + pix = np.array(tex.pixels).reshape((h, w, -1)) + if cubemap_layout[0] == "v": + pix[h//4 * 0: h//4 * 1, w//3 * 1: w//3 * 2] = (pix[h//4 * 0: h//4 * 1, w//3 * 1: w//3 * 2])[::-1, ::-1] + else: + pix[h//3 * 1: h//3 * 2, w//4 * 3: w//4 * 4] = (pix[h//3 * 1: h//3 * 2, w//4 * 3: w//4 * 4])[::-1, ::-1] + pix = pix.flatten() + tex.pixels = list(pix) + + tex.update() + except Exception as e: if tex is not None: bpy.data.images.remove(tex) raise e - if cubemap_layout.endswith("-fnz"): - w, h = tex.size - pix = np.array(tex.pixels).reshape((h, w, -1)) - if cubemap_layout[0] == "v": - pix[h//4 * 0: h//4 * 1, w//3 * 1: w//3 * 2] = (pix[h//4 * 0: h//4 * 1, w//3 * 1: w//3 * 2])[::-1, ::-1] - else: - pix[h//3 * 1: h//3 * 2, w//4 * 3: w//4 * 4] = (pix[h//3 * 1: h//3 * 2, w//4 * 3: w//4 * 4])[::-1, ::-1] - pix = pix.flatten() - tex.pixels = list(pix) - tex.update() return tex @@ -90,6 +96,7 @@ def import_dds(context, file): def import_dds_rec(context, folder, count=0): + """Search a folder recursively, and import found dds files.""" for file in sorted(os.listdir(folder)): if os.path.isdir(file): count += import_dds_rec(context, os.path.join(folder, file), count=count) @@ -100,11 +107,13 @@ def import_dds_rec(context, folder, count=0): class DDS_OT_import_base(Operator): + """Base class for imoprt operators.""" + def draw(self, context): """Draw options for file picker.""" layout = self.layout layout.use_property_split = False - layout.use_property_decorate = False # No animation. + layout.use_property_decorate = False dds_options = context.scene.dds_options layout.prop(dds_options, 'invert_normals') layout.prop(dds_options, 'cubemap_layout') @@ -116,11 +125,14 @@ def execute_base(self, context, files=None, directory=None, is_dir=False): try: start_time = time.time() if is_dir: + # For DDS_OT_import_dir count = import_dds_rec(context, directory) else: + # For DDS_OT_import_dds count = 0 for _, file in enumerate(files): import_dds(context, os.path.join(directory, file.name)) + flush_stdout() count += 1 elapsed_s = f'{(time.time() - start_time):.2f}s' if count == 0: