From 25f2c7fe20a50670218260a8af9709cd521b1f45 Mon Sep 17 00:00:00 2001 From: Isamu Mogi Date: Sun, 21 Jul 2024 22:39:17 +0900 Subject: [PATCH] feat: show progress for heavy task --- src/io_scene_vrm/common/progress.py | 78 +++++++++++++++++++ src/io_scene_vrm/editor/migration.py | 7 +- src/io_scene_vrm/editor/mtoon1/migration.py | 20 +++-- src/io_scene_vrm/exporter/vrm0_exporter.py | 71 ++++++++--------- .../importer/abstract_base_vrm_importer.py | 31 ++++---- src/io_scene_vrm/importer/vrm0_importer.py | 8 +- src/io_scene_vrm/importer/vrm1_importer.py | 6 +- src/io_scene_vrm/registration.py | 2 +- 8 files changed, 159 insertions(+), 64 deletions(-) create mode 100644 src/io_scene_vrm/common/progress.py diff --git a/src/io_scene_vrm/common/progress.py b/src/io_scene_vrm/common/progress.py new file mode 100644 index 000000000..ace644f02 --- /dev/null +++ b/src/io_scene_vrm/common/progress.py @@ -0,0 +1,78 @@ +import math +from collections.abc import Iterator +from contextlib import contextmanager +from typing import Final, Optional +from uuid import uuid4 + +from bpy.types import Context + +from .logging import get_logger + +logger = get_logger(__name__) + + +class PartialProgress: + def __init__( + self, progress: "Progress", partial_start_ratio: float, partial_end_ratio: float + ) -> None: + self.progress: Final = progress + self.partial_start_ratio: Final = partial_start_ratio + self.partial_end_ratio: Final = partial_end_ratio + + def update(self, ratio: float) -> None: + ratio = min(max(0.0, ratio), 1.0) + self.progress.update( + self.partial_start_ratio + + ratio * (self.partial_end_ratio - self.partial_start_ratio) + ) + + +class Progress: + active_progress_uuid: Optional[str] = None + + def __init__(self, context: Context, *, show_progress: bool) -> None: + self.context: Final = context + self.show_progress: Final = show_progress + self.uuid: Final = uuid4().hex + self.last_ratio = 0.0 + + def partial_progress(self, partial_end_ratio: float) -> PartialProgress: + partial_end_ratio = min(max(0.0, partial_end_ratio), 1.0) + return PartialProgress(self, self.last_ratio, partial_end_ratio) + + def update(self, ratio: float) -> None: + ratio = min(max(0.0, ratio), 1.0) + self.last_ratio = ratio + if self.active_progress_uuid != self.uuid: + logger.error( + "Progress.update() called from different progress active=%s self=%s", + self.active_progress_uuid, + self.uuid, + ) + return + + if not self.show_progress: + return + + # マウスカーソルが四桁の数値になり、0から9999までの値が表示できる領域がある + # しかし、進捗率をそのまま0から9999の数値に変換すると、下二桁の数値が頻繁に + # ラウンドトリップし進捗状況が分かりにくくなる。そのため、下二桁の表示領域 + # だけを利用し0から99の数値で進捗率を表示する + self.context.window_manager.progress_update(math.floor(ratio * 99)) + + +@contextmanager +def create_progress( + context: Context, *, show_progress: bool = True +) -> Iterator[Progress]: + saved_progress_uuid = Progress.active_progress_uuid + try: + if show_progress and Progress.active_progress_uuid is None: + context.window_manager.progress_begin(0, 9999) + progress = Progress(context, show_progress=show_progress) + Progress.active_progress_uuid = progress.uuid + yield progress + finally: + Progress.active_progress_uuid = saved_progress_uuid + if show_progress and Progress.active_progress_uuid is None: + context.window_manager.progress_end() diff --git a/src/io_scene_vrm/editor/migration.py b/src/io_scene_vrm/editor/migration.py index 645ea08e5..e3cd35587 100644 --- a/src/io_scene_vrm/editor/migration.py +++ b/src/io_scene_vrm/editor/migration.py @@ -106,7 +106,10 @@ def migrate(context: Optional[Context], armature_object_name: str) -> bool: def migrate_all_objects( - context: Context, *, skip_non_migrated_armatures: bool = False + context: Context, + *, + skip_non_migrated_armatures: bool = False, + show_progress: bool = False, ) -> None: for obj in context.blend_data.objects: if obj.type == "ARMATURE": @@ -124,7 +127,7 @@ def migrate_all_objects( VrmAddonSceneExtensionPropertyGroup.update_vrm0_material_property_names( context, context.scene.name ) - mtoon1_migration.migrate(context) + mtoon1_migration.migrate(context, show_progress=show_progress) validate_blend_file_compatibility(context) validate_blend_file_addon_compatibility(context) diff --git a/src/io_scene_vrm/editor/mtoon1/migration.py b/src/io_scene_vrm/editor/mtoon1/migration.py index cf743611e..cde8a3135 100644 --- a/src/io_scene_vrm/editor/mtoon1/migration.py +++ b/src/io_scene_vrm/editor/mtoon1/migration.py @@ -16,6 +16,7 @@ from ...common import convert, ops, shader, version from ...common.gl import GL_LINEAR, GL_NEAREST from ...common.logging import get_logger +from ...common.progress import create_progress from ..extension import get_material_extension from .property_group import ( GL_LINEAR_IMAGE_INTERPOLATIONS, @@ -54,12 +55,17 @@ def show_material_blender_4_2_warning_delay(material_name_lines: str) -> None: ) -def migrate(context: Context) -> None: +def migrate(context: Context, *, show_progress: bool = False) -> None: blender_4_2_migrated_material_names: list[str] = [] - for material in context.blend_data.materials: - if not material: - continue - migrate_material(context, material, blender_4_2_migrated_material_names) + + with create_progress(context, show_progress=show_progress) as progress: + for material_index, material in enumerate(context.blend_data.materials): + if not material: + continue + migrate_material(context, material, blender_4_2_migrated_material_names) + progress.update(float(material_index) / len(context.blend_data.materials)) + progress.update(1) + if ( blender_4_2_migrated_material_names and tuple(context.blend_data.version) < (4, 2) @@ -416,7 +422,9 @@ def migrate_material( ) typed_mtoon1.setup_drivers() - typed_mtoon1.addon_version = version.addon_version() + updated_addon_version = version.addon_version() + if tuple(typed_mtoon1.addon_version) != updated_addon_version: + typed_mtoon1.addon_version = updated_addon_version def backup_texture_info(texture_info: object) -> Optional[TextureInfoBackup]: diff --git a/src/io_scene_vrm/exporter/vrm0_exporter.py b/src/io_scene_vrm/exporter/vrm0_exporter.py index 06ead0ec8..29a0bcb9a 100644 --- a/src/io_scene_vrm/exporter/vrm0_exporter.py +++ b/src/io_scene_vrm/exporter/vrm0_exporter.py @@ -52,6 +52,7 @@ from ..common.legacy_gltf import TEXTURE_INPUT_NAMES from ..common.logging import get_logger from ..common.mtoon_unversioned import MtoonUnversioned +from ..common.progress import PartialProgress, create_progress from ..common.version import addon_version from ..common.vrm0.human_bone import HumanBoneSpecifications from ..common.workspace import save_workspace @@ -119,42 +120,35 @@ def armature_data(self) -> Armature: return armature_data def export_vrm(self) -> Optional[bytes]: - wm = self.context.window_manager - wm.progress_begin(0, 8) - - self.setup_mtoon_gltf_fallback_nodes(self.context, is_vrm0=True) - - try: - with ( - save_workspace(self.context), - self.setup_armature(), - self.clear_blend_shape_proxy_previews(self.armature_data), - self.hide_mtoon1_outline_geometry_nodes(self.context), - self.setup_pose( - self.armature, - self.armature_data, - get_armature_extension(self.armature_data).vrm0.humanoid, - ), - ): - wm.progress_update(1) - self.image_to_bin() - wm.progress_update(2) - self.make_scene_node_skin_dicts() - wm.progress_update(3) - self.material_to_dict() - wm.progress_update(4) - # 内部でcontext.view_layer.objects.active = meshをするので復元する - with save_workspace(self.context): - self.mesh_to_bin_and_dict() - wm.progress_update(5) - self.json_dict["scene"] = 0 - self.gltf_meta_to_dict() - wm.progress_update(6) - self.vrm_meta_to_dict() # colliderとかmetaとか.... - wm.progress_update(7) - self.pack() - finally: - wm.progress_end() + with ( + create_progress(self.context) as progress, + save_workspace(self.context), + self.setup_armature(), + self.clear_blend_shape_proxy_previews(self.armature_data), + self.hide_mtoon1_outline_geometry_nodes(self.context), + self.setup_pose( + self.armature, + self.armature_data, + get_armature_extension(self.armature_data).vrm0.humanoid, + ), + ): + self.setup_mtoon_gltf_fallback_nodes(self.context, is_vrm0=True) + progress.update(0.1) + self.image_to_bin() + progress.update(0.2) + self.make_scene_node_skin_dicts() + progress.update(0.3) + self.material_to_dict() + progress.update(0.4) + # 内部でcontext.view_layer.objects.active = meshをするので復元する + with save_workspace(self.context): + self.mesh_to_bin_and_dict(progress.partial_progress(0.8)) + self.json_dict["scene"] = 0 + self.gltf_meta_to_dict() + progress.update(0.9) + self.vrm_meta_to_dict() # colliderとかmetaとか.... + progress.update(1) + self.pack() return self.result @staticmethod @@ -2112,7 +2106,7 @@ def tessface_fan( ) return polys - def mesh_to_bin_and_dict(self) -> None: + def mesh_to_bin_and_dict(self, progress: PartialProgress) -> None: mesh_dicts = self.json_dict.get("meshes") if not isinstance(mesh_dicts, list): mesh_dicts = [] @@ -2699,6 +2693,9 @@ def mesh_to_bin_and_dict(self) -> None: mesh_dicts.append(mesh_dict) bm.free() + progress.update(float(mesh_index) / len(meshes)) + progress.update(1) + def exporter_name(self) -> str: v = addon_version() if environ.get("BLENDER_VRM_USE_TEST_EXPORTER_VERSION") == "true": diff --git a/src/io_scene_vrm/importer/abstract_base_vrm_importer.py b/src/io_scene_vrm/importer/abstract_base_vrm_importer.py index 363ba3912..107d50bf6 100644 --- a/src/io_scene_vrm/importer/abstract_base_vrm_importer.py +++ b/src/io_scene_vrm/importer/abstract_base_vrm_importer.py @@ -41,6 +41,7 @@ from ..common.gltf import FLOAT_NEGATIVE_MAX, FLOAT_POSITIVE_MAX, pack_glb, parse_glb from ..common.logging import get_logger from ..common.preferences import ImportPreferencesProtocol +from ..common.progress import PartialProgress, create_progress from ..common.workspace import save_workspace from ..editor.extension import get_armature_extension from .gltf2_addon_importer_user_extension import Gltf2AddonImporterUserExtension @@ -72,7 +73,7 @@ def __init__( self.mesh_object_names: dict[int, str] = {} @abstractmethod - def make_materials(self) -> None: + def make_materials(self, progress: PartialProgress) -> None: pass @abstractmethod @@ -84,34 +85,32 @@ def find_vrm_bone_node_indices(self) -> list[int]: pass def import_vrm(self) -> None: - wm = self.context.window_manager - wm.progress_begin(0, 7) try: - with save_workspace(self.context): - wm.progress_update(1) + with ( + create_progress(self.context) as progress, + save_workspace(self.context), + ): + progress.update(0.1) self.import_gltf2_with_indices() - wm.progress_update(2) + progress.update(0.2) if self.preferences.extract_textures_into_folder: self.extract_textures(repack=False) elif bpy.app.version < (3, 1): self.extract_textures(repack=True) else: self.assign_packed_image_filepaths() - wm.progress_update(3) + progress.update(0.3) self.use_fake_user_for_thumbnail() - wm.progress_update(4) + progress.update(0.4) if self.parse_result.vrm1_extension or self.parse_result.vrm0_extension: - self.make_materials() - wm.progress_update(5) + self.make_materials(progress.partial_progress(0.9)) if self.parse_result.vrm1_extension or self.parse_result.vrm0_extension: self.load_gltf_extensions() - wm.progress_update(6) - self.viewport_setup() + self.viewport_setup() + self.context.view_layer.update() + progress.update(1) finally: - try: - Gltf2AddonImporterUserExtension.clear_current_import_id() - finally: - wm.progress_end() + Gltf2AddonImporterUserExtension.clear_current_import_id() @property def armature_data(self) -> Armature: diff --git a/src/io_scene_vrm/importer/vrm0_importer.py b/src/io_scene_vrm/importer/vrm0_importer.py index 9825ecb2b..23b84b4b6 100644 --- a/src/io_scene_vrm/importer/vrm0_importer.py +++ b/src/io_scene_vrm/importer/vrm0_importer.py @@ -18,6 +18,7 @@ from ..common import convert, deep, shader from ..common.convert import Json from ..common.logging import get_logger +from ..common.progress import PartialProgress from ..common.version import addon_version from ..common.vrm0.human_bone import HumanBoneName, HumanBoneSpecifications from ..editor import make_armature, migration @@ -49,7 +50,7 @@ class Vrm0Importer(AbstractBaseVrmImporter): - def make_materials(self) -> None: + def make_materials(self, progress: PartialProgress) -> None: shader_to_build_method = { "VRM/MToon": self.build_material_from_mtoon0, "VRM/UnlitTransparentZWrite": self.build_material_from_transparent_z_write, @@ -69,6 +70,11 @@ def make_materials(self) -> None: self.reset_material(material) build_method(material, material_property) + progress.update( + float(index) / len(self.parse_result.vrm0_material_properties) + ) + + progress.update(1) def assign_mtoon0_texture( self, diff --git a/src/io_scene_vrm/importer/vrm1_importer.py b/src/io_scene_vrm/importer/vrm1_importer.py index 38c9a9345..128c20983 100644 --- a/src/io_scene_vrm/importer/vrm1_importer.py +++ b/src/io_scene_vrm/importer/vrm1_importer.py @@ -17,6 +17,7 @@ from ..common import convert, deep, ops, shader from ..common.convert import Json from ..common.logging import get_logger +from ..common.progress import PartialProgress from ..common.version import addon_version from ..common.vrm1 import human_bone as vrm1_human_bone from ..common.vrm1.human_bone import HumanBoneName, HumanBoneSpecifications @@ -363,13 +364,16 @@ def make_mtoon1_material( uv_animation_scroll_y_speed_factor ) - def make_materials(self) -> None: + def make_materials(self, progress: PartialProgress) -> None: material_dicts = self.parse_result.json_dict.get("materials") if not isinstance(material_dicts, list): + progress.update(1) return for index, material_dict in enumerate(material_dicts): if isinstance(material_dict, dict): self.make_mtoon1_material(index, material_dict) + progress.update(float(index) / len(material_dicts)) + progress.update(1) def find_vrm1_bone_node_indices(self) -> list[int]: result: list[int] = [] diff --git a/src/io_scene_vrm/registration.py b/src/io_scene_vrm/registration.py index ed1852c6e..e74942ed8 100644 --- a/src/io_scene_vrm/registration.py +++ b/src/io_scene_vrm/registration.py @@ -76,7 +76,7 @@ def setup(*, load_post: bool) -> None: context = bpy.context shader.add_shaders(context) - migration.migrate_all_objects(context) + migration.migrate_all_objects(context, show_progress=True) mtoon1_property_group.setup_drivers(context) subscription.setup_subscription(load_post=load_post)