From ea1e1f3fc85dfaf092fcd20ab563fadf29dc6dd5 Mon Sep 17 00:00:00 2001 From: matyalatte Date: Sat, 28 Jan 2023 11:01:28 +0900 Subject: [PATCH] v0.2.1 update (#5) * Added "Image Filter" option. (Possible to use cubic filter for mip generation.) * Fixed a bug that Blender can't remove the addon after using it in the process. * Improved error messages for unsupported files. --- addons/blender_dds_addon/__init__.py | 79 +++++++++---------- addons/blender_dds_addon/directx/dds.py | 11 ++- addons/blender_dds_addon/directx/texconv.py | 56 +++++++++++++ .../blender_dds_addon/ui/custom_properties.py | 11 +++ addons/blender_dds_addon/ui/export_dds.py | 4 + changelog.txt | 5 ++ docs/README.md | 2 +- external/Texconv-Custom-DLL | 2 +- tests/test_dds.py | 21 ++++- 9 files changed, 145 insertions(+), 46 deletions(-) diff --git a/addons/blender_dds_addon/__init__.py b/addons/blender_dds_addon/__init__.py index d70c2f0..f2f2b57 100644 --- a/addons/blender_dds_addon/__init__.py +++ b/addons/blender_dds_addon/__init__.py @@ -1,9 +1,14 @@ """Blender addon to import .dds files.""" +import importlib +from pathlib import Path + +from .ui import import_dds, export_dds, custom_properties, preferences +from .directx.texconv import unload_texconv bl_info = { 'name': 'DDS textures', 'author': 'Matyalatte', - 'version': (0, 2, 0), + 'version': (0, 2, 1), 'blender': (2, 83, 20), 'location': 'Image Editor > Sidebar > DDS Tab', 'description': 'Import and export .dds files', @@ -12,42 +17,36 @@ 'category': 'Import-Export', } -try: - def reload_package(module_dict_main): - """Reload Scripts.""" - import importlib - from pathlib import Path - - def reload_package_recursive(current_dir, module_dict): - for path in current_dir.iterdir(): - if "__init__" in str(path) or path.stem not in module_dict: - continue - if path.is_file() and path.suffix == ".py": - importlib.reload(module_dict[path.stem]) - elif path.is_dir(): - reload_package_recursive(path, module_dict[path.stem].__dict__) - - reload_package_recursive(Path(__file__).parent, module_dict_main) - - if ".import_dds" in locals(): - reload_package(locals()) - - from .ui import import_dds, export_dds, custom_properties, preferences - - def register(): - """Add addon.""" - preferences.register() - import_dds.register() - export_dds.register() - custom_properties.register() - - def unregister(): - """Remove addon.""" - preferences.unregister() - import_dds.unregister() - export_dds.unregister() - custom_properties.unregister() - -except ModuleNotFoundError as exc: - print(exc) - print('Failed to load the addon.') + +def reload_package(module_dict_main): + def reload_package_recursive(current_dir, module_dict): + for path in current_dir.iterdir(): + if "__init__" in str(path) or path.stem not in module_dict: + continue + if path.is_file() and path.suffix == ".py": + importlib.reload(module_dict[path.stem]) + elif path.is_dir(): + reload_package_recursive(path, module_dict[path.stem].__dict__) + + reload_package_recursive(Path(__file__).parent, module_dict_main) + + +if ".import_dds" in locals(): + reload_package(locals()) + + +def register(): + """Add addon.""" + preferences.register() + import_dds.register() + export_dds.register() + custom_properties.register() + + +def unregister(): + """Remove addon.""" + preferences.unregister() + import_dds.unregister() + export_dds.unregister() + custom_properties.unregister() + unload_texconv() diff --git a/addons/blender_dds_addon/directx/dds.py b/addons/blender_dds_addon/directx/dds.py index 0afa45b..423eab4 100644 --- a/addons/blender_dds_addon/directx/dds.py +++ b/addons/blender_dds_addon/directx/dds.py @@ -145,10 +145,17 @@ def read(f): raise RuntimeError(f"Unsupported DXGI format detected. ({fmt})\n" + ERR_MSG) head.dxgi_format = DXGI_FORMAT(fmt) # dxgiFormat - util.read_const_uint32(f, 3) # resourceDimension == 3 + resource_dimension = util.read_uint32(f) f.seek(4, 1) # miscFlag == 0 or 4 (0 for 2D textures, 4 for Cube maps) - util.read_const_uint32(f, 1) # arraySize == 1 + array_size = util.read_uint32(f) f.seek(4, 1) # miscFlag2 + + # Raise errors for unsupported files + if resource_dimension == 2: + raise RuntimeError("1D textures are unsupported.") + if resource_dimension == 4: + raise RuntimeError("3D textures are unsupported.") + util.check(array_size, 1, msg="Texture arrays are unsupported.") else: head.dxgi_format = head.get_dxgi_from_header() return head diff --git a/addons/blender_dds_addon/directx/texconv.py b/addons/blender_dds_addon/directx/texconv.py index 37c9e10..81e0457 100644 --- a/addons/blender_dds_addon/directx/texconv.py +++ b/addons/blender_dds_addon/directx/texconv.py @@ -5,6 +5,7 @@ And put the dll in the same directory as texconv.py. """ import ctypes +from ctypes.util import find_library import os import tempfile @@ -12,6 +13,43 @@ from .dxgi_format import DXGI_FORMAT from . import util +DLL = None + + +def get_dll_close(): + """Get dll function to unload DLL.""" + if util.is_windows(): + return ctypes.windll.kernel32.FreeLibrary + else: + dlpath = find_library("c") + if dlpath is None: + dlpath = find_library("System") + elif dlpath is None: + # Failed to find the path to stdlib. + return None + + try: + stdlib = ctypes.CDLL(dlpath) + return stdlib.dlclose + except OSError: + # Failed to load stdlib. + return None + + +def unload_texconv(): + global DLL + if DLL is None: + return + + dll_close = get_dll_close() + if dll_close is None: + raise RuntimeError("Failed to unload DLL. Restart Blender if you will remove the addon.") + + handle = DLL._handle + dll_close.argtypes = [ctypes.c_void_p] + dll_close(handle) + DLL = None + class Texconv: """Texture converter.""" @@ -19,6 +57,11 @@ def __init__(self, dll_path=None): self.load_dll(dll_path=dll_path) def load_dll(self, dll_path=None): + global DLL + if DLL is not None: + self.dll = DLL + return + if dll_path is None: file_path = os.path.realpath(__file__) if util.is_windows(): @@ -40,9 +83,17 @@ def load_dll(self, dll_path=None): raise RuntimeError(f'texconv not found. ({dll_path})') self.dll = ctypes.cdll.LoadLibrary(dll_path) + DLL = self.dll + + def unload_dll(self): + unload_texconv() + self.dll = None def convert_to_tga(self, file, out=None, cubemap_layout="h-cross", invert_normals=False, verbose=True): """Convert dds to tga.""" + if self.dll is None: + raise RuntimeError("texconv is unloaded.") + dds_header = DDSHeader.read_from_file(file) if dds_header.is_3d(): @@ -86,10 +137,13 @@ def convert_to_tga(self, file, out=None, cubemap_layout="h-cross", invert_normal def convert_to_dds(self, file, dds_fmt, out=None, invert_normals=False, no_mip=False, + image_filter="LINEAR", export_as_cubemap=False, cubemap_layout="h-cross", verbose=True, allow_slow_codec=False): """Convert texture to dds.""" + if self.dll is None: + raise RuntimeError("texconv is unloaded.") ext = util.get_ext(file) @@ -110,6 +164,8 @@ def convert_to_dds(self, file, dds_fmt, out=None, args = ['-f', dds_fmt] if no_mip: args += ['-m', '1'] + if image_filter != "LINEAR": + args += ["-if", image_filter] if ("BC5" in dds_fmt) and invert_normals: args += ['-inverty'] diff --git a/addons/blender_dds_addon/ui/custom_properties.py b/addons/blender_dds_addon/ui/custom_properties.py index 2ed5d4d..777306f 100644 --- a/addons/blender_dds_addon/ui/custom_properties.py +++ b/addons/blender_dds_addon/ui/custom_properties.py @@ -53,6 +53,17 @@ class DDSOptions(PropertyGroup): default=False, ) + image_filter: EnumProperty( + name='Image Filter', + description="Image filter for mipmap generation", + items=[ + ('POINT', 'Point', 'Nearest neighbor'), + ('LINEAR', 'Linear', 'Bilinear interpolation (or box filter)'), + ('CUBIC', 'Cubic', 'Bicubic interpolation'), + ], + default='LINEAR', + ) + allow_slow_codec: BoolProperty( name='Allow Slow Codec', description=("Allow to use CPU codec for BC6 and BC7.\n" diff --git a/addons/blender_dds_addon/ui/export_dds.py b/addons/blender_dds_addon/ui/export_dds.py index b5e05d1..d93f915 100644 --- a/addons/blender_dds_addon/ui/export_dds.py +++ b/addons/blender_dds_addon/ui/export_dds.py @@ -18,6 +18,7 @@ def save_dds(tex, file, dds_fmt, invert_normals=False, no_mip=False, + image_filter='LINEAR', allow_slow_codec=False, export_as_cubemap=False, cubemap_layout='h-cross', @@ -102,6 +103,7 @@ def save_temp_dds(tex, temp_dir, ext, fmt, texconv, verbose=True): temp_dds = texconv.convert_to_dds(temp, dds_fmt, out=temp_dir, invert_normals=invert_normals, no_mip=no_mip, + image_filter=image_filter, export_as_cubemap=export_as_cubemap, cubemap_layout=cubemap_layout, allow_slow_codec=allow_slow_codec, verbose=verbose) @@ -153,6 +155,7 @@ def export_as_dds(context, tex, file): save_dds(tex, file, dxgi, invert_normals=dds_options.invert_normals, no_mip=no_mip, + image_filter=dds_options.image_filter, allow_slow_codec=dds_options.allow_slow_codec, export_as_cubemap=is_cube, cubemap_layout=cubemap_layout) @@ -162,6 +165,7 @@ def put_export_options(context, layout): dds_options = context.scene.dds_options if not dds_properties_exist(): layout.prop(dds_options, 'dxgi_format') + layout.prop(dds_options, 'image_filter') layout.prop(dds_options, 'invert_normals') if not dds_properties_exist(): layout.prop(dds_options, 'no_mip') diff --git a/changelog.txt b/changelog.txt index fac75c9..db8a385 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +ver 0.2.1 +- Fixed a bug that Blender can't remove the addon after using it in the process. +- Added "Image Filter" option. (Possible to use cubic filter for mip generation.) +- Improved error messages for unsupported files. + ver 0.2.0 - Added "Import from a Directory" operation - Added "Export All Images" operation diff --git a/docs/README.md b/docs/README.md index 86d3c10..00fd7f8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# Blender-DDS-Addon v0.2.0 +# Blender-DDS-Addon v0.2.1 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![build](https://github.com/matyalatte/Blender-DDS-Addon/actions/workflows/build.yml/badge.svg) diff --git a/external/Texconv-Custom-DLL b/external/Texconv-Custom-DLL index f1020a6..3b9dc6e 160000 --- a/external/Texconv-Custom-DLL +++ b/external/Texconv-Custom-DLL @@ -1 +1 @@ -Subproject commit f1020a6e88329dac715fd5e5ee5c91447ce87de8 +Subproject commit 3b9dc6eae1e7325b52e9a04b4946e4143d8c4f4a diff --git a/tests/test_dds.py b/tests/test_dds.py index e32a97d..02e2a56 100644 --- a/tests/test_dds.py +++ b/tests/test_dds.py @@ -5,7 +5,7 @@ from blender_dds_addon.ui import import_dds from blender_dds_addon.ui import export_dds from blender_dds_addon.ui import custom_properties - +from blender_dds_addon.directx.texconv import unload_texconv import bpy bpy.utils.register_class(custom_properties.DDSCustomProperties) custom_properties.add_custom_props_for_dds() @@ -16,7 +16,16 @@ def get_test_dds(): return test_file -@pytest.mark.parametrize('export_format', ["BC4_UNORM", "B8G8R8A8_UNORM_SRGB", "R16G16B16A16_FLOAT"]) +def test_unload_empty_dll(): + unload_texconv() + + +def test_unload_dll(): + import_dds.load_dds(get_test_dds()) + unload_texconv() + + +@pytest.mark.parametrize("export_format", ["BC4_UNORM", "B8G8R8A8_UNORM_SRGB", "R16G16B16A16_FLOAT"]) def test_io(export_format): """Cehck if the addon can import and export dds.""" tex = import_dds.load_dds(get_test_dds()) @@ -44,3 +53,11 @@ def test_io_cubemap(): tex = import_dds.load_dds(os.path.join("tests", "cube.dds")) tex = export_dds.save_dds(tex, "saved.dds", "BC1_UNORM", export_as_cubemap=True) os.remove("saved.dds") + + +@pytest.mark.parametrize("image_filter", ["POINT", "CUBIC"]) +def test_io_filter(image_filter): + """Test filter options.""" + tex = import_dds.load_dds(get_test_dds()) + tex = export_dds.save_dds(tex, "saved.dds", "BC1_UNORM", image_filter=image_filter) + os.remove("saved.dds")