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

View File

@@ -0,0 +1,198 @@
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import contextlib
from typing import Optional
from urllib.parse import ParseResult, parse_qsl, urlparse
from bpy.app.translations import pgettext
from ..common.convert import Json
class LicenseConfirmationRequiredProp:
def __init__(
self,
url: Optional[str],
json_key: Optional[str],
message: str,
) -> None:
self.url = url
self.json_key = json_key
self.message = message
def description(self) -> str:
return f"""class=LicenseConfirmationRequired
url={self.url}
json_key={self.json_key}
message={self.message}
"""
class LicenseConfirmationRequiredError(Exception):
def __init__(self, props: list[LicenseConfirmationRequiredProp]) -> None:
self.props = props
super().__init__(self.description())
def description(self) -> str:
return "\n".join(prop.description() for prop in self.props)
def license_confirmations(self) -> list[dict[str, str]]:
return [
{
"name": "LicenseConfirmation" + str(index),
"url": prop.url or "",
"json_key": prop.json_key or "",
"message": prop.message or "",
}
for index, prop in enumerate(self.props)
]
def validate_license_url(
url_str: str, json_key: str, props: list[LicenseConfirmationRequiredProp]
) -> None:
if not url_str:
return
url = None
with contextlib.suppress(ValueError):
url = urlparse(url_str)
if url:
query_dict = dict(parse_qsl(url.query))
if validate_vroid_hub_license_url(
url, query_dict, json_key, props
) or validate_uni_virtual_license_url(url, query_dict, json_key, props):
return
props.append(
LicenseConfirmationRequiredProp(
url_str,
json_key,
pgettext("This VRM is not allowed to be edited. Please check its license"),
)
)
def validate_vroid_hub_license_url(
url: ParseResult,
query_dict: dict[str, str],
json_key: str,
props: list[LicenseConfirmationRequiredProp],
) -> bool:
# https://hub.vroid.com/en/license?allowed_to_use_user=everyone&characterization_allowed_user=everyone&corporate_commercial_use=allow&credit=unnecessary&modification=allow&personal_commercial_use=profit&redistribution=allow&sexual_expression=allow&version=1&violent_expression=allow
if url.hostname != "hub.vroid.com" or not url.path.endswith("/license"):
return False
if query_dict.get("modification") == "disallow":
props.append(
LicenseConfirmationRequiredProp(
url.geturl(),
json_key,
pgettext(
'This VRM is licensed by VRoid Hub License "Alterations: No".'
),
)
)
return True
def validate_uni_virtual_license_url(
url: ParseResult,
query_dict: dict[str, str],
json_key: str,
props: list[LicenseConfirmationRequiredProp],
) -> bool:
# https://uv-license.com/en/license?utf8=%E2%9C%93&pcu=true
if url.hostname != "uv-license.com" or not url.path.endswith("/license"):
return False
if query_dict.get("remarks") == "true":
props.append(
LicenseConfirmationRequiredProp(
url.geturl(),
json_key,
pgettext('This VRM is licensed by UV License with "Remarks".'),
)
)
return True
def validate_vrm1_license(
json_dict: dict[str, Json], _confirmations: list[LicenseConfirmationRequiredProp]
) -> None:
extensions_dict = json_dict.get("extensions")
if not isinstance(extensions_dict, dict):
return
vrmc_vrm_dict = extensions_dict.get("VRMC_vrm")
if not isinstance(vrmc_vrm_dict, dict):
return
return
def validate_vrm0_license(
json_dict: dict[str, Json], confirmations: list[LicenseConfirmationRequiredProp]
) -> None:
extensions_dict = json_dict.get("extensions")
if not isinstance(extensions_dict, dict):
return
vrm_dict = extensions_dict.get("VRM")
if not isinstance(vrm_dict, dict):
return
meta_dict = vrm_dict.get("meta")
if not isinstance(meta_dict, dict):
return
license_name = meta_dict.get("licenseName")
if license_name in [
# https://github.com/vrm-c/vrm-specification/blob/master/specification/0.0/schema/vrm.meta.schema.json#L56
"CC_BY_ND",
"CC_BY_NC_ND",
]:
confirmations.append(
LicenseConfirmationRequiredProp(
None,
None,
pgettext(
"This VRM is not allowed to be edited. Please check its license"
),
)
)
if license_name == "Other":
other_license_url = meta_dict.get("otherLicenseUrl")
if other_license_url is None:
confirmations.append(
LicenseConfirmationRequiredProp(
None,
None,
pgettext(
'The VRM selects "Other" license but no license url is found.'
),
)
)
else:
validate_license_url(
str(other_license_url), "otherLicenseUrl", confirmations
)
other_permission_url = meta_dict.get("otherPermissionUrl")
if other_permission_url is not None:
validate_license_url(
str(other_permission_url),
"otherPermissionUrl",
confirmations,
)
def validate_license(
json_dict: dict[str, Json], spec_version_number: tuple[int, int]
) -> None:
"""Validate that the license is not a non-modifiable license, such as CC_ND."""
confirmations: list[LicenseConfirmationRequiredProp] = []
if tuple(spec_version_number) >= (1, 0):
validate_vrm1_license(json_dict, confirmations)
else:
validate_vrm0_license(json_dict, confirmations)
if confirmations:
raise LicenseConfirmationRequiredError(confirmations)