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