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:
297
common/convert.py
Normal file
297
common/convert.py
Normal file
@@ -0,0 +1,297 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import math
|
||||
from collections.abc import Iterator, Mapping, Sequence
|
||||
from sys import float_info
|
||||
from typing import Optional, Union
|
||||
|
||||
from . import convert_any
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
Json = Union[
|
||||
None,
|
||||
bool,
|
||||
int,
|
||||
float,
|
||||
str,
|
||||
list["Json"],
|
||||
dict[str, "Json"],
|
||||
]
|
||||
|
||||
|
||||
def iterator_or_none(v: object) -> Optional[Iterator[object]]:
|
||||
try:
|
||||
# "isinstance(v, Iterable)" doesn't work.
|
||||
# https://github.com/python/cpython/blob/3.9/Doc/library/collections.abc.rst?plain=1#L126-L127
|
||||
iterator = iter(v) # type: ignore[call-overload]
|
||||
except TypeError:
|
||||
return None
|
||||
return convert_any.iterator_to_object_iterator(iterator)
|
||||
|
||||
|
||||
def sequence_or_none(
|
||||
sequence_object: object,
|
||||
) -> Optional[Sequence[object]]:
|
||||
sequence = sequence_object
|
||||
if not isinstance(sequence, Sequence):
|
||||
return None
|
||||
iterator = iterator_or_none(sequence_object)
|
||||
if iterator is None:
|
||||
return None
|
||||
return list(iterator)
|
||||
|
||||
|
||||
def mapping_or_none(
|
||||
mapping_object: object,
|
||||
) -> Optional[Mapping[object, object]]:
|
||||
mapping = mapping_object
|
||||
if not isinstance(mapping, Mapping):
|
||||
return None
|
||||
key_iterator = iterator_or_none(mapping_object)
|
||||
if key_iterator is None:
|
||||
return None
|
||||
return {key: convert_any.to_object(mapping[key]) for key in key_iterator}
|
||||
|
||||
|
||||
def vrm_json_vector3_to_tuple(
|
||||
value: Json,
|
||||
) -> Optional[tuple[float, float, float]]:
|
||||
if not isinstance(value, Mapping):
|
||||
return None
|
||||
x = float_or(value.get("x"), 0.0)
|
||||
y = float_or(value.get("y"), 0.0)
|
||||
z = float_or(value.get("z"), 0.0)
|
||||
return (x, y, z)
|
||||
|
||||
|
||||
def vrm_json_curve_to_list(curve: Json) -> Optional[list[float]]:
|
||||
if not isinstance(curve, list):
|
||||
return None
|
||||
values: list[float] = [float_or(v, 0.0) for v in curve]
|
||||
while len(values) < 8:
|
||||
values.append(0)
|
||||
while len(values) > 8:
|
||||
values.pop()
|
||||
return values
|
||||
|
||||
|
||||
def vrm_json_array_to_float_vector(
|
||||
input_values: Json, defaults: list[float]
|
||||
) -> list[float]:
|
||||
if not isinstance(input_values, list):
|
||||
return defaults
|
||||
|
||||
output_values: list[float] = []
|
||||
for index, default in enumerate(defaults):
|
||||
if index >= len(input_values):
|
||||
output_values.append(default)
|
||||
continue
|
||||
|
||||
input_value = float_or(input_values[index], default)
|
||||
output_values.append(input_value)
|
||||
|
||||
return output_values
|
||||
|
||||
|
||||
BPY_TRACK_AXIS_TO_VRM_AIM_AXIS = {
|
||||
"TRACK_X": "PositiveX",
|
||||
"TRACK_Y": "PositiveY",
|
||||
"TRACK_Z": "PositiveZ",
|
||||
"TRACK_NEGATIVE_X": "NegativeX",
|
||||
"TRACK_NEGATIVE_Y": "NegativeY",
|
||||
"TRACK_NEGATIVE_Z": "NegativeZ",
|
||||
}
|
||||
|
||||
VRM_AIM_AXIS_TO_BPY_TRACK_AXIS = {
|
||||
v: k for k, v in BPY_TRACK_AXIS_TO_VRM_AIM_AXIS.items()
|
||||
}
|
||||
|
||||
|
||||
def mtoon_shading_toony_1_to_0(shading_toony: float, shading_shift: float) -> float:
|
||||
base = 2 - shading_toony + shading_shift
|
||||
if abs(base) < float_info.epsilon:
|
||||
# https://github.com/Santarh/MToon/blob/43f02fe8c9b19c3cf9f3238003b7d8b332933833/MToon/Resources/Shaders/MToon.shader#L17
|
||||
return 0.9
|
||||
return max(0, min((shading_toony + shading_shift) / base, 1))
|
||||
|
||||
|
||||
def mtoon_shading_shift_1_to_0(shading_toony: float, shading_shift: float) -> float:
|
||||
return max(-1, min(shading_toony - shading_shift - 1, 1))
|
||||
|
||||
|
||||
def mtoon_gi_equalization_to_intensity(gi_equalization: float) -> float:
|
||||
return max(0, min(1, 1 - gi_equalization))
|
||||
|
||||
|
||||
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L10-L19
|
||||
def mtoon_shading_toony_0_to_1(
|
||||
shading_toony_0x: float, shading_shift_0x: float
|
||||
) -> float:
|
||||
(range_min, range_max) = get_shading_range_0x(shading_toony_0x, shading_shift_0x)
|
||||
return max(0, min(1, (2 - (range_max - range_min)) * 0.5))
|
||||
|
||||
|
||||
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L21-L30
|
||||
def mtoon_shading_shift_0_to_1(
|
||||
shading_toony_0x: float, shading_shift_0x: float
|
||||
) -> float:
|
||||
(range_min, range_max) = get_shading_range_0x(shading_toony_0x, shading_shift_0x)
|
||||
return max(-1, min(1, ((range_max + range_min) * 0.5 * -1)))
|
||||
|
||||
|
||||
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L32-L38
|
||||
def mtoon_intensity_to_gi_equalization(gi_intensity_0x: float) -> float:
|
||||
return max(0, min(1, 1 - gi_intensity_0x))
|
||||
|
||||
|
||||
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L40-L46
|
||||
def get_shading_range_0x(
|
||||
shading_toony_0x: float, shading_shift_0x: float
|
||||
) -> tuple[float, float]:
|
||||
range_min = shading_shift_0x
|
||||
range_max = (1 - shading_toony_0x) + shading_toony_0x * shading_shift_0x
|
||||
return (range_min, range_max)
|
||||
|
||||
|
||||
def float_or_none(
|
||||
v: object,
|
||||
min_value: float = -float_info.max,
|
||||
max_value: float = float_info.max,
|
||||
) -> Optional[float]:
|
||||
if isinstance(v, float):
|
||||
if math.isnan(v):
|
||||
return None
|
||||
return max(min_value, min(v, max_value))
|
||||
|
||||
if isinstance(v, bool):
|
||||
return 1.0 if v else 0.0
|
||||
|
||||
if isinstance(v, int):
|
||||
return max(min_value, min(float(v), max_value))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def float_or(
|
||||
v: object,
|
||||
default: float,
|
||||
min_value: float = -float_info.max,
|
||||
max_value: float = float_info.max,
|
||||
) -> float:
|
||||
float_or_none_value = float_or_none(v, min_value, max_value)
|
||||
if float_or_none_value is not None:
|
||||
return float_or_none_value
|
||||
|
||||
if min_value <= default <= max_value:
|
||||
return default
|
||||
|
||||
logger.warning(
|
||||
"Default float value %s is out of range [%s, %s]", default, min_value, max_value
|
||||
)
|
||||
|
||||
return max(min_value, min(default, max_value))
|
||||
|
||||
|
||||
def float4_or_none(value4: object) -> Optional[tuple[float, float, float, float]]:
|
||||
value4_iterator = iterator_or_none(value4)
|
||||
if value4_iterator is None:
|
||||
return None
|
||||
|
||||
result: list[float] = []
|
||||
for value in value4_iterator:
|
||||
if len(result) == 4:
|
||||
return None
|
||||
float_value = float_or_none(value)
|
||||
if float_value is None:
|
||||
return None
|
||||
result.append(float_value)
|
||||
if len(result) != 4:
|
||||
return None
|
||||
return (result[0], result[1], result[2], result[3])
|
||||
|
||||
|
||||
def float4_or(
|
||||
value4: object, default: tuple[float, float, float, float]
|
||||
) -> tuple[float, float, float, float]:
|
||||
float4 = float4_or_none(value4)
|
||||
return float4 if float4 is not None else default
|
||||
|
||||
|
||||
def float3_or_none(value3: object) -> Optional[tuple[float, float, float]]:
|
||||
value3_iterator = iterator_or_none(value3)
|
||||
if value3_iterator is None:
|
||||
return None
|
||||
|
||||
result: list[float] = []
|
||||
for value in value3_iterator:
|
||||
if len(result) == 3:
|
||||
return None
|
||||
float_value = float_or_none(value)
|
||||
if float_value is None:
|
||||
return None
|
||||
result.append(float_value)
|
||||
if len(result) != 3:
|
||||
return None
|
||||
return (result[0], result[1], result[2])
|
||||
|
||||
|
||||
def float3_or(
|
||||
value3: object, default: tuple[float, float, float]
|
||||
) -> tuple[float, float, float]:
|
||||
float3 = float3_or_none(value3)
|
||||
return float3 if float3 is not None else default
|
||||
|
||||
|
||||
def float2_or_none(value2: object) -> Optional[tuple[float, float]]:
|
||||
value2_iterator = iterator_or_none(value2)
|
||||
if value2_iterator is None:
|
||||
return None
|
||||
result: list[float] = []
|
||||
for value in value2_iterator:
|
||||
if len(result) == 2:
|
||||
return None
|
||||
float_value = float_or_none(value)
|
||||
if float_value is None:
|
||||
return None
|
||||
result.append(float_value)
|
||||
if len(result) != 2:
|
||||
return None
|
||||
return (result[0], result[1])
|
||||
|
||||
|
||||
def float2_or(value2: object, default: tuple[float, float]) -> tuple[float, float]:
|
||||
float2 = float2_or_none(value2)
|
||||
return float2 if float2 is not None else default
|
||||
|
||||
|
||||
def str_or(v: object, default: str) -> str:
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
return default
|
||||
|
||||
|
||||
def axis_blender_to_gltf(vector3: Sequence[float]) -> tuple[float, float, float]:
|
||||
return (
|
||||
-vector3[0],
|
||||
vector3[2],
|
||||
vector3[1],
|
||||
)
|
||||
|
||||
|
||||
def linear_to_srgb(
|
||||
non_color: Sequence[float],
|
||||
) -> Sequence[float]:
|
||||
return [
|
||||
math.pow(channel_value, 1.0 / 2.2) if channel_index < 3 else channel_value
|
||||
for channel_index, channel_value in enumerate(non_color)
|
||||
]
|
||||
|
||||
|
||||
def srgb_to_linear(
|
||||
srgb_color: Sequence[float],
|
||||
) -> Sequence[float]:
|
||||
return [
|
||||
math.pow(channel_value, 2.2) if channel_index < 3 else channel_value
|
||||
for channel_index, channel_value in enumerate(srgb_color)
|
||||
]
|
||||
Reference in New Issue
Block a user