From baa0391c5b74e59a8bd5d953ba9bdda2b0e0ddba Mon Sep 17 00:00:00 2001 From: Isamu Mogi Date: Wed, 2 Oct 2024 23:33:12 +0900 Subject: [PATCH] chore: immutable parser logic --- src/io_scene_vrm/common/deep.py | 3 +- .../importer/abstract_base_vrm_importer.py | 10 +- src/io_scene_vrm/importer/import_scene.py | 239 +++++++++++++++++- src/io_scene_vrm/importer/vrm0_importer.py | 4 +- src/io_scene_vrm/importer/vrm1_importer.py | 4 +- src/io_scene_vrm/importer/vrm_parser.py | 239 ------------------ 6 files changed, 239 insertions(+), 260 deletions(-) delete mode 100644 src/io_scene_vrm/importer/vrm_parser.py diff --git a/src/io_scene_vrm/common/deep.py b/src/io_scene_vrm/common/deep.py index 0c0f10a49..4eb94e2db 100644 --- a/src/io_scene_vrm/common/deep.py +++ b/src/io_scene_vrm/common/deep.py @@ -1,5 +1,6 @@ import difflib import math +from collections.abc import Mapping from json import dumps as json_dumps from typing import Union @@ -41,7 +42,7 @@ def make_json(v: object) -> Json: def get( - json: Json, + json: Union[Json, Mapping[str, Json]], attrs: list[Union[int, str]], default: Json = None, ) -> Json: 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 6fde573ec..4f7c98e57 100644 --- a/src/io_scene_vrm/importer/abstract_base_vrm_importer.py +++ b/src/io_scene_vrm/importer/abstract_base_vrm_importer.py @@ -101,13 +101,13 @@ def import_vrm(self) -> None: self.use_fake_user_for_thumbnail() progress.update(0.4) if ( - self.parse_result.vrm1_extension - or self.parse_result.vrm0_extension + self.parse_result.vrm1_extension_dict + or self.parse_result.vrm0_extension_dict ): self.make_materials(progress.partial_progress(0.9)) if ( - self.parse_result.vrm1_extension - or self.parse_result.vrm0_extension + self.parse_result.vrm1_extension_dict + or self.parse_result.vrm0_extension_dict ): self.load_gltf_extensions() progress.update(0.92) @@ -187,7 +187,7 @@ def use_fake_user_for_thumbnail(self) -> None: # のインデックスになっている # https://github.com/vrm-c/UniVRM/blob/v0.67.0/Assets/VRM/Runtime/IO/VRMImporterself.context.cs#L308 json_texture_index = deep.get( - self.parse_result.vrm0_extension, ["meta", "texture"] + self.parse_result.vrm0_extension_dict, ["meta", "texture"] ) if not isinstance(json_texture_index, int): return diff --git a/src/io_scene_vrm/importer/import_scene.py b/src/io_scene_vrm/importer/import_scene.py index 579561d66..6af328f4f 100644 --- a/src/io_scene_vrm/importer/import_scene.py +++ b/src/io_scene_vrm/importer/import_scene.py @@ -1,7 +1,9 @@ +from collections.abc import Mapping, Sequence from collections.abc import Set as AbstractSet +from dataclasses import dataclass from os import environ from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import bpy from bpy.app.translations import pgettext @@ -17,7 +19,9 @@ ) from bpy_extras.io_utils import ImportHelper -from ..common import ops, version +from ..common import deep, ops, version +from ..common.convert import Json +from ..common.gltf import parse_glb from ..common.logging import get_logger from ..common.preferences import ( ImportPreferencesProtocol, @@ -31,11 +35,10 @@ from ..editor.ops import VRM_OT_open_url_in_web_browser, layout_operator from ..editor.property_group import CollectionPropertyProtocol, StringPropertyGroup from .abstract_base_vrm_importer import AbstractBaseVrmImporter -from .license_validation import LicenseConfirmationRequiredError +from .license_validation import LicenseConfirmationRequiredError, validate_license from .vrm0_importer import Vrm0Importer from .vrm1_importer import Vrm1Importer from .vrm_animation_importer import VrmAnimationImporter -from .vrm_parser import VrmParser logger = get_logger(__name__) @@ -324,13 +327,7 @@ def import_vrm( *, license_validation: bool, ) -> set[str]: - parse_result = VrmParser( - filepath, - preferences.extract_textures_into_folder, - preferences.make_new_texture_folder, - license_validation=license_validation, - ).parse() - + parse_result = parse_vrm_json(filepath, license_validation=license_validation) if parse_result.spec_version_number >= (1,): vrm_importer: AbstractBaseVrmImporter = Vrm1Importer( context, @@ -528,3 +525,223 @@ def draw(self, context: Context) -> None: armature_object_name_candidates: CollectionPropertyProtocol[ # type: ignore[no-redef] StringPropertyGroup ] + + +@dataclass(frozen=True) +class Vrm0MaterialProperty: + name: str + shader: str + render_queue: Optional[int] + keyword_map: Mapping[str, bool] + tag_map: Mapping[str, str] + float_properties: Mapping[str, float] + vector_properties: Mapping[str, Sequence[float]] + texture_properties: Mapping[str, int] + + @staticmethod + def create(json_dict: Json) -> "Vrm0MaterialProperty": + fallback = Vrm0MaterialProperty( + name="Undefined", + shader="VRM_USE_GLTFSHADER", + render_queue=None, + keyword_map={}, + tag_map={}, + float_properties={}, + vector_properties={}, + texture_properties={}, + ) + if not isinstance(json_dict, dict): + return fallback + + name = json_dict.get("name") + if not isinstance(name, str): + name = fallback.name + + shader = json_dict.get("shader") + if not isinstance(shader, str): + shader = fallback.shader + + render_queue = json_dict.get("renderQueue") + if not isinstance(render_queue, int): + render_queue = fallback.render_queue + + raw_keyword_map = json_dict.get("keywordMap") + if isinstance(raw_keyword_map, dict): + keyword_map: Mapping[str, bool] = { + k: v for k, v in raw_keyword_map.items() if isinstance(v, bool) + } + else: + keyword_map = fallback.keyword_map + + raw_tag_map = json_dict.get("tagMap") + if isinstance(raw_tag_map, dict): + tag_map: Mapping[str, str] = { + k: v for k, v in raw_tag_map.items() if isinstance(v, str) + } + else: + tag_map = fallback.tag_map + + raw_float_properties = json_dict.get("floatProperties") + if isinstance(raw_float_properties, dict): + float_properties: Mapping[str, float] = { + k: float(v) + for k, v in raw_float_properties.items() + if isinstance(v, (float, int)) + } + else: + float_properties = fallback.float_properties + + raw_vector_properties = json_dict.get("vectorProperties") + if isinstance(raw_vector_properties, dict): + vector_properties: Mapping[str, Sequence[float]] = { + k: [ + vector_element + for vector_element in vector + if isinstance(vector_element, (float, int)) + ] + for k, vector in raw_vector_properties.items() + if isinstance(vector, list) + } + else: + vector_properties = fallback.vector_properties + + raw_texture_properties = json_dict.get("textureProperties") + if isinstance(raw_texture_properties, dict): + texture_properties: Mapping[str, int] = { + k: v for k, v in raw_texture_properties.items() if isinstance(v, int) + } + else: + texture_properties = fallback.texture_properties + + return Vrm0MaterialProperty( + name=name, + shader=shader, + render_queue=render_queue, + keyword_map=keyword_map, + tag_map=tag_map, + float_properties=float_properties, + vector_properties=vector_properties, + texture_properties=texture_properties, + ) + + +@dataclass(frozen=True) +class ParseResult: + filepath: Path + json_dict: Mapping[str, Json] + spec_version_number: tuple[int, int] + spec_version_str: str + spec_version_is_stable: bool + vrm0_extension_dict: Mapping[str, Json] + vrm1_extension_dict: Mapping[str, Json] + hips_node_index: Optional[int] + vrm0_material_properties: Sequence[Vrm0MaterialProperty] + + +def parse_vrm_json(filepath: Path, *, license_validation: bool) -> ParseResult: + json_dict, _ = parse_glb(filepath.read_bytes()) + + if license_validation: + validate_license(json_dict) + + vrm1_extension_dict = deep.get(json_dict, ["extensions", "VRMC_vrm"]) + vrm0_extension_dict = deep.get(json_dict, ["extensions", "VRM"]) + vrm0_material_properties: Sequence[Vrm0MaterialProperty] = [] + if isinstance(vrm1_extension_dict, dict): + ( + spec_version_number, + spec_version_str, + spec_version_is_stable, + hips_node_index, + ) = read_vrm1_extension(vrm1_extension_dict) + vrm0_extension_dict = {} + elif isinstance(vrm0_extension_dict, dict): + ( + spec_version_number, + spec_version_str, + spec_version_is_stable, + hips_node_index, + ) = read_vrm0_extension(vrm0_extension_dict) + vrm0_material_properties = read_vrm0_material_properties(json_dict) + vrm1_extension_dict = {} + else: + spec_version_number = (0, 0) + spec_version_str = "0.0" + spec_version_is_stable = True + hips_node_index = None + vrm0_extension_dict = {} + vrm1_extension_dict = {} + + return ParseResult( + filepath=filepath, + json_dict=json_dict, + spec_version_number=spec_version_number, + spec_version_str=spec_version_str, + spec_version_is_stable=spec_version_is_stable, + vrm0_extension_dict=vrm0_extension_dict, + vrm1_extension_dict=vrm1_extension_dict, + hips_node_index=hips_node_index, + vrm0_material_properties=vrm0_material_properties, + ) + + +def read_vrm0_extension( + vrm0_dict: dict[str, Json], +) -> tuple[tuple[int, int], str, bool, Optional[int]]: + spec_version_number = (0, 0) + spec_version_str = "0.0" + spec_version_is_stable = True + hips_node_index = None + + spec_version = vrm0_dict.get("specVersion") + if isinstance(spec_version, str): + spec_version_str = spec_version + + human_bones = deep.get(vrm0_dict, ["humanoid", "humanBones"], []) + if isinstance(human_bones, list): + for human_bone in human_bones: + if isinstance(human_bone, dict) and human_bone.get("bone") == "hips": + index = human_bone.get("node") + if isinstance(index, int): + hips_node_index = index + + return ( + spec_version_number, + spec_version_str, + spec_version_is_stable, + hips_node_index, + ) + + +def read_vrm1_extension( + vrm1_dict: dict[str, Json], +) -> tuple[tuple[int, int], str, bool, Optional[int]]: + spec_version_number = (1, 0) + spec_version_str = "1.0" + spec_version_is_stable = True + hips_node_index = deep.get(vrm1_dict, ["humanoid", "humanBones", "hips", "node"]) + if not isinstance(hips_node_index, int): + hips_node_index = None + return ( + spec_version_number, + spec_version_str, + spec_version_is_stable, + hips_node_index, + ) + + +def read_vrm0_material_properties( + json_dict: dict[str, Json], +) -> Sequence[Vrm0MaterialProperty]: + material_dicts = json_dict.get("materials") + if not isinstance(material_dicts, list): + return [] + return [ + Vrm0MaterialProperty.create( + deep.get( + json_dict, + ["extensions", "VRM", "materialProperties", index], + ) + ) + for index in range(len(material_dicts)) + ] diff --git a/src/io_scene_vrm/importer/vrm0_importer.py b/src/io_scene_vrm/importer/vrm0_importer.py index f67c01f03..d85d2748d 100644 --- a/src/io_scene_vrm/importer/vrm0_importer.py +++ b/src/io_scene_vrm/importer/vrm0_importer.py @@ -487,7 +487,7 @@ def load_gltf_extensions(self) -> None: if self.parse_result.spec_version_number >= (1, 0): return - vrm0_extension = self.parse_result.vrm0_extension + vrm0_extension = self.parse_result.vrm0_extension_dict addon_extension.addon_version = addon_version() @@ -1021,7 +1021,7 @@ def load_vrm0_secondary_animation( def find_vrm0_bone_node_indices(self) -> list[int]: result: list[int] = [] - vrm0_dict = self.parse_result.vrm0_extension + vrm0_dict = self.parse_result.vrm0_extension_dict first_person_bone_index = deep.get( vrm0_dict, ["firstPerson", "firstPersonBone"] diff --git a/src/io_scene_vrm/importer/vrm1_importer.py b/src/io_scene_vrm/importer/vrm1_importer.py index 05f7f02a0..63a8facc4 100644 --- a/src/io_scene_vrm/importer/vrm1_importer.py +++ b/src/io_scene_vrm/importer/vrm1_importer.py @@ -384,7 +384,7 @@ def make_materials(self, progress: PartialProgress) -> None: def find_vrm1_bone_node_indices(self) -> list[int]: result: list[int] = [] - vrm1_dict = self.parse_result.vrm1_extension + vrm1_dict = self.parse_result.vrm1_extension_dict human_bones_dict = deep.get(vrm1_dict, ["humanoid", "humanBones"]) if isinstance(human_bones_dict, dict): for human_bone_dict in human_bones_dict.values(): @@ -835,7 +835,7 @@ def load_gltf_extensions(self) -> None: vrm1 = addon_extension.vrm1 addon_extension.spec_version = addon_extension.SPEC_VERSION_VRM1 - vrm1_extension_dict = self.parse_result.vrm1_extension + vrm1_extension_dict = self.parse_result.vrm1_extension_dict addon_extension.addon_version = addon_version() diff --git a/src/io_scene_vrm/importer/vrm_parser.py b/src/io_scene_vrm/importer/vrm_parser.py deleted file mode 100644 index d922bd565..000000000 --- a/src/io_scene_vrm/importer/vrm_parser.py +++ /dev/null @@ -1,239 +0,0 @@ -# SPDX-License-Identifier: MIT -# SPDX-FileCopyrightText: 2018 iCyP - -import sys -from dataclasses import dataclass, field -from pathlib import Path -from typing import Optional, Union - -from ..common import deep -from ..common.convert import Json -from ..common.gltf import parse_glb -from ..common.logging import get_logger -from .license_validation import validate_license - -logger = get_logger(__name__) - - -@dataclass(frozen=True) -class Vrm0MaterialProperty: - name: str - shader: str - render_queue: Optional[int] - keyword_map: dict[str, bool] - tag_map: dict[str, str] - float_properties: dict[str, float] - vector_properties: dict[str, list[float]] - texture_properties: dict[str, int] - - @staticmethod - def create(json_dict: Json) -> "Vrm0MaterialProperty": - fallback = Vrm0MaterialProperty( - name="Undefined", - shader="VRM_USE_GLTFSHADER", - render_queue=None, - keyword_map={}, - tag_map={}, - float_properties={}, - vector_properties={}, - texture_properties={}, - ) - if not isinstance(json_dict, dict): - return fallback - - name = json_dict.get("name") - if not isinstance(name, str): - name = fallback.name - - shader = json_dict.get("shader") - if not isinstance(shader, str): - shader = fallback.shader - - render_queue = json_dict.get("renderQueue") - if not isinstance(render_queue, int): - render_queue = fallback.render_queue - - raw_keyword_map = json_dict.get("keywordMap") - if isinstance(raw_keyword_map, dict): - keyword_map = { - k: v for k, v in raw_keyword_map.items() if isinstance(v, bool) - } - else: - keyword_map = fallback.keyword_map - - raw_tag_map = json_dict.get("tagMap") - if isinstance(raw_tag_map, dict): - tag_map = {k: v for k, v in raw_tag_map.items() if isinstance(v, str)} - else: - tag_map = fallback.tag_map - - raw_float_properties = json_dict.get("floatProperties") - if isinstance(raw_float_properties, dict): - float_properties = { - k: float(v) - for k, v in raw_float_properties.items() - if isinstance(v, (float, int)) - } - else: - float_properties = fallback.float_properties - - raw_vector_properties = json_dict.get("vectorProperties") - if isinstance(raw_vector_properties, dict): - vector_properties: dict[str, list[float]] = {} - for k, v in raw_vector_properties.items(): - if not isinstance(v, list): - continue - float_v: list[float] = [] - ok = True - for e in v: - if not isinstance(e, (float, int)): - ok = False - break - float_v.append(float(e)) - if ok: - vector_properties[k] = float_v - else: - vector_properties = fallback.vector_properties - - raw_texture_properties = json_dict.get("textureProperties") - if isinstance(raw_texture_properties, dict): - texture_properties = { - k: v for k, v in raw_texture_properties.items() if isinstance(v, int) - } - else: - texture_properties = fallback.texture_properties - - return Vrm0MaterialProperty( - name=name, - shader=shader, - render_queue=render_queue, - keyword_map=keyword_map, - tag_map=tag_map, - float_properties=float_properties, - vector_properties=vector_properties, - texture_properties=texture_properties, - ) - - -@dataclass -class ImageProperties: - name: str - filepath: Path - filetype: str - - -@dataclass -class ParseResult: - filepath: Path - json_dict: dict[str, Json] = field(default_factory=dict) - spec_version_number: tuple[int, int] = (0, 0) - spec_version_str: str = "0.0" - spec_version_is_stable: bool = True - vrm0_extension: dict[str, Json] = field(init=False, default_factory=dict) - vrm1_extension: dict[str, Json] = field(init=False, default_factory=dict) - hips_node_index: Optional[int] = None - image_properties: list[ImageProperties] = field(init=False, default_factory=list) - vrm0_material_properties: list[Vrm0MaterialProperty] = field( - init=False, default_factory=list - ) - skins_joints_list: list[list[int]] = field(init=False, default_factory=list) - skins_root_node_list: list[int] = field(init=False, default_factory=list) - - -@dataclass -class VrmParser: - filepath: Path - extract_textures_into_folder: bool - make_new_texture_folder: bool - license_validation: bool - decoded_binary: list[list[Union[int, float, list[int], list[float]]]] = field( - init=False, default_factory=list - ) - json_dict: dict[str, Json] = field(init=False, default_factory=dict) - - def parse(self) -> ParseResult: - json_dict, _ = parse_glb(self.filepath.read_bytes()) - self.json_dict = json_dict - - if self.license_validation: - validate_license(self.json_dict) - - parse_result = ParseResult(filepath=self.filepath, json_dict=self.json_dict) - self.vrm_extension_read(parse_result) - self.material_read(parse_result) - - return parse_result - - def vrm_extension_read(self, parse_result: ParseResult) -> None: - vrm1_dict = deep.get(self.json_dict, ["extensions", "VRMC_vrm"]) - if isinstance(vrm1_dict, dict): - self.vrm1_extension_read(parse_result, vrm1_dict) - return - - vrm0_dict = deep.get(self.json_dict, ["extensions", "VRM"]) - if isinstance(vrm0_dict, dict): - self.vrm0_extension_read(parse_result, vrm0_dict) - - def vrm0_extension_read( - self, parse_result: ParseResult, vrm0_dict: dict[str, Json] - ) -> None: - spec_version = vrm0_dict.get("specVersion") - if isinstance(spec_version, str): - parse_result.spec_version_str = spec_version - parse_result.vrm0_extension = vrm0_dict - - human_bones = deep.get(vrm0_dict, ["humanoid", "humanBones"], []) - if not isinstance(human_bones, list): - message = "No human bones" - raise TypeError(message) - - hips_node_index: Optional[int] = None - for human_bone in human_bones: - if isinstance(human_bone, dict) and human_bone.get("bone") == "hips": - index = human_bone.get("node") - if isinstance(index, int): - hips_node_index = index - - if not isinstance(hips_node_index, int): - logger.warning("No hips bone index found") - return - - parse_result.hips_node_index = hips_node_index - - def vrm1_extension_read( - self, parse_result: ParseResult, vrm1_dict: dict[str, Json] - ) -> None: - parse_result.vrm1_extension = vrm1_dict - parse_result.spec_version_number = (1, 0) - parse_result.spec_version_is_stable = False - - hips_node_index = deep.get( - vrm1_dict, ["humanoid", "humanBones", "hips", "node"] - ) - if not isinstance(hips_node_index, int): - logger.warning("No hips bone index found") - return - parse_result.hips_node_index = hips_node_index - - def material_read(self, parse_result: ParseResult) -> None: - material_dicts = self.json_dict.get("materials") - if not isinstance(material_dicts, list): - return - for index in range(len(material_dicts)): - parse_result.vrm0_material_properties.append( - Vrm0MaterialProperty.create( - deep.get( - self.json_dict, - ["extensions", "VRM", "materialProperties", index], - ) - ) - ) - - -if __name__ == "__main__": - VrmParser( - Path(sys.argv[1]), - extract_textures_into_folder=True, - make_new_texture_folder=True, - license_validation=True, - ).parse()