feat: Add VRM Blender addon with complete import/export functionality
- Add core VRM addon infrastructure with manifest and registration - Add common utilities module with file system, logging, and conversion helpers - Add human bone mapper with support for multiple rigging standards (Mixamo, MMD, Unreal, Rigify, etc.) - Add VRM 0.x and 1.x format support with property groups and handlers - Add editor UI panels for VRM metadata, spring bones, and MToon materials - Add exporter with glTF2 extension support for VRM format serialization - Add importer with scene reconstruction and armature generation - Add MToon shader support with auto-setup and material migration - Add spring bone physics simulation with constraint handling - Add node constraint editor for advanced rigging control - Add comprehensive validation and error handling with user dialogs - Add scene watcher for real-time property synchronization - Add workspace management and preference system - Include Python cache files and Blender manifest configuration - This is the initial commit establishing the complete VRM addon ecosystem for Blender
This commit is contained in:
3
README.txt
Normal file
3
README.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# VRM Add-on for Blender
|
||||
|
||||
See https://vrm-addon-for-blender.info for more information.
|
||||
41
__init__.py
Normal file
41
__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2018 iCyP
|
||||
|
||||
from . import registration
|
||||
from .exporter import gltf2_export_user_extension
|
||||
from .importer import gltf2_import_user_extension
|
||||
|
||||
bl_info = {
|
||||
"name": "VRM format",
|
||||
"author": "saturday06, iCyP",
|
||||
"version": (
|
||||
3, # x-release-please-major
|
||||
17, # x-release-please-minor
|
||||
1, # x-release-please-patch
|
||||
),
|
||||
"location": "File > Import-Export",
|
||||
"description": "Import-Edit-Export VRM",
|
||||
"blender": (2, 93, 0),
|
||||
"warning": "",
|
||||
"support": "COMMUNITY",
|
||||
"wiki_url": "",
|
||||
"doc_url": "https://vrm-addon-for-blender.info",
|
||||
"tracker_url": "https://github.com/saturday06/VRM-Addon-for-Blender/issues",
|
||||
"category": "Import-Export",
|
||||
}
|
||||
|
||||
|
||||
def register() -> None:
|
||||
registration.register()
|
||||
|
||||
|
||||
def unregister() -> None:
|
||||
registration.unregister()
|
||||
|
||||
|
||||
class glTF2ImportUserExtension(gltf2_import_user_extension.glTF2ImportUserExtension):
|
||||
pass
|
||||
|
||||
|
||||
class glTF2ExportUserExtension(gltf2_export_user_extension.glTF2ExportUserExtension):
|
||||
pass
|
||||
BIN
__pycache__/__init__.cpython-311.pyc
Normal file
BIN
__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/registration.cpython-311.pyc
Normal file
BIN
__pycache__/registration.cpython-311.pyc
Normal file
Binary file not shown.
26
blender_manifest.toml
Normal file
26
blender_manifest.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "vrm"
|
||||
version = "3.17.1"
|
||||
|
||||
name = "VRM format"
|
||||
tagline = "VRM import, export and editing capabilities"
|
||||
maintainer = "Isamu Mogi <dyob@lunaport.net>"
|
||||
type = "add-on"
|
||||
|
||||
website = "https://vrm-addon-for-blender.info"
|
||||
|
||||
tags = ["Import-Export", "Animation", "Modeling", "Material", "Physics"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
blender_version_max = "5.1.0"
|
||||
|
||||
license = ["SPDX:MIT", "SPDX:GPL-3.0-or-later"]
|
||||
|
||||
copyright = [
|
||||
"2020 Isamu Mogi",
|
||||
"2018 iCyP",
|
||||
]
|
||||
|
||||
[permissions]
|
||||
files = "Import/export VRM from/to disk"
|
||||
1
common/__init__.py
Normal file
1
common/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
common/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
common/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/blender_manifest.cpython-311.pyc
Normal file
BIN
common/__pycache__/blender_manifest.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/char.cpython-311.pyc
Normal file
BIN
common/__pycache__/char.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/convert.cpython-311.pyc
Normal file
BIN
common/__pycache__/convert.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/convert_any.cpython-311.pyc
Normal file
BIN
common/__pycache__/convert_any.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/debug.cpython-311.pyc
Normal file
BIN
common/__pycache__/debug.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/deep.cpython-311.pyc
Normal file
BIN
common/__pycache__/deep.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/error_dialog.cpython-311.pyc
Normal file
BIN
common/__pycache__/error_dialog.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/fs.cpython-311.pyc
Normal file
BIN
common/__pycache__/fs.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/gl.cpython-311.pyc
Normal file
BIN
common/__pycache__/gl.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/gltf.cpython-311.pyc
Normal file
BIN
common/__pycache__/gltf.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/legacy_gltf.cpython-311.pyc
Normal file
BIN
common/__pycache__/legacy_gltf.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/logger.cpython-311.pyc
Normal file
BIN
common/__pycache__/logger.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/mtoon_unversioned.cpython-311.pyc
Normal file
BIN
common/__pycache__/mtoon_unversioned.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/preferences.cpython-311.pyc
Normal file
BIN
common/__pycache__/preferences.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/progress.cpython-311.pyc
Normal file
BIN
common/__pycache__/progress.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/rotation.cpython-311.pyc
Normal file
BIN
common/__pycache__/rotation.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/safe_removal.cpython-311.pyc
Normal file
BIN
common/__pycache__/safe_removal.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/scene_watcher.cpython-311.pyc
Normal file
BIN
common/__pycache__/scene_watcher.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/shader.cpython-311.pyc
Normal file
BIN
common/__pycache__/shader.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/version.cpython-311.pyc
Normal file
BIN
common/__pycache__/version.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/__pycache__/workspace.cpython-311.pyc
Normal file
BIN
common/__pycache__/workspace.cpython-311.pyc
Normal file
Binary file not shown.
82
common/blender_manifest.py
Normal file
82
common/blender_manifest.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BlenderManifest:
|
||||
id: str
|
||||
version: tuple[int, int, int]
|
||||
blender_version_min: tuple[int, int, int]
|
||||
blender_version_max: Optional[tuple[int, int, int]]
|
||||
|
||||
@classmethod
|
||||
def default_blender_manifest_path(cls) -> Path:
|
||||
return Path(__file__).parent.parent / "blender_manifest.toml"
|
||||
|
||||
@classmethod
|
||||
def read_str(cls, blender_manifest: str, key: str) -> Optional[str]:
|
||||
# When the version of Python used by the minimum supported version of Blender
|
||||
# exceeds 3.11, it is rewritten in the tomli library.
|
||||
lines = list(map(str.strip, blender_manifest.splitlines()))
|
||||
|
||||
pattern = key + r' = "([^"]+)"'
|
||||
for line in lines:
|
||||
match = re.fullmatch(pattern, line)
|
||||
if not match:
|
||||
continue
|
||||
return match[1]
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def read_3_tuple_version(
|
||||
cls, blender_manifest: str, key: str
|
||||
) -> Optional[tuple[int, int, int]]:
|
||||
# When the version of Python used by the minimum supported version of Blender
|
||||
# exceeds 3.11, it is rewritten in the tomli library.
|
||||
lines = list(map(str.strip, blender_manifest.splitlines()))
|
||||
|
||||
pattern = key + r' = "(\d+)\.(\d+)\.(\d+)"'
|
||||
for line in lines:
|
||||
match = re.fullmatch(pattern, line)
|
||||
if not match:
|
||||
continue
|
||||
return (int(match[1]), int(match[2]), int(match[3]))
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def read(cls, blender_manifest: Optional[str] = None) -> "BlenderManifest":
|
||||
if blender_manifest is None:
|
||||
blender_manifest = cls.default_blender_manifest_path().read_text(
|
||||
encoding="UTF-8"
|
||||
)
|
||||
|
||||
addon_id = cls.read_str(blender_manifest, "id")
|
||||
if addon_id is None:
|
||||
message = "'id' was not found in blender manifest"
|
||||
raise ValueError(message)
|
||||
|
||||
version = cls.read_3_tuple_version(blender_manifest, "version")
|
||||
if version is None:
|
||||
message = "'version' was not found in blender manifest"
|
||||
raise ValueError(message)
|
||||
|
||||
blender_version_min = cls.read_3_tuple_version(
|
||||
blender_manifest, "blender_version_min"
|
||||
)
|
||||
if blender_version_min is None:
|
||||
message = "'blender_version_min' was not found in blender manifest"
|
||||
raise ValueError(message)
|
||||
|
||||
return BlenderManifest(
|
||||
id=addon_id,
|
||||
version=version,
|
||||
blender_version_min=blender_version_min,
|
||||
blender_version_max=cls.read_3_tuple_version(
|
||||
blender_manifest, "blender_version_max"
|
||||
),
|
||||
)
|
||||
16
common/char.py
Normal file
16
common/char.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
INTERNAL_NAME_PREFIX: Final = "\N{FULLWIDTH BROKEN BAR}"
|
||||
"""
|
||||
The letter to be prefixed when naming internal invisible objects. It was chosen so that
|
||||
the sort order would be backward and also so that it would be easy to identify the
|
||||
object as internal.
|
||||
"""
|
||||
|
||||
DISABLE_TRANSLATION: Final = "\N{ZERO WIDTH SPACE}"
|
||||
|
||||
FULLWIDTH_ASCII_TO_ASCII_MAP: Final[Mapping[str, str]] = {
|
||||
chr(ascii_char + 0x0000_FEE0): chr(ascii_char) for ascii_char in range(0x21, 0x7E)
|
||||
}
|
||||
297
common/convert.py
Normal file
297
common/convert.py
Normal file
@@ -0,0 +1,297 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import math
|
||||
from collections.abc import Iterator, Mapping, Sequence
|
||||
from sys import float_info
|
||||
from typing import Optional, Union
|
||||
|
||||
from . import convert_any
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
Json = Union[
|
||||
None,
|
||||
bool,
|
||||
int,
|
||||
float,
|
||||
str,
|
||||
list["Json"],
|
||||
dict[str, "Json"],
|
||||
]
|
||||
|
||||
|
||||
def iterator_or_none(v: object) -> Optional[Iterator[object]]:
|
||||
try:
|
||||
# "isinstance(v, Iterable)" doesn't work.
|
||||
# https://github.com/python/cpython/blob/3.9/Doc/library/collections.abc.rst?plain=1#L126-L127
|
||||
iterator = iter(v) # type: ignore[call-overload]
|
||||
except TypeError:
|
||||
return None
|
||||
return convert_any.iterator_to_object_iterator(iterator)
|
||||
|
||||
|
||||
def sequence_or_none(
|
||||
sequence_object: object,
|
||||
) -> Optional[Sequence[object]]:
|
||||
sequence = sequence_object
|
||||
if not isinstance(sequence, Sequence):
|
||||
return None
|
||||
iterator = iterator_or_none(sequence_object)
|
||||
if iterator is None:
|
||||
return None
|
||||
return list(iterator)
|
||||
|
||||
|
||||
def mapping_or_none(
|
||||
mapping_object: object,
|
||||
) -> Optional[Mapping[object, object]]:
|
||||
mapping = mapping_object
|
||||
if not isinstance(mapping, Mapping):
|
||||
return None
|
||||
key_iterator = iterator_or_none(mapping_object)
|
||||
if key_iterator is None:
|
||||
return None
|
||||
return {key: convert_any.to_object(mapping[key]) for key in key_iterator}
|
||||
|
||||
|
||||
def vrm_json_vector3_to_tuple(
|
||||
value: Json,
|
||||
) -> Optional[tuple[float, float, float]]:
|
||||
if not isinstance(value, Mapping):
|
||||
return None
|
||||
x = float_or(value.get("x"), 0.0)
|
||||
y = float_or(value.get("y"), 0.0)
|
||||
z = float_or(value.get("z"), 0.0)
|
||||
return (x, y, z)
|
||||
|
||||
|
||||
def vrm_json_curve_to_list(curve: Json) -> Optional[list[float]]:
|
||||
if not isinstance(curve, list):
|
||||
return None
|
||||
values: list[float] = [float_or(v, 0.0) for v in curve]
|
||||
while len(values) < 8:
|
||||
values.append(0)
|
||||
while len(values) > 8:
|
||||
values.pop()
|
||||
return values
|
||||
|
||||
|
||||
def vrm_json_array_to_float_vector(
|
||||
input_values: Json, defaults: list[float]
|
||||
) -> list[float]:
|
||||
if not isinstance(input_values, list):
|
||||
return defaults
|
||||
|
||||
output_values: list[float] = []
|
||||
for index, default in enumerate(defaults):
|
||||
if index >= len(input_values):
|
||||
output_values.append(default)
|
||||
continue
|
||||
|
||||
input_value = float_or(input_values[index], default)
|
||||
output_values.append(input_value)
|
||||
|
||||
return output_values
|
||||
|
||||
|
||||
BPY_TRACK_AXIS_TO_VRM_AIM_AXIS = {
|
||||
"TRACK_X": "PositiveX",
|
||||
"TRACK_Y": "PositiveY",
|
||||
"TRACK_Z": "PositiveZ",
|
||||
"TRACK_NEGATIVE_X": "NegativeX",
|
||||
"TRACK_NEGATIVE_Y": "NegativeY",
|
||||
"TRACK_NEGATIVE_Z": "NegativeZ",
|
||||
}
|
||||
|
||||
VRM_AIM_AXIS_TO_BPY_TRACK_AXIS = {
|
||||
v: k for k, v in BPY_TRACK_AXIS_TO_VRM_AIM_AXIS.items()
|
||||
}
|
||||
|
||||
|
||||
def mtoon_shading_toony_1_to_0(shading_toony: float, shading_shift: float) -> float:
|
||||
base = 2 - shading_toony + shading_shift
|
||||
if abs(base) < float_info.epsilon:
|
||||
# https://github.com/Santarh/MToon/blob/43f02fe8c9b19c3cf9f3238003b7d8b332933833/MToon/Resources/Shaders/MToon.shader#L17
|
||||
return 0.9
|
||||
return max(0, min((shading_toony + shading_shift) / base, 1))
|
||||
|
||||
|
||||
def mtoon_shading_shift_1_to_0(shading_toony: float, shading_shift: float) -> float:
|
||||
return max(-1, min(shading_toony - shading_shift - 1, 1))
|
||||
|
||||
|
||||
def mtoon_gi_equalization_to_intensity(gi_equalization: float) -> float:
|
||||
return max(0, min(1, 1 - gi_equalization))
|
||||
|
||||
|
||||
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L10-L19
|
||||
def mtoon_shading_toony_0_to_1(
|
||||
shading_toony_0x: float, shading_shift_0x: float
|
||||
) -> float:
|
||||
(range_min, range_max) = get_shading_range_0x(shading_toony_0x, shading_shift_0x)
|
||||
return max(0, min(1, (2 - (range_max - range_min)) * 0.5))
|
||||
|
||||
|
||||
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L21-L30
|
||||
def mtoon_shading_shift_0_to_1(
|
||||
shading_toony_0x: float, shading_shift_0x: float
|
||||
) -> float:
|
||||
(range_min, range_max) = get_shading_range_0x(shading_toony_0x, shading_shift_0x)
|
||||
return max(-1, min(1, ((range_max + range_min) * 0.5 * -1)))
|
||||
|
||||
|
||||
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L32-L38
|
||||
def mtoon_intensity_to_gi_equalization(gi_intensity_0x: float) -> float:
|
||||
return max(0, min(1, 1 - gi_intensity_0x))
|
||||
|
||||
|
||||
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L40-L46
|
||||
def get_shading_range_0x(
|
||||
shading_toony_0x: float, shading_shift_0x: float
|
||||
) -> tuple[float, float]:
|
||||
range_min = shading_shift_0x
|
||||
range_max = (1 - shading_toony_0x) + shading_toony_0x * shading_shift_0x
|
||||
return (range_min, range_max)
|
||||
|
||||
|
||||
def float_or_none(
|
||||
v: object,
|
||||
min_value: float = -float_info.max,
|
||||
max_value: float = float_info.max,
|
||||
) -> Optional[float]:
|
||||
if isinstance(v, float):
|
||||
if math.isnan(v):
|
||||
return None
|
||||
return max(min_value, min(v, max_value))
|
||||
|
||||
if isinstance(v, bool):
|
||||
return 1.0 if v else 0.0
|
||||
|
||||
if isinstance(v, int):
|
||||
return max(min_value, min(float(v), max_value))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def float_or(
|
||||
v: object,
|
||||
default: float,
|
||||
min_value: float = -float_info.max,
|
||||
max_value: float = float_info.max,
|
||||
) -> float:
|
||||
float_or_none_value = float_or_none(v, min_value, max_value)
|
||||
if float_or_none_value is not None:
|
||||
return float_or_none_value
|
||||
|
||||
if min_value <= default <= max_value:
|
||||
return default
|
||||
|
||||
logger.warning(
|
||||
"Default float value %s is out of range [%s, %s]", default, min_value, max_value
|
||||
)
|
||||
|
||||
return max(min_value, min(default, max_value))
|
||||
|
||||
|
||||
def float4_or_none(value4: object) -> Optional[tuple[float, float, float, float]]:
|
||||
value4_iterator = iterator_or_none(value4)
|
||||
if value4_iterator is None:
|
||||
return None
|
||||
|
||||
result: list[float] = []
|
||||
for value in value4_iterator:
|
||||
if len(result) == 4:
|
||||
return None
|
||||
float_value = float_or_none(value)
|
||||
if float_value is None:
|
||||
return None
|
||||
result.append(float_value)
|
||||
if len(result) != 4:
|
||||
return None
|
||||
return (result[0], result[1], result[2], result[3])
|
||||
|
||||
|
||||
def float4_or(
|
||||
value4: object, default: tuple[float, float, float, float]
|
||||
) -> tuple[float, float, float, float]:
|
||||
float4 = float4_or_none(value4)
|
||||
return float4 if float4 is not None else default
|
||||
|
||||
|
||||
def float3_or_none(value3: object) -> Optional[tuple[float, float, float]]:
|
||||
value3_iterator = iterator_or_none(value3)
|
||||
if value3_iterator is None:
|
||||
return None
|
||||
|
||||
result: list[float] = []
|
||||
for value in value3_iterator:
|
||||
if len(result) == 3:
|
||||
return None
|
||||
float_value = float_or_none(value)
|
||||
if float_value is None:
|
||||
return None
|
||||
result.append(float_value)
|
||||
if len(result) != 3:
|
||||
return None
|
||||
return (result[0], result[1], result[2])
|
||||
|
||||
|
||||
def float3_or(
|
||||
value3: object, default: tuple[float, float, float]
|
||||
) -> tuple[float, float, float]:
|
||||
float3 = float3_or_none(value3)
|
||||
return float3 if float3 is not None else default
|
||||
|
||||
|
||||
def float2_or_none(value2: object) -> Optional[tuple[float, float]]:
|
||||
value2_iterator = iterator_or_none(value2)
|
||||
if value2_iterator is None:
|
||||
return None
|
||||
result: list[float] = []
|
||||
for value in value2_iterator:
|
||||
if len(result) == 2:
|
||||
return None
|
||||
float_value = float_or_none(value)
|
||||
if float_value is None:
|
||||
return None
|
||||
result.append(float_value)
|
||||
if len(result) != 2:
|
||||
return None
|
||||
return (result[0], result[1])
|
||||
|
||||
|
||||
def float2_or(value2: object, default: tuple[float, float]) -> tuple[float, float]:
|
||||
float2 = float2_or_none(value2)
|
||||
return float2 if float2 is not None else default
|
||||
|
||||
|
||||
def str_or(v: object, default: str) -> str:
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
return default
|
||||
|
||||
|
||||
def axis_blender_to_gltf(vector3: Sequence[float]) -> tuple[float, float, float]:
|
||||
return (
|
||||
-vector3[0],
|
||||
vector3[2],
|
||||
vector3[1],
|
||||
)
|
||||
|
||||
|
||||
def linear_to_srgb(
|
||||
non_color: Sequence[float],
|
||||
) -> Sequence[float]:
|
||||
return [
|
||||
math.pow(channel_value, 1.0 / 2.2) if channel_index < 3 else channel_value
|
||||
for channel_index, channel_value in enumerate(non_color)
|
||||
]
|
||||
|
||||
|
||||
def srgb_to_linear(
|
||||
srgb_color: Sequence[float],
|
||||
) -> Sequence[float]:
|
||||
return [
|
||||
math.pow(channel_value, 2.2) if channel_index < 3 else channel_value
|
||||
for channel_index, channel_value in enumerate(srgb_color)
|
||||
]
|
||||
36
common/convert_any.py
Normal file
36
common/convert_any.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
"""A module provides a function to convert a variable of type Any to a concrete type.
|
||||
|
||||
Inevitably, variables of type Any may occur, and such variables cannot be handled
|
||||
in type checkers in strict mode. Any is allowed only in the module here.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
from typing import (
|
||||
Any, # Any is allowed only in the module here.
|
||||
Optional,
|
||||
)
|
||||
|
||||
|
||||
def to_object( # type: ignore[explicit-any]
|
||||
any_object: Any, # noqa: ANN401 # Any is allowed only in the module here.
|
||||
) -> object:
|
||||
# Interpret Unknown as object
|
||||
# https://github.com/microsoft/pyright/issues/3650
|
||||
if not isinstance(any_object, object):
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import assert_never
|
||||
|
||||
assert_never(any_object)
|
||||
raise TypeError
|
||||
return any_object
|
||||
|
||||
|
||||
def iterator_to_object_iterator( # type: ignore[explicit-any]
|
||||
any_iterator: Any, # noqa: ANN401 # Any is allowed only in the module here.
|
||||
) -> Optional[Iterator[object]]:
|
||||
any_iterator_without_partial_type_narrowing = any_iterator
|
||||
if not isinstance(any_iterator, Iterator):
|
||||
return None
|
||||
return map(to_object, any_iterator_without_partial_type_narrowing)
|
||||
60
common/debug.py
Normal file
60
common/debug.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import math
|
||||
from collections.abc import Sequence
|
||||
from typing import Union
|
||||
|
||||
from mathutils import Matrix, Quaternion, Vector
|
||||
|
||||
|
||||
def dump(v: Union[Matrix, Vector, Quaternion, float]) -> str:
|
||||
if isinstance(v, (float, int)):
|
||||
return str(v)
|
||||
|
||||
if isinstance(v, Matrix):
|
||||
t, r, s = v.decompose()
|
||||
return f"Matrix(T={dump(t)},R={dump(r)},S={dump(s)})"
|
||||
|
||||
if isinstance(v, Vector):
|
||||
return f"({v.x:.3f},{v.y:.3f},{v.z:.3f})"
|
||||
|
||||
x, y, z = (round(math.degrees(xyz)) for xyz in v.to_euler()[:])
|
||||
return f"Euler({x},{y},{z})"
|
||||
|
||||
|
||||
def assert_vector3_equals(
|
||||
expected: Vector, actual: Sequence[float], message: str
|
||||
) -> None:
|
||||
if len(actual) != 3:
|
||||
message = f"actual length is not 3: {actual}"
|
||||
raise AssertionError(message)
|
||||
|
||||
threshold = 0.0001
|
||||
if abs(expected[0] - actual[0]) > threshold:
|
||||
message = f"{message}: {tuple(expected)} is different from {tuple(actual)}"
|
||||
raise AssertionError(message)
|
||||
if abs(expected[1] - actual[1]) > threshold:
|
||||
message = f"{message}: {tuple(expected)} is different from {tuple(actual)}"
|
||||
raise AssertionError(message)
|
||||
if abs(expected[2] - actual[2]) > threshold:
|
||||
message = f"{message}: {tuple(expected)} is different from {tuple(actual)}"
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def cleanse_modules() -> None:
|
||||
"""Search for your plugin modules in blender python sys.modules and remove them.
|
||||
|
||||
To support reload properly, try to access a package var, if it's there,
|
||||
reload everything.
|
||||
|
||||
This function may cause errors that are difficult to investigate. Please use with
|
||||
caution. See also:
|
||||
https://github.com/saturday06/VRM-Addon-for-Blender/issues/506#issuecomment-2183766778
|
||||
"""
|
||||
import sys
|
||||
|
||||
all_modules = sys.modules
|
||||
all_modules = dict(sorted(all_modules.items(), key=lambda x: x[0])) # sort them
|
||||
|
||||
for k in all_modules:
|
||||
if k == __name__ or k.startswith(__name__ + "."):
|
||||
del sys.modules[k]
|
||||
146
common/deep.py
Normal file
146
common/deep.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import difflib
|
||||
import math
|
||||
from collections.abc import Mapping
|
||||
from json import dumps as json_dumps
|
||||
|
||||
from . import convert
|
||||
from .convert import Json
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def make_json(v: object) -> Json:
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, int):
|
||||
return v
|
||||
if isinstance(v, float):
|
||||
if not math.isfinite(v):
|
||||
logger.warning("%s %s is non-finite value", v, type(v))
|
||||
return 0.0
|
||||
return v
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
|
||||
mapping = convert.mapping_or_none(v)
|
||||
if mapping is not None:
|
||||
dict_result: dict[str, Json] = {}
|
||||
for key, value in mapping.items():
|
||||
if isinstance(key, str):
|
||||
dict_result[key] = make_json(value)
|
||||
continue
|
||||
logger.warning("%s %s is unrecognized type for dict key", key, type(key))
|
||||
return dict_result
|
||||
|
||||
iterator = convert.iterator_or_none(v)
|
||||
if iterator is not None:
|
||||
return [make_json(x) for x in iterator]
|
||||
|
||||
logger.warning("%s %s is unrecognized type", v, type(v))
|
||||
return None
|
||||
|
||||
|
||||
def make_json_dict(v: Mapping[str, object]) -> dict[str, Json]:
|
||||
return {key: make_json(value) for key, value in v.items()}
|
||||
|
||||
|
||||
def diff(
|
||||
left: Json,
|
||||
right: Json,
|
||||
float_tolerance: float = 0,
|
||||
path: str = "",
|
||||
) -> list[str]:
|
||||
if isinstance(left, list):
|
||||
if not isinstance(right, list):
|
||||
return [f"{path}: left is list but right is {type(right)}"]
|
||||
if len(left) != len(right):
|
||||
result = [
|
||||
f"{path}: left length is {len(left)} but right length is {len(right)}"
|
||||
]
|
||||
|
||||
right_json_str = json_dumps(right, indent=4, sort_keys=True)
|
||||
right_json_lines = right_json_str.splitlines(keepends=True)
|
||||
left_json_str = json_dumps(left, indent=4, sort_keys=True)
|
||||
left_json_lines = left_json_str.splitlines(keepends=True)
|
||||
|
||||
if len(right_json_lines) > 1000 or len(left_json_lines) > 1000:
|
||||
return result
|
||||
|
||||
unified_diff = [
|
||||
line.rstrip()
|
||||
for line in difflib.unified_diff(
|
||||
right_json_lines,
|
||||
left_json_lines,
|
||||
f"{path}/right",
|
||||
f"{path}/left",
|
||||
)
|
||||
]
|
||||
if len(unified_diff) > 1000:
|
||||
return result
|
||||
return result + unified_diff
|
||||
diffs: list[str] = []
|
||||
for i, (left_child, right_child) in enumerate(zip(left, right)):
|
||||
diffs.extend(diff(left_child, right_child, float_tolerance, f"{path}[{i}]"))
|
||||
return diffs
|
||||
|
||||
if isinstance(left, dict):
|
||||
if not isinstance(right, dict):
|
||||
return [f"{path}: left is dict but right is {type(right)}"]
|
||||
diffs = []
|
||||
for key in sorted(set(list(left.keys()) + list(right.keys()))):
|
||||
if key not in left:
|
||||
diffs.append(f'{path}: {key} not in left. right["{key}"]={right[key]}')
|
||||
continue
|
||||
if key not in right:
|
||||
diffs.append(f'{path}: {key} not in right, left["{key}"]={left[key]}')
|
||||
continue
|
||||
diffs.extend(
|
||||
diff(left[key], right[key], float_tolerance, f'{path}["{key}"]')
|
||||
)
|
||||
return diffs
|
||||
|
||||
if isinstance(left, bool):
|
||||
if not isinstance(right, bool):
|
||||
return [f"{path}: left is bool but right is {type(right)}"]
|
||||
if left != right:
|
||||
return [f"{path}: left is {left} but right is {right}"]
|
||||
return []
|
||||
|
||||
if isinstance(left, str):
|
||||
if not isinstance(right, str):
|
||||
return [f"{path}: left is str but right is {type(right)}"]
|
||||
if left != right:
|
||||
return [f'{path}: left is "{left}" but right is "{right}"']
|
||||
return []
|
||||
|
||||
if left is None and right is not None:
|
||||
return [f"{path}: left is None but right is {type(right)}"]
|
||||
|
||||
if isinstance(left, int) and isinstance(right, int):
|
||||
if left != right:
|
||||
return [f"{path}: left is {left} but right is {right}"]
|
||||
return []
|
||||
|
||||
if isinstance(left, (int, float)) and isinstance(right, (int, float)):
|
||||
if (isinstance(left, float) and not math.isfinite(left)) or (
|
||||
isinstance(right, float) and not math.isfinite(right)
|
||||
):
|
||||
return [
|
||||
f"{path}: left is {left}"
|
||||
+ f" but right is {right}, They are not comparable numbers"
|
||||
]
|
||||
|
||||
error = math.fabs(float(left) - float(right))
|
||||
if error > float_tolerance:
|
||||
return [
|
||||
f"{path}: left is {float(left):20.17f}"
|
||||
+ f" but right is {float(right):20.17f}, error={error:19.17f}"
|
||||
]
|
||||
return []
|
||||
|
||||
message = f"{path}: unexpected type left={type(left)} right={type(right)}"
|
||||
raise ValueError(message)
|
||||
242
common/error_dialog.py
Normal file
242
common/error_dialog.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
|
||||
import contextlib
|
||||
import datetime
|
||||
import getpass
|
||||
import platform
|
||||
import sys
|
||||
import sysconfig
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Set as AbstractSet
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
import bpy
|
||||
from bpy.props import CollectionProperty, IntProperty, StringProperty
|
||||
from bpy.types import (
|
||||
Context,
|
||||
Event,
|
||||
Operator,
|
||||
PropertyGroup,
|
||||
UILayout,
|
||||
UIList,
|
||||
)
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
|
||||
from ..common import ops
|
||||
from ..common.logger import get_logger
|
||||
from ..common.version import get_addon_version
|
||||
from ..editor.ops import VRM_OT_open_url_in_web_browser, layout_operator
|
||||
from ..editor.property_group import CollectionPropertyProtocol
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def show_error_dialog(
|
||||
title: str,
|
||||
lines: Union[str, Sequence[str]],
|
||||
*,
|
||||
append_environment: bool = True,
|
||||
) -> set[str]:
|
||||
if isinstance(lines, str):
|
||||
lines = list(map(str.rstrip, lines.splitlines()))
|
||||
while lines and not lines[0].strip():
|
||||
del lines[0]
|
||||
while lines and not lines[-1].strip():
|
||||
del lines[-1]
|
||||
else:
|
||||
lines = list(lines)
|
||||
if not lines:
|
||||
return {"CANCELLED"}
|
||||
if append_environment:
|
||||
if lines:
|
||||
lines[0] = f"Python: {lines[0]}"
|
||||
|
||||
os_name = None
|
||||
platform_system, _, _ = platform.system_alias(
|
||||
platform.system(), platform.release(), platform.version()
|
||||
)
|
||||
if sys.version_info >= (3, 10) and platform_system not in ["Windows", "Darwin"]:
|
||||
# https://github.com/python/cpython/blob/v3.10.18/Doc/library/platform.rst?plain=1#L271-L272
|
||||
with contextlib.suppress(OSError):
|
||||
os_name = platform.freedesktop_os_release().get("PRETTY_NAME")
|
||||
|
||||
if not os_name:
|
||||
os_name = platform_system
|
||||
|
||||
python_version = (
|
||||
platform.python_implementation()
|
||||
+ " "
|
||||
+ ".".join(map(str, sys.version_info[:3]))
|
||||
)
|
||||
runtime_platform = os_name + " " + platform.machine()
|
||||
build_platform = sysconfig.get_platform()
|
||||
lines.insert(
|
||||
0,
|
||||
"Environment: Blender {} / VRM Add-on {} / {} / {} ({})".format(
|
||||
bpy.app.version_string,
|
||||
".".join(map(str, get_addon_version())),
|
||||
python_version,
|
||||
runtime_platform,
|
||||
build_platform,
|
||||
),
|
||||
)
|
||||
return ops.wm.vrm_error_dialog(
|
||||
"INVOKE_DEFAULT",
|
||||
title=title,
|
||||
lines=[
|
||||
{
|
||||
"name": f"error_line_{i}",
|
||||
"line": mask_private_string(line),
|
||||
}
|
||||
for i, line in enumerate(lines)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def mask_private_string(message: str) -> str:
|
||||
script_path = Path(__file__).parent.parent.parent.parent.parent
|
||||
if len(script_path.parts) > 3:
|
||||
message = message.replace(str(script_path), "<script path>")
|
||||
|
||||
message = message.replace(str(Path.home()), "<home path>")
|
||||
message = message.replace(getpass.getuser(), "<user name>")
|
||||
return message
|
||||
|
||||
|
||||
class VrmErrorDialogMessageLine(PropertyGroup):
|
||||
line: StringProperty() # type: ignore[valid-type]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
line: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class VRM_UL_vrm_error_dialog_message(UIList):
|
||||
bl_idname = "VRM_UL_vrm_error_dialog_message"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
line: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(line, VrmErrorDialogMessageLine):
|
||||
return
|
||||
|
||||
column = layout.column(align=True)
|
||||
column.label(text=line.line, translate=False)
|
||||
|
||||
|
||||
class VRM_OT_save_error_dialog_message(Operator, ExportHelper):
|
||||
bl_idname = "vrm.save_error_dialog_message"
|
||||
bl_label = "Save Error Message"
|
||||
bl_description = "Save Error Message"
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
filename_ext = ".txt"
|
||||
filter_glob: StringProperty( # type: ignore[valid-type]
|
||||
default="*.txt",
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
title: StringProperty(options={"HIDDEN"}) # type: ignore[valid-type]
|
||||
lines: CollectionProperty( # type: ignore[valid-type]
|
||||
type=VrmErrorDialogMessageLine, options={"HIDDEN"}
|
||||
)
|
||||
|
||||
def restore_error_dialog(self) -> set[str]:
|
||||
if bpy.app.version >= (4, 1):
|
||||
return {"FINISHED"}
|
||||
|
||||
return show_error_dialog(
|
||||
self.title,
|
||||
[output.line for output in self.lines],
|
||||
append_environment=False,
|
||||
)
|
||||
|
||||
def cancel(self, _context: Context) -> None:
|
||||
self.restore_error_dialog()
|
||||
|
||||
def execute(self, _context: Context) -> set[str]:
|
||||
message = "\n".join(output.line for output in self.lines)
|
||||
Path(self.filepath).write_bytes(message.encode())
|
||||
return self.restore_error_dialog()
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
if not self.filepath:
|
||||
local_now = datetime.datetime.now(tz=None) # Get local time # noqa: DTZ005
|
||||
formatted_time = local_now.strftime("%Y%m%d_%H%M%S")
|
||||
self.filepath = f"vrm_error_message_{formatted_time}.txt"
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
filter_glob: str # type: ignore[no-redef]
|
||||
title: str # type: ignore[no-redef]
|
||||
lines: CollectionPropertyProtocol[ # type: ignore[no-redef]
|
||||
VrmErrorDialogMessageLine
|
||||
]
|
||||
|
||||
|
||||
class WM_OT_vrm_error_dialog(Operator):
|
||||
bl_label = "Error"
|
||||
bl_idname = "wm.vrm_error_dialog"
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
title: StringProperty(options={"HIDDEN"}) # type: ignore[valid-type]
|
||||
lines: CollectionProperty( # type: ignore[valid-type]
|
||||
type=VrmErrorDialogMessageLine, options={"HIDDEN"}
|
||||
)
|
||||
active_line_index: IntProperty(options={"HIDDEN"}) # type: ignore[valid-type]
|
||||
|
||||
def execute(self, _context: Context) -> set[str]:
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, _event: Event) -> set[str]:
|
||||
self.active_line_index = len(self.lines) - 1
|
||||
return context.window_manager.invoke_props_dialog(self, width=700)
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
layout = self.layout.column()
|
||||
layout.emboss = "NONE"
|
||||
box = layout.box()
|
||||
box.emboss = "NORMAL"
|
||||
row = box.split(factor=0.5, align=True)
|
||||
row.label(text=self.title, icon="ERROR")
|
||||
save_op = layout_operator(row, VRM_OT_save_error_dialog_message, icon="FILE")
|
||||
save_op.title = self.title
|
||||
for process_output_line in self.lines:
|
||||
output = save_op.lines.add()
|
||||
output.line = process_output_line.line
|
||||
open_url_op = layout_operator(
|
||||
row, VRM_OT_open_url_in_web_browser, icon="URL", text="Open Support Site"
|
||||
)
|
||||
open_url_op.url = "https://github.com/saturday06/VRM-Addon-for-Blender/issues"
|
||||
box.template_list(
|
||||
VRM_UL_vrm_error_dialog_message.bl_idname,
|
||||
"",
|
||||
self,
|
||||
"lines",
|
||||
self,
|
||||
"active_line_index",
|
||||
rows=20,
|
||||
sort_lock=True,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
title: str # type: ignore[no-redef]
|
||||
lines: CollectionPropertyProtocol[ # type: ignore[no-redef]
|
||||
VrmErrorDialogMessageLine
|
||||
]
|
||||
active_line_index: int # type: ignore[no-redef]
|
||||
42
common/fs.py
Normal file
42
common/fs.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def create_unique_indexed_directory_path(path: Path) -> Path:
|
||||
name = path.name
|
||||
for count in range(sys.maxsize):
|
||||
count_str = f".{count}" if count else ""
|
||||
path = path.with_name(name + count_str)
|
||||
try:
|
||||
path.mkdir(parents=True)
|
||||
except OSError:
|
||||
continue
|
||||
return path
|
||||
message = f"Failed to create unique directory path: {path}"
|
||||
raise RuntimeError(message)
|
||||
|
||||
|
||||
def create_unique_indexed_file_path(path: Path, binary: Optional[bytes] = None) -> Path:
|
||||
suffix = path.suffix
|
||||
stem = path.stem
|
||||
for count in range(sys.maxsize):
|
||||
count_str = f".{count}" if count else ""
|
||||
path = path.with_name(stem + count_str + suffix)
|
||||
|
||||
if binary is None:
|
||||
if not path.exists():
|
||||
return path
|
||||
continue
|
||||
|
||||
try:
|
||||
path.touch(exist_ok=False)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
path.write_bytes(binary)
|
||||
return path
|
||||
|
||||
message = f"Failed to create unique file path: {path}"
|
||||
raise RuntimeError(message)
|
||||
20
common/gl.py
Normal file
20
common/gl.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from typing import Final
|
||||
|
||||
GL_TRIANGLES: Final = 0x0004
|
||||
GL_BYTE: Final = 0x1400
|
||||
GL_UNSIGNED_BYTE: Final = 0x1401
|
||||
GL_SHORT: Final = 0x1402
|
||||
GL_UNSIGNED_SHORT: Final = 0x1403
|
||||
GL_INT: Final = 0x1404
|
||||
GL_UNSIGNED_INT: Final = 0x1405
|
||||
GL_FLOAT: Final = 0x1406
|
||||
GL_NEAREST: Final = 0x2600
|
||||
GL_LINEAR: Final = 0x2601
|
||||
GL_REPEAT: Final = 0x2901
|
||||
GL_CLAMP_TO_EDGE: Final = 0x812F
|
||||
GL_MIRRORED_REPEAT: Final = 0x8370
|
||||
GL_NEAREST_MIPMAP_NEAREST: Final = 0x2700
|
||||
GL_LINEAR_MIPMAP_NEAREST: Final = 0x2701
|
||||
GL_NEAREST_MIPMAP_LINEAR: Final = 0x2702
|
||||
GL_LINEAR_MIPMAP_LINEAR: Final = 0x2703
|
||||
605
common/gltf.py
Normal file
605
common/gltf.py
Normal file
@@ -0,0 +1,605 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import base64
|
||||
import json
|
||||
import struct
|
||||
from io import BytesIO
|
||||
from typing import Final, Optional, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from mathutils import Quaternion, Vector
|
||||
|
||||
from .convert import Json
|
||||
from .deep import make_json
|
||||
from .gl import (
|
||||
GL_BYTE,
|
||||
GL_FLOAT,
|
||||
GL_INT,
|
||||
GL_SHORT,
|
||||
GL_UNSIGNED_BYTE,
|
||||
GL_UNSIGNED_INT,
|
||||
GL_UNSIGNED_SHORT,
|
||||
)
|
||||
|
||||
# https://www.khronos.org/opengl/wiki/Small_Float_Formats#Numeric_limits_and_precision
|
||||
FLOAT_POSITIVE_MAX: Final = 3.4028237e38
|
||||
FLOAT_NEGATIVE_MAX: Final = -FLOAT_POSITIVE_MAX
|
||||
|
||||
|
||||
def parse_glb(data: bytes) -> tuple[dict[str, Json], bytes]:
|
||||
with BytesIO(data) as glb:
|
||||
header_bytes = glb.read(12)
|
||||
if len(header_bytes) != 12:
|
||||
message = "Failed to read VRM glTF header: " + ",".join(
|
||||
chr(b) for b in header_bytes
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
header: tuple[bytes, int, int] = struct.unpack("<4sII", header_bytes)
|
||||
magic, version, length = header
|
||||
if magic != b"glTF":
|
||||
message = "Invalid VRM glTF magic bytes: " + ",".join(chr(b) for b in magic)
|
||||
raise ValueError(message)
|
||||
|
||||
if version != 2:
|
||||
message = f"Unsupported VRM glTF Version: {version}"
|
||||
raise ValueError(message)
|
||||
|
||||
chunks_bytes_length = length - 12
|
||||
if chunks_bytes_length < 0:
|
||||
message = f"Invalid VRM glTF length: {length}"
|
||||
raise ValueError(message)
|
||||
|
||||
chunks_bytes = glb.read(chunks_bytes_length)
|
||||
if len(chunks_bytes) != chunks_bytes_length:
|
||||
message = "Failed to read VRM chunks bytes"
|
||||
raise ValueError(message)
|
||||
|
||||
with BytesIO(chunks_bytes) as chunks:
|
||||
json_chunk_length_bytes = chunks.read(4)
|
||||
if len(json_chunk_length_bytes) != 4:
|
||||
message = "Failed to read VRM json chunk length bytes"
|
||||
raise ValueError(message)
|
||||
|
||||
json_chunk_type_bytes = chunks.read(4)
|
||||
if len(json_chunk_type_bytes) != 4:
|
||||
message = "Failed to read VRM json chunk type bytes"
|
||||
raise ValueError(message)
|
||||
|
||||
if json_chunk_type_bytes != b"JSON":
|
||||
message = "Invalid VRM json chunk type bytes: " + ",".join(
|
||||
chr(b) for b in json_chunk_type_bytes
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
json_chunk_data_length: int = struct.unpack("<I", json_chunk_length_bytes)[0]
|
||||
json_chunk_data_bytes = chunks.read(json_chunk_data_length)
|
||||
if len(json_chunk_data_bytes) != json_chunk_data_length:
|
||||
message = "Failed to read VRM json chunk"
|
||||
raise ValueError(message)
|
||||
|
||||
raw_json = json.loads(json_chunk_data_bytes) # raises json.JSONDecodeError
|
||||
json_obj = make_json(raw_json)
|
||||
if not isinstance(json_obj, dict):
|
||||
message = f"Unexpected VRM json format: {type(json_obj)}"
|
||||
raise TypeError(message)
|
||||
|
||||
bin_chunk_length_bytes = chunks.read(4)
|
||||
if not bin_chunk_length_bytes:
|
||||
return json_obj, b""
|
||||
if len(bin_chunk_length_bytes) != 4:
|
||||
message = "Failed to read VRM bin chunk length bytes"
|
||||
raise ValueError(message)
|
||||
|
||||
bin_chunk_type_bytes = chunks.read(4)
|
||||
if len(bin_chunk_type_bytes) != 4:
|
||||
message = "Failed to read VRM bin chunk type bytes"
|
||||
raise ValueError(message)
|
||||
|
||||
if bin_chunk_type_bytes != b"BIN\x00":
|
||||
message = "Invalid VRM bin chunk type bytes: " + ",".join(
|
||||
chr(b) for b in bin_chunk_type_bytes
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
bin_chunk_data_length: int = struct.unpack("<I", bin_chunk_length_bytes)[0]
|
||||
bin_chunk_data_bytes = chunks.read(bin_chunk_data_length)
|
||||
if len(bin_chunk_data_bytes) != bin_chunk_data_length:
|
||||
message = "Failed to read VRM bin chunk"
|
||||
raise ValueError(message)
|
||||
|
||||
return json_obj, bin_chunk_data_bytes
|
||||
|
||||
|
||||
def pack_glb(
|
||||
json_dict: dict[str, Json], bin_chunk_bytes: Union[bytes, bytearray]
|
||||
) -> bytes:
|
||||
# https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#binary-gltf-layout
|
||||
json_chunk_bytes = json.dumps(
|
||||
json_dict,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
# UniVRM 0.56.3 cannot import a json containing unicode escape chars into
|
||||
# Unity Editor.
|
||||
ensure_ascii=False,
|
||||
).encode()
|
||||
|
||||
while len(json_chunk_bytes) % 4 != 0:
|
||||
json_chunk_bytes += b"\x20"
|
||||
|
||||
while len(bin_chunk_bytes) % 4 != 0:
|
||||
bin_chunk_bytes += b"\x00"
|
||||
|
||||
glb = bytearray()
|
||||
|
||||
glb.extend(b"glTF") # magic
|
||||
glb.extend(struct.pack("<I", 2)) # version
|
||||
glb.extend( # length
|
||||
struct.pack(
|
||||
"<I",
|
||||
# header
|
||||
12
|
||||
# json chunk
|
||||
+ 8
|
||||
+ len(json_chunk_bytes)
|
||||
# binary chunk
|
||||
+ 8
|
||||
+ len(bin_chunk_bytes),
|
||||
)
|
||||
)
|
||||
|
||||
glb.extend(struct.pack("<I", len(json_chunk_bytes)))
|
||||
glb.extend(b"JSON")
|
||||
glb.extend(json_chunk_bytes)
|
||||
|
||||
glb.extend(struct.pack("<I", len(bin_chunk_bytes)))
|
||||
glb.extend(b"BIN\x00")
|
||||
glb.extend(bin_chunk_bytes)
|
||||
|
||||
return bytes(glb)
|
||||
|
||||
|
||||
def read_accessor_as_bytes(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
bin_chunk_bytes: Optional[bytes],
|
||||
) -> Optional[bytes]:
|
||||
buffer_view_index = accessor_dict.get("bufferView")
|
||||
if not isinstance(buffer_view_index, int):
|
||||
return None
|
||||
if not 0 <= buffer_view_index < len(buffer_view_dicts):
|
||||
return None
|
||||
buffer_view_dict = buffer_view_dicts[buffer_view_index]
|
||||
if not isinstance(buffer_view_dict, dict):
|
||||
return None
|
||||
buffer_index = buffer_view_dict.get("buffer")
|
||||
if not isinstance(buffer_index, int):
|
||||
return None
|
||||
if not 0 <= buffer_index < len(buffer_dicts):
|
||||
return None
|
||||
|
||||
if buffer_index == 0 and bin_chunk_bytes is not None:
|
||||
buffer_bytes = bin_chunk_bytes
|
||||
else:
|
||||
prefix = "application/gltf-buffer;base64,"
|
||||
|
||||
buffer_dict = buffer_dicts[buffer_index]
|
||||
if not isinstance(buffer_dict, dict):
|
||||
return None
|
||||
uri = buffer_dict.get("uri")
|
||||
|
||||
if not isinstance(uri, str):
|
||||
return None
|
||||
try:
|
||||
parsed_url = urlparse(uri)
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed_url.scheme != "data":
|
||||
return None
|
||||
if not parsed_url.path.startswith(prefix): # TODO: all variants
|
||||
return None
|
||||
buffer_base64 = parsed_url.path.removeprefix(prefix)
|
||||
buffer_bytes = base64.b64decode(buffer_base64)
|
||||
|
||||
byte_offset = buffer_view_dict.get("byteOffset", 0)
|
||||
if not isinstance(byte_offset, int):
|
||||
return None
|
||||
if not 0 <= byte_offset < len(buffer_bytes):
|
||||
return None
|
||||
byte_length = buffer_view_dict.get("byteLength")
|
||||
if not isinstance(byte_length, int):
|
||||
return None
|
||||
if not 0 <= byte_offset + byte_length <= len(buffer_bytes):
|
||||
return None
|
||||
return buffer_bytes[byte_offset : byte_offset + byte_length]
|
||||
|
||||
|
||||
def unpack_component(
|
||||
component_type: int, unpack_count: int, buffer_bytes: bytes
|
||||
) -> Optional[Union[tuple[int, ...], tuple[float, ...]]]:
|
||||
for search_component_type, component_count, unpack_symbol in [
|
||||
(GL_BYTE, 1, "b"),
|
||||
(GL_UNSIGNED_BYTE, 1, "B"),
|
||||
(GL_SHORT, 2, "h"),
|
||||
(GL_UNSIGNED_SHORT, 2, "H"),
|
||||
(GL_INT, 4, "i"),
|
||||
(GL_UNSIGNED_INT, 4, "I"),
|
||||
(GL_FLOAT, 4, "f"),
|
||||
]:
|
||||
if search_component_type != component_type:
|
||||
continue
|
||||
if unpack_count == 0:
|
||||
return ()
|
||||
if not 1 <= unpack_count * component_count <= len(buffer_bytes):
|
||||
return None
|
||||
return struct.unpack("<" + unpack_symbol * unpack_count, buffer_bytes)
|
||||
return None
|
||||
|
||||
|
||||
def unpack_accessor_as_scalar_components(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
buffer0_bytes: bytes,
|
||||
unpack_count: int,
|
||||
) -> Union[tuple[int, ...], tuple[float, ...], None]:
|
||||
component_type = accessor_dict.get("componentType")
|
||||
if not isinstance(component_type, int):
|
||||
return None
|
||||
|
||||
raw_bytes = read_accessor_as_bytes(
|
||||
accessor_dict,
|
||||
buffer_view_dicts,
|
||||
buffer_dicts,
|
||||
buffer0_bytes,
|
||||
)
|
||||
if not raw_bytes:
|
||||
return None
|
||||
|
||||
return unpack_component(component_type, unpack_count, raw_bytes)
|
||||
|
||||
|
||||
def read_scalar_accessor(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
buffer0_bytes: bytes,
|
||||
) -> Union[tuple[int, ...], tuple[float, ...], None]:
|
||||
accessor_type = accessor_dict.get("type")
|
||||
if accessor_type != "SCALAR":
|
||||
return None
|
||||
count = accessor_dict.get("count")
|
||||
if not isinstance(count, int):
|
||||
return None
|
||||
return unpack_accessor_as_scalar_components(
|
||||
accessor_dict,
|
||||
buffer_view_dicts,
|
||||
buffer_dicts,
|
||||
buffer0_bytes,
|
||||
count,
|
||||
)
|
||||
|
||||
|
||||
def read_vec2_accessor(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
buffer0_bytes: bytes,
|
||||
) -> Union[tuple[tuple[int, int], ...], tuple[tuple[float, float], ...], None]:
|
||||
accessor_type = accessor_dict.get("type")
|
||||
if accessor_type != "VEC2":
|
||||
return None
|
||||
count = accessor_dict.get("count")
|
||||
if not isinstance(count, int):
|
||||
return None
|
||||
components = unpack_accessor_as_scalar_components(
|
||||
accessor_dict,
|
||||
buffer_view_dicts,
|
||||
buffer_dicts,
|
||||
buffer0_bytes,
|
||||
count * 2,
|
||||
)
|
||||
if components is None:
|
||||
return None
|
||||
return tuple(
|
||||
(
|
||||
components[i],
|
||||
components[i + 1],
|
||||
)
|
||||
for i in range(0, count * 2, 2)
|
||||
)
|
||||
|
||||
|
||||
def read_vec3_accessor(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
buffer0_bytes: bytes,
|
||||
) -> Union[
|
||||
tuple[tuple[int, int, int], ...], tuple[tuple[float, float, float], ...], None
|
||||
]:
|
||||
accessor_type = accessor_dict.get("type")
|
||||
if accessor_type != "VEC3":
|
||||
return None
|
||||
count = accessor_dict.get("count")
|
||||
if not isinstance(count, int):
|
||||
return None
|
||||
components = unpack_accessor_as_scalar_components(
|
||||
accessor_dict,
|
||||
buffer_view_dicts,
|
||||
buffer_dicts,
|
||||
buffer0_bytes,
|
||||
count * 3,
|
||||
)
|
||||
if components is None:
|
||||
return None
|
||||
return tuple(
|
||||
(
|
||||
components[i],
|
||||
components[i + 1],
|
||||
components[i + 2],
|
||||
)
|
||||
for i in range(0, count * 3, 3)
|
||||
)
|
||||
|
||||
|
||||
def read_vec4_accessor(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
buffer0_bytes: bytes,
|
||||
) -> Union[
|
||||
tuple[tuple[int, int, int, int], ...],
|
||||
tuple[tuple[float, float, float, float], ...],
|
||||
None,
|
||||
]:
|
||||
accessor_type = accessor_dict.get("type")
|
||||
if accessor_type != "VEC4":
|
||||
return None
|
||||
count = accessor_dict.get("count")
|
||||
if not isinstance(count, int):
|
||||
return None
|
||||
components = unpack_accessor_as_scalar_components(
|
||||
accessor_dict,
|
||||
buffer_view_dicts,
|
||||
buffer_dicts,
|
||||
buffer0_bytes,
|
||||
count * 4,
|
||||
)
|
||||
if components is None:
|
||||
return None
|
||||
return tuple(
|
||||
(
|
||||
components[i],
|
||||
components[i + 1],
|
||||
components[i + 2],
|
||||
components[i + 3],
|
||||
)
|
||||
for i in range(0, count * 4, 4)
|
||||
)
|
||||
|
||||
|
||||
def read_mat4_accessor(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
buffer0_bytes: bytes,
|
||||
) -> Union[
|
||||
tuple[
|
||||
tuple[
|
||||
tuple[int, int, int, int],
|
||||
tuple[int, int, int, int],
|
||||
tuple[int, int, int, int],
|
||||
tuple[int, int, int, int],
|
||||
],
|
||||
...,
|
||||
],
|
||||
tuple[
|
||||
tuple[
|
||||
tuple[float, float, float, float],
|
||||
tuple[float, float, float, float],
|
||||
tuple[float, float, float, float],
|
||||
tuple[float, float, float, float],
|
||||
],
|
||||
...,
|
||||
],
|
||||
None,
|
||||
]:
|
||||
accessor_type = accessor_dict.get("type")
|
||||
if accessor_type != "MAT4":
|
||||
return None
|
||||
count = accessor_dict.get("count")
|
||||
if not isinstance(count, int):
|
||||
return None
|
||||
components = unpack_accessor_as_scalar_components(
|
||||
accessor_dict,
|
||||
buffer_view_dicts,
|
||||
buffer_dicts,
|
||||
buffer0_bytes,
|
||||
count * 16,
|
||||
)
|
||||
if components is None:
|
||||
return None
|
||||
return tuple(
|
||||
(
|
||||
(
|
||||
components[i],
|
||||
components[i + 1],
|
||||
components[i + 2],
|
||||
components[i + 3],
|
||||
),
|
||||
(
|
||||
components[i + 4],
|
||||
components[i + 5],
|
||||
components[i + 6],
|
||||
components[i + 7],
|
||||
),
|
||||
(
|
||||
components[i + 8],
|
||||
components[i + 9],
|
||||
components[i + 10],
|
||||
components[i + 11],
|
||||
),
|
||||
(
|
||||
components[i + 12],
|
||||
components[i + 13],
|
||||
components[i + 14],
|
||||
components[i + 15],
|
||||
),
|
||||
)
|
||||
for i in range(0, count * 16, 16)
|
||||
)
|
||||
|
||||
|
||||
def read_accessor(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
buffer0_bytes: bytes,
|
||||
) -> Union[
|
||||
tuple[int, ...],
|
||||
tuple[float, ...],
|
||||
tuple[tuple[int, int], ...],
|
||||
tuple[tuple[float, float], ...],
|
||||
tuple[tuple[int, int, int], ...],
|
||||
tuple[tuple[float, float, float], ...],
|
||||
tuple[tuple[int, int, int, int], ...],
|
||||
tuple[tuple[float, float, float, float], ...],
|
||||
tuple[
|
||||
tuple[
|
||||
tuple[int, int, int, int],
|
||||
tuple[int, int, int, int],
|
||||
tuple[int, int, int, int],
|
||||
tuple[int, int, int, int],
|
||||
],
|
||||
...,
|
||||
],
|
||||
tuple[
|
||||
tuple[
|
||||
tuple[float, float, float, float],
|
||||
tuple[float, float, float, float],
|
||||
tuple[float, float, float, float],
|
||||
tuple[float, float, float, float],
|
||||
],
|
||||
...,
|
||||
],
|
||||
None,
|
||||
]:
|
||||
accessor_type = accessor_dict.get("type")
|
||||
if accessor_type == "SCALAR":
|
||||
return read_scalar_accessor(
|
||||
accessor_dict, buffer_view_dicts, buffer_dicts, buffer0_bytes
|
||||
)
|
||||
if accessor_type == "VEC2":
|
||||
return read_vec2_accessor(
|
||||
accessor_dict, buffer_view_dicts, buffer_dicts, buffer0_bytes
|
||||
)
|
||||
if accessor_type == "VEC3":
|
||||
return read_vec3_accessor(
|
||||
accessor_dict, buffer_view_dicts, buffer_dicts, buffer0_bytes
|
||||
)
|
||||
if accessor_type == "VEC4":
|
||||
return read_vec4_accessor(
|
||||
accessor_dict, buffer_view_dicts, buffer_dicts, buffer0_bytes
|
||||
)
|
||||
if accessor_type == "MAT4":
|
||||
return read_mat4_accessor(
|
||||
accessor_dict, buffer_view_dicts, buffer_dicts, buffer0_bytes
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def read_accessors(
|
||||
json_dict: dict[str, Json],
|
||||
buffer0_bytes: bytes,
|
||||
) -> tuple[
|
||||
Union[
|
||||
tuple[int, ...],
|
||||
tuple[float, ...],
|
||||
tuple[tuple[int, int], ...],
|
||||
tuple[tuple[float, float], ...],
|
||||
tuple[tuple[int, int, int], ...],
|
||||
tuple[tuple[float, float, float], ...],
|
||||
tuple[tuple[int, int, int, int], ...],
|
||||
tuple[tuple[float, float, float, float], ...],
|
||||
tuple[
|
||||
tuple[
|
||||
tuple[int, int, int, int],
|
||||
tuple[int, int, int, int],
|
||||
tuple[int, int, int, int],
|
||||
tuple[int, int, int, int],
|
||||
],
|
||||
...,
|
||||
],
|
||||
tuple[
|
||||
tuple[
|
||||
tuple[float, float, float, float],
|
||||
tuple[float, float, float, float],
|
||||
tuple[float, float, float, float],
|
||||
tuple[float, float, float, float],
|
||||
],
|
||||
...,
|
||||
],
|
||||
None,
|
||||
],
|
||||
...,
|
||||
]:
|
||||
accessor_dicts = json_dict.get("accessors")
|
||||
if not isinstance(accessor_dicts, list):
|
||||
accessor_dicts = []
|
||||
|
||||
buffer_view_dicts = json_dict.get("bufferViews")
|
||||
if not isinstance(buffer_view_dicts, list):
|
||||
buffer_view_dicts = []
|
||||
|
||||
buffer_dicts = json_dict.get("buffers")
|
||||
if not isinstance(buffer_dicts, list):
|
||||
buffer_dicts = []
|
||||
|
||||
return tuple(
|
||||
read_accessor(accessor_dict, buffer_view_dicts, buffer_dicts, buffer0_bytes)
|
||||
for accessor_dict in accessor_dicts
|
||||
if isinstance(accessor_dict, dict)
|
||||
)
|
||||
|
||||
|
||||
def read_accessor_as_animation_sampler_input(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
buffer0_bytes: bytes,
|
||||
) -> Optional[list[float]]:
|
||||
scalar_accessor = read_scalar_accessor(
|
||||
accessor_dict, buffer_view_dicts, buffer_dicts, buffer0_bytes
|
||||
)
|
||||
if scalar_accessor is None:
|
||||
return None
|
||||
return [float(v) for v in scalar_accessor]
|
||||
|
||||
|
||||
def read_accessor_as_animation_sampler_translation_output(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
buffer0_bytes: bytes,
|
||||
) -> Optional[list[Vector]]:
|
||||
vec3_accessor = read_vec3_accessor(
|
||||
accessor_dict, buffer_view_dicts, buffer_dicts, buffer0_bytes
|
||||
)
|
||||
if vec3_accessor is None:
|
||||
return None
|
||||
return [Vector((x, -z, y)) for x, y, z in vec3_accessor]
|
||||
|
||||
|
||||
def read_accessor_as_animation_sampler_rotation_output(
|
||||
accessor_dict: dict[str, Json],
|
||||
buffer_view_dicts: list[Json],
|
||||
buffer_dicts: list[Json],
|
||||
buffer0_bytes: bytes,
|
||||
) -> Optional[list[Quaternion]]:
|
||||
vec4_accessor = read_vec4_accessor(
|
||||
accessor_dict, buffer_view_dicts, buffer_dicts, buffer0_bytes
|
||||
)
|
||||
if vec4_accessor is None:
|
||||
return None
|
||||
return [Quaternion((w, x, -z, y)).normalized() for x, y, z, w in vec4_accessor]
|
||||
1
common/human_bone_mapper/__init__.py
Normal file
1
common/human_bone_mapper/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
common/human_bone_mapper/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
common/human_bone_mapper/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
common/human_bone_mapper/__pycache__/mmd_mapping.cpython-311.pyc
Normal file
BIN
common/human_bone_mapper/__pycache__/mmd_mapping.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
84
common/human_bone_mapper/biped_mapping.py
Normal file
84
common/human_bone_mapper/biped_mapping.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping
|
||||
|
||||
from bpy.types import Object
|
||||
|
||||
from ..logger import get_logger
|
||||
from ..vrm1.human_bone import HumanBoneSpecification, HumanBoneSpecifications
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
biped_mapping = {
|
||||
(None, "Pelvis"): HumanBoneSpecifications.HIPS,
|
||||
(None, "Spine"): HumanBoneSpecifications.SPINE,
|
||||
(None, "Spine2"): HumanBoneSpecifications.CHEST,
|
||||
(None, "Neck"): HumanBoneSpecifications.NECK,
|
||||
(None, "Head"): HumanBoneSpecifications.HEAD,
|
||||
("R", "Clavicle"): HumanBoneSpecifications.RIGHT_SHOULDER,
|
||||
("R", "UpperArm"): HumanBoneSpecifications.RIGHT_UPPER_ARM,
|
||||
("R", "Forearm"): HumanBoneSpecifications.RIGHT_LOWER_ARM,
|
||||
("R", "Hand"): HumanBoneSpecifications.RIGHT_HAND,
|
||||
("L", "Clavicle"): HumanBoneSpecifications.LEFT_SHOULDER,
|
||||
("L", "UpperArm"): HumanBoneSpecifications.LEFT_UPPER_ARM,
|
||||
("L", "Forearm"): HumanBoneSpecifications.LEFT_LOWER_ARM,
|
||||
("L", "Hand"): HumanBoneSpecifications.LEFT_HAND,
|
||||
("R", "Thigh"): HumanBoneSpecifications.RIGHT_UPPER_LEG,
|
||||
("R", "Calf"): HumanBoneSpecifications.RIGHT_LOWER_LEG,
|
||||
("R", "Foot"): HumanBoneSpecifications.RIGHT_FOOT,
|
||||
("R", "Toe0"): HumanBoneSpecifications.RIGHT_TOES,
|
||||
("L", "Thigh"): HumanBoneSpecifications.LEFT_UPPER_LEG,
|
||||
("L", "Calf"): HumanBoneSpecifications.LEFT_LOWER_LEG,
|
||||
("L", "Foot"): HumanBoneSpecifications.LEFT_FOOT,
|
||||
("L", "Toe0"): HumanBoneSpecifications.LEFT_TOES,
|
||||
("R", "Finger0"): HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
("R", "Finger01"): HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
("R", "Finger02"): HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
("L", "Finger0"): HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
("L", "Finger01"): HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
("L", "Finger02"): HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
("R", "Finger1"): HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
("R", "Finger11"): HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
("R", "Finger12"): HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
("L", "Finger1"): HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
("L", "Finger11"): HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
("L", "Finger12"): HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
("R", "Finger2"): HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
("R", "Finger21"): HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
("R", "Finger22"): HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
("L", "Finger2"): HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
("L", "Finger21"): HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
("L", "Finger22"): HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
("R", "Finger3"): HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
("R", "Finger31"): HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
("R", "Finger32"): HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
("L", "Finger3"): HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
("L", "Finger31"): HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
("L", "Finger32"): HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
("R", "Finger4"): HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
("R", "Finger41"): HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
("R", "Finger42"): HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
("L", "Finger4"): HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
("L", "Finger41"): HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
("L", "Finger42"): HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
}
|
||||
|
||||
|
||||
def create_config(
|
||||
armature: Object,
|
||||
) -> tuple[str, Mapping[str, HumanBoneSpecification]]:
|
||||
mapping: dict[str, HumanBoneSpecification] = {}
|
||||
for bone in armature.pose.bones:
|
||||
for (bone_lr, bone_suffix), specification in biped_mapping.items():
|
||||
if (
|
||||
bone.name.startswith("Bip001")
|
||||
and bone.name.endswith(bone_suffix)
|
||||
and (bone_lr is None or bone_lr in bone.name)
|
||||
):
|
||||
mapping[bone.name] = specification
|
||||
|
||||
name = "Bip001"
|
||||
for specification in HumanBoneSpecifications.all_human_bones:
|
||||
if specification.requirement and specification not in mapping.values():
|
||||
return (name, {})
|
||||
|
||||
return (name, mapping)
|
||||
@@ -0,0 +1,63 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
from ..vrm1.human_bone import HumanBoneSpecification, HumanBoneSpecifications
|
||||
|
||||
MAPPING: Final[Mapping[str, HumanBoneSpecification]] = {
|
||||
"Hips": HumanBoneSpecifications.HIPS,
|
||||
"Spine": HumanBoneSpecifications.SPINE,
|
||||
"Chest": HumanBoneSpecifications.CHEST,
|
||||
"Neck": HumanBoneSpecifications.NECK,
|
||||
"Head": HumanBoneSpecifications.HEAD,
|
||||
"Eye_R": HumanBoneSpecifications.RIGHT_EYE,
|
||||
"Eye_L": HumanBoneSpecifications.LEFT_EYE,
|
||||
"Right shoulder": HumanBoneSpecifications.RIGHT_SHOULDER,
|
||||
"Right arm": HumanBoneSpecifications.RIGHT_UPPER_ARM,
|
||||
"Right elbow": HumanBoneSpecifications.RIGHT_LOWER_ARM,
|
||||
"Right wrist": HumanBoneSpecifications.RIGHT_HAND,
|
||||
"Left shoulder": HumanBoneSpecifications.LEFT_SHOULDER,
|
||||
"Left arm": HumanBoneSpecifications.LEFT_UPPER_ARM,
|
||||
"Left elbow": HumanBoneSpecifications.LEFT_LOWER_ARM,
|
||||
"Left wrist": HumanBoneSpecifications.LEFT_HAND,
|
||||
"Right leg": HumanBoneSpecifications.RIGHT_UPPER_LEG,
|
||||
"Right knee": HumanBoneSpecifications.RIGHT_LOWER_LEG,
|
||||
"Right ankle": HumanBoneSpecifications.RIGHT_FOOT,
|
||||
"Right toe": HumanBoneSpecifications.RIGHT_TOES,
|
||||
"Left leg": HumanBoneSpecifications.LEFT_UPPER_LEG,
|
||||
"Left knee": HumanBoneSpecifications.LEFT_LOWER_LEG,
|
||||
"Left ankle": HumanBoneSpecifications.LEFT_FOOT,
|
||||
"Left toe": HumanBoneSpecifications.LEFT_TOES,
|
||||
"Thumb0_R": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"Thumb1_R": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"Thumb2_R": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
"Thumb0_L": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"Thumb1_L": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"Thumb2_L": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"IndexFinger1_R": HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
"IndexFinger2_R": HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
"IndexFinger3_R": HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
"IndexFinger1_L": HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
"IndexFinger2_L": HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
"IndexFinger3_L": HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
"MiddleFinger1_R": HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
"MiddleFinger2_R": HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
"MiddleFinger3_R": HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
"MiddleFinger1_L": HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
"MiddleFinger2_L": HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
"MiddleFinger3_L": HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
"RingFinger1_R": HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
"RingFinger2_R": HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
"RingFinger3_R": HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
"RingFinger1_L": HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
"RingFinger2_L": HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
"RingFinger3_L": HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
"LittleFinger1_R": HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
"LittleFinger2_R": HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
"LittleFinger3_R": HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
"LittleFinger1_L": HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
"LittleFinger2_L": HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
"LittleFinger3_L": HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
}
|
||||
|
||||
CONFIG = ("Cats Blender Plugin Fixed Model", MAPPING)
|
||||
179
common/human_bone_mapper/human_bone_mapper.py
Normal file
179
common/human_bone_mapper/human_bone_mapper.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from typing import Optional
|
||||
|
||||
from bpy.types import Armature, Object
|
||||
|
||||
from ..char import FULLWIDTH_ASCII_TO_ASCII_MAP
|
||||
from ..logger import get_logger
|
||||
from ..vrm1.human_bone import HumanBoneSpecification
|
||||
from . import (
|
||||
biped_mapping,
|
||||
cats_blender_plugin_fix_model_mapping,
|
||||
microsoft_rocketbox_mapping,
|
||||
mixamo_mapping,
|
||||
mmd_mapping,
|
||||
ready_player_me_mapping,
|
||||
rigify_meta_rig_mapping,
|
||||
unreal_mapping,
|
||||
vrm_addon_mapping,
|
||||
vroid_mapping,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def canonicalize_bone_name(bone_name: str) -> str:
|
||||
bone_name = "".join(FULLWIDTH_ASCII_TO_ASCII_MAP.get(c, c) for c in bone_name)
|
||||
bone_name = re.sub(r"([a-z])([A-Z])", r"\1.\2", bone_name)
|
||||
bone_name = bone_name.lower()
|
||||
bone_name = "".join(" " if c.isspace() else c for c in bone_name)
|
||||
bone_name = re.sub(r"(\d+)", r".\1.", bone_name).strip(".")
|
||||
bone_name_components = re.split(r"[-._: (){}[\]<>]+", bone_name)
|
||||
for patterns, replacement in {
|
||||
("l", "左"): "left",
|
||||
("r", "右"): "right",
|
||||
}.items():
|
||||
bone_name_components = [
|
||||
replacement if bone_name_component in patterns else bone_name_component
|
||||
for bone_name_component in bone_name_components
|
||||
]
|
||||
return ".".join(bone_name_components)
|
||||
|
||||
|
||||
def match_bone_name(bone_name1: str, bone_name2: str) -> bool:
|
||||
return canonicalize_bone_name(bone_name1) == canonicalize_bone_name(bone_name2)
|
||||
|
||||
|
||||
def match_count(
|
||||
armature: Armature, mapping: Mapping[str, HumanBoneSpecification]
|
||||
) -> int:
|
||||
count = 0
|
||||
|
||||
mapping = {
|
||||
bpy_name: specification
|
||||
for bpy_name, specification in mapping.items()
|
||||
if any(match_bone_name(bpy_name, bone.name) for bone in armature.bones)
|
||||
}
|
||||
|
||||
# Validate bone ordering
|
||||
for bpy_name, specification in mapping.items():
|
||||
bone = next(
|
||||
(bone for bone in armature.bones if match_bone_name(bpy_name, bone.name)),
|
||||
None,
|
||||
)
|
||||
if not bone:
|
||||
continue
|
||||
|
||||
parent_specification: Optional[HumanBoneSpecification] = None
|
||||
search_parent_specification = specification.parent()
|
||||
while search_parent_specification:
|
||||
if search_parent_specification in mapping.values():
|
||||
parent_specification = search_parent_specification
|
||||
break
|
||||
search_parent_specification = search_parent_specification.parent()
|
||||
|
||||
found = False
|
||||
bone = bone.parent
|
||||
while bone:
|
||||
search_specification = next(
|
||||
(
|
||||
search_specification
|
||||
for bpy_name, search_specification in mapping.items()
|
||||
if match_bone_name(bpy_name, bone.name)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if search_specification:
|
||||
found = search_specification == parent_specification
|
||||
break
|
||||
bone = bone.parent
|
||||
|
||||
if found or not parent_specification:
|
||||
count += 1
|
||||
continue
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def match_counts(
|
||||
armature: object, mapping: Mapping[str, HumanBoneSpecification]
|
||||
) -> tuple[int, int]:
|
||||
if not isinstance(armature, Armature):
|
||||
message = f"{type(armature)} is not an Armature"
|
||||
raise TypeError(message)
|
||||
required_mapping = {
|
||||
bpy_name: required_specification
|
||||
for bpy_name, required_specification in mapping.items()
|
||||
if required_specification.requirement
|
||||
}
|
||||
return (match_count(armature, required_mapping), match_count(armature, mapping))
|
||||
|
||||
|
||||
def sorted_required_first(
|
||||
armature: Armature,
|
||||
mapping: Mapping[str, HumanBoneSpecification],
|
||||
) -> dict[str, HumanBoneSpecification]:
|
||||
bpy_bone_name_mapping: dict[str, HumanBoneSpecification] = {
|
||||
bpy_bone_name: specification
|
||||
for original_bone_name, specification in mapping.items()
|
||||
if (
|
||||
bpy_bone_name := next(
|
||||
(
|
||||
bone.name
|
||||
for bone in armature.bones
|
||||
if match_bone_name(original_bone_name, bone.name)
|
||||
),
|
||||
None,
|
||||
)
|
||||
)
|
||||
}
|
||||
sorted_mapping: dict[str, HumanBoneSpecification] = {}
|
||||
sorted_mapping.update(
|
||||
{
|
||||
bpy_name: specification
|
||||
for bpy_name, specification in bpy_bone_name_mapping.items()
|
||||
if specification.requirement
|
||||
}
|
||||
)
|
||||
sorted_mapping.update(
|
||||
{
|
||||
bpy_name: specification
|
||||
for bpy_name, specification in bpy_bone_name_mapping.items()
|
||||
if not specification.requirement
|
||||
}
|
||||
)
|
||||
return sorted_mapping
|
||||
|
||||
|
||||
def create_human_bone_mapping(
|
||||
armature: Object,
|
||||
) -> dict[str, HumanBoneSpecification]:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
raise TypeError
|
||||
((required_count, _all_count), name, mapping) = sorted(
|
||||
[
|
||||
(match_counts(armature_data, mapping), name, mapping)
|
||||
for name, mapping in [
|
||||
mmd_mapping.create_config(armature),
|
||||
biped_mapping.create_config(armature),
|
||||
mixamo_mapping.CONFIG,
|
||||
unreal_mapping.CONFIG,
|
||||
ready_player_me_mapping.CONFIG,
|
||||
cats_blender_plugin_fix_model_mapping.CONFIG,
|
||||
microsoft_rocketbox_mapping.CONFIG_BIP01,
|
||||
microsoft_rocketbox_mapping.CONFIG_BIP02,
|
||||
rigify_meta_rig_mapping.CONFIG,
|
||||
vroid_mapping.CONFIG,
|
||||
vroid_mapping.CONFIG_SYMMETRICAL,
|
||||
vrm_addon_mapping.CONFIG_VRM1,
|
||||
vrm_addon_mapping.CONFIG_VRM0,
|
||||
]
|
||||
]
|
||||
)[-1]
|
||||
if required_count:
|
||||
logger.debug('Treat as "%s" bone mappings', name)
|
||||
return sorted_required_first(armature_data, mapping)
|
||||
return {}
|
||||
69
common/human_bone_mapper/microsoft_rocketbox_mapping.py
Normal file
69
common/human_bone_mapper/microsoft_rocketbox_mapping.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
from ..vrm1.human_bone import HumanBoneSpecification, HumanBoneSpecifications
|
||||
|
||||
MAPPING_TEMPLATE: Final[Mapping[str, HumanBoneSpecification]] = {
|
||||
"Head": HumanBoneSpecifications.HEAD,
|
||||
"REye": HumanBoneSpecifications.RIGHT_EYE,
|
||||
"LEye": HumanBoneSpecifications.LEFT_EYE,
|
||||
"MJaw": HumanBoneSpecifications.JAW,
|
||||
"Spine2": HumanBoneSpecifications.CHEST,
|
||||
"Spine1": HumanBoneSpecifications.SPINE,
|
||||
"Pelvis": HumanBoneSpecifications.HIPS,
|
||||
"R Clavicle": HumanBoneSpecifications.RIGHT_SHOULDER,
|
||||
"R UpperArm": HumanBoneSpecifications.RIGHT_UPPER_ARM,
|
||||
"R Forearm": HumanBoneSpecifications.RIGHT_LOWER_ARM,
|
||||
"R Hand": HumanBoneSpecifications.RIGHT_HAND,
|
||||
"L Clavicle": HumanBoneSpecifications.LEFT_SHOULDER,
|
||||
"L UpperArm": HumanBoneSpecifications.LEFT_UPPER_ARM,
|
||||
"L Forearm": HumanBoneSpecifications.LEFT_LOWER_ARM,
|
||||
"L Hand": HumanBoneSpecifications.LEFT_HAND,
|
||||
"R Thigh": HumanBoneSpecifications.RIGHT_UPPER_LEG,
|
||||
"R Calf": HumanBoneSpecifications.RIGHT_LOWER_LEG,
|
||||
"R Foot": HumanBoneSpecifications.RIGHT_FOOT,
|
||||
"R Toe0": HumanBoneSpecifications.RIGHT_TOES,
|
||||
"L Thigh": HumanBoneSpecifications.LEFT_UPPER_LEG,
|
||||
"L Calf": HumanBoneSpecifications.LEFT_LOWER_LEG,
|
||||
"L Foot": HumanBoneSpecifications.LEFT_FOOT,
|
||||
"L Toe0": HumanBoneSpecifications.LEFT_TOES,
|
||||
"R Finger0": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"R Finger01": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"R Finger02": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
"R Finger1": HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
"R Finger11": HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
"R Finger12": HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
"R Finger2": HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
"R Finger21": HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
"R Finger22": HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
"R Finger3": HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
"R Finger31": HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
"R Finger32": HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
"R Finger4": HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
"R Finger41": HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
"R Finger42": HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
"L Finger0": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"L Finger01": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"L Finger02": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"L Finger1": HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
"L Finger11": HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
"L Finger12": HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
"L Finger2": HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
"L Finger21": HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
"L Finger22": HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
"L Finger3": HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
"L Finger31": HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
"L Finger32": HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
"L Finger4": HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
"L Finger41": HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
"L Finger42": HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
}
|
||||
|
||||
|
||||
def prefixed_mapping(key_prefix: str) -> dict[str, HumanBoneSpecification]:
|
||||
return {key_prefix + k: v for k, v in MAPPING_TEMPLATE.items()}
|
||||
|
||||
|
||||
CONFIG_BIP01: Final = ("Microsoft Rocketbox (Bip01)", prefixed_mapping("Bip01 "))
|
||||
CONFIG_BIP02: Final = ("Microsoft Rocketbox (Bip02)", prefixed_mapping("Bip02 "))
|
||||
62
common/human_bone_mapper/mixamo_mapping.py
Normal file
62
common/human_bone_mapper/mixamo_mapping.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
from ..vrm1.human_bone import HumanBoneSpecification, HumanBoneSpecifications
|
||||
|
||||
MAPPING: Final[Mapping[str, HumanBoneSpecification]] = {
|
||||
"mixamorig:Head": HumanBoneSpecifications.HEAD,
|
||||
"mixamorig:Neck": HumanBoneSpecifications.NECK,
|
||||
"mixamorig:Spine2": HumanBoneSpecifications.UPPER_CHEST,
|
||||
"mixamorig:Spine1": HumanBoneSpecifications.CHEST,
|
||||
"mixamorig:Spine": HumanBoneSpecifications.SPINE,
|
||||
"mixamorig:Hips": HumanBoneSpecifications.HIPS,
|
||||
"mixamorig:RightShoulder": HumanBoneSpecifications.RIGHT_SHOULDER,
|
||||
"mixamorig:RightArm": HumanBoneSpecifications.RIGHT_UPPER_ARM,
|
||||
"mixamorig:RightForeArm": HumanBoneSpecifications.RIGHT_LOWER_ARM,
|
||||
"mixamorig:RightHand": HumanBoneSpecifications.RIGHT_HAND,
|
||||
"mixamorig:LeftShoulder": HumanBoneSpecifications.LEFT_SHOULDER,
|
||||
"mixamorig:LeftArm": HumanBoneSpecifications.LEFT_UPPER_ARM,
|
||||
"mixamorig:LeftForeArm": HumanBoneSpecifications.LEFT_LOWER_ARM,
|
||||
"mixamorig:LeftHand": HumanBoneSpecifications.LEFT_HAND,
|
||||
"mixamorig:RightUpLeg": HumanBoneSpecifications.RIGHT_UPPER_LEG,
|
||||
"mixamorig:RightLeg": HumanBoneSpecifications.RIGHT_LOWER_LEG,
|
||||
"mixamorig:RightFoot": HumanBoneSpecifications.RIGHT_FOOT,
|
||||
"mixamorig:RightToeBase": HumanBoneSpecifications.RIGHT_TOES,
|
||||
"mixamorig:LeftUpLeg": HumanBoneSpecifications.LEFT_UPPER_LEG,
|
||||
"mixamorig:LeftLeg": HumanBoneSpecifications.LEFT_LOWER_LEG,
|
||||
"mixamorig:LeftFoot": HumanBoneSpecifications.LEFT_FOOT,
|
||||
"mixamorig:LeftToeBase": HumanBoneSpecifications.LEFT_TOES,
|
||||
"mixamorig:RightHandThumb1": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"mixamorig:RightHandThumb2": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"mixamorig:RightHandThumb3": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
"mixamorig:RightHandIndex1": HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
"mixamorig:RightHandIndex2": HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
"mixamorig:RightHandIndex3": HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
"mixamorig:RightHandMiddle1": HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
"mixamorig:RightHandMiddle2": HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
"mixamorig:RightHandMiddle3": HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
"mixamorig:RightHandRing1": HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
"mixamorig:RightHandRing2": HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
"mixamorig:RightHandRing3": HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
"mixamorig:RightHandPinky1": HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
"mixamorig:RightHandPinky2": HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
"mixamorig:RightHandPinky3": HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
"mixamorig:LeftHandThumb1": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"mixamorig:LeftHandThumb2": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"mixamorig:LeftHandThumb3": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"mixamorig:LeftHandIndex1": HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
"mixamorig:LeftHandIndex2": HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
"mixamorig:LeftHandIndex3": HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
"mixamorig:LeftHandMiddle1": HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
"mixamorig:LeftHandMiddle2": HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
"mixamorig:LeftHandMiddle3": HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
"mixamorig:LeftHandRing1": HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
"mixamorig:LeftHandRing2": HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
"mixamorig:LeftHandRing3": HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
"mixamorig:LeftHandPinky1": HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
"mixamorig:LeftHandPinky2": HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
"mixamorig:LeftHandPinky3": HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
}
|
||||
|
||||
CONFIG: Final = ("Mixamo", MAPPING)
|
||||
115
common/human_bone_mapper/mmd_mapping.py
Normal file
115
common/human_bone_mapper/mmd_mapping.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Final
|
||||
|
||||
from bpy.types import Object
|
||||
|
||||
from ..logger import get_logger
|
||||
from ..vrm1.human_bone import HumanBoneSpecification, HumanBoneSpecifications
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Mapping table for MMD (MikuMikuDance) bone names to VRM human bone specifications.
|
||||
# These Japanese bone names are standard MMD naming conventions and should be preserved.
|
||||
MMD_BONE_NAME_AND_HUMAN_BONE_SPECIFICATION_PAIRS: Final[
|
||||
Sequence[tuple[str, HumanBoneSpecification]]
|
||||
] = [
|
||||
("頭", HumanBoneSpecifications.HEAD),
|
||||
("右目", HumanBoneSpecifications.RIGHT_EYE),
|
||||
("左目", HumanBoneSpecifications.LEFT_EYE),
|
||||
("首", HumanBoneSpecifications.NECK),
|
||||
("上半身2", HumanBoneSpecifications.CHEST),
|
||||
("上半身", HumanBoneSpecifications.SPINE),
|
||||
("センター", HumanBoneSpecifications.HIPS),
|
||||
("右肩", HumanBoneSpecifications.RIGHT_SHOULDER),
|
||||
("右腕", HumanBoneSpecifications.RIGHT_UPPER_ARM),
|
||||
("右ひじ", HumanBoneSpecifications.RIGHT_LOWER_ARM),
|
||||
("右手", HumanBoneSpecifications.RIGHT_HAND),
|
||||
("右手首", HumanBoneSpecifications.RIGHT_HAND),
|
||||
("右足", HumanBoneSpecifications.RIGHT_UPPER_LEG),
|
||||
("右ひざ", HumanBoneSpecifications.RIGHT_LOWER_LEG),
|
||||
("右足首", HumanBoneSpecifications.RIGHT_FOOT),
|
||||
("右つま先", HumanBoneSpecifications.RIGHT_TOES),
|
||||
("右足先EX", HumanBoneSpecifications.RIGHT_TOES),
|
||||
("左肩", HumanBoneSpecifications.LEFT_SHOULDER),
|
||||
("左腕", HumanBoneSpecifications.LEFT_UPPER_ARM),
|
||||
("左ひじ", HumanBoneSpecifications.LEFT_LOWER_ARM),
|
||||
("左手", HumanBoneSpecifications.LEFT_HAND),
|
||||
("左手首", HumanBoneSpecifications.LEFT_HAND),
|
||||
("左足", HumanBoneSpecifications.LEFT_UPPER_LEG),
|
||||
("左ひざ", HumanBoneSpecifications.LEFT_LOWER_LEG),
|
||||
("左足首", HumanBoneSpecifications.LEFT_FOOT),
|
||||
("左つま先", HumanBoneSpecifications.LEFT_TOES),
|
||||
("左足先EX", HumanBoneSpecifications.LEFT_TOES),
|
||||
("右親指0", HumanBoneSpecifications.RIGHT_THUMB_METACARPAL),
|
||||
("右親指1", HumanBoneSpecifications.RIGHT_THUMB_METACARPAL),
|
||||
("右親指1", HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL),
|
||||
("右親指2", HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL),
|
||||
("右親指2", HumanBoneSpecifications.RIGHT_THUMB_DISTAL),
|
||||
("右人指1", HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL),
|
||||
("右人指2", HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE),
|
||||
("右人指3", HumanBoneSpecifications.RIGHT_INDEX_DISTAL),
|
||||
("右中指1", HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL),
|
||||
("右中指2", HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE),
|
||||
("右中指3", HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL),
|
||||
("右薬指1", HumanBoneSpecifications.RIGHT_RING_PROXIMAL),
|
||||
("右薬指2", HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE),
|
||||
("右薬指3", HumanBoneSpecifications.RIGHT_RING_DISTAL),
|
||||
("右小指1", HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL),
|
||||
("右小指2", HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE),
|
||||
("右小指3", HumanBoneSpecifications.RIGHT_LITTLE_DISTAL),
|
||||
("左親指0", HumanBoneSpecifications.LEFT_THUMB_METACARPAL),
|
||||
("左親指1", HumanBoneSpecifications.LEFT_THUMB_METACARPAL),
|
||||
("左親指1", HumanBoneSpecifications.LEFT_THUMB_PROXIMAL),
|
||||
("左親指2", HumanBoneSpecifications.LEFT_THUMB_PROXIMAL),
|
||||
("左親指2", HumanBoneSpecifications.LEFT_THUMB_DISTAL),
|
||||
("左人指1", HumanBoneSpecifications.LEFT_INDEX_PROXIMAL),
|
||||
("左人指2", HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE),
|
||||
("左人指3", HumanBoneSpecifications.LEFT_INDEX_DISTAL),
|
||||
("左中指1", HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL),
|
||||
("左中指2", HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE),
|
||||
("左中指3", HumanBoneSpecifications.LEFT_MIDDLE_DISTAL),
|
||||
("左薬指1", HumanBoneSpecifications.LEFT_RING_PROXIMAL),
|
||||
("左薬指2", HumanBoneSpecifications.LEFT_RING_INTERMEDIATE),
|
||||
("左薬指3", HumanBoneSpecifications.LEFT_RING_DISTAL),
|
||||
("左小指1", HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL),
|
||||
("左小指2", HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE),
|
||||
("左小指3", HumanBoneSpecifications.LEFT_LITTLE_DISTAL),
|
||||
]
|
||||
|
||||
|
||||
def create_config(
|
||||
armature: Object,
|
||||
) -> tuple[str, Mapping[str, HumanBoneSpecification]]:
|
||||
mmd_bone_name_to_bpy_bone_name: dict[str, str] = {}
|
||||
for bone in armature.pose.bones:
|
||||
# https://github.com/UuuNyaa/blender_mmd_tools/blob/97861e3c32ba423833b6c5fc3432127b1ec1182a/mmd_tools/properties/__init__.py#L45-L46
|
||||
mmd_bone = getattr(bone, "mmd_bone", None)
|
||||
if not mmd_bone:
|
||||
continue
|
||||
|
||||
# https://github.com/UuuNyaa/blender_mmd_tools/blob/97861e3c32ba423833b6c5fc3432127b1ec1182a/mmd_tools/properties/bone.py#L43-L48
|
||||
name_j = getattr(mmd_bone, "name_j", None)
|
||||
if not isinstance(name_j, str):
|
||||
continue
|
||||
|
||||
mmd_bone_name_to_bpy_bone_name[name_j] = bone.name
|
||||
|
||||
mapping: dict[str, HumanBoneSpecification] = {}
|
||||
for (
|
||||
mmd_bone_name,
|
||||
human_bone_specification,
|
||||
) in MMD_BONE_NAME_AND_HUMAN_BONE_SPECIFICATION_PAIRS:
|
||||
bpy_bone_name = mmd_bone_name_to_bpy_bone_name.get(mmd_bone_name)
|
||||
if not bpy_bone_name:
|
||||
continue
|
||||
if bpy_bone_name in mapping or human_bone_specification in mapping.values():
|
||||
continue
|
||||
mapping[bpy_bone_name] = human_bone_specification
|
||||
|
||||
name = "MikuMikuDance"
|
||||
for specification in HumanBoneSpecifications.all_human_bones:
|
||||
if specification.requirement and specification not in mapping.values():
|
||||
return (name, {})
|
||||
|
||||
return (name, mapping)
|
||||
65
common/human_bone_mapper/ready_player_me_mapping.py
Normal file
65
common/human_bone_mapper/ready_player_me_mapping.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
from ..vrm1.human_bone import HumanBoneSpecification, HumanBoneSpecifications
|
||||
|
||||
MAPPING: Final[Mapping[str, HumanBoneSpecification]] = {
|
||||
"Head": HumanBoneSpecifications.HEAD,
|
||||
"RightEye": HumanBoneSpecifications.RIGHT_EYE,
|
||||
"LeftEye": HumanBoneSpecifications.LEFT_EYE,
|
||||
"Neck": HumanBoneSpecifications.NECK,
|
||||
"Spine2": HumanBoneSpecifications.UPPER_CHEST,
|
||||
"Spine1": HumanBoneSpecifications.CHEST,
|
||||
"Spine": HumanBoneSpecifications.SPINE,
|
||||
"Hips": HumanBoneSpecifications.HIPS,
|
||||
"RightShoulder": HumanBoneSpecifications.RIGHT_SHOULDER,
|
||||
"RightArm": HumanBoneSpecifications.RIGHT_UPPER_ARM,
|
||||
"RightForeArm": HumanBoneSpecifications.RIGHT_LOWER_ARM,
|
||||
"RightHand": HumanBoneSpecifications.RIGHT_HAND,
|
||||
"LeftShoulder": HumanBoneSpecifications.LEFT_SHOULDER,
|
||||
"LeftArm": HumanBoneSpecifications.LEFT_UPPER_ARM,
|
||||
"LeftForeArm": HumanBoneSpecifications.LEFT_LOWER_ARM,
|
||||
"LeftHand": HumanBoneSpecifications.LEFT_HAND,
|
||||
"RightUpLeg": HumanBoneSpecifications.RIGHT_UPPER_LEG,
|
||||
"RightLeg": HumanBoneSpecifications.RIGHT_LOWER_LEG,
|
||||
"RightFoot": HumanBoneSpecifications.RIGHT_FOOT,
|
||||
"RightToeBase": HumanBoneSpecifications.RIGHT_TOES,
|
||||
"LeftUpLeg": HumanBoneSpecifications.LEFT_UPPER_LEG,
|
||||
"LeftLeg": HumanBoneSpecifications.LEFT_LOWER_LEG,
|
||||
"LeftFoot": HumanBoneSpecifications.LEFT_FOOT,
|
||||
"LeftToeBase": HumanBoneSpecifications.LEFT_TOES,
|
||||
"RightHandThumb1": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"RightHandThumb2": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"RightHandThumb3": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
"RightHandIndex1": HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
"RightHandIndex2": HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
"RightHandIndex3": HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
"RightHandMiddle1": HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
"RightHandMiddle2": HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
"RightHandMiddle3": HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
"RightHandRing1": HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
"RightHandRing2": HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
"RightHandRing3": HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
"RightHandPinky1": HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
"RightHandPinky2": HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
"RightHandPinky3": HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
"LeftHandThumb1": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"LeftHandThumb2": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"LeftHandThumb3": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"LeftHandIndex1": HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
"LeftHandIndex2": HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
"LeftHandIndex3": HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
"LeftHandMiddle1": HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
"LeftHandMiddle2": HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
"LeftHandMiddle3": HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
"LeftHandRing1": HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
"LeftHandRing2": HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
"LeftHandRing3": HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
"LeftHandPinky1": HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
"LeftHandPinky2": HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
"LeftHandPinky3": HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
}
|
||||
|
||||
|
||||
CONFIG: Final = ("Ready Player Me", MAPPING)
|
||||
65
common/human_bone_mapper/rigify_meta_rig_mapping.py
Normal file
65
common/human_bone_mapper/rigify_meta_rig_mapping.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
from ..vrm1.human_bone import HumanBoneSpecification, HumanBoneSpecifications
|
||||
|
||||
MAPPING: Final[Mapping[str, HumanBoneSpecification]] = {
|
||||
"spine.006": HumanBoneSpecifications.HEAD,
|
||||
"spine.001": HumanBoneSpecifications.SPINE,
|
||||
"spine": HumanBoneSpecifications.HIPS,
|
||||
"upper_arm.R": HumanBoneSpecifications.RIGHT_UPPER_ARM,
|
||||
"forearm.R": HumanBoneSpecifications.RIGHT_LOWER_ARM,
|
||||
"hand.R": HumanBoneSpecifications.RIGHT_HAND,
|
||||
"upper_arm.L": HumanBoneSpecifications.LEFT_UPPER_ARM,
|
||||
"forearm.L": HumanBoneSpecifications.LEFT_LOWER_ARM,
|
||||
"hand.L": HumanBoneSpecifications.LEFT_HAND,
|
||||
"thigh.R": HumanBoneSpecifications.RIGHT_UPPER_LEG,
|
||||
"shin.R": HumanBoneSpecifications.RIGHT_LOWER_LEG,
|
||||
"foot.R": HumanBoneSpecifications.RIGHT_FOOT,
|
||||
"thigh.L": HumanBoneSpecifications.LEFT_UPPER_LEG,
|
||||
"shin.L": HumanBoneSpecifications.LEFT_LOWER_LEG,
|
||||
"foot.L": HumanBoneSpecifications.LEFT_FOOT,
|
||||
"eye.R": HumanBoneSpecifications.RIGHT_EYE,
|
||||
"eye.L": HumanBoneSpecifications.LEFT_EYE,
|
||||
"jaw": HumanBoneSpecifications.JAW,
|
||||
"spine.004": HumanBoneSpecifications.NECK,
|
||||
"shoulder.L": HumanBoneSpecifications.LEFT_SHOULDER,
|
||||
"shoulder.R": HumanBoneSpecifications.RIGHT_SHOULDER,
|
||||
"spine.003": HumanBoneSpecifications.UPPER_CHEST,
|
||||
"spine.002": HumanBoneSpecifications.CHEST,
|
||||
"toe.R": HumanBoneSpecifications.RIGHT_TOES,
|
||||
"toe.L": HumanBoneSpecifications.LEFT_TOES,
|
||||
"thumb.01.L": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"thumb.02.L": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"thumb.03.L": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"f_index.01.L": HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
"f_index.02.L": HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
"f_index.03.L": HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
"f_middle.01.L": HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
"f_middle.02.L": HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
"f_middle.03.L": HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
"f_ring.01.L": HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
"f_ring.02.L": HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
"f_ring.03.L": HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
"f_pinky.01.L": HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
"f_pinky.02.L": HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
"f_pinky.03.L": HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
"thumb.01.R": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"thumb.02.R": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"thumb.03.R": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
"f_index.01.R": HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
"f_index.02.R": HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
"f_index.03.R": HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
"f_middle.01.R": HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
"f_middle.02.R": HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
"f_middle.03.R": HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
"f_ring.01.R": HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
"f_ring.02.R": HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
"f_ring.03.R": HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
"f_pinky.01.R": HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
"f_pinky.02.R": HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
"f_pinky.03.R": HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
}
|
||||
|
||||
CONFIG: Final = ("Rigify Meta-Rig", MAPPING)
|
||||
62
common/human_bone_mapper/unreal_mapping.py
Normal file
62
common/human_bone_mapper/unreal_mapping.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
from ..vrm1.human_bone import HumanBoneSpecification, HumanBoneSpecifications
|
||||
|
||||
MAPPING: Final[Mapping[str, HumanBoneSpecification]] = {
|
||||
"pelvis": HumanBoneSpecifications.HIPS,
|
||||
"spine_01": HumanBoneSpecifications.SPINE,
|
||||
"spine_02": HumanBoneSpecifications.CHEST,
|
||||
"spine_03": HumanBoneSpecifications.UPPER_CHEST,
|
||||
"neck_01": HumanBoneSpecifications.NECK,
|
||||
"head": HumanBoneSpecifications.HEAD,
|
||||
"clavicle_r": HumanBoneSpecifications.RIGHT_SHOULDER,
|
||||
"upperarm_r": HumanBoneSpecifications.RIGHT_UPPER_ARM,
|
||||
"lowerarm_r": HumanBoneSpecifications.RIGHT_LOWER_ARM,
|
||||
"hand_r": HumanBoneSpecifications.RIGHT_HAND,
|
||||
"clavicle_l": HumanBoneSpecifications.LEFT_SHOULDER,
|
||||
"upperarm_l": HumanBoneSpecifications.LEFT_UPPER_ARM,
|
||||
"lowerarm_l": HumanBoneSpecifications.LEFT_LOWER_ARM,
|
||||
"hand_l": HumanBoneSpecifications.LEFT_HAND,
|
||||
"thigh_r": HumanBoneSpecifications.RIGHT_UPPER_LEG,
|
||||
"calf_r": HumanBoneSpecifications.RIGHT_LOWER_LEG,
|
||||
"foot_r": HumanBoneSpecifications.RIGHT_FOOT,
|
||||
"ball_r": HumanBoneSpecifications.RIGHT_TOES,
|
||||
"thigh_l": HumanBoneSpecifications.LEFT_UPPER_LEG,
|
||||
"calf_l": HumanBoneSpecifications.LEFT_LOWER_LEG,
|
||||
"foot_l": HumanBoneSpecifications.LEFT_FOOT,
|
||||
"ball_l": HumanBoneSpecifications.LEFT_TOES,
|
||||
"thumb_01_r": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"thumb_02_r": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"thumb_03_r": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
"thumb_01_l": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"thumb_02_l": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"thumb_03_l": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"index_01_r": HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
"index_02_r": HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
"index_03_r": HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
"index_01_l": HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
"index_02_l": HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
"index_03_l": HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
"middle_01_r": HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
"middle_02_r": HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
"middle_03_r": HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
"middle_01_l": HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
"middle_02_l": HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
"middle_03_l": HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
"ring_01_r": HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
"ring_02_r": HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
"ring_03_r": HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
"ring_01_l": HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
"ring_02_l": HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
"ring_03_l": HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
"pinky_01_r": HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
"pinky_02_r": HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
"pinky_03_r": HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
"pinky_01_l": HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
"pinky_02_l": HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
"pinky_03_l": HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
}
|
||||
|
||||
CONFIG: Final = ("Unreal", MAPPING)
|
||||
112
common/human_bone_mapper/vrm_addon_mapping.py
Normal file
112
common/human_bone_mapper/vrm_addon_mapping.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
from ..vrm1.human_bone import HumanBoneSpecification, HumanBoneSpecifications
|
||||
|
||||
MAPPING: Final[Mapping[str, HumanBoneSpecification]] = {
|
||||
"head": HumanBoneSpecifications.HEAD,
|
||||
"spine": HumanBoneSpecifications.SPINE,
|
||||
"hips": HumanBoneSpecifications.HIPS,
|
||||
"upper_arm.R": HumanBoneSpecifications.RIGHT_UPPER_ARM,
|
||||
"lower_arm.R": HumanBoneSpecifications.RIGHT_LOWER_ARM,
|
||||
"hand.R": HumanBoneSpecifications.RIGHT_HAND,
|
||||
"upper_arm.L": HumanBoneSpecifications.LEFT_UPPER_ARM,
|
||||
"lower_arm.L": HumanBoneSpecifications.LEFT_LOWER_ARM,
|
||||
"hand.L": HumanBoneSpecifications.LEFT_HAND,
|
||||
"upper_leg.R": HumanBoneSpecifications.RIGHT_UPPER_LEG,
|
||||
"lower_leg.R": HumanBoneSpecifications.RIGHT_LOWER_LEG,
|
||||
"foot.R": HumanBoneSpecifications.RIGHT_FOOT,
|
||||
"upper_leg.L": HumanBoneSpecifications.LEFT_UPPER_LEG,
|
||||
"lower_leg.L": HumanBoneSpecifications.LEFT_LOWER_LEG,
|
||||
"foot.L": HumanBoneSpecifications.LEFT_FOOT,
|
||||
"eye.R": HumanBoneSpecifications.RIGHT_EYE,
|
||||
"eye.L": HumanBoneSpecifications.LEFT_EYE,
|
||||
"neck": HumanBoneSpecifications.NECK,
|
||||
"shoulder.L": HumanBoneSpecifications.LEFT_SHOULDER,
|
||||
"shoulder.R": HumanBoneSpecifications.RIGHT_SHOULDER,
|
||||
"upper_chest": HumanBoneSpecifications.UPPER_CHEST,
|
||||
"chest": HumanBoneSpecifications.CHEST,
|
||||
"toes.R": HumanBoneSpecifications.RIGHT_TOES,
|
||||
"toes.L": HumanBoneSpecifications.LEFT_TOES,
|
||||
"thumb_metacarpal.L": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"thumb.metacarpal.L": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"thumb_proximal.L": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"thumb.proximal.L": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"thumb_distal.L": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"thumb.distal.L": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"index_proximal.L": HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
"index.proximal.L": HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
"index_intermediate.L": HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
"index.intermediate.L": HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
"index_distal.L": HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
"index.distal.L": HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
"middle_proximal.L": HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
"middle.proximal.L": HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
"middle_intermediate.L": HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
"middle.intermediate.L": HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
"middle_distal.L": HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
"middle.distal.L": HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
"ring_proximal.L": HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
"ring.proximal.L": HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
"ring_intermediate.L": HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
"ring.intermediate.L": HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
"ring_distal.L": HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
"ring.distal.L": HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
"little_proximal.L": HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
"little.proximal.L": HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
"little_intermediate.L": HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
"little.intermediate.L": HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
"little_distal.L": HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
"little.distal.L": HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
"thumb_metacarpal.R": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"thumb.metacarpal.R": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"thumb_proximal.R": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"thumb.proximal.R": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"thumb_distal.R": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
"thumb.distal.R": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
"index_proximal.R": HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
"index.proximal.R": HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
"index_intermediate.R": HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
"index.intermediate.R": HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
"index_distal.R": HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
"index.distal.R": HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
"middle_proximal.R": HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
"middle.proximal.R": HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
"middle_intermediate.R": HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
"middle.intermediate.R": HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
"middle_distal.R": HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
"middle.distal.R": HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
"ring_proximal.R": HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
"ring.proximal.R": HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
"ring_intermediate.R": HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
"ring.intermediate.R": HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
"ring_distal.R": HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
"ring.distal.R": HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
"little_proximal.R": HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
"little.proximal.R": HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
"little_intermediate.R": HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
"little.intermediate.R": HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
"little_distal.R": HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
"little.distal.R": HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
}
|
||||
|
||||
CONFIG_VRM1: Final = ("VRM Add-on (VRM1)", MAPPING)
|
||||
CONFIG_VRM0: Final = (
|
||||
"VRM Add-on (VRM0)",
|
||||
{
|
||||
**MAPPING,
|
||||
"thumb_proximal.L": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"thumb.proximal.L": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"thumb_intermediate.L": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"thumb.intermediate.L": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"thumb_distal.L": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"thumb.distal.L": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"thumb_proximal.R": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"thumb.proximal.R": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"thumb_intermediate.R": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"thumb.intermediate.R": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"thumb_distal.R": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
"thumb.distal.R": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
},
|
||||
)
|
||||
89
common/human_bone_mapper/vroid_mapping.py
Normal file
89
common/human_bone_mapper/vroid_mapping.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
from ..vrm1.human_bone import HumanBoneSpecification, HumanBoneSpecifications
|
||||
|
||||
LEFT_PATTERN, RIGHT_PATTERN, FULL_PATTERN = (
|
||||
re.compile("^J_(Adj|Bip|Opt|Sec)_L_"),
|
||||
re.compile("^J_(Adj|Bip|Opt|Sec)_R_"),
|
||||
re.compile("^J_(Adj|Bip|Opt|Sec)_([CLR]_)?"),
|
||||
)
|
||||
|
||||
|
||||
def symmetrise_vroid_bone_name(bone_name: str) -> str:
|
||||
left = LEFT_PATTERN.sub("", bone_name)
|
||||
if left != bone_name:
|
||||
return left + "_L"
|
||||
|
||||
right = RIGHT_PATTERN.sub("", bone_name)
|
||||
if right != bone_name:
|
||||
return right + "_R"
|
||||
|
||||
return FULL_PATTERN.sub("", bone_name)
|
||||
|
||||
|
||||
MAPPING: Final[Mapping[str, HumanBoneSpecification]] = {
|
||||
"J_Bip_C_Hips": HumanBoneSpecifications.HIPS,
|
||||
"J_Bip_C_Spine": HumanBoneSpecifications.SPINE,
|
||||
"J_Bip_C_Chest": HumanBoneSpecifications.CHEST,
|
||||
"J_Bip_C_UpperChest": HumanBoneSpecifications.UPPER_CHEST,
|
||||
"J_Bip_C_Neck": HumanBoneSpecifications.NECK,
|
||||
"J_Bip_C_Head": HumanBoneSpecifications.HEAD,
|
||||
"J_Adj_L_FaceEye": HumanBoneSpecifications.LEFT_EYE,
|
||||
"J_Adj_R_FaceEye": HumanBoneSpecifications.RIGHT_EYE,
|
||||
"J_Bip_L_UpperLeg": HumanBoneSpecifications.LEFT_UPPER_LEG,
|
||||
"J_Bip_L_LowerLeg": HumanBoneSpecifications.LEFT_LOWER_LEG,
|
||||
"J_Bip_L_Foot": HumanBoneSpecifications.LEFT_FOOT,
|
||||
"J_Bip_L_ToeBase": HumanBoneSpecifications.LEFT_TOES,
|
||||
"J_Bip_R_UpperLeg": HumanBoneSpecifications.RIGHT_UPPER_LEG,
|
||||
"J_Bip_R_LowerLeg": HumanBoneSpecifications.RIGHT_LOWER_LEG,
|
||||
"J_Bip_R_Foot": HumanBoneSpecifications.RIGHT_FOOT,
|
||||
"J_Bip_R_ToeBase": HumanBoneSpecifications.RIGHT_TOES,
|
||||
"J_Bip_L_Shoulder": HumanBoneSpecifications.LEFT_SHOULDER,
|
||||
"J_Bip_L_UpperArm": HumanBoneSpecifications.LEFT_UPPER_ARM,
|
||||
"J_Bip_L_LowerArm": HumanBoneSpecifications.LEFT_LOWER_ARM,
|
||||
"J_Bip_L_Hand": HumanBoneSpecifications.LEFT_HAND,
|
||||
"J_Bip_R_Shoulder": HumanBoneSpecifications.RIGHT_SHOULDER,
|
||||
"J_Bip_R_UpperArm": HumanBoneSpecifications.RIGHT_UPPER_ARM,
|
||||
"J_Bip_R_LowerArm": HumanBoneSpecifications.RIGHT_LOWER_ARM,
|
||||
"J_Bip_R_Hand": HumanBoneSpecifications.RIGHT_HAND,
|
||||
"J_Bip_L_Thumb1": HumanBoneSpecifications.LEFT_THUMB_METACARPAL,
|
||||
"J_Bip_L_Thumb2": HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
"J_Bip_L_Thumb3": HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
"J_Bip_L_Index1": HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
"J_Bip_L_Index2": HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
"J_Bip_L_Index3": HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
"J_Bip_L_Middle1": HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
"J_Bip_L_Middle2": HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
"J_Bip_L_Middle3": HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
"J_Bip_L_Ring1": HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
"J_Bip_L_Ring2": HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
"J_Bip_L_Ring3": HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
"J_Bip_L_Little1": HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
"J_Bip_L_Little2": HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
"J_Bip_L_Little3": HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
"J_Bip_R_Thumb1": HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,
|
||||
"J_Bip_R_Thumb2": HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
"J_Bip_R_Thumb3": HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
"J_Bip_R_Index1": HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
"J_Bip_R_Index2": HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
"J_Bip_R_Index3": HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
"J_Bip_R_Middle1": HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
"J_Bip_R_Middle2": HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
"J_Bip_R_Middle3": HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
"J_Bip_R_Ring1": HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
"J_Bip_R_Ring2": HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
"J_Bip_R_Ring3": HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
"J_Bip_R_Little1": HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
"J_Bip_R_Little2": HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
"J_Bip_R_Little3": HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
}
|
||||
|
||||
|
||||
CONFIG: Final = ("VRoid", MAPPING)
|
||||
CONFIG_SYMMETRICAL: Final = (
|
||||
"VRoid (Symmetrised)",
|
||||
{symmetrise_vroid_bone_name(k): v for k, v in MAPPING.items()},
|
||||
)
|
||||
16
common/legacy_gltf.py
Normal file
16
common/legacy_gltf.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
"""Legacy glTF shader that existed before version 2.10.5.
|
||||
|
||||
https://github.com/saturday06/VRM-Addon-for-Blender/tree/2_10_5
|
||||
"""
|
||||
|
||||
from typing import Final
|
||||
|
||||
TEXTURE_INPUT_NAMES: Final = (
|
||||
"color_texture",
|
||||
"normal",
|
||||
"emissive_texture",
|
||||
"occlusion_texture",
|
||||
)
|
||||
VAL_INPUT_NAMES: Final = ("metallic", "roughness", "unlit")
|
||||
RGBA_INPUT_NAMES: Final = ("base_Color", "emissive_color")
|
||||
58
common/logger.py
Normal file
58
common/logger.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import logging
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from os import environ
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
import bpy
|
||||
|
||||
# https://github.com/python/typeshed/issues/7855
|
||||
if TYPE_CHECKING or sys.version_info >= (3, 11):
|
||||
LoggerAdapter = logging.LoggerAdapter[logging.Logger]
|
||||
else:
|
||||
LoggerAdapter = logging.LoggerAdapter
|
||||
|
||||
|
||||
class VrmAddonLoggerAdapter(LoggerAdapter):
|
||||
def log(
|
||||
self,
|
||||
level: int,
|
||||
msg: object,
|
||||
*args: object,
|
||||
exc_info: Union[
|
||||
None,
|
||||
bool,
|
||||
Union[
|
||||
tuple[type[BaseException], BaseException, Optional[TracebackType]],
|
||||
tuple[None, None, None],
|
||||
],
|
||||
BaseException,
|
||||
] = None,
|
||||
stack_info: bool = False,
|
||||
stacklevel: int = 1,
|
||||
extra: Optional[Mapping[str, object]] = None,
|
||||
**kwargs: object,
|
||||
) -> None:
|
||||
level_name = logging.getLevelName(level)
|
||||
super().log(
|
||||
level,
|
||||
f"[VRM Add-on:{level_name}] {msg}",
|
||||
*args,
|
||||
exc_info=exc_info,
|
||||
stack_info=stack_info,
|
||||
stacklevel=stacklevel,
|
||||
extra=extra,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# https://docs.python.org/3.7/library/logging.html#logging.getLogger
|
||||
def get_logger(name: str) -> LoggerAdapter:
|
||||
logger = logging.getLogger(name)
|
||||
if bpy.app.debug or environ.get("BLENDER_VRM_LOGGING_LEVEL_DEBUG") == "yes":
|
||||
logger.setLevel(min(logging.DEBUG, logger.getEffectiveLevel()))
|
||||
else:
|
||||
logger.setLevel(max(logging.INFO, logger.getEffectiveLevel()))
|
||||
return VrmAddonLoggerAdapter(logger, {})
|
||||
BIN
common/mtoon0.blend
Normal file
BIN
common/mtoon0.blend
Normal file
Binary file not shown.
BIN
common/mtoon1.blend
Normal file
BIN
common/mtoon1.blend
Normal file
Binary file not shown.
BIN
common/mtoon1_outline.blend
Normal file
BIN
common/mtoon1_outline.blend
Normal file
Binary file not shown.
BIN
common/mtoon_auto_setup.blend
Normal file
BIN
common/mtoon_auto_setup.blend
Normal file
Binary file not shown.
62
common/mtoon_unversioned.py
Normal file
62
common/mtoon_unversioned.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Mapping
|
||||
from typing import ClassVar, Optional
|
||||
|
||||
|
||||
class MtoonUnversioned:
|
||||
# {key = MToonProp, val = ShaderNodeGroup_member_name}
|
||||
version = 32
|
||||
float_props_exchange_dict: Mapping[str, Optional[str]] = {
|
||||
"_MToonVersion": None,
|
||||
"_Cutoff": "CutoffRate",
|
||||
"_BumpScale": "BumpScale",
|
||||
"_ReceiveShadowRate": "ReceiveShadowRate",
|
||||
"_ShadeShift": "ShadeShift",
|
||||
"_ShadeToony": "ShadeToony",
|
||||
"_RimLightingMix": "RimLightingMix",
|
||||
"_RimFresnelPower": "RimFresnelPower",
|
||||
"_RimLift": "RimLift",
|
||||
"_ShadingGradeRate": "ShadingGradeRate",
|
||||
"_LightColorAttenuation": "LightColorAttenuation",
|
||||
"_IndirectLightIntensity": "IndirectLightIntensity",
|
||||
"_OutlineWidth": "OutlineWidth",
|
||||
"_OutlineScaledMaxDistance": "OutlineScaleMaxDistance",
|
||||
"_OutlineLightingMix": "OutlineLightingMix",
|
||||
"_UvAnimScrollX": "UV_Scroll_X", # TODO: #####
|
||||
"_UvAnimScrollY": "UV_Scroll_Y", # TODO: #####
|
||||
"_UvAnimRotation": "UV_Scroll_Rotation", # TODO: #####
|
||||
"_DebugMode": None,
|
||||
"_BlendMode": None,
|
||||
"_OutlineWidthMode": "OutlineWidthMode",
|
||||
"_OutlineColorMode": "OutlineColorMode",
|
||||
"_CullMode": None,
|
||||
"_OutlineCullMode": None,
|
||||
"_SrcBlend": None,
|
||||
"_DstBlend": None,
|
||||
"_ZWrite": None,
|
||||
"_IsFirstSetup": None,
|
||||
}
|
||||
|
||||
texture_kind_exchange_dict: Mapping[str, str] = {
|
||||
"_MainTex": "MainTexture",
|
||||
"_ShadeTexture": "ShadeTexture",
|
||||
"_BumpMap": "NormalmapTexture",
|
||||
"_ReceiveShadowTexture": "ReceiveShadow_Texture",
|
||||
"_ShadingGradeTexture": "ShadingGradeTexture",
|
||||
"_EmissionMap": "Emission_Texture",
|
||||
"_SphereAdd": "SphereAddTexture",
|
||||
"_RimTexture": "RimTexture",
|
||||
"_OutlineWidthTexture": "OutlineWidthTexture",
|
||||
"_UvAnimMaskTexture": "UV_Animation_Mask_Texture", # TODO: ####
|
||||
}
|
||||
vector_base_props_exchange_dict: Mapping[str, str] = {
|
||||
"_Color": "DiffuseColor",
|
||||
"_ShadeColor": "ShadeColor",
|
||||
"_EmissionColor": "EmissionColor",
|
||||
"_RimColor": "RimColor",
|
||||
"_OutlineColor": "OutlineColor",
|
||||
}
|
||||
# texture offset and scaling props by texture
|
||||
vector_props_exchange_dict: ClassVar[dict[str, str]] = {}
|
||||
vector_props_exchange_dict.update(vector_base_props_exchange_dict)
|
||||
vector_props_exchange_dict.update(texture_kind_exchange_dict)
|
||||
10
common/ops/__init__.py
Normal file
10
common/ops/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from . import export_scene, icyp, import_scene, vrm, wm
|
||||
|
||||
__all__ = [
|
||||
"export_scene",
|
||||
"icyp",
|
||||
"import_scene",
|
||||
"vrm",
|
||||
"wm",
|
||||
]
|
||||
BIN
common/ops/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
common/ops/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/ops/__pycache__/export_scene.cpython-311.pyc
Normal file
BIN
common/ops/__pycache__/export_scene.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/ops/__pycache__/icyp.cpython-311.pyc
Normal file
BIN
common/ops/__pycache__/icyp.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/ops/__pycache__/import_scene.cpython-311.pyc
Normal file
BIN
common/ops/__pycache__/import_scene.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/ops/__pycache__/vrm.cpython-311.pyc
Normal file
BIN
common/ops/__pycache__/vrm.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/ops/__pycache__/wm.cpython-311.pyc
Normal file
BIN
common/ops/__pycache__/wm.cpython-311.pyc
Normal file
Binary file not shown.
67
common/ops/export_scene.py
Normal file
67
common/ops/export_scene.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Optional, Union
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrm(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
filter_glob: str = "*.vrm",
|
||||
use_addon_preferences: bool = False,
|
||||
export_invisibles: bool = False,
|
||||
export_only_selections: bool = False,
|
||||
enable_advanced_preferences: bool = False,
|
||||
export_all_influences: bool = False,
|
||||
export_lights: bool = False,
|
||||
export_gltf_animations: bool = False,
|
||||
export_try_sparse_sk: bool = False,
|
||||
errors: Optional[Sequence[Mapping[str, Union[str, int, float, bool]]]] = None,
|
||||
armature_object_name: str = "",
|
||||
ignore_warning: bool = False,
|
||||
filepath: str = "",
|
||||
check_existing: bool = True,
|
||||
) -> set[str]:
|
||||
return bpy.ops.export_scene.vrm( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
filter_glob=filter_glob,
|
||||
use_addon_preferences=use_addon_preferences,
|
||||
export_invisibles=export_invisibles,
|
||||
export_only_selections=export_only_selections,
|
||||
enable_advanced_preferences=enable_advanced_preferences,
|
||||
export_all_influences=export_all_influences,
|
||||
export_lights=export_lights,
|
||||
export_gltf_animations=export_gltf_animations,
|
||||
export_try_sparse_sk=export_try_sparse_sk,
|
||||
errors=errors if errors is not None else [],
|
||||
armature_object_name=armature_object_name,
|
||||
ignore_warning=ignore_warning,
|
||||
filepath=filepath,
|
||||
check_existing=check_existing,
|
||||
)
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrma(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
filter_glob: str = "*.vrma",
|
||||
armature_object_name: str = "",
|
||||
filepath: str = "",
|
||||
check_existing: bool = True,
|
||||
) -> set[str]:
|
||||
return bpy.ops.export_scene.vrma( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
filter_glob=filter_glob,
|
||||
armature_object_name=armature_object_name,
|
||||
filepath=filepath,
|
||||
check_existing=check_existing,
|
||||
)
|
||||
53
common/ops/icyp.py
Normal file
53
common/ops/icyp.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def make_basic_armature(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
skip_heavy_armature_setup: bool = False,
|
||||
wip_with_template_mesh: bool = False,
|
||||
tall: float = 1.7,
|
||||
head_ratio: float = 8.0,
|
||||
head_width_ratio: float = 0.6666666666666666,
|
||||
aging_ratio: float = 0.5,
|
||||
eye_depth: float = -0.03,
|
||||
shoulder_in_width: float = 0.05,
|
||||
shoulder_width: float = 0.08,
|
||||
arm_length_ratio: float = 1,
|
||||
hand_ratio: float = 1,
|
||||
finger_1_2_ratio: float = 0.75,
|
||||
finger_2_3_ratio: float = 0.75,
|
||||
nail_bone: bool = False,
|
||||
leg_length_ratio: float = 0.5,
|
||||
leg_width_ratio: float = 1,
|
||||
leg_size: float = 0.26,
|
||||
custom_property_name: str = "",
|
||||
) -> set[str]:
|
||||
return bpy.ops.icyp.make_basic_armature( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
skip_heavy_armature_setup=skip_heavy_armature_setup,
|
||||
wip_with_template_mesh=wip_with_template_mesh,
|
||||
tall=tall,
|
||||
head_ratio=head_ratio,
|
||||
head_width_ratio=head_width_ratio,
|
||||
aging_ratio=aging_ratio,
|
||||
eye_depth=eye_depth,
|
||||
shoulder_in_width=shoulder_in_width,
|
||||
shoulder_width=shoulder_width,
|
||||
arm_length_ratio=arm_length_ratio,
|
||||
hand_ratio=hand_ratio,
|
||||
finger_1_2_ratio=finger_1_2_ratio,
|
||||
finger_2_3_ratio=finger_2_3_ratio,
|
||||
nail_bone=nail_bone,
|
||||
leg_length_ratio=leg_length_ratio,
|
||||
leg_width_ratio=leg_width_ratio,
|
||||
leg_size=leg_size,
|
||||
custom_property_name=custom_property_name,
|
||||
)
|
||||
57
common/ops/import_scene.py
Normal file
57
common/ops/import_scene.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrm(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
filter_glob: str = "*.vrm",
|
||||
use_addon_preferences: bool = False,
|
||||
extract_textures_into_folder: bool = False,
|
||||
make_new_texture_folder: bool = True,
|
||||
set_shading_type_to_material_on_import: bool = True,
|
||||
set_view_transform_to_standard_on_import: bool = True,
|
||||
set_armature_display_to_wire: bool = True,
|
||||
set_armature_display_to_show_in_front: bool = True,
|
||||
set_armature_bone_shape_to_default: bool = True,
|
||||
enable_mtoon_outline_preview: bool = True,
|
||||
filepath: str = "",
|
||||
) -> set[str]:
|
||||
return bpy.ops.import_scene.vrm( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
filter_glob=filter_glob,
|
||||
use_addon_preferences=use_addon_preferences,
|
||||
extract_textures_into_folder=extract_textures_into_folder,
|
||||
make_new_texture_folder=make_new_texture_folder,
|
||||
set_shading_type_to_material_on_import=set_shading_type_to_material_on_import,
|
||||
set_view_transform_to_standard_on_import=set_view_transform_to_standard_on_import,
|
||||
set_armature_display_to_wire=set_armature_display_to_wire,
|
||||
set_armature_display_to_show_in_front=set_armature_display_to_show_in_front,
|
||||
set_armature_bone_shape_to_default=set_armature_bone_shape_to_default,
|
||||
enable_mtoon_outline_preview=enable_mtoon_outline_preview,
|
||||
filepath=filepath,
|
||||
)
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrma(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
filter_glob: str = "*.vrma",
|
||||
armature_object_name: str = "",
|
||||
filepath: str = "",
|
||||
) -> set[str]:
|
||||
return bpy.ops.import_scene.vrma( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
filter_glob=filter_glob,
|
||||
armature_object_name=armature_object_name,
|
||||
filepath=filepath,
|
||||
)
|
||||
1890
common/ops/vrm.py
Normal file
1890
common/ops/vrm.py
Normal file
File diff suppressed because it is too large
Load Diff
165
common/ops/wm.py
Normal file
165
common/ops/wm.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Optional, Union
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrm_gltf2_addon_disabled_warning(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
) -> set[str]:
|
||||
return bpy.ops.wm.vrm_gltf2_addon_disabled_warning( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
)
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrm_export_human_bones_assignment(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
armature_object_name: str = "",
|
||||
) -> set[str]:
|
||||
return bpy.ops.wm.vrm_export_human_bones_assignment( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
armature_object_name=armature_object_name,
|
||||
)
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrm_export_confirmation(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
errors: Optional[Sequence[Mapping[str, Union[str, int, float, bool]]]] = None,
|
||||
armature_object_name: str = "",
|
||||
export_anyway: bool = False,
|
||||
) -> set[str]:
|
||||
return bpy.ops.wm.vrm_export_confirmation( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
errors=errors if errors is not None else [],
|
||||
armature_object_name=armature_object_name,
|
||||
export_anyway=export_anyway,
|
||||
)
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrm_export_armature_selection(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
armature_object_name: str = "",
|
||||
armature_object_name_candidates: Optional[
|
||||
Sequence[Mapping[str, Union[str, int, float, bool]]]
|
||||
] = None,
|
||||
) -> set[str]:
|
||||
return bpy.ops.wm.vrm_export_armature_selection( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
armature_object_name=armature_object_name,
|
||||
armature_object_name_candidates=armature_object_name_candidates
|
||||
if armature_object_name_candidates is not None
|
||||
else [],
|
||||
)
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrma_export_prerequisite(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
armature_object_name: str = "",
|
||||
armature_object_name_candidates: Optional[
|
||||
Sequence[Mapping[str, Union[str, int, float, bool]]]
|
||||
] = None,
|
||||
) -> set[str]:
|
||||
return bpy.ops.wm.vrma_export_prerequisite( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
armature_object_name=armature_object_name,
|
||||
armature_object_name_candidates=armature_object_name_candidates
|
||||
if armature_object_name_candidates is not None
|
||||
else [],
|
||||
)
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrm_error_dialog(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
title: str = "",
|
||||
lines: Optional[Sequence[Mapping[str, Union[str, int, float, bool]]]] = None,
|
||||
active_line_index: int = 0,
|
||||
) -> set[str]:
|
||||
return bpy.ops.wm.vrm_error_dialog( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
title=title,
|
||||
lines=lines if lines is not None else [],
|
||||
active_line_index=active_line_index,
|
||||
)
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrm_license_warning(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
filepath: str = "",
|
||||
license_confirmations: Optional[
|
||||
Sequence[Mapping[str, Union[str, int, float, bool]]]
|
||||
] = None,
|
||||
import_anyway: bool = False,
|
||||
extract_textures_into_folder: bool = False,
|
||||
make_new_texture_folder: bool = False,
|
||||
set_shading_type_to_material_on_import: bool = False,
|
||||
set_view_transform_to_standard_on_import: bool = False,
|
||||
set_armature_display_to_wire: bool = False,
|
||||
set_armature_display_to_show_in_front: bool = False,
|
||||
set_armature_bone_shape_to_default: bool = False,
|
||||
enable_mtoon_outline_preview: bool = False,
|
||||
) -> set[str]:
|
||||
return bpy.ops.wm.vrm_license_warning( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
filepath=filepath,
|
||||
license_confirmations=license_confirmations
|
||||
if license_confirmations is not None
|
||||
else [],
|
||||
import_anyway=import_anyway,
|
||||
extract_textures_into_folder=extract_textures_into_folder,
|
||||
make_new_texture_folder=make_new_texture_folder,
|
||||
set_shading_type_to_material_on_import=set_shading_type_to_material_on_import,
|
||||
set_view_transform_to_standard_on_import=set_view_transform_to_standard_on_import,
|
||||
set_armature_display_to_wire=set_armature_display_to_wire,
|
||||
set_armature_display_to_show_in_front=set_armature_display_to_show_in_front,
|
||||
set_armature_bone_shape_to_default=set_armature_bone_shape_to_default,
|
||||
enable_mtoon_outline_preview=enable_mtoon_outline_preview,
|
||||
)
|
||||
|
||||
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
def vrma_import_prerequisite(
|
||||
execution_context: str = "EXEC_DEFAULT",
|
||||
/,
|
||||
*,
|
||||
armature_object_name: str = "",
|
||||
armature_object_name_candidates: Optional[
|
||||
Sequence[Mapping[str, Union[str, int, float, bool]]]
|
||||
] = None,
|
||||
) -> set[str]:
|
||||
return bpy.ops.wm.vrma_import_prerequisite( # type: ignore[attr-defined, no-any-return]
|
||||
execution_context,
|
||||
armature_object_name=armature_object_name,
|
||||
armature_object_name_candidates=armature_object_name_candidates
|
||||
if armature_object_name_candidates is not None
|
||||
else [],
|
||||
)
|
||||
361
common/preferences.py
Normal file
361
common/preferences.py
Normal file
@@ -0,0 +1,361 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Protocol, TypedDict, Union
|
||||
|
||||
from bpy.app.translations import pgettext
|
||||
from bpy.props import BoolProperty, IntVectorProperty
|
||||
from bpy.types import AddonPreferences, Context, Operator, UILayout
|
||||
|
||||
from . import version
|
||||
from .logger import get_logger
|
||||
from .shader import (
|
||||
add_mtoon1_auto_setup_shader_node_group,
|
||||
remove_mtoon1_auto_setup_shader_node_group,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
addon_package_name = ".".join(__name__.split(".")[:-2])
|
||||
|
||||
|
||||
class ImportPreferencesProtocol(Protocol):
|
||||
extract_textures_into_folder: bool
|
||||
make_new_texture_folder: bool
|
||||
set_shading_type_to_material_on_import: bool
|
||||
set_view_transform_to_standard_on_import: bool
|
||||
set_armature_display_to_wire: bool
|
||||
set_armature_display_to_show_in_front: bool
|
||||
set_armature_bone_shape_to_default: bool
|
||||
enable_mtoon_outline_preview: bool
|
||||
|
||||
|
||||
class ImportPreferencesDict(TypedDict):
|
||||
extract_textures_into_folder: bool
|
||||
make_new_texture_folder: bool
|
||||
set_shading_type_to_material_on_import: bool
|
||||
set_view_transform_to_standard_on_import: bool
|
||||
set_armature_display_to_wire: bool
|
||||
set_armature_display_to_show_in_front: bool
|
||||
set_armature_bone_shape_to_default: bool
|
||||
enable_mtoon_outline_preview: bool
|
||||
|
||||
|
||||
def create_import_preferences_dict(
|
||||
source: ImportPreferencesProtocol,
|
||||
) -> ImportPreferencesDict:
|
||||
return {
|
||||
"extract_textures_into_folder": source.extract_textures_into_folder,
|
||||
"make_new_texture_folder": source.make_new_texture_folder,
|
||||
"set_shading_type_to_material_on_import": (
|
||||
source.set_shading_type_to_material_on_import
|
||||
),
|
||||
"set_view_transform_to_standard_on_import": (
|
||||
source.set_view_transform_to_standard_on_import
|
||||
),
|
||||
"set_armature_display_to_wire": (source.set_armature_display_to_wire),
|
||||
"set_armature_display_to_show_in_front": (
|
||||
source.set_armature_display_to_show_in_front
|
||||
),
|
||||
"set_armature_bone_shape_to_default": source.set_armature_bone_shape_to_default,
|
||||
"enable_mtoon_outline_preview": source.enable_mtoon_outline_preview,
|
||||
}
|
||||
|
||||
|
||||
def copy_import_preferences(
|
||||
*, source: ImportPreferencesProtocol, destination: ImportPreferencesProtocol
|
||||
) -> None:
|
||||
(
|
||||
destination.extract_textures_into_folder,
|
||||
destination.make_new_texture_folder,
|
||||
destination.set_shading_type_to_material_on_import,
|
||||
destination.set_view_transform_to_standard_on_import,
|
||||
destination.set_armature_display_to_wire,
|
||||
destination.set_armature_display_to_show_in_front,
|
||||
destination.set_armature_bone_shape_to_default,
|
||||
destination.enable_mtoon_outline_preview,
|
||||
) = (
|
||||
source.extract_textures_into_folder,
|
||||
source.make_new_texture_folder,
|
||||
source.set_shading_type_to_material_on_import,
|
||||
source.set_view_transform_to_standard_on_import,
|
||||
source.set_armature_display_to_wire,
|
||||
source.set_armature_display_to_show_in_front,
|
||||
source.set_armature_bone_shape_to_default,
|
||||
source.enable_mtoon_outline_preview,
|
||||
)
|
||||
|
||||
|
||||
def draw_import_preferences_layout(
|
||||
preferences: ImportPreferencesProtocol, layout: UILayout
|
||||
) -> None:
|
||||
if not isinstance(preferences, (AddonPreferences, Operator)):
|
||||
return
|
||||
|
||||
layout.prop(preferences, "extract_textures_into_folder")
|
||||
layout.prop(preferences, "make_new_texture_folder")
|
||||
layout.prop(preferences, "set_shading_type_to_material_on_import")
|
||||
layout.prop(preferences, "set_view_transform_to_standard_on_import")
|
||||
layout.prop(preferences, "set_armature_display_to_wire")
|
||||
layout.prop(preferences, "set_armature_display_to_show_in_front")
|
||||
layout.prop(preferences, "set_armature_bone_shape_to_default")
|
||||
layout.prop(preferences, "enable_mtoon_outline_preview")
|
||||
|
||||
|
||||
class ExportPreferencesProtocol(Protocol):
|
||||
export_invisibles: bool
|
||||
export_only_selections: bool
|
||||
enable_advanced_preferences: bool
|
||||
export_all_influences: bool
|
||||
export_lights: bool
|
||||
export_gltf_animations: bool
|
||||
export_try_sparse_sk: bool
|
||||
|
||||
|
||||
def copy_export_preferences(
|
||||
*, source: ExportPreferencesProtocol, destination: ExportPreferencesProtocol
|
||||
) -> None:
|
||||
(
|
||||
destination.export_invisibles,
|
||||
destination.export_only_selections,
|
||||
destination.enable_advanced_preferences,
|
||||
destination.export_all_influences,
|
||||
destination.export_lights,
|
||||
destination.export_gltf_animations,
|
||||
destination.export_try_sparse_sk,
|
||||
) = (
|
||||
source.export_invisibles,
|
||||
source.export_only_selections,
|
||||
source.enable_advanced_preferences,
|
||||
source.export_all_influences,
|
||||
source.export_lights,
|
||||
source.export_gltf_animations,
|
||||
source.export_try_sparse_sk,
|
||||
)
|
||||
|
||||
|
||||
def draw_advanced_options_description(
|
||||
preferences: Union[AddonPreferences, Operator],
|
||||
property_name: str,
|
||||
layout: UILayout,
|
||||
description: str,
|
||||
) -> None:
|
||||
column = layout.box().column(align=True)
|
||||
column.prop(preferences, property_name)
|
||||
description_column = column.box().column(align=True)
|
||||
for i, line in enumerate(description.splitlines()):
|
||||
icon = "ERROR" if i == 0 else "NONE"
|
||||
description_column.label(text=line, translate=False, icon=icon)
|
||||
|
||||
|
||||
def draw_export_preferences_layout(
|
||||
preferences: ExportPreferencesProtocol,
|
||||
layout: UILayout,
|
||||
*,
|
||||
show_vrm1_options: bool,
|
||||
) -> None:
|
||||
if not isinstance(preferences, (AddonPreferences, Operator)):
|
||||
return
|
||||
|
||||
layout.prop(preferences, "export_invisibles")
|
||||
layout.prop(preferences, "export_only_selections")
|
||||
|
||||
if not show_vrm1_options:
|
||||
return
|
||||
|
||||
layout.prop(preferences, "enable_advanced_preferences")
|
||||
if not preferences.enable_advanced_preferences:
|
||||
return
|
||||
|
||||
advanced_options_column = layout.box().column()
|
||||
|
||||
# UniVRM 0.115.0 doesn't support `export_try_sparse_sk`
|
||||
# https://github.com/saturday06/VRM-Addon-for-Blender/issues/381#issuecomment-1838365762
|
||||
draw_advanced_options_description(
|
||||
preferences,
|
||||
"export_try_sparse_sk",
|
||||
advanced_options_column,
|
||||
pgettext(
|
||||
"The file size will be reduced,\n"
|
||||
+ "but it will no longer be readable by\n"
|
||||
+ "older apps with UniVRM 0.115.0 or\n"
|
||||
+ "earlier."
|
||||
),
|
||||
)
|
||||
|
||||
# The upstream says that Models may appear incorrectly in many viewers.
|
||||
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/356b3dda976303d3ecce8b3bd1591245e576db38/addons/io_scene_gltf2/__init__.py#L760
|
||||
draw_advanced_options_description(
|
||||
preferences,
|
||||
"export_all_influences",
|
||||
advanced_options_column,
|
||||
pgettext(
|
||||
"By default, 4 bone influences\n"
|
||||
+ "are exported for each vertex. Many\n"
|
||||
+ "apps truncate to 4. Increasing it\n"
|
||||
+ "may cause jagged meshes."
|
||||
),
|
||||
)
|
||||
|
||||
draw_advanced_options_description(
|
||||
preferences,
|
||||
"export_lights",
|
||||
advanced_options_column,
|
||||
pgettext(
|
||||
"There is no consensus on how\n"
|
||||
+ "to handle lights in VRM, so it is\n"
|
||||
+ "impossible to predict what the\n"
|
||||
+ "outcome will be."
|
||||
),
|
||||
)
|
||||
|
||||
draw_advanced_options_description(
|
||||
preferences,
|
||||
"export_gltf_animations",
|
||||
advanced_options_column,
|
||||
pgettext(
|
||||
"UniVRM does not export\n"
|
||||
+ "glTF Animations, so it is disabled\n"
|
||||
+ "by default. Please consider using\n"
|
||||
+ "VRM Animation."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class VrmAddonPreferences(AddonPreferences):
|
||||
bl_idname = addon_package_name
|
||||
|
||||
INITIAL_ADDON_VERSION: tuple[int, int, int] = (0, 0, 0)
|
||||
|
||||
addon_version: IntVectorProperty( # type: ignore[valid-type]
|
||||
size=3,
|
||||
default=INITIAL_ADDON_VERSION,
|
||||
)
|
||||
|
||||
def update_add_mtoon_shader_node_group(self, context: Context) -> None:
|
||||
if self.add_mtoon_shader_node_group:
|
||||
add_mtoon1_auto_setup_shader_node_group(context)
|
||||
else:
|
||||
remove_mtoon1_auto_setup_shader_node_group(context)
|
||||
|
||||
add_mtoon_shader_node_group: BoolProperty( # type: ignore[valid-type]
|
||||
name="Add MToon shader node group",
|
||||
default=True,
|
||||
update=update_add_mtoon_shader_node_group,
|
||||
)
|
||||
|
||||
extract_textures_into_folder: BoolProperty( # type: ignore[valid-type]
|
||||
name="Extract texture images into the folder",
|
||||
default=False,
|
||||
)
|
||||
make_new_texture_folder: BoolProperty( # type: ignore[valid-type]
|
||||
name="Don't overwrite existing texture folder",
|
||||
default=True,
|
||||
)
|
||||
set_shading_type_to_material_on_import: BoolProperty( # type: ignore[valid-type]
|
||||
name='Set shading type to "Material"',
|
||||
default=True,
|
||||
)
|
||||
set_view_transform_to_standard_on_import: BoolProperty( # type: ignore[valid-type]
|
||||
name='Set view transform to "Standard"',
|
||||
default=True,
|
||||
)
|
||||
set_armature_display_to_wire: BoolProperty( # type: ignore[valid-type]
|
||||
name='Set an imported armature display to "Wire"',
|
||||
default=True,
|
||||
)
|
||||
set_armature_display_to_show_in_front: BoolProperty( # type: ignore[valid-type]
|
||||
name='Set an imported armature display to show "In-Front"',
|
||||
default=True,
|
||||
)
|
||||
set_armature_bone_shape_to_default: BoolProperty( # type: ignore[valid-type]
|
||||
name="Set an imported bone shape to default",
|
||||
default=True,
|
||||
)
|
||||
|
||||
enable_mtoon_outline_preview: BoolProperty( # type: ignore[valid-type]
|
||||
name="Enable MToon Outline Preview",
|
||||
default=True,
|
||||
)
|
||||
|
||||
export_invisibles: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export Invisible Objects",
|
||||
)
|
||||
export_only_selections: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export Only Selections",
|
||||
)
|
||||
enable_advanced_preferences: BoolProperty( # type: ignore[valid-type]
|
||||
name="Enable Advanced Options",
|
||||
)
|
||||
export_all_influences: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export All Bone Influences",
|
||||
)
|
||||
export_lights: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export Lights",
|
||||
)
|
||||
export_gltf_animations: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export glTF Animations",
|
||||
)
|
||||
export_try_sparse_sk: BoolProperty( # type: ignore[valid-type]
|
||||
name="Use Sparse Accessors",
|
||||
)
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
layout = self.layout
|
||||
|
||||
warning_message = version.preferences_warning_message()
|
||||
if warning_message:
|
||||
box = layout.box()
|
||||
warning_column = box.column()
|
||||
for index, warning_line in enumerate(warning_message.splitlines()):
|
||||
warning_column.label(
|
||||
text=warning_line,
|
||||
translate=False,
|
||||
icon="NONE" if index else "ERROR",
|
||||
)
|
||||
|
||||
layout.prop(self, "add_mtoon_shader_node_group")
|
||||
|
||||
import_box = layout.box()
|
||||
import_box.label(text="Import", icon="IMPORT")
|
||||
draw_import_preferences_layout(self, import_box)
|
||||
|
||||
export_box = layout.box()
|
||||
export_box.label(text="Export", icon="EXPORT")
|
||||
draw_export_preferences_layout(self, export_box, show_vrm1_options=True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
addon_version: Sequence[int] # type: ignore[no-redef]
|
||||
add_mtoon_shader_node_group: bool # type: ignore[no-redef]
|
||||
extract_textures_into_folder: bool # type: ignore[no-redef]
|
||||
make_new_texture_folder: bool # type: ignore[no-redef]
|
||||
set_shading_type_to_material_on_import: bool # type: ignore[no-redef]
|
||||
set_view_transform_to_standard_on_import: bool # type: ignore[no-redef]
|
||||
set_armature_display_to_wire: bool # type: ignore[no-redef]
|
||||
set_armature_display_to_show_in_front: bool # type: ignore[no-redef]
|
||||
set_armature_bone_shape_to_default: bool # type: ignore[no-redef]
|
||||
enable_mtoon_outline_preview: bool # type: ignore[no-redef]
|
||||
export_invisibles: bool # type: ignore[no-redef]
|
||||
export_only_selections: bool # type: ignore[no-redef]
|
||||
enable_advanced_preferences: bool # type: ignore[no-redef]
|
||||
export_all_influences: bool # type: ignore[no-redef]
|
||||
export_lights: bool # type: ignore[no-redef]
|
||||
export_gltf_animations: bool # type: ignore[no-redef]
|
||||
export_try_sparse_sk: bool # type: ignore[no-redef]
|
||||
|
||||
|
||||
def get_preferences(context: Context) -> VrmAddonPreferences:
|
||||
addon = context.preferences.addons.get(addon_package_name)
|
||||
if not addon:
|
||||
message = f"No add-on preferences for {addon_package_name}"
|
||||
raise AssertionError(message)
|
||||
|
||||
preferences = addon.preferences
|
||||
if not isinstance(preferences, VrmAddonPreferences):
|
||||
raise TypeError(
|
||||
f"Add-on preferences for {addon_package_name} is not a VrmAddonPreferences"
|
||||
+ f" but {type(preferences)}"
|
||||
)
|
||||
|
||||
return preferences
|
||||
87
common/progress.py
Normal file
87
common/progress.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import math
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from typing import Final, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context
|
||||
|
||||
from .logger 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
|
||||
|
||||
if bpy.app.version >= (5, 0):
|
||||
progress_value = ratio * 9999
|
||||
else:
|
||||
# The mouse cursor becomes a four-digit number, and there is an area
|
||||
# where values from 0 to 9999 can be displayed. However, if the
|
||||
# progress rate is directly converted to a number from 0 to 9999, the
|
||||
# last two digits will frequently round-trip, making the progress
|
||||
# difficult to understand. Therefore, only the last two digits display
|
||||
# area is used to show the progress rate as a number from 0 to 99.
|
||||
progress_value = math.floor(ratio * 99)
|
||||
|
||||
self.context.window_manager.progress_update(progress_value)
|
||||
|
||||
|
||||
@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()
|
||||
87
common/rotation.py
Normal file
87
common/rotation.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from typing import Final, Union
|
||||
|
||||
from bpy.types import (
|
||||
Object,
|
||||
PoseBone,
|
||||
)
|
||||
from mathutils import Quaternion
|
||||
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
ROTATION_MODE_QUATERNION: Final = "QUATERNION"
|
||||
ROTATION_MODE_AXIS_ANGLE: Final = "AXIS_ANGLE"
|
||||
ROTATION_MODE_EULER: Final = ("XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX")
|
||||
|
||||
|
||||
def get_rotation_as_quaternion(
|
||||
object_or_pose_bone: Union[Object, PoseBone],
|
||||
) -> Quaternion:
|
||||
if object_or_pose_bone.rotation_mode == ROTATION_MODE_QUATERNION:
|
||||
return object_or_pose_bone.rotation_quaternion.copy()
|
||||
|
||||
if object_or_pose_bone.rotation_mode == ROTATION_MODE_AXIS_ANGLE:
|
||||
axis_x, axis_y, axis_z, angle = object_or_pose_bone.rotation_axis_angle
|
||||
return Quaternion((axis_x, axis_y, axis_z), angle)
|
||||
|
||||
if object_or_pose_bone.rotation_mode in ROTATION_MODE_EULER:
|
||||
return object_or_pose_bone.rotation_euler.to_quaternion()
|
||||
|
||||
logger.error(
|
||||
"Unexpected rotation mode for %s %s: %s",
|
||||
type(object_or_pose_bone),
|
||||
object_or_pose_bone.name,
|
||||
object_or_pose_bone.rotation_mode,
|
||||
)
|
||||
|
||||
return Quaternion()
|
||||
|
||||
|
||||
def set_rotation_without_mode_change(
|
||||
object_or_pose_bone: Union[Object, PoseBone], quaternion: Quaternion
|
||||
) -> None:
|
||||
if object_or_pose_bone.rotation_mode == ROTATION_MODE_QUATERNION:
|
||||
object_or_pose_bone.rotation_quaternion = quaternion.copy()
|
||||
return
|
||||
|
||||
if object_or_pose_bone.rotation_mode == ROTATION_MODE_AXIS_ANGLE:
|
||||
axis, angle = quaternion.to_axis_angle()
|
||||
object_or_pose_bone.rotation_axis_angle = [axis.x, axis.y, axis.z, angle]
|
||||
return
|
||||
|
||||
if object_or_pose_bone.rotation_mode in ROTATION_MODE_EULER:
|
||||
object_or_pose_bone.rotation_euler = quaternion.to_euler(
|
||||
object_or_pose_bone.rotation_mode
|
||||
)
|
||||
return
|
||||
|
||||
logger.error(
|
||||
"Unexpected rotation mode for %s %s: %s",
|
||||
type(object_or_pose_bone),
|
||||
object_or_pose_bone.name,
|
||||
object_or_pose_bone.rotation_mode,
|
||||
)
|
||||
|
||||
|
||||
def insert_rotation_keyframe(
|
||||
object_or_pose_bone: Union[Object, PoseBone], *, frame: int
|
||||
) -> None:
|
||||
if object_or_pose_bone.rotation_mode == ROTATION_MODE_QUATERNION:
|
||||
data_path = "rotation_quaternion"
|
||||
elif object_or_pose_bone.rotation_mode == ROTATION_MODE_AXIS_ANGLE:
|
||||
data_path = "rotation_axis_angle"
|
||||
elif object_or_pose_bone.rotation_mode in ROTATION_MODE_EULER:
|
||||
data_path = "rotation_euler"
|
||||
else:
|
||||
logger.error(
|
||||
"Unexpected rotation mode for %s %s: %s",
|
||||
type(object_or_pose_bone),
|
||||
object_or_pose_bone.name,
|
||||
object_or_pose_bone.rotation_mode,
|
||||
)
|
||||
return
|
||||
|
||||
object_or_pose_bone.keyframe_insert(data_path, frame=frame)
|
||||
28
common/safe_removal.py
Normal file
28
common/safe_removal.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Context, Object
|
||||
|
||||
|
||||
def remove_object(context: Context, obj: Object) -> bool:
|
||||
for collection in [
|
||||
*[scene.collection for scene in context.blend_data.scenes],
|
||||
*context.blend_data.collections,
|
||||
]:
|
||||
for collection_object in collection.objects:
|
||||
if collection_object.parent != obj:
|
||||
continue
|
||||
if collection_object.parent_type != obj.parent_type:
|
||||
collection_object.parent_type = obj.parent_type
|
||||
if collection_object.parent != obj.parent:
|
||||
collection_object.parent = obj.parent
|
||||
if collection_object.parent_bone != obj.parent_bone:
|
||||
collection_object.parent_bone = obj.parent_bone
|
||||
|
||||
if obj.name in collection.objects:
|
||||
collection.objects.unlink(obj)
|
||||
|
||||
if obj.users:
|
||||
return False
|
||||
|
||||
context.blend_data.objects.remove(obj, do_unlink=True)
|
||||
return True
|
||||
228
common/scene_watcher.py
Normal file
228
common/scene_watcher.py
Normal file
@@ -0,0 +1,228 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import base64
|
||||
import hashlib
|
||||
import inspect
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
from typing import Final, Optional, Protocol
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.types import Context
|
||||
|
||||
|
||||
class RunState(Enum):
|
||||
PREEMPT = 1
|
||||
FINISH = 2
|
||||
|
||||
|
||||
class SceneWatcher(Protocol):
|
||||
"""Protocol for detecting scene changes and performing some action when detected."""
|
||||
|
||||
def run(self, context: Context) -> RunState:
|
||||
"""Detect scene changes and perform some action when detected.
|
||||
|
||||
If no changes that need to be detected have occurred in the scene, this method
|
||||
must return in less than 100 microseconds. If there is not enough time, save
|
||||
the current state to instance variables and interrupt the process.
|
||||
|
||||
This method is executed multiple times across Blender frames. Usually such
|
||||
processing can be efficiently implemented using generators or async, but this
|
||||
class does not do so.
|
||||
|
||||
The reason is that references to Blender's native objects cannot be held
|
||||
across frames, but programming with generators and async is difficult when
|
||||
considering this limitation.
|
||||
|
||||
:return FINISH if processing is complete. PREEMPT if not complete.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_fast_path_performance_test_objects(self, context: Context) -> None:
|
||||
"""Create objects for testing whether run() normally completes.
|
||||
|
||||
This tests whether the run() method normally completes in less than 100
|
||||
microseconds.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def reset_run_progress(self) -> None:
|
||||
"""Reset the interrupted state of run()."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class SceneWatcherSchedule:
|
||||
"""Holds SceneWatcher and its operation status."""
|
||||
|
||||
scene_watcher: SceneWatcher
|
||||
requires_run_once_more: bool = False
|
||||
finished: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class SceneWatcherScheduler:
|
||||
"""Periodically executes registered SceneWatchers in order.
|
||||
|
||||
This scheduler is careful not to block the UI while executing the
|
||||
SceneWatcher instances.
|
||||
"""
|
||||
|
||||
INTERVAL: Final[float] = 0.2
|
||||
scene_watcher_schedule_index: int = 0
|
||||
scene_watcher_type_to_schedule: dict[type[SceneWatcher], SceneWatcherSchedule] = (
|
||||
field(default_factory=dict[type[SceneWatcher], SceneWatcherSchedule])
|
||||
)
|
||||
scene_watcher_schedules: list[SceneWatcherSchedule] = field(
|
||||
default_factory=list[SceneWatcherSchedule]
|
||||
)
|
||||
|
||||
def trigger(self, scene_watcher_type: type[SceneWatcher]) -> None:
|
||||
scene_watcher_schedule = self.scene_watcher_type_to_schedule.get(
|
||||
scene_watcher_type
|
||||
)
|
||||
if scene_watcher_schedule:
|
||||
# High-frequency fast path.
|
||||
# Allow returning with minimal processing.
|
||||
|
||||
if scene_watcher_schedule.finished:
|
||||
# If the task is finished, restart it
|
||||
scene_watcher_schedule.finished = False
|
||||
scene_watcher_schedule.scene_watcher.reset_run_progress()
|
||||
else:
|
||||
# If the task is running,
|
||||
# set it to "run once more after completion"
|
||||
scene_watcher_schedule.requires_run_once_more = True
|
||||
return
|
||||
|
||||
# Low-frequency slow path.
|
||||
# Heavy processing is okay.
|
||||
|
||||
# To prevent unregistered SceneWatchers from being missed in testing,
|
||||
# throw an error if trying to instantiate an unregistered SceneWatcher.
|
||||
if scene_watcher_type not in self.get_all_scene_watcher_types():
|
||||
message = f"{scene_watcher_type} is not registered"
|
||||
raise NotImplementedError(message)
|
||||
|
||||
new_scene_watcher = scene_watcher_type()
|
||||
new_schedule = SceneWatcherSchedule(scene_watcher=new_scene_watcher)
|
||||
self.scene_watcher_type_to_schedule[scene_watcher_type] = new_schedule
|
||||
self.scene_watcher_schedules.append(new_schedule)
|
||||
|
||||
@staticmethod
|
||||
def get_all_scene_watcher_types() -> Sequence[type[SceneWatcher]]:
|
||||
from ..editor.mtoon1.scene_watcher import MToon1AutoSetup, OutlineUpdater
|
||||
from ..editor.vrm1.scene_watcher import LookAtPreviewUpdater
|
||||
|
||||
return [OutlineUpdater, LookAtPreviewUpdater, MToon1AutoSetup]
|
||||
|
||||
def process(self, context: Context) -> bool:
|
||||
"""Execute one SceneWatcher.
|
||||
|
||||
Use index values to reduce GC allocation.
|
||||
"""
|
||||
if not self.scene_watcher_schedules:
|
||||
return False
|
||||
|
||||
for _ in range(len(self.scene_watcher_schedules)):
|
||||
self.scene_watcher_schedule_index += 1
|
||||
self.scene_watcher_schedule_index %= len(self.scene_watcher_schedules)
|
||||
|
||||
# Skip tasks that are already completed
|
||||
scene_watcher_schedule = self.scene_watcher_schedules[
|
||||
self.scene_watcher_schedule_index
|
||||
]
|
||||
if scene_watcher_schedule.finished:
|
||||
continue
|
||||
|
||||
# Execute the task
|
||||
run_state = scene_watcher_schedule.scene_watcher.run(context)
|
||||
if run_state == RunState.FINISH:
|
||||
if scene_watcher_schedule.requires_run_once_more:
|
||||
scene_watcher_schedule.requires_run_once_more = False
|
||||
scene_watcher_schedule.scene_watcher.reset_run_progress()
|
||||
else:
|
||||
scene_watcher_schedule.finished = True
|
||||
|
||||
# Return if at least one task was executed
|
||||
return True
|
||||
return False
|
||||
|
||||
def flush(self, context: Context) -> None:
|
||||
while self.process(context):
|
||||
pass
|
||||
|
||||
|
||||
scene_watcher_scheduler = SceneWatcherScheduler()
|
||||
|
||||
|
||||
def process_scene_watcher_scheduler() -> Optional[float]:
|
||||
context = bpy.context
|
||||
|
||||
scene_watcher_scheduler.process(context)
|
||||
return SceneWatcherScheduler.INTERVAL
|
||||
|
||||
|
||||
def create_fast_path_performance_test_scene(
|
||||
context: Context, scene_watcher: SceneWatcher
|
||||
) -> None:
|
||||
class_file_path_str = inspect.getfile(type(scene_watcher))
|
||||
if not class_file_path_str:
|
||||
message = f"No path for class {type(scene_watcher)}"
|
||||
raise ValueError(message)
|
||||
class_file_path = Path(class_file_path_str)
|
||||
if not class_file_path.exists():
|
||||
message = f"No {class_file_path} found"
|
||||
raise ValueError(message)
|
||||
class_source_hash = (
|
||||
base64.urlsafe_b64encode(
|
||||
hashlib.sha3_224(class_file_path.read_bytes()).digest()
|
||||
)
|
||||
.rstrip(b"=")
|
||||
.decode()
|
||||
)
|
||||
repository_root_path = (
|
||||
Path(__file__).resolve(strict=True).parent.parent.parent.parent
|
||||
)
|
||||
if (repository_root_path / ".git").exists() and (
|
||||
repository_root_path / "pyproject.toml"
|
||||
).exists():
|
||||
temp_path = repository_root_path / "tests" / "temp"
|
||||
else:
|
||||
temp_path = Path(mkdtemp(prefix="vrm-format-"))
|
||||
cached_blend_path = temp_path / (
|
||||
type(scene_watcher).__name__
|
||||
+ "-"
|
||||
+ "_".join(map(str, bpy.app.version))
|
||||
+ "-"
|
||||
+ class_source_hash
|
||||
+ ".blend"
|
||||
)
|
||||
if not cached_blend_path.exists():
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
scene_watcher.create_fast_path_performance_test_objects(context)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(cached_blend_path))
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
bpy.ops.wm.open_mainfile(filepath=str(cached_blend_path))
|
||||
|
||||
|
||||
def trigger_scene_watcher(scene_watcher_type: type[SceneWatcher]) -> None:
|
||||
scene_watcher_scheduler.trigger(scene_watcher_type)
|
||||
|
||||
|
||||
def setup() -> None:
|
||||
if bpy.app.timers.is_registered(process_scene_watcher_scheduler):
|
||||
return
|
||||
bpy.app.timers.register(
|
||||
process_scene_watcher_scheduler, first_interval=SceneWatcherScheduler.INTERVAL
|
||||
)
|
||||
|
||||
|
||||
@persistent
|
||||
def save_pre(_unused: object) -> None:
|
||||
context = bpy.context
|
||||
|
||||
scene_watcher_scheduler.flush(context)
|
||||
2045
common/shader.py
Normal file
2045
common/shader.py
Normal file
File diff suppressed because it is too large
Load Diff
59
common/test_helper.py
Normal file
59
common/test_helper.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from typing import Optional
|
||||
from unittest import TestCase
|
||||
|
||||
import bpy
|
||||
|
||||
from .blender_manifest import BlenderManifest
|
||||
|
||||
DEVELOPMENT_MODULE = ".".join(__name__.split(".")[:-2])
|
||||
MANIFEST_ID = BlenderManifest.read().id
|
||||
|
||||
|
||||
def make_test_method_name(text: str) -> str:
|
||||
replaced_chars: list[str] = []
|
||||
for char in text:
|
||||
if char == "«":
|
||||
replaced_chars.append("««")
|
||||
elif char == "»":
|
||||
replaced_chars.append("»»")
|
||||
elif ("_" + char).isidentifier():
|
||||
replaced_chars.append(char)
|
||||
else:
|
||||
replaced_chars.append(f"«{ord(char):x}»")
|
||||
return "test_" + ("".join(replaced_chars))
|
||||
|
||||
|
||||
class AddonTestCase(TestCase):
|
||||
disabled_installed_module: Optional[str]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
|
||||
cls.disabled_installed_module = None
|
||||
for addon in bpy.context.preferences.addons:
|
||||
module = addon.module
|
||||
if not module.endswith("." + MANIFEST_ID):
|
||||
continue
|
||||
bpy.ops.preferences.addon_disable(module=module)
|
||||
cls.disabled_installed_module = module
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
bpy.ops.preferences.addon_enable(module=DEVELOPMENT_MODULE)
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
super().tearDown()
|
||||
bpy.ops.preferences.addon_disable(module=DEVELOPMENT_MODULE)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
super().tearDownClass()
|
||||
|
||||
disabled_installed_module = cls.disabled_installed_module
|
||||
if disabled_installed_module is not None:
|
||||
bpy.ops.preferences.addon_enable(module=disabled_installed_module)
|
||||
|
||||
cls.disabled_installed_module = None
|
||||
BIN
common/transparent_z_write.blend
Normal file
BIN
common/transparent_z_write.blend
Normal file
Binary file not shown.
161
common/version.py
Normal file
161
common/version.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import platform
|
||||
from dataclasses import dataclass
|
||||
from sys import float_info
|
||||
from typing import Optional
|
||||
|
||||
import bpy
|
||||
from bpy.app.translations import pgettext
|
||||
|
||||
from .blender_manifest import BlenderManifest
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Cache:
|
||||
use: bool
|
||||
last_blender_restart_required: bool
|
||||
last_blender_manifest_modification_time: float
|
||||
initial_blender_manifest_content: bytes
|
||||
|
||||
|
||||
cache = Cache(
|
||||
use=False,
|
||||
last_blender_restart_required=False,
|
||||
last_blender_manifest_modification_time=0.0,
|
||||
initial_blender_manifest_content=BlenderManifest.default_blender_manifest_path().read_bytes(),
|
||||
)
|
||||
|
||||
|
||||
def clear_addon_version_cache() -> Optional[float]:
|
||||
cache.use = False
|
||||
return None
|
||||
|
||||
|
||||
def trigger_clear_addon_version_cache() -> None:
|
||||
if tuple(bpy.app.version) >= (4, 2):
|
||||
return
|
||||
if bpy.app.timers.is_registered(clear_addon_version_cache):
|
||||
return
|
||||
bpy.app.timers.register(clear_addon_version_cache, first_interval=0.5)
|
||||
|
||||
|
||||
def min_unsupported_blender_major_minor_version() -> Optional[tuple[int, int]]:
|
||||
blender_version_max = BlenderManifest.read().blender_version_max
|
||||
if blender_version_max is None:
|
||||
return None
|
||||
return (blender_version_max[0], blender_version_max[1])
|
||||
|
||||
|
||||
def get_addon_version() -> tuple[int, int, int]:
|
||||
return BlenderManifest.read().version
|
||||
|
||||
|
||||
def blender_restart_required() -> bool:
|
||||
if tuple(bpy.app.version) >= (4, 2):
|
||||
return False
|
||||
|
||||
if cache.use:
|
||||
return cache.last_blender_restart_required
|
||||
|
||||
cache.use = True
|
||||
|
||||
if cache.last_blender_restart_required:
|
||||
return True
|
||||
|
||||
blender_manifest_path = BlenderManifest.default_blender_manifest_path()
|
||||
blender_manifest_modification_time = blender_manifest_path.stat().st_mtime
|
||||
if (
|
||||
abs(
|
||||
cache.last_blender_manifest_modification_time
|
||||
- blender_manifest_modification_time
|
||||
)
|
||||
< float_info.epsilon
|
||||
):
|
||||
return False
|
||||
|
||||
cache.last_blender_manifest_modification_time = blender_manifest_modification_time
|
||||
|
||||
blender_manifest_content = blender_manifest_path.read_bytes()
|
||||
if blender_manifest_content == cache.initial_blender_manifest_content:
|
||||
return False
|
||||
|
||||
cache.last_blender_restart_required = True
|
||||
return True
|
||||
|
||||
|
||||
def stable_release() -> bool:
|
||||
if bpy.app.version_cycle == "release":
|
||||
return True
|
||||
|
||||
# Microsoft Store distributes RC versions of 3.3.11 and 3.6.3 as
|
||||
# release versions.
|
||||
return platform.system() == "Windows" and bpy.app.version_cycle == "rc"
|
||||
|
||||
|
||||
def supported() -> bool:
|
||||
v = min_unsupported_blender_major_minor_version()
|
||||
if v is None:
|
||||
return True
|
||||
return bpy.app.version[:2] < v
|
||||
|
||||
|
||||
def preferences_warning_message() -> Optional[str]:
|
||||
if blender_restart_required():
|
||||
return pgettext(
|
||||
"The VRM add-on has been updated."
|
||||
+ " Please restart Blender to apply the changes."
|
||||
)
|
||||
if not stable_release():
|
||||
return pgettext(
|
||||
"VRM add-on is not compatible with Blender {blender_version_cycle}."
|
||||
).format(blender_version_cycle=bpy.app.version_cycle.capitalize())
|
||||
if not supported():
|
||||
return pgettext(
|
||||
"The installed VRM add-on is not compatible with Blender {blender_version}."
|
||||
+ " Please upgrade the add-on.",
|
||||
).format(blender_version=".".join(map(str, bpy.app.version[:2])))
|
||||
return None
|
||||
|
||||
|
||||
def panel_warning_message() -> Optional[str]:
|
||||
if blender_restart_required():
|
||||
return pgettext(
|
||||
"The VRM add-on has been\n"
|
||||
+ "updated. Please restart Blender\n"
|
||||
+ "to apply the changes."
|
||||
)
|
||||
if not stable_release():
|
||||
return pgettext(
|
||||
"VRM add-on is\n"
|
||||
+ "not compatible with\n"
|
||||
+ "Blender {blender_version_cycle}."
|
||||
).format(blender_version_cycle=bpy.app.version_cycle.capitalize())
|
||||
if not supported():
|
||||
return pgettext(
|
||||
"The installed VRM add-\n"
|
||||
+ "on is not compatible with\n"
|
||||
+ "Blender {blender_version}. Please update."
|
||||
).format(blender_version=".".join(map(str, bpy.app.version[:2])))
|
||||
return None
|
||||
|
||||
|
||||
def validation_warning_message() -> Optional[str]:
|
||||
if blender_restart_required():
|
||||
return pgettext(
|
||||
"The VRM add-on has been updated."
|
||||
+ " Please restart Blender to apply the changes."
|
||||
)
|
||||
if not stable_release():
|
||||
return pgettext(
|
||||
"VRM add-on is not compatible with Blender {blender_version_cycle}."
|
||||
+ " The VRM may not be exported correctly.",
|
||||
).format(blender_version_cycle=bpy.app.version_cycle.capitalize())
|
||||
if not supported():
|
||||
return pgettext(
|
||||
"The installed VRM add-on is not compatible with Blender {blender_version}."
|
||||
+ " The VRM may not be exported correctly."
|
||||
).format(blender_version=".".join(map(str, bpy.app.version)))
|
||||
return None
|
||||
1
common/vrm0/__init__.py
Normal file
1
common/vrm0/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
common/vrm0/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
common/vrm0/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/vrm0/__pycache__/human_bone.cpython-311.pyc
Normal file
BIN
common/vrm0/__pycache__/human_bone.cpython-311.pyc
Normal file
Binary file not shown.
743
common/vrm0/human_bone.py
Normal file
743
common/vrm0/human_bone.py
Normal file
@@ -0,0 +1,743 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2018 iCyP
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import ClassVar, Optional
|
||||
|
||||
|
||||
# https://github.com/vrm-c/vrm-specification/tree/b5793b4ca250ed3acbde3dd7a47ee9ee1b3d60e9/specification/0.0#vrm-extension-models-bone-mapping-jsonextensionsvrmhumanoid
|
||||
class HumanBoneName(Enum):
|
||||
# Torso
|
||||
HIPS = "hips"
|
||||
SPINE = "spine"
|
||||
CHEST = "chest"
|
||||
UPPER_CHEST = "upperChest"
|
||||
NECK = "neck"
|
||||
|
||||
# Head
|
||||
HEAD = "head"
|
||||
LEFT_EYE = "leftEye"
|
||||
RIGHT_EYE = "rightEye"
|
||||
JAW = "jaw"
|
||||
|
||||
# Leg
|
||||
LEFT_UPPER_LEG = "leftUpperLeg"
|
||||
LEFT_LOWER_LEG = "leftLowerLeg"
|
||||
LEFT_FOOT = "leftFoot"
|
||||
LEFT_TOES = "leftToes"
|
||||
RIGHT_UPPER_LEG = "rightUpperLeg"
|
||||
RIGHT_LOWER_LEG = "rightLowerLeg"
|
||||
RIGHT_FOOT = "rightFoot"
|
||||
RIGHT_TOES = "rightToes"
|
||||
|
||||
# Arm
|
||||
LEFT_SHOULDER = "leftShoulder"
|
||||
LEFT_UPPER_ARM = "leftUpperArm"
|
||||
LEFT_LOWER_ARM = "leftLowerArm"
|
||||
LEFT_HAND = "leftHand"
|
||||
RIGHT_SHOULDER = "rightShoulder"
|
||||
RIGHT_UPPER_ARM = "rightUpperArm"
|
||||
RIGHT_LOWER_ARM = "rightLowerArm"
|
||||
RIGHT_HAND = "rightHand"
|
||||
|
||||
# Finger
|
||||
LEFT_THUMB_PROXIMAL = "leftThumbProximal"
|
||||
LEFT_THUMB_INTERMEDIATE = "leftThumbIntermediate"
|
||||
LEFT_THUMB_DISTAL = "leftThumbDistal"
|
||||
LEFT_INDEX_PROXIMAL = "leftIndexProximal"
|
||||
LEFT_INDEX_INTERMEDIATE = "leftIndexIntermediate"
|
||||
LEFT_INDEX_DISTAL = "leftIndexDistal"
|
||||
LEFT_MIDDLE_PROXIMAL = "leftMiddleProximal"
|
||||
LEFT_MIDDLE_INTERMEDIATE = "leftMiddleIntermediate"
|
||||
LEFT_MIDDLE_DISTAL = "leftMiddleDistal"
|
||||
LEFT_RING_PROXIMAL = "leftRingProximal"
|
||||
LEFT_RING_INTERMEDIATE = "leftRingIntermediate"
|
||||
LEFT_RING_DISTAL = "leftRingDistal"
|
||||
LEFT_LITTLE_PROXIMAL = "leftLittleProximal"
|
||||
LEFT_LITTLE_INTERMEDIATE = "leftLittleIntermediate"
|
||||
LEFT_LITTLE_DISTAL = "leftLittleDistal"
|
||||
RIGHT_THUMB_PROXIMAL = "rightThumbProximal"
|
||||
RIGHT_THUMB_INTERMEDIATE = "rightThumbIntermediate"
|
||||
RIGHT_THUMB_DISTAL = "rightThumbDistal"
|
||||
RIGHT_INDEX_PROXIMAL = "rightIndexProximal"
|
||||
RIGHT_INDEX_INTERMEDIATE = "rightIndexIntermediate"
|
||||
RIGHT_INDEX_DISTAL = "rightIndexDistal"
|
||||
RIGHT_MIDDLE_PROXIMAL = "rightMiddleProximal"
|
||||
RIGHT_MIDDLE_INTERMEDIATE = "rightMiddleIntermediate"
|
||||
RIGHT_MIDDLE_DISTAL = "rightMiddleDistal"
|
||||
RIGHT_RING_PROXIMAL = "rightRingProximal"
|
||||
RIGHT_RING_INTERMEDIATE = "rightRingIntermediate"
|
||||
RIGHT_RING_DISTAL = "rightRingDistal"
|
||||
RIGHT_LITTLE_PROXIMAL = "rightLittleProximal"
|
||||
RIGHT_LITTLE_INTERMEDIATE = "rightLittleIntermediate"
|
||||
RIGHT_LITTLE_DISTAL = "rightLittleDistal"
|
||||
|
||||
@staticmethod
|
||||
def from_str(human_bone_name_str: str) -> Optional["HumanBoneName"]:
|
||||
try:
|
||||
return HumanBoneName(human_bone_name_str)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
# https://github.com/vrm-c/vrm.dev/blob/cd1d367417c53ba0f1d46588180c17b5e2768e22/docs/univrm/humanoid/humanoid_overview.md?plain=1#L99-L106
|
||||
HumanBoneStructure = dict[HumanBoneName, "HumanBoneStructure"]
|
||||
HUMAN_BONE_STRUCTURE: HumanBoneStructure = {
|
||||
HumanBoneName.HIPS: {
|
||||
HumanBoneName.SPINE: {
|
||||
HumanBoneName.CHEST: {
|
||||
HumanBoneName.UPPER_CHEST: {
|
||||
HumanBoneName.NECK: {
|
||||
HumanBoneName.HEAD: {
|
||||
HumanBoneName.LEFT_EYE: {},
|
||||
HumanBoneName.RIGHT_EYE: {},
|
||||
HumanBoneName.JAW: {},
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_SHOULDER: {
|
||||
HumanBoneName.LEFT_UPPER_ARM: {
|
||||
HumanBoneName.LEFT_LOWER_ARM: {
|
||||
HumanBoneName.LEFT_HAND: {
|
||||
HumanBoneName.LEFT_THUMB_PROXIMAL: {
|
||||
HumanBoneName.LEFT_THUMB_INTERMEDIATE: {
|
||||
HumanBoneName.LEFT_THUMB_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_INDEX_PROXIMAL: {
|
||||
HumanBoneName.LEFT_INDEX_INTERMEDIATE: {
|
||||
HumanBoneName.LEFT_INDEX_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_MIDDLE_PROXIMAL: {
|
||||
HumanBoneName.LEFT_MIDDLE_INTERMEDIATE: {
|
||||
HumanBoneName.LEFT_MIDDLE_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_RING_PROXIMAL: {
|
||||
HumanBoneName.LEFT_RING_INTERMEDIATE: {
|
||||
HumanBoneName.LEFT_RING_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_LITTLE_PROXIMAL: {
|
||||
HumanBoneName.LEFT_LITTLE_INTERMEDIATE: {
|
||||
HumanBoneName.LEFT_LITTLE_DISTAL: {}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_SHOULDER: {
|
||||
HumanBoneName.RIGHT_UPPER_ARM: {
|
||||
HumanBoneName.RIGHT_LOWER_ARM: {
|
||||
HumanBoneName.RIGHT_HAND: {
|
||||
HumanBoneName.RIGHT_THUMB_PROXIMAL: {
|
||||
HumanBoneName.RIGHT_THUMB_INTERMEDIATE: {
|
||||
HumanBoneName.RIGHT_THUMB_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_INDEX_PROXIMAL: {
|
||||
HumanBoneName.RIGHT_INDEX_INTERMEDIATE: {
|
||||
HumanBoneName.RIGHT_INDEX_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_MIDDLE_PROXIMAL: {
|
||||
HumanBoneName.RIGHT_MIDDLE_INTERMEDIATE: {
|
||||
HumanBoneName.RIGHT_MIDDLE_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_RING_PROXIMAL: {
|
||||
HumanBoneName.RIGHT_RING_INTERMEDIATE: {
|
||||
HumanBoneName.RIGHT_RING_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_LITTLE_PROXIMAL: {
|
||||
HumanBoneName.RIGHT_LITTLE_INTERMEDIATE: {
|
||||
HumanBoneName.RIGHT_LITTLE_DISTAL: {}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_UPPER_LEG: {
|
||||
HumanBoneName.LEFT_LOWER_LEG: {
|
||||
HumanBoneName.LEFT_FOOT: {HumanBoneName.LEFT_TOES: {}}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_UPPER_LEG: {
|
||||
HumanBoneName.RIGHT_LOWER_LEG: {
|
||||
HumanBoneName.RIGHT_FOOT: {HumanBoneName.RIGHT_TOES: {}}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HumanBoneSpecification:
|
||||
name: HumanBoneName
|
||||
icon: str
|
||||
title: str
|
||||
label: str
|
||||
label_no_left_right: str
|
||||
requirement: bool
|
||||
parent_name: Optional[HumanBoneName]
|
||||
children_names: list[HumanBoneName]
|
||||
|
||||
def parent(self) -> Optional["HumanBoneSpecification"]:
|
||||
if self.parent_name is None:
|
||||
return None
|
||||
return HumanBoneSpecifications.get(self.parent_name)
|
||||
|
||||
def children(self) -> list["HumanBoneSpecification"]:
|
||||
return list(map(HumanBoneSpecifications.get, self.children_names))
|
||||
|
||||
def descendants(self) -> list["HumanBoneSpecification"]:
|
||||
result: list[HumanBoneSpecification] = []
|
||||
searching_children = self.children()
|
||||
while searching_children:
|
||||
child = searching_children.pop()
|
||||
result.append(child)
|
||||
searching_children.extend(child.children())
|
||||
return result
|
||||
|
||||
def connected(self) -> list["HumanBoneSpecification"]:
|
||||
children = self.children()
|
||||
parent = self.parent()
|
||||
if parent is None:
|
||||
return children
|
||||
return [*children, parent]
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
human_bone_name: HumanBoneName, *, requirement: bool, icon: str
|
||||
) -> "HumanBoneSpecification":
|
||||
# https://stackoverflow.com/a/1176023
|
||||
words = re.sub(r"(?<!^)(?=[A-Z])", "#", human_bone_name.value).split("#")
|
||||
title = " ".join(map(str.capitalize, words))
|
||||
label = title + ":"
|
||||
label_no_left_right = re.sub(r"Right ", "", re.sub(r"^Left ", "", label))
|
||||
|
||||
return HumanBoneSpecification(
|
||||
name=human_bone_name,
|
||||
icon=icon,
|
||||
title=title,
|
||||
label=label,
|
||||
label_no_left_right=label_no_left_right,
|
||||
requirement=requirement,
|
||||
parent_name=HumanBoneSpecification.find_parent_human_bone_name(
|
||||
human_bone_name, None, HUMAN_BONE_STRUCTURE
|
||||
),
|
||||
children_names=HumanBoneSpecification.find_children_human_bone_names(
|
||||
human_bone_name, HUMAN_BONE_STRUCTURE
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def find_parent_human_bone_name(
|
||||
child_human_bone_name: HumanBoneName,
|
||||
parent_human_bone_name: Optional[HumanBoneName],
|
||||
human_bone_structure: HumanBoneStructure,
|
||||
) -> Optional[HumanBoneName]:
|
||||
for (
|
||||
next_human_bone_name,
|
||||
next_human_bone_structure,
|
||||
) in human_bone_structure.items():
|
||||
if child_human_bone_name == next_human_bone_name:
|
||||
return parent_human_bone_name
|
||||
|
||||
name = HumanBoneSpecification.find_parent_human_bone_name(
|
||||
child_human_bone_name, next_human_bone_name, next_human_bone_structure
|
||||
)
|
||||
if name:
|
||||
return name
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_children_human_bone_names(
|
||||
human_bone_name: HumanBoneName,
|
||||
human_bone_structure: HumanBoneStructure,
|
||||
) -> list[HumanBoneName]:
|
||||
for (
|
||||
next_human_bone_name,
|
||||
next_human_bone_structure,
|
||||
) in human_bone_structure.items():
|
||||
if human_bone_name == next_human_bone_name:
|
||||
return list(next_human_bone_structure.keys())
|
||||
|
||||
children = HumanBoneSpecification.find_children_human_bone_names(
|
||||
human_bone_name, next_human_bone_structure
|
||||
)
|
||||
if children:
|
||||
return children
|
||||
|
||||
return []
|
||||
|
||||
def is_ancestor_of(
|
||||
self, human_bone_specification: "HumanBoneSpecification"
|
||||
) -> bool:
|
||||
parent = human_bone_specification.parent()
|
||||
while parent:
|
||||
if parent == self:
|
||||
return True
|
||||
parent = parent.parent()
|
||||
return False
|
||||
|
||||
|
||||
def create_and_append_human_bone_specification(
|
||||
human_bone_specifications: list[HumanBoneSpecification],
|
||||
human_bone_name: HumanBoneName,
|
||||
*,
|
||||
requirement: bool,
|
||||
icon: str,
|
||||
) -> HumanBoneSpecification:
|
||||
human_bone_specification = HumanBoneSpecification.create(
|
||||
human_bone_name, requirement=requirement, icon=icon
|
||||
)
|
||||
human_bone_specifications.append(human_bone_specification)
|
||||
return human_bone_specification
|
||||
|
||||
|
||||
class HumanBoneSpecifications:
|
||||
all_human_bones: ClassVar[list[HumanBoneSpecification]] = []
|
||||
|
||||
# https://github.com/vrm-c/vrm-specification/tree/b5793b4ca250ed3acbde3dd7a47ee9ee1b3d60e9/specification/0.0#vrm-extension-models-bone-mapping-jsonextensionsvrmhumanoid
|
||||
# Torso
|
||||
HIPS = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.HIPS,
|
||||
requirement=True,
|
||||
icon="USER",
|
||||
)
|
||||
SPINE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.SPINE,
|
||||
requirement=True,
|
||||
icon="USER",
|
||||
)
|
||||
CHEST = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.CHEST,
|
||||
requirement=True,
|
||||
icon="USER",
|
||||
)
|
||||
UPPER_CHEST = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.UPPER_CHEST,
|
||||
requirement=False,
|
||||
icon="USER",
|
||||
)
|
||||
NECK = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.NECK,
|
||||
requirement=True,
|
||||
icon="USER",
|
||||
)
|
||||
|
||||
# Head
|
||||
HEAD = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.HEAD,
|
||||
requirement=True,
|
||||
icon="USER",
|
||||
)
|
||||
LEFT_EYE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_EYE,
|
||||
requirement=False,
|
||||
icon="HIDE_OFF",
|
||||
)
|
||||
RIGHT_EYE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_EYE,
|
||||
requirement=False,
|
||||
icon="HIDE_OFF",
|
||||
)
|
||||
JAW = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.JAW,
|
||||
requirement=False,
|
||||
icon="USER",
|
||||
)
|
||||
|
||||
# Leg
|
||||
LEFT_UPPER_LEG = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_UPPER_LEG,
|
||||
requirement=True,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
LEFT_LOWER_LEG = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_LOWER_LEG,
|
||||
requirement=True,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
LEFT_FOOT = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_FOOT,
|
||||
requirement=True,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
LEFT_TOES = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_TOES,
|
||||
requirement=False,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
RIGHT_UPPER_LEG = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_UPPER_LEG,
|
||||
requirement=True,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
RIGHT_LOWER_LEG = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_LOWER_LEG,
|
||||
requirement=True,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
RIGHT_FOOT = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_FOOT,
|
||||
requirement=True,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
RIGHT_TOES = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_TOES,
|
||||
requirement=False,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
|
||||
# Arm
|
||||
LEFT_SHOULDER = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_SHOULDER,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_UPPER_ARM = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_UPPER_ARM,
|
||||
requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_LOWER_ARM = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_LOWER_ARM,
|
||||
requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_HAND = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_HAND,
|
||||
requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_SHOULDER = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_SHOULDER,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_UPPER_ARM = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_UPPER_ARM,
|
||||
requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_LOWER_ARM = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_LOWER_ARM,
|
||||
requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_HAND = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_HAND,
|
||||
requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
|
||||
# Finger
|
||||
LEFT_THUMB_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_THUMB_PROXIMAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_THUMB_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_THUMB_INTERMEDIATE,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_THUMB_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_THUMB_DISTAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_INDEX_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_INDEX_PROXIMAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_INDEX_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_INDEX_INTERMEDIATE,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_INDEX_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_INDEX_DISTAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_MIDDLE_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_MIDDLE_PROXIMAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_MIDDLE_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_MIDDLE_INTERMEDIATE,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_MIDDLE_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_MIDDLE_DISTAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_RING_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_RING_PROXIMAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_RING_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_RING_INTERMEDIATE,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_RING_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_RING_DISTAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_LITTLE_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_LITTLE_PROXIMAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_LITTLE_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_LITTLE_INTERMEDIATE,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_LITTLE_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_LITTLE_DISTAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_THUMB_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_THUMB_PROXIMAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_THUMB_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_THUMB_INTERMEDIATE,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_THUMB_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_THUMB_DISTAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_INDEX_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_INDEX_PROXIMAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_INDEX_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_INDEX_INTERMEDIATE,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_INDEX_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_INDEX_DISTAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_MIDDLE_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_MIDDLE_PROXIMAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_MIDDLE_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_MIDDLE_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_MIDDLE_DISTAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_RING_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_RING_PROXIMAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_RING_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_RING_INTERMEDIATE,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_RING_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_RING_DISTAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_LITTLE_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_LITTLE_PROXIMAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_LITTLE_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_LITTLE_INTERMEDIATE,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_LITTLE_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_LITTLE_DISTAL,
|
||||
requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
|
||||
human_bone_name_to_human_bone: Mapping[HumanBoneName, HumanBoneSpecification] = {
|
||||
human_bone.name: human_bone for human_bone in all_human_bones
|
||||
}
|
||||
|
||||
all_names = tuple(b.name.value for b in all_human_bones)
|
||||
|
||||
required_names = tuple(b.name.value for b in all_human_bones if b.requirement)
|
||||
|
||||
optional_names = tuple(b.name.value for b in all_human_bones if not b.requirement)
|
||||
|
||||
@staticmethod
|
||||
def get(name: HumanBoneName) -> HumanBoneSpecification:
|
||||
return HumanBoneSpecifications.human_bone_name_to_human_bone[name]
|
||||
|
||||
center_req = tuple(b.name.value for b in [HIPS, SPINE, CHEST, NECK, HEAD])
|
||||
left_leg_req = tuple(
|
||||
b.name.value for b in [LEFT_UPPER_LEG, LEFT_LOWER_LEG, LEFT_FOOT]
|
||||
)
|
||||
left_arm_req = tuple(
|
||||
b.name.value for b in [LEFT_UPPER_ARM, LEFT_LOWER_ARM, LEFT_HAND]
|
||||
)
|
||||
right_leg_req = tuple(
|
||||
b.name.value for b in [RIGHT_UPPER_LEG, RIGHT_LOWER_LEG, RIGHT_FOOT]
|
||||
)
|
||||
right_arm_req = tuple(
|
||||
b.name.value for b in [RIGHT_UPPER_ARM, RIGHT_LOWER_ARM, RIGHT_HAND]
|
||||
)
|
||||
|
||||
requires = (
|
||||
*center_req[:],
|
||||
*left_leg_req[:],
|
||||
*right_leg_req[:],
|
||||
*left_arm_req[:],
|
||||
*right_arm_req[:],
|
||||
)
|
||||
|
||||
left_arm_def = tuple(
|
||||
b.name.value
|
||||
for b in [
|
||||
LEFT_SHOULDER,
|
||||
LEFT_THUMB_PROXIMAL,
|
||||
LEFT_THUMB_INTERMEDIATE,
|
||||
LEFT_THUMB_DISTAL,
|
||||
LEFT_INDEX_PROXIMAL,
|
||||
LEFT_INDEX_INTERMEDIATE,
|
||||
LEFT_INDEX_DISTAL,
|
||||
LEFT_MIDDLE_PROXIMAL,
|
||||
LEFT_MIDDLE_INTERMEDIATE,
|
||||
LEFT_MIDDLE_DISTAL,
|
||||
LEFT_RING_PROXIMAL,
|
||||
LEFT_RING_INTERMEDIATE,
|
||||
LEFT_RING_DISTAL,
|
||||
LEFT_LITTLE_PROXIMAL,
|
||||
LEFT_LITTLE_INTERMEDIATE,
|
||||
LEFT_LITTLE_DISTAL,
|
||||
]
|
||||
)
|
||||
|
||||
right_arm_def = tuple(
|
||||
b.name.value
|
||||
for b in [
|
||||
RIGHT_SHOULDER,
|
||||
RIGHT_THUMB_PROXIMAL,
|
||||
RIGHT_THUMB_INTERMEDIATE,
|
||||
RIGHT_THUMB_DISTAL,
|
||||
RIGHT_INDEX_PROXIMAL,
|
||||
RIGHT_INDEX_INTERMEDIATE,
|
||||
RIGHT_INDEX_DISTAL,
|
||||
RIGHT_MIDDLE_PROXIMAL,
|
||||
RIGHT_MIDDLE_INTERMEDIATE,
|
||||
RIGHT_MIDDLE_DISTAL,
|
||||
RIGHT_RING_PROXIMAL,
|
||||
RIGHT_RING_INTERMEDIATE,
|
||||
RIGHT_RING_DISTAL,
|
||||
RIGHT_LITTLE_PROXIMAL,
|
||||
RIGHT_LITTLE_INTERMEDIATE,
|
||||
RIGHT_LITTLE_DISTAL,
|
||||
]
|
||||
)
|
||||
center_def = (UPPER_CHEST.name.value, JAW.name.value)
|
||||
left_leg_def = (LEFT_TOES.name.value,)
|
||||
right_leg_def = (RIGHT_TOES.name.value,)
|
||||
defines = (
|
||||
LEFT_EYE.name.value,
|
||||
RIGHT_EYE.name.value,
|
||||
*center_def[:],
|
||||
*left_leg_def[:],
|
||||
*right_leg_def[:],
|
||||
*left_arm_def[:],
|
||||
*right_arm_def[:],
|
||||
)
|
||||
1
common/vrm1/__init__.py
Normal file
1
common/vrm1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
common/vrm1/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
common/vrm1/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
common/vrm1/__pycache__/human_bone.cpython-311.pyc
Normal file
BIN
common/vrm1/__pycache__/human_bone.cpython-311.pyc
Normal file
Binary file not shown.
790
common/vrm1/human_bone.py
Normal file
790
common/vrm1/human_bone.py
Normal file
@@ -0,0 +1,790 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2018 iCyP
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import ClassVar, Optional
|
||||
|
||||
from ..vrm0.human_bone import HumanBoneName as Vrm0HumanBoneName
|
||||
from ..vrm0.human_bone import HumanBoneSpecification as Vrm0HumanBoneSpecification
|
||||
from ..vrm0.human_bone import HumanBoneSpecifications as Vrm0HumanBoneSpecifications
|
||||
|
||||
|
||||
# https://github.com/vrm-c/vrm-specification/blob/6a996eb151770149ea4534b1edb70d913bb4014e/specification/VRMC_vrm-1.0-beta/humanoid.md#list-of-humanoid-bones
|
||||
class HumanBoneName(Enum):
|
||||
# Torso
|
||||
HIPS = "hips"
|
||||
SPINE = "spine"
|
||||
CHEST = "chest"
|
||||
UPPER_CHEST = "upperChest"
|
||||
NECK = "neck"
|
||||
|
||||
# Head
|
||||
HEAD = "head"
|
||||
LEFT_EYE = "leftEye"
|
||||
RIGHT_EYE = "rightEye"
|
||||
JAW = "jaw"
|
||||
|
||||
# Leg
|
||||
LEFT_UPPER_LEG = "leftUpperLeg"
|
||||
LEFT_LOWER_LEG = "leftLowerLeg"
|
||||
LEFT_FOOT = "leftFoot"
|
||||
LEFT_TOES = "leftToes"
|
||||
RIGHT_UPPER_LEG = "rightUpperLeg"
|
||||
RIGHT_LOWER_LEG = "rightLowerLeg"
|
||||
RIGHT_FOOT = "rightFoot"
|
||||
RIGHT_TOES = "rightToes"
|
||||
|
||||
# Arm
|
||||
LEFT_SHOULDER = "leftShoulder"
|
||||
LEFT_UPPER_ARM = "leftUpperArm"
|
||||
LEFT_LOWER_ARM = "leftLowerArm"
|
||||
LEFT_HAND = "leftHand"
|
||||
RIGHT_SHOULDER = "rightShoulder"
|
||||
RIGHT_UPPER_ARM = "rightUpperArm"
|
||||
RIGHT_LOWER_ARM = "rightLowerArm"
|
||||
RIGHT_HAND = "rightHand"
|
||||
|
||||
# Finger
|
||||
LEFT_THUMB_METACARPAL = "leftThumbMetacarpal"
|
||||
LEFT_THUMB_PROXIMAL = "leftThumbProximal"
|
||||
LEFT_THUMB_DISTAL = "leftThumbDistal"
|
||||
LEFT_INDEX_PROXIMAL = "leftIndexProximal"
|
||||
LEFT_INDEX_INTERMEDIATE = "leftIndexIntermediate"
|
||||
LEFT_INDEX_DISTAL = "leftIndexDistal"
|
||||
LEFT_MIDDLE_PROXIMAL = "leftMiddleProximal"
|
||||
LEFT_MIDDLE_INTERMEDIATE = "leftMiddleIntermediate"
|
||||
LEFT_MIDDLE_DISTAL = "leftMiddleDistal"
|
||||
LEFT_RING_PROXIMAL = "leftRingProximal"
|
||||
LEFT_RING_INTERMEDIATE = "leftRingIntermediate"
|
||||
LEFT_RING_DISTAL = "leftRingDistal"
|
||||
LEFT_LITTLE_PROXIMAL = "leftLittleProximal"
|
||||
LEFT_LITTLE_INTERMEDIATE = "leftLittleIntermediate"
|
||||
LEFT_LITTLE_DISTAL = "leftLittleDistal"
|
||||
RIGHT_THUMB_METACARPAL = "rightThumbMetacarpal"
|
||||
RIGHT_THUMB_PROXIMAL = "rightThumbProximal"
|
||||
RIGHT_THUMB_DISTAL = "rightThumbDistal"
|
||||
RIGHT_INDEX_PROXIMAL = "rightIndexProximal"
|
||||
RIGHT_INDEX_INTERMEDIATE = "rightIndexIntermediate"
|
||||
RIGHT_INDEX_DISTAL = "rightIndexDistal"
|
||||
RIGHT_MIDDLE_PROXIMAL = "rightMiddleProximal"
|
||||
RIGHT_MIDDLE_INTERMEDIATE = "rightMiddleIntermediate"
|
||||
RIGHT_MIDDLE_DISTAL = "rightMiddleDistal"
|
||||
RIGHT_RING_PROXIMAL = "rightRingProximal"
|
||||
RIGHT_RING_INTERMEDIATE = "rightRingIntermediate"
|
||||
RIGHT_RING_DISTAL = "rightRingDistal"
|
||||
RIGHT_LITTLE_PROXIMAL = "rightLittleProximal"
|
||||
RIGHT_LITTLE_INTERMEDIATE = "rightLittleIntermediate"
|
||||
RIGHT_LITTLE_DISTAL = "rightLittleDistal"
|
||||
|
||||
@staticmethod
|
||||
def from_str(human_bone_name_str: str) -> Optional["HumanBoneName"]:
|
||||
try:
|
||||
return HumanBoneName(human_bone_name_str)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/humanoid.md#humanoid-bone-parent-child-relationship
|
||||
HumanBoneStructure = dict[HumanBoneName, "HumanBoneStructure"]
|
||||
HUMAN_BONE_STRUCTURE: HumanBoneStructure = {
|
||||
HumanBoneName.HIPS: {
|
||||
HumanBoneName.SPINE: {
|
||||
HumanBoneName.CHEST: {
|
||||
HumanBoneName.UPPER_CHEST: {
|
||||
HumanBoneName.NECK: {
|
||||
HumanBoneName.HEAD: {
|
||||
HumanBoneName.LEFT_EYE: {},
|
||||
HumanBoneName.RIGHT_EYE: {},
|
||||
HumanBoneName.JAW: {},
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_SHOULDER: {
|
||||
HumanBoneName.LEFT_UPPER_ARM: {
|
||||
HumanBoneName.LEFT_LOWER_ARM: {
|
||||
HumanBoneName.LEFT_HAND: {
|
||||
HumanBoneName.LEFT_THUMB_METACARPAL: {
|
||||
HumanBoneName.LEFT_THUMB_PROXIMAL: {
|
||||
HumanBoneName.LEFT_THUMB_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_INDEX_PROXIMAL: {
|
||||
HumanBoneName.LEFT_INDEX_INTERMEDIATE: {
|
||||
HumanBoneName.LEFT_INDEX_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_MIDDLE_PROXIMAL: {
|
||||
HumanBoneName.LEFT_MIDDLE_INTERMEDIATE: {
|
||||
HumanBoneName.LEFT_MIDDLE_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_RING_PROXIMAL: {
|
||||
HumanBoneName.LEFT_RING_INTERMEDIATE: {
|
||||
HumanBoneName.LEFT_RING_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_LITTLE_PROXIMAL: {
|
||||
HumanBoneName.LEFT_LITTLE_INTERMEDIATE: {
|
||||
HumanBoneName.LEFT_LITTLE_DISTAL: {}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_SHOULDER: {
|
||||
HumanBoneName.RIGHT_UPPER_ARM: {
|
||||
HumanBoneName.RIGHT_LOWER_ARM: {
|
||||
HumanBoneName.RIGHT_HAND: {
|
||||
HumanBoneName.RIGHT_THUMB_METACARPAL: {
|
||||
HumanBoneName.RIGHT_THUMB_PROXIMAL: {
|
||||
HumanBoneName.RIGHT_THUMB_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_INDEX_PROXIMAL: {
|
||||
HumanBoneName.RIGHT_INDEX_INTERMEDIATE: {
|
||||
HumanBoneName.RIGHT_INDEX_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_MIDDLE_PROXIMAL: {
|
||||
HumanBoneName.RIGHT_MIDDLE_INTERMEDIATE: {
|
||||
HumanBoneName.RIGHT_MIDDLE_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_RING_PROXIMAL: {
|
||||
HumanBoneName.RIGHT_RING_INTERMEDIATE: {
|
||||
HumanBoneName.RIGHT_RING_DISTAL: {}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_LITTLE_PROXIMAL: {
|
||||
HumanBoneName.RIGHT_LITTLE_INTERMEDIATE: {
|
||||
HumanBoneName.RIGHT_LITTLE_DISTAL: {}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
HumanBoneName.LEFT_UPPER_LEG: {
|
||||
HumanBoneName.LEFT_LOWER_LEG: {
|
||||
HumanBoneName.LEFT_FOOT: {HumanBoneName.LEFT_TOES: {}}
|
||||
}
|
||||
},
|
||||
HumanBoneName.RIGHT_UPPER_LEG: {
|
||||
HumanBoneName.RIGHT_LOWER_LEG: {
|
||||
HumanBoneName.RIGHT_FOOT: {HumanBoneName.RIGHT_TOES: {}}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HumanBoneSpecification:
|
||||
name: HumanBoneName
|
||||
icon: str
|
||||
title: str
|
||||
label: str
|
||||
label_no_left_right: str
|
||||
requirement: bool
|
||||
parent_requirement: bool
|
||||
parent_name: Optional[HumanBoneName]
|
||||
children_names: list[HumanBoneName]
|
||||
vrm0_name: Vrm0HumanBoneName
|
||||
|
||||
def parent(self) -> Optional["HumanBoneSpecification"]:
|
||||
if self.parent_name is None:
|
||||
return None
|
||||
return HumanBoneSpecifications.get(self.parent_name)
|
||||
|
||||
def children(self) -> list["HumanBoneSpecification"]:
|
||||
return list(map(HumanBoneSpecifications.get, self.children_names))
|
||||
|
||||
def descendants(self) -> list["HumanBoneSpecification"]:
|
||||
result: list[HumanBoneSpecification] = []
|
||||
searching_children = self.children()
|
||||
while searching_children:
|
||||
child = searching_children.pop()
|
||||
result.append(child)
|
||||
searching_children.extend(child.children())
|
||||
return result
|
||||
|
||||
def connected(self) -> list["HumanBoneSpecification"]:
|
||||
children = self.children()
|
||||
parent = self.parent()
|
||||
if parent is None:
|
||||
return children
|
||||
return [*children, parent]
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
human_bone_name: HumanBoneName,
|
||||
vrm0_human_bone_specification: Vrm0HumanBoneSpecification,
|
||||
*,
|
||||
requirement: bool,
|
||||
parent_requirement: bool,
|
||||
icon: str,
|
||||
) -> "HumanBoneSpecification":
|
||||
# https://stackoverflow.com/a/1176023
|
||||
words = re.sub(r"(?<!^)(?=[A-Z])", "#", human_bone_name.value).split("#")
|
||||
title = " ".join(map(str.capitalize, words))
|
||||
label = title + ":"
|
||||
label_no_left_right = re.sub(r"Right ", "", re.sub(r"^Left ", "", label))
|
||||
|
||||
return HumanBoneSpecification(
|
||||
name=human_bone_name,
|
||||
icon=icon,
|
||||
title=title,
|
||||
label=label,
|
||||
label_no_left_right=label_no_left_right,
|
||||
requirement=requirement,
|
||||
parent_requirement=parent_requirement,
|
||||
parent_name=HumanBoneSpecification.find_parent_human_bone_name(
|
||||
human_bone_name, None, HUMAN_BONE_STRUCTURE
|
||||
),
|
||||
children_names=HumanBoneSpecification.find_children_human_bone_names(
|
||||
human_bone_name, HUMAN_BONE_STRUCTURE
|
||||
),
|
||||
vrm0_name=vrm0_human_bone_specification.name,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def find_parent_human_bone_name(
|
||||
child_human_bone_name: HumanBoneName,
|
||||
parent_human_bone_name: Optional[HumanBoneName],
|
||||
human_bone_structure: HumanBoneStructure,
|
||||
) -> Optional[HumanBoneName]:
|
||||
for (
|
||||
next_human_bone_name,
|
||||
next_human_bone_structure,
|
||||
) in human_bone_structure.items():
|
||||
if child_human_bone_name == next_human_bone_name:
|
||||
return parent_human_bone_name
|
||||
|
||||
name = HumanBoneSpecification.find_parent_human_bone_name(
|
||||
child_human_bone_name, next_human_bone_name, next_human_bone_structure
|
||||
)
|
||||
if name:
|
||||
return name
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_children_human_bone_names(
|
||||
human_bone_name: HumanBoneName,
|
||||
human_bone_structure: HumanBoneStructure,
|
||||
) -> list[HumanBoneName]:
|
||||
for (
|
||||
next_human_bone_name,
|
||||
next_human_bone_structure,
|
||||
) in human_bone_structure.items():
|
||||
if human_bone_name == next_human_bone_name:
|
||||
return list(next_human_bone_structure.keys())
|
||||
|
||||
children = HumanBoneSpecification.find_children_human_bone_names(
|
||||
human_bone_name, next_human_bone_structure
|
||||
)
|
||||
if children:
|
||||
return children
|
||||
|
||||
return []
|
||||
|
||||
def is_ancestor_of(
|
||||
self, human_bone_specification: "HumanBoneSpecification"
|
||||
) -> bool:
|
||||
parent = human_bone_specification.parent()
|
||||
while parent:
|
||||
if parent == self:
|
||||
return True
|
||||
parent = parent.parent()
|
||||
return False
|
||||
|
||||
|
||||
def create_and_append_human_bone_specification(
|
||||
human_bone_specifications: list[HumanBoneSpecification],
|
||||
human_bone_name: HumanBoneName,
|
||||
vrm0_human_bone_specification: Vrm0HumanBoneSpecification,
|
||||
*,
|
||||
requirement: bool,
|
||||
parent_requirement: bool,
|
||||
icon: str,
|
||||
) -> HumanBoneSpecification:
|
||||
human_bone_specification = HumanBoneSpecification.create(
|
||||
human_bone_name,
|
||||
vrm0_human_bone_specification,
|
||||
requirement=requirement,
|
||||
parent_requirement=parent_requirement,
|
||||
icon=icon,
|
||||
)
|
||||
human_bone_specifications.append(human_bone_specification)
|
||||
return human_bone_specification
|
||||
|
||||
|
||||
class HumanBoneSpecifications:
|
||||
all_human_bones: ClassVar[list[HumanBoneSpecification]] = []
|
||||
|
||||
# https://github.com/vrm-c/vrm-specification/blob/6a996eb151770149ea4534b1edb70d913bb4014e/specification/VRMC_vrm-1.0-beta/humanoid.md#list-of-humanoid-bones
|
||||
# Torso
|
||||
HIPS = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.HIPS,
|
||||
Vrm0HumanBoneSpecifications.HIPS,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="USER",
|
||||
)
|
||||
SPINE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.SPINE,
|
||||
Vrm0HumanBoneSpecifications.SPINE,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="USER",
|
||||
)
|
||||
CHEST = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.CHEST,
|
||||
Vrm0HumanBoneSpecifications.CHEST,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="USER",
|
||||
)
|
||||
UPPER_CHEST = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.UPPER_CHEST,
|
||||
Vrm0HumanBoneSpecifications.UPPER_CHEST,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="USER",
|
||||
)
|
||||
NECK = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.NECK,
|
||||
Vrm0HumanBoneSpecifications.NECK,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="USER",
|
||||
)
|
||||
|
||||
# Head
|
||||
HEAD = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.HEAD,
|
||||
Vrm0HumanBoneSpecifications.HEAD,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="USER",
|
||||
)
|
||||
LEFT_EYE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_EYE,
|
||||
Vrm0HumanBoneSpecifications.LEFT_EYE,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="HIDE_OFF",
|
||||
)
|
||||
RIGHT_EYE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_EYE,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_EYE,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="HIDE_OFF",
|
||||
)
|
||||
JAW = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.JAW,
|
||||
Vrm0HumanBoneSpecifications.JAW,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="USER",
|
||||
)
|
||||
|
||||
# Leg
|
||||
LEFT_UPPER_LEG = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_UPPER_LEG,
|
||||
Vrm0HumanBoneSpecifications.LEFT_UPPER_LEG,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
LEFT_LOWER_LEG = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_LOWER_LEG,
|
||||
Vrm0HumanBoneSpecifications.LEFT_LOWER_LEG,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
LEFT_FOOT = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_FOOT,
|
||||
Vrm0HumanBoneSpecifications.LEFT_FOOT,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
LEFT_TOES = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_TOES,
|
||||
Vrm0HumanBoneSpecifications.LEFT_TOES,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
RIGHT_UPPER_LEG = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_UPPER_LEG,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_UPPER_LEG,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
RIGHT_LOWER_LEG = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_LOWER_LEG,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_LOWER_LEG,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
RIGHT_FOOT = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_FOOT,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_FOOT,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
RIGHT_TOES = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_TOES,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_TOES,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="MOD_DYNAMICPAINT",
|
||||
)
|
||||
|
||||
# Arm
|
||||
LEFT_SHOULDER = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_SHOULDER,
|
||||
Vrm0HumanBoneSpecifications.LEFT_SHOULDER,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_UPPER_ARM = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_UPPER_ARM,
|
||||
Vrm0HumanBoneSpecifications.LEFT_UPPER_ARM,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_LOWER_ARM = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_LOWER_ARM,
|
||||
Vrm0HumanBoneSpecifications.LEFT_LOWER_ARM,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_HAND = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_HAND,
|
||||
Vrm0HumanBoneSpecifications.LEFT_HAND,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_SHOULDER = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_SHOULDER,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_SHOULDER,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_UPPER_ARM = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_UPPER_ARM,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_UPPER_ARM,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_LOWER_ARM = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_LOWER_ARM,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_LOWER_ARM,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_HAND = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_HAND,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_HAND,
|
||||
requirement=True,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
|
||||
# Finger
|
||||
LEFT_THUMB_METACARPAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_THUMB_METACARPAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_THUMB_PROXIMAL,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_THUMB_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_THUMB_PROXIMAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_THUMB_INTERMEDIATE,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_THUMB_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_THUMB_DISTAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_THUMB_DISTAL,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_INDEX_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_INDEX_PROXIMAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_INDEX_PROXIMAL,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_INDEX_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_INDEX_INTERMEDIATE,
|
||||
Vrm0HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_INDEX_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_INDEX_DISTAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_INDEX_DISTAL,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_MIDDLE_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_MIDDLE_PROXIMAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_MIDDLE_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_MIDDLE_INTERMEDIATE,
|
||||
Vrm0HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_MIDDLE_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_MIDDLE_DISTAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_MIDDLE_DISTAL,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_RING_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_RING_PROXIMAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_RING_PROXIMAL,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_RING_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_RING_INTERMEDIATE,
|
||||
Vrm0HumanBoneSpecifications.LEFT_RING_INTERMEDIATE,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_RING_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_RING_DISTAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_RING_DISTAL,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_LITTLE_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_LITTLE_PROXIMAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_LITTLE_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_LITTLE_INTERMEDIATE,
|
||||
Vrm0HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
LEFT_LITTLE_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.LEFT_LITTLE_DISTAL,
|
||||
Vrm0HumanBoneSpecifications.LEFT_LITTLE_DISTAL,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_THUMB_METACARPAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_THUMB_METACARPAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_THUMB_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_THUMB_PROXIMAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_THUMB_INTERMEDIATE,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_THUMB_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_THUMB_DISTAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_THUMB_DISTAL,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_INDEX_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_INDEX_PROXIMAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_INDEX_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_INDEX_INTERMEDIATE,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_INDEX_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_INDEX_DISTAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_INDEX_DISTAL,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_MIDDLE_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_MIDDLE_PROXIMAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_MIDDLE_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_MIDDLE_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_MIDDLE_DISTAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_RING_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_RING_PROXIMAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_RING_PROXIMAL,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_RING_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_RING_INTERMEDIATE,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_RING_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_RING_DISTAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_RING_DISTAL,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_LITTLE_PROXIMAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_LITTLE_PROXIMAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL,
|
||||
requirement=False,
|
||||
parent_requirement=False,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_LITTLE_INTERMEDIATE = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_LITTLE_INTERMEDIATE,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
RIGHT_LITTLE_DISTAL = create_and_append_human_bone_specification(
|
||||
all_human_bones,
|
||||
HumanBoneName.RIGHT_LITTLE_DISTAL,
|
||||
Vrm0HumanBoneSpecifications.RIGHT_LITTLE_DISTAL,
|
||||
requirement=False,
|
||||
parent_requirement=True,
|
||||
icon="VIEW_PAN",
|
||||
)
|
||||
|
||||
human_bone_name_to_human_bone: Mapping[HumanBoneName, HumanBoneSpecification] = {
|
||||
human_bone.name: human_bone for human_bone in all_human_bones
|
||||
}
|
||||
|
||||
all_names: tuple[str, ...] = tuple(b.name.value for b in all_human_bones)
|
||||
|
||||
@staticmethod
|
||||
def get(name: HumanBoneName) -> HumanBoneSpecification:
|
||||
return HumanBoneSpecifications.human_bone_name_to_human_bone[name]
|
||||
221
common/workspace.py
Normal file
221
common/workspace.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context, Object
|
||||
from mathutils import Matrix
|
||||
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SavedWorkspace:
|
||||
# Be careful not to include objects that might disappear before and after yield
|
||||
cursor_matrix: Matrix
|
||||
previous_object_name: Optional[str]
|
||||
previous_object_mode: Optional[str]
|
||||
active_object_name: Optional[str]
|
||||
active_object_hide_viewport: bool
|
||||
|
||||
|
||||
def enter_save_workspace(
|
||||
context: Context, obj: Optional[Object] = None, *, mode: str = "OBJECT"
|
||||
) -> SavedWorkspace:
|
||||
# Save the position of the 3D cursor
|
||||
cursor_matrix = context.scene.cursor.matrix.copy()
|
||||
|
||||
previous_object_name = None
|
||||
previous_object_mode = None
|
||||
previous_object = context.view_layer.objects.active
|
||||
if previous_object:
|
||||
# previous_object may disappear after yield.
|
||||
# We intern it just in case, but it might not be necessary.
|
||||
previous_object_name = sys.intern(previous_object.name)
|
||||
previous_object_mode = sys.intern(previous_object.mode)
|
||||
|
||||
# If an obj argument is passed, set the mode to "OBJECT". If the mode
|
||||
# stays as is, it may not be possible to change the active object
|
||||
if previous_object != obj and previous_object_mode != "OBJECT":
|
||||
previous_object_hide_viewport = previous_object.hide_viewport
|
||||
if previous_object_hide_viewport:
|
||||
# If hide_viewport is True, mode_set may fail if left as is
|
||||
previous_object.hide_viewport = False
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
if previous_object.hide_viewport != previous_object_hide_viewport:
|
||||
previous_object.hide_viewport = previous_object_hide_viewport
|
||||
|
||||
# If an object is passed, make it active
|
||||
if obj is not None:
|
||||
context.view_layer.objects.active = obj
|
||||
context.view_layer.update()
|
||||
|
||||
active_object_name = None
|
||||
active_object_hide_viewport = False
|
||||
# Change the mode of the active object
|
||||
active_object = context.view_layer.objects.active
|
||||
if active_object:
|
||||
# active_object may disappear after yield.
|
||||
# We intern it just in case, but it might not be necessary.
|
||||
active_object_name = sys.intern(active_object.name)
|
||||
active_object_hide_viewport = active_object.hide_viewport
|
||||
# If hide_viewport is True, mode_set may fail if left as is
|
||||
# Currently we force it to be visible even when not changing mode,
|
||||
# but this may be inappropriate
|
||||
if active_object_hide_viewport:
|
||||
active_object.hide_viewport = False
|
||||
if active_object.mode != mode:
|
||||
bpy.ops.object.mode_set(mode=mode)
|
||||
|
||||
return SavedWorkspace(
|
||||
cursor_matrix=cursor_matrix,
|
||||
previous_object_name=previous_object_name,
|
||||
previous_object_mode=previous_object_mode,
|
||||
active_object_name=active_object_name,
|
||||
active_object_hide_viewport=active_object_hide_viewport,
|
||||
)
|
||||
|
||||
|
||||
def exit_save_workspace(context: Context, saved_workspace: SavedWorkspace) -> None:
|
||||
cursor_matrix = saved_workspace.cursor_matrix
|
||||
previous_object_name = saved_workspace.previous_object_name
|
||||
previous_object_mode = saved_workspace.previous_object_mode
|
||||
active_object_name = saved_workspace.active_object_name
|
||||
active_object_hide_viewport = saved_workspace.active_object_hide_viewport
|
||||
|
||||
previous_object = None
|
||||
if previous_object_name is not None:
|
||||
previous_object = context.blend_data.objects.get(previous_object_name)
|
||||
|
||||
current_active_object = context.view_layer.objects.active
|
||||
|
||||
# Set the mode of the currently active object to "OBJECT". If the mode stays as is,
|
||||
# it may not be possible to change the active object
|
||||
if (
|
||||
current_active_object
|
||||
and current_active_object != previous_object
|
||||
and current_active_object.mode != "OBJECT"
|
||||
):
|
||||
current_active_object_hide_viewport = current_active_object.hide_viewport
|
||||
if current_active_object.hide_viewport:
|
||||
current_active_object.hide_viewport = False
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
if current_active_object.hide_viewport != current_active_object_hide_viewport:
|
||||
current_active_object.hide_viewport = current_active_object_hide_viewport
|
||||
|
||||
# Restore the hide_viewport of the activated object
|
||||
if active_object_name is not None:
|
||||
active_object = context.blend_data.objects.get(active_object_name)
|
||||
if active_object and active_object.hide_viewport != active_object_hide_viewport:
|
||||
active_object.hide_viewport = active_object_hide_viewport
|
||||
|
||||
# Return to the originally active object
|
||||
previous_object = None
|
||||
if previous_object_name is not None:
|
||||
previous_object = context.blend_data.objects.get(previous_object_name)
|
||||
|
||||
if context.view_layer.objects.active != previous_object:
|
||||
context.view_layer.objects.active = previous_object
|
||||
context.view_layer.update()
|
||||
|
||||
# Restore the mode of the originally active object
|
||||
if (
|
||||
previous_object
|
||||
and previous_object_mode is not None
|
||||
and previous_object_mode != previous_object.mode
|
||||
):
|
||||
# If hide_viewport is True, mode_set may fail if left as is
|
||||
# Restore hide_viewport after mode_set is completed
|
||||
previous_object_hide_viewport = previous_object.hide_viewport
|
||||
if previous_object_hide_viewport:
|
||||
previous_object.hide_viewport = False
|
||||
bpy.ops.object.mode_set(mode=previous_object_mode)
|
||||
if previous_object.hide_viewport != previous_object_hide_viewport:
|
||||
previous_object.hide_viewport = previous_object_hide_viewport
|
||||
|
||||
# Restore the 3D cursor position
|
||||
context.scene.cursor.matrix = cursor_matrix
|
||||
|
||||
|
||||
@contextmanager
|
||||
def save_workspace(
|
||||
context: Context, obj: Optional[Object] = None, *, mode: str = "OBJECT"
|
||||
) -> Iterator[None]:
|
||||
saved_workspace = enter_save_workspace(context, obj, mode=mode)
|
||||
try:
|
||||
yield
|
||||
# After yield, bpy native objects may be deleted or become invalid
|
||||
# as frames advance. Accessing them in this state can cause crashes,
|
||||
# so be careful not to access potentially invalid native objects
|
||||
# after yield
|
||||
finally:
|
||||
exit_save_workspace(context, saved_workspace)
|
||||
|
||||
|
||||
def wm_append_without_library(
|
||||
context: Context,
|
||||
blend_path: Path,
|
||||
*,
|
||||
append_filepath: str,
|
||||
append_filename: str,
|
||||
append_directory: str,
|
||||
) -> set[str]:
|
||||
"""Call wm.append and remove the added libraries.
|
||||
|
||||
Used to work around the issue where wm.append adding libraries causes
|
||||
asset library addition to fail.
|
||||
https://github.com/saturday06/VRM-Addon-for-Blender/issues/631
|
||||
https://github.com/saturday06/VRM-Addon-for-Blender/issues/646
|
||||
"""
|
||||
# https://projects.blender.org/blender/blender/src/tag/v2.93.18/source/blender/windowmanager/intern/wm_files_link.c#L85-L90
|
||||
with save_workspace(context):
|
||||
# List of pointers for library addition detection.
|
||||
# Used only for addition detection. Be careful not to dereference,
|
||||
# as it is dangerous.
|
||||
existing_library_pointers: list[int] = [
|
||||
library.as_pointer() for library in context.blend_data.libraries
|
||||
]
|
||||
|
||||
result = bpy.ops.wm.append(
|
||||
filepath=append_filepath,
|
||||
filename=append_filename,
|
||||
directory=append_directory,
|
||||
link=False,
|
||||
)
|
||||
if result != {"FINISHED"}:
|
||||
return result
|
||||
|
||||
# Remove one added library.
|
||||
# Reverse order to handle recursive calls, but effectiveness is unconfirmed.
|
||||
for library in reversed(list(context.blend_data.libraries)):
|
||||
if not blend_path.samefile(library.filepath):
|
||||
continue
|
||||
|
||||
if library.as_pointer() in existing_library_pointers:
|
||||
continue
|
||||
|
||||
if bpy.app.version >= (3, 2) and library.use_extra_user:
|
||||
library.use_extra_user = False
|
||||
if library.users:
|
||||
if bpy.app.version >= (3, 2):
|
||||
logger.warning(
|
||||
'Failed to remove "%s" with %d users'
|
||||
" while appending blend file:"
|
||||
' filepath="%s" filename="%s" directory="%s"',
|
||||
library.name,
|
||||
library.users,
|
||||
append_filepath,
|
||||
append_filename,
|
||||
append_directory,
|
||||
)
|
||||
else:
|
||||
context.blend_data.libraries.remove(library)
|
||||
|
||||
break
|
||||
return result
|
||||
1
editor/__init__.py
Normal file
1
editor/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user