Files
blender-vrm/common/convert.py
小煜 091ad6a49a 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
2026-01-01 14:21:56 +08:00

298 lines
8.6 KiB
Python

# 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)
]