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:
2026-01-01 14:21:56 +08:00
commit 091ad6a49a
243 changed files with 60636 additions and 0 deletions

1
common/__init__.py Normal file
View File

@@ -0,0 +1 @@
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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]

View File

@@ -0,0 +1 @@
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later

View 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)

View File

@@ -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)

View 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 {}

View 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 "))

View 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)

View 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)

View 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)

View 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)

View 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)

View 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,
},
)

View 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
View 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
View 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

Binary file not shown.

BIN
common/mtoon1.blend Normal file

Binary file not shown.

BIN
common/mtoon1_outline.blend Normal file

Binary file not shown.

Binary file not shown.

View 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
View 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",
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
View 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,
)

View 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

File diff suppressed because it is too large Load Diff

165
common/ops/wm.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

59
common/test_helper.py Normal file
View 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

Binary file not shown.

161
common/version.py Normal file
View 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
View File

@@ -0,0 +1 @@
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later

Binary file not shown.

Binary file not shown.

743
common/vrm0/human_bone.py Normal file
View 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
View File

@@ -0,0 +1 @@
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later

Binary file not shown.

Binary file not shown.

790
common/vrm1/human_bone.py Normal file
View 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
View 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