@volter/blender-engine 0.1.148 → 0.1.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -0
- package/README.md +4 -11
- package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
- package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
- package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
- package/browser/addons/vrm/README.txt +3 -0
- package/browser/addons/vrm/__init__.py +41 -0
- package/browser/addons/vrm/blender_manifest.toml +16 -0
- package/browser/addons/vrm/common/__init__.py +1 -0
- package/browser/addons/vrm/common/animation.py +58 -0
- package/browser/addons/vrm/common/blender_manifest.py +82 -0
- package/browser/addons/vrm/common/char.py +16 -0
- package/browser/addons/vrm/common/convert.py +287 -0
- package/browser/addons/vrm/common/convert_any.py +48 -0
- package/browser/addons/vrm/common/debug.py +40 -0
- package/browser/addons/vrm/common/deep.py +162 -0
- package/browser/addons/vrm/common/error_dialog.py +274 -0
- package/browser/addons/vrm/common/fs.py +71 -0
- package/browser/addons/vrm/common/gl.py +20 -0
- package/browser/addons/vrm/common/gltf.py +1149 -0
- package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
- package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
- package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
- package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
- package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
- package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
- package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
- package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
- package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
- package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
- package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
- package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
- package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
- package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
- package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
- package/browser/addons/vrm/common/legacy_gltf.py +16 -0
- package/browser/addons/vrm/common/logger.py +84 -0
- package/browser/addons/vrm/common/mtoon0.blend +0 -0
- package/browser/addons/vrm/common/mtoon1.blend +0 -0
- package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
- package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
- package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
- package/browser/addons/vrm/common/ops/__init__.py +10 -0
- package/browser/addons/vrm/common/ops/export_scene.py +67 -0
- package/browser/addons/vrm/common/ops/icyp.py +53 -0
- package/browser/addons/vrm/common/ops/import_scene.py +57 -0
- package/browser/addons/vrm/common/ops/vrm.py +2380 -0
- package/browser/addons/vrm/common/ops/wm.py +167 -0
- package/browser/addons/vrm/common/preferences.py +362 -0
- package/browser/addons/vrm/common/progress.py +87 -0
- package/browser/addons/vrm/common/rotation.py +89 -0
- package/browser/addons/vrm/common/safe_removal.py +46 -0
- package/browser/addons/vrm/common/scene_watcher.py +230 -0
- package/browser/addons/vrm/common/shader.py +2243 -0
- package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
- package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
- package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
- package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
- package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
- package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
- package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
- package/browser/addons/vrm/common/version.py +161 -0
- package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
- package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
- package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
- package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
- package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
- package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
- package/browser/addons/vrm/common/workspace.py +227 -0
- package/browser/addons/vrm/common/writable_context.py +77 -0
- package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
- package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
- package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
- package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
- package/browser/addons/vrm/documentation/en-us/index.md +41 -0
- package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
- package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
- package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
- package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
- package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
- package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
- package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
- package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
- package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
- package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
- package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
- package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
- package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
- package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
- package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
- package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
- package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
- package/browser/addons/vrm/editor/__init__.py +1 -0
- package/browser/addons/vrm/editor/extension.py +425 -0
- package/browser/addons/vrm/editor/extension_accessor.py +75 -0
- package/browser/addons/vrm/editor/handler.py +84 -0
- package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
- package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
- package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
- package/browser/addons/vrm/editor/make_armature.py +709 -0
- package/browser/addons/vrm/editor/menu.py +147 -0
- package/browser/addons/vrm/editor/migration.py +297 -0
- package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
- package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
- package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
- package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
- package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
- package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
- package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
- package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
- package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
- package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
- package/browser/addons/vrm/editor/ops.py +471 -0
- package/browser/addons/vrm/editor/panel.py +315 -0
- package/browser/addons/vrm/editor/property_group.py +888 -0
- package/browser/addons/vrm/editor/search.py +682 -0
- package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
- package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
- package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
- package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
- package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
- package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
- package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
- package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
- package/browser/addons/vrm/editor/subscription.py +123 -0
- package/browser/addons/vrm/editor/t_pose.py +713 -0
- package/browser/addons/vrm/editor/validation.py +1271 -0
- package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
- package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
- package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
- package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
- package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
- package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
- package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
- package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
- package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
- package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
- package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
- package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
- package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
- package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
- package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
- package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
- package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
- package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
- package/browser/addons/vrm/exporter/__init__.py +1 -0
- package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
- package/browser/addons/vrm/exporter/export_scene.py +930 -0
- package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
- package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
- package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
- package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
- package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
- package/browser/addons/vrm/external/__init__.py +1 -0
- package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
- package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
- package/browser/addons/vrm/external/rigify_support.py +1 -0
- package/browser/addons/vrm/importer/__init__.py +1 -0
- package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
- package/browser/addons/vrm/importer/file_handler.py +34 -0
- package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
- package/browser/addons/vrm/importer/import_scene.py +646 -0
- package/browser/addons/vrm/importer/license_validation.py +198 -0
- package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
- package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
- package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
- package/browser/addons/vrm/importer/vrm_diff.py +316 -0
- package/browser/addons/vrm/locale/__init__.py +1 -0
- package/browser/addons/vrm/locale/ja_jp.py +826 -0
- package/browser/addons/vrm/locale/ruff.toml +8 -0
- package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
- package/browser/addons/vrm/locale/zh_hans.py +928 -0
- package/browser/addons/vrm/registration.py +670 -0
- package/browser/addons/vrm.json +18 -0
- package/browser/blender-emscripten-engine.mts +60 -128
- package/browser/blender-engine.mts +20 -186
- package/browser/blender-wali-engine.mts +35 -81
- package/browser/describe-thrown.mts +3 -6
- package/browser/document-chunks.mts +4 -16
- package/browser/gltf_export.py +3161 -0
- package/browser/heap-growth.mts +23 -0
- package/browser/index.ts +2 -0
- package/browser/protocol.ts +119 -35
- package/browser/pull-job.mts +2 -10
- package/browser/rna.ts +190 -73
- package/browser/runtime.ts +197 -82
- package/browser/session-addons.mts +33 -0
- package/browser/session-frame.mts +5 -24
- package/browser/session.py +1338 -1427
- package/browser/three/attach-presenter.ts +11 -39
- package/browser/three/blender-base64.ts +1 -36
- package/browser/three/blender-corner-normals.ts +1 -23
- package/browser/three/blender-display-shaders.json +1 -1
- package/browser/three/blender-display-shaders.lut +0 -0
- package/browser/three/blender-display-transform.ts +2 -1
- package/browser/three/blender-frame-columns.ts +5 -13
- package/browser/three/blender-graph-material.ts +18 -3
- package/browser/three/blender-node-graph.ts +10 -34
- package/browser/three/blender-physical-material.ts +2 -31
- package/browser/three/blender-runtime-armature.ts +7 -60
- package/browser/three/blender-runtime-frame.ts +9 -23
- package/browser/three/blender-runtime-geometry.ts +22 -26
- package/browser/three/blender-runtime-lighting.ts +48 -178
- package/browser/three/blender-runtime-shadows.ts +187 -1
- package/browser/three/blender-runtime-skeleton.ts +53 -1
- package/browser/three/blender-runtime-view.ts +505 -283
- package/browser/three/blender-runtime-weights.ts +9 -72
- package/browser/three/blender-sky.ts +13 -33
- package/browser/three/blender-triangulate.ts +7 -35
- package/browser/three/blender-viewport-sampling.ts +235 -0
- package/browser/three/blender-workbench-material.ts +25 -29
- package/browser/three/blender-workbench-outline.ts +199 -0
- package/browser/three/blender-world-volume.ts +62 -10
- package/browser/three/presenter.ts +10 -26
- package/browser/three/release.ts +5 -21
- package/browser/three/sky-precompute-worker.ts +1 -18
- package/browser/three/sky-worker.ts +5 -23
- package/browser/three/world-field-sampler.ts +12 -26
- package/browser/volter_three.py +9 -105
- package/browser/worker.ts +156 -151
- package/package.json +5 -2
- package/wasm/BUNDLE.json +17 -17
- package/wasm/DEPENDENCY-LICENSES.txt +14 -0
- package/wasm/blender_browser.data.br +0 -0
- package/wasm/blender_browser.js +1 -1
- package/wasm/blender_browser.wasm.br +0 -0
- package/browser/frame-transfer.mts +0 -19
- package/browser/three/agx-base-srgb.lut +0 -0
- package/browser/three/agx-look-medium-high-contrast.lut +0 -0
- package/browser/three/agx-look-punchy.lut +0 -0
- package/browser/three/blender-agx.ts +0 -235
- package/browser/three/blender-display-lut.ts +0 -145
- package/browser/three/blender-filmic.ts +0 -49
- package/browser/three/blender-standard.ts +0 -68
- package/browser/three/blender-world-visibility.ts +0 -278
- package/browser/three/filmic-srgb.lut +0 -0
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from sys import float_info
|
|
5
|
+
from typing import TYPE_CHECKING, Final, Optional
|
|
6
|
+
from urllib.parse import urlsplit
|
|
7
|
+
|
|
8
|
+
import bpy
|
|
9
|
+
from bpy.app.translations import pgettext
|
|
10
|
+
from bpy.props import BoolProperty, CollectionProperty, IntProperty, StringProperty
|
|
11
|
+
from bpy.types import (
|
|
12
|
+
Armature,
|
|
13
|
+
Bone,
|
|
14
|
+
Context,
|
|
15
|
+
Event,
|
|
16
|
+
Image,
|
|
17
|
+
Material,
|
|
18
|
+
Mesh,
|
|
19
|
+
NodesModifier,
|
|
20
|
+
Object,
|
|
21
|
+
Operator,
|
|
22
|
+
PropertyGroup,
|
|
23
|
+
ShaderNodeGroup,
|
|
24
|
+
ShaderNodeTexImage,
|
|
25
|
+
UILayout,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from ..common import shader, version
|
|
29
|
+
from ..common.legacy_gltf import RGBA_INPUT_NAMES, TEXTURE_INPUT_NAMES, VAL_INPUT_NAMES
|
|
30
|
+
from ..common.logger import get_logger
|
|
31
|
+
from ..common.mtoon_unversioned import MtoonUnversioned
|
|
32
|
+
from ..common.preferences import get_preferences
|
|
33
|
+
from ..common.shader import LegacyAddonMaterial
|
|
34
|
+
from ..common.vrm0 import human_bone as vrm0_human_bone
|
|
35
|
+
from ..common.vrm1 import human_bone as vrm1_human_bone
|
|
36
|
+
from . import migration, search
|
|
37
|
+
from .extension import (
|
|
38
|
+
VrmAddonArmatureExtensionPropertyGroup,
|
|
39
|
+
)
|
|
40
|
+
from .extension_accessor import get_armature_extension, get_material_extension
|
|
41
|
+
from .property_group import CollectionPropertyProtocol
|
|
42
|
+
|
|
43
|
+
_logger = get_logger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ValidationState:
|
|
48
|
+
error_messages: Final[list[str]] = field(default_factory=list[str])
|
|
49
|
+
warning_messages: Final[list[str]] = field(default_factory=list[str])
|
|
50
|
+
skippable_warning_messages: Final[list[str]] = field(default_factory=list[str])
|
|
51
|
+
info_messages: Final[list[str]] = field(default_factory=list[str])
|
|
52
|
+
node_names: Final[list[str]] = field(default_factory=list[str])
|
|
53
|
+
used_images: Final[list[Image]] = field(default_factory=list[Image])
|
|
54
|
+
used_materials: Final[list[Material]] = field(default_factory=list[Material])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class VrmValidationError(PropertyGroup):
|
|
58
|
+
message: StringProperty() # type: ignore[valid-type]
|
|
59
|
+
severity: IntProperty(min=0) # type: ignore[valid-type]
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
# This code is auto generated.
|
|
63
|
+
# To regenerate, run the `uv run tools/property_typing.py` command.
|
|
64
|
+
message: str # type: ignore[no-redef]
|
|
65
|
+
severity: int # type: ignore[no-redef]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class WM_OT_vrm_validator(Operator):
|
|
69
|
+
bl_idname = "vrm.model_validate"
|
|
70
|
+
bl_label = "Check VRM Model"
|
|
71
|
+
bl_description = "NO Quad_Poly & N_GON, NO unSkined Mesh etc..."
|
|
72
|
+
|
|
73
|
+
show_successful_message: BoolProperty( # type: ignore[valid-type]
|
|
74
|
+
default=True
|
|
75
|
+
)
|
|
76
|
+
errors: CollectionProperty(type=VrmValidationError) # type: ignore[valid-type]
|
|
77
|
+
armature_object_name: StringProperty() # type: ignore[valid-type]
|
|
78
|
+
|
|
79
|
+
def execute(self, context: Context) -> set[str]:
|
|
80
|
+
has_error = self.detect_errors(
|
|
81
|
+
context,
|
|
82
|
+
self.errors,
|
|
83
|
+
self.armature_object_name,
|
|
84
|
+
execute_migration=True,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
fatal_error_count = 0
|
|
88
|
+
for error in self.errors:
|
|
89
|
+
if error.severity > 0:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if fatal_error_count > 10:
|
|
93
|
+
_logger.warning(" (truncated)")
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
if not fatal_error_count:
|
|
97
|
+
_logger.warning(
|
|
98
|
+
"Validating filepath=%s, changed=%s",
|
|
99
|
+
context.blend_data.filepath,
|
|
100
|
+
context.blend_data.is_dirty,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
_logger.warning(" - %s", error.message)
|
|
104
|
+
fatal_error_count += 1
|
|
105
|
+
|
|
106
|
+
if has_error:
|
|
107
|
+
return {"CANCELLED"}
|
|
108
|
+
|
|
109
|
+
return {"FINISHED"}
|
|
110
|
+
|
|
111
|
+
def invoke(self, context: Context, _event: Event) -> set[str]:
|
|
112
|
+
if (
|
|
113
|
+
not self.detect_errors(
|
|
114
|
+
context,
|
|
115
|
+
self.errors,
|
|
116
|
+
self.armature_object_name,
|
|
117
|
+
execute_migration=True,
|
|
118
|
+
)
|
|
119
|
+
and not self.show_successful_message
|
|
120
|
+
):
|
|
121
|
+
return {"FINISHED"}
|
|
122
|
+
return context.window_manager.invoke_props_dialog(self, width=800)
|
|
123
|
+
|
|
124
|
+
def draw(self, _context: Context) -> None:
|
|
125
|
+
self.draw_errors(
|
|
126
|
+
self.layout,
|
|
127
|
+
self.errors,
|
|
128
|
+
show_successful_message=self.show_successful_message,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def validate_bone_order_vrm0(
|
|
133
|
+
context: Context,
|
|
134
|
+
messages: list[str],
|
|
135
|
+
armature: Object,
|
|
136
|
+
) -> None:
|
|
137
|
+
armature_data = armature.data
|
|
138
|
+
if not isinstance(armature_data, Armature):
|
|
139
|
+
message = f"{type(armature_data)} is not an Armature"
|
|
140
|
+
raise TypeError(message)
|
|
141
|
+
humanoid = get_armature_extension(armature_data).vrm0.humanoid
|
|
142
|
+
if not humanoid.filter_by_human_bone_hierarchy:
|
|
143
|
+
return
|
|
144
|
+
humanoid.update_all_bone_name_candidates(context, armature_data.name)
|
|
145
|
+
for human_bone in humanoid.human_bones:
|
|
146
|
+
if (
|
|
147
|
+
not human_bone.node.bone_name
|
|
148
|
+
or human_bone.node.bone_name in human_bone.node.bone_name_candidates
|
|
149
|
+
):
|
|
150
|
+
continue
|
|
151
|
+
messages.append(
|
|
152
|
+
pgettext(
|
|
153
|
+
'Couldn\'t assign "{bone}" bone'
|
|
154
|
+
+ ' to VRM Human Bone "{human_bone}". '
|
|
155
|
+
+ 'Confirm hierarchy of "{bone}" and its children. '
|
|
156
|
+
+ '"VRM" Panel → "Humanoid" → "{human_bone}" is empty'
|
|
157
|
+
+ " if wrong hierarchy."
|
|
158
|
+
).format(
|
|
159
|
+
bone=human_bone.node.bone_name,
|
|
160
|
+
human_bone=human_bone.specification().title,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def validate_bone_order_vrm1(
|
|
167
|
+
context: Context,
|
|
168
|
+
messages: list[str],
|
|
169
|
+
armature: Object,
|
|
170
|
+
) -> None:
|
|
171
|
+
armature_data = armature.data
|
|
172
|
+
if not isinstance(armature_data, Armature):
|
|
173
|
+
message = f"{type(armature_data)} is not an Armature"
|
|
174
|
+
raise TypeError(message)
|
|
175
|
+
human_bones = get_armature_extension(armature_data).vrm1.humanoid.human_bones
|
|
176
|
+
if not human_bones.filter_by_human_bone_hierarchy:
|
|
177
|
+
return
|
|
178
|
+
human_bones.update_all_bone_name_candidates(context, armature_data.name)
|
|
179
|
+
for (
|
|
180
|
+
human_bone_name,
|
|
181
|
+
human_bone,
|
|
182
|
+
) in human_bones.human_bone_name_to_human_bone().items():
|
|
183
|
+
if (
|
|
184
|
+
not human_bone.node.bone_name
|
|
185
|
+
or human_bone.node.bone_name in human_bone.node.bone_name_candidates
|
|
186
|
+
):
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
specification = vrm1_human_bone.HumanBoneSpecifications.get(human_bone_name)
|
|
190
|
+
messages.append(
|
|
191
|
+
pgettext(
|
|
192
|
+
'Couldn\'t assign "{bone}" bone'
|
|
193
|
+
+ ' to VRM Human Bone "{human_bone}". '
|
|
194
|
+
+ 'Confirm hierarchy of "{bone}" and its children. '
|
|
195
|
+
+ '"VRM" Panel → "Humanoid" → "{human_bone}" is empty'
|
|
196
|
+
+ " if wrong hierarchy."
|
|
197
|
+
).format(
|
|
198
|
+
bone=human_bone.node.bone_name,
|
|
199
|
+
human_bone=specification.title,
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def validate_bone_order(
|
|
206
|
+
context: Context,
|
|
207
|
+
messages: list[str],
|
|
208
|
+
armature: Object,
|
|
209
|
+
) -> None:
|
|
210
|
+
armature_data = armature.data
|
|
211
|
+
if not isinstance(armature_data, Armature):
|
|
212
|
+
message = f"{type(armature_data)} is not an Armature"
|
|
213
|
+
raise TypeError(message)
|
|
214
|
+
if get_armature_extension(armature_data).is_vrm0():
|
|
215
|
+
WM_OT_vrm_validator.validate_bone_order_vrm0(context, messages, armature)
|
|
216
|
+
else:
|
|
217
|
+
WM_OT_vrm_validator.validate_bone_order_vrm1(context, messages, armature)
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def validate_export_objects(
|
|
221
|
+
context: Context,
|
|
222
|
+
export_objects: list[Object],
|
|
223
|
+
armature: Optional[Object],
|
|
224
|
+
armature_data: Optional[Armature],
|
|
225
|
+
*,
|
|
226
|
+
is_vrm0: bool,
|
|
227
|
+
is_vrm1: bool,
|
|
228
|
+
execute_migration: bool,
|
|
229
|
+
state: ValidationState,
|
|
230
|
+
) -> None:
|
|
231
|
+
armature_count = sum(1 for obj in export_objects if obj.type == "ARMATURE")
|
|
232
|
+
if armature_count >= 2: # only one armature
|
|
233
|
+
state.error_messages.append(
|
|
234
|
+
pgettext("VRM exporter needs only one armature not some armatures.")
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
for obj in export_objects:
|
|
238
|
+
if (parent_obj := obj.parent) and obj.parent_type in {"VERTEX", "VERTEX_3"}:
|
|
239
|
+
state.skippable_warning_messages.append(
|
|
240
|
+
pgettext(
|
|
241
|
+
'The vertex "{parent_name}" is set as the parent of "{name}",'
|
|
242
|
+
+ " but this is not supported in VRM."
|
|
243
|
+
).format(name=obj.name, parent_name=parent_obj.name)
|
|
244
|
+
)
|
|
245
|
+
if (parent_obj := obj.parent) and obj.parent_type == "LATTICE":
|
|
246
|
+
state.skippable_warning_messages.append(
|
|
247
|
+
pgettext(
|
|
248
|
+
'"{lattice}" is set as the {parent_type} for "{name}",'
|
|
249
|
+
+ " but this is not supported in VRM."
|
|
250
|
+
).format(
|
|
251
|
+
name=obj.name,
|
|
252
|
+
parent_type=pgettext("Parent Type"),
|
|
253
|
+
lattice=pgettext("Lattice"),
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
if obj.type in search.MESH_CONVERTIBLE_OBJECT_TYPES:
|
|
257
|
+
if is_vrm0 and obj.name in state.node_names:
|
|
258
|
+
state.error_messages.append(
|
|
259
|
+
pgettext(
|
|
260
|
+
"The same name cannot be used"
|
|
261
|
+
+ " for a mesh object and a bone."
|
|
262
|
+
+ ' Rename either one whose name is "{name}".'
|
|
263
|
+
).format(name=obj.name)
|
|
264
|
+
)
|
|
265
|
+
if obj.name not in state.node_names:
|
|
266
|
+
state.node_names.append(obj.name)
|
|
267
|
+
if (
|
|
268
|
+
obj.type == "MESH"
|
|
269
|
+
and isinstance(obj_data := obj.data, Mesh)
|
|
270
|
+
and (obj_data_shape_keys := obj_data.shape_keys) is not None
|
|
271
|
+
and len(obj_data_shape_keys.key_blocks)
|
|
272
|
+
>= 2 # Exclude a "Basis" shape key
|
|
273
|
+
and any(
|
|
274
|
+
modifier.type != "ARMATURE"
|
|
275
|
+
and not (
|
|
276
|
+
modifier.type == "NODES"
|
|
277
|
+
and isinstance(modifier, NodesModifier)
|
|
278
|
+
and (modifier_node_group := modifier.node_group)
|
|
279
|
+
and modifier_node_group.name
|
|
280
|
+
== shader.OUTLINE_GEOMETRY_GROUP_NAME
|
|
281
|
+
)
|
|
282
|
+
for modifier in obj.modifiers
|
|
283
|
+
)
|
|
284
|
+
):
|
|
285
|
+
state.skippable_warning_messages.append(
|
|
286
|
+
pgettext(
|
|
287
|
+
'The "{name}" mesh has both a non-armature modifier'
|
|
288
|
+
+ " and a shape key. However, they cannot coexist"
|
|
289
|
+
+ ", so shape keys may not be exported correctly."
|
|
290
|
+
).format(name=obj.name)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if (
|
|
294
|
+
armature
|
|
295
|
+
and armature_data
|
|
296
|
+
and obj.type == "ARMATURE"
|
|
297
|
+
and obj.name == armature.name
|
|
298
|
+
):
|
|
299
|
+
WM_OT_vrm_validator.validate_armature_object(
|
|
300
|
+
context,
|
|
301
|
+
export_objects,
|
|
302
|
+
armature,
|
|
303
|
+
armature_data,
|
|
304
|
+
is_vrm0=is_vrm0,
|
|
305
|
+
is_vrm1=is_vrm1,
|
|
306
|
+
execute_migration=execute_migration,
|
|
307
|
+
state=state,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if obj.type == "MESH":
|
|
311
|
+
mesh_data = obj.data
|
|
312
|
+
if not isinstance(mesh_data, Mesh):
|
|
313
|
+
_logger.error("%s is not a Mesh", type(mesh_data))
|
|
314
|
+
continue
|
|
315
|
+
for poly in mesh_data.polygons:
|
|
316
|
+
if poly.loop_total > 3: # polygons need all triangle
|
|
317
|
+
state.info_messages.append(
|
|
318
|
+
pgettext(
|
|
319
|
+
'Non-triangular faces detected in "{name}". '
|
|
320
|
+
+ "They will be triangulated automatically.",
|
|
321
|
+
).format(name=obj.name)
|
|
322
|
+
)
|
|
323
|
+
break
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def validate_armature_object(
|
|
327
|
+
context: Context,
|
|
328
|
+
export_objects: list[Object],
|
|
329
|
+
armature: Object,
|
|
330
|
+
armature_data: Armature,
|
|
331
|
+
*,
|
|
332
|
+
is_vrm0: bool,
|
|
333
|
+
is_vrm1: bool,
|
|
334
|
+
execute_migration: bool,
|
|
335
|
+
state: ValidationState,
|
|
336
|
+
) -> None:
|
|
337
|
+
armature_extension = get_armature_extension(armature_data)
|
|
338
|
+
if is_vrm0:
|
|
339
|
+
humanoid = armature_extension.vrm0.humanoid
|
|
340
|
+
state.error_messages.extend(
|
|
341
|
+
[
|
|
342
|
+
message
|
|
343
|
+
for _, message in (humanoid.human_bone_duplication_error_messages())
|
|
344
|
+
]
|
|
345
|
+
)
|
|
346
|
+
elif is_vrm1:
|
|
347
|
+
human_bones = armature_extension.vrm1.humanoid.human_bones
|
|
348
|
+
if not human_bones.allow_non_humanoid_rig:
|
|
349
|
+
state.error_messages.extend(
|
|
350
|
+
[
|
|
351
|
+
message
|
|
352
|
+
for _, message in (
|
|
353
|
+
human_bones.human_bone_duplication_error_messages()
|
|
354
|
+
)
|
|
355
|
+
]
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if execute_migration:
|
|
359
|
+
migration.migrate(context, armature.name, heavy_migration=True)
|
|
360
|
+
|
|
361
|
+
for bone in armature_data.bones:
|
|
362
|
+
if is_vrm0 and bone.name in state.node_names: # nodes name is unique
|
|
363
|
+
state.error_messages.append(
|
|
364
|
+
pgettext(
|
|
365
|
+
"The same name cannot be used"
|
|
366
|
+
+ " for a mesh object and a bone."
|
|
367
|
+
+ ' Rename either one whose name is "{name}".'
|
|
368
|
+
).format(name=bone.name)
|
|
369
|
+
)
|
|
370
|
+
if bone.name not in state.node_names:
|
|
371
|
+
state.node_names.append(bone.name)
|
|
372
|
+
|
|
373
|
+
all_required_bones_exist = True
|
|
374
|
+
if is_vrm1:
|
|
375
|
+
all_required_bones_exist = WM_OT_vrm_validator.validate_vrm1_humanoid(
|
|
376
|
+
export_objects,
|
|
377
|
+
armature,
|
|
378
|
+
armature_extension,
|
|
379
|
+
armature_data,
|
|
380
|
+
state,
|
|
381
|
+
)
|
|
382
|
+
elif is_vrm0:
|
|
383
|
+
all_required_bones_exist = WM_OT_vrm_validator.validate_vrm0_humanoid(
|
|
384
|
+
armature_extension,
|
|
385
|
+
armature_data,
|
|
386
|
+
state,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if all_required_bones_exist:
|
|
390
|
+
WM_OT_vrm_validator.validate_bone_order(
|
|
391
|
+
context, state.error_messages, armature
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def validate_vrm1_humanoid(
|
|
396
|
+
export_objects: list[Object],
|
|
397
|
+
armature: Object,
|
|
398
|
+
armature_extension: VrmAddonArmatureExtensionPropertyGroup,
|
|
399
|
+
armature_data: Armature,
|
|
400
|
+
state: ValidationState,
|
|
401
|
+
) -> bool:
|
|
402
|
+
_, _, constraint_warning_messages = search.export_constraints(
|
|
403
|
+
export_objects, armature
|
|
404
|
+
)
|
|
405
|
+
state.skippable_warning_messages.extend(constraint_warning_messages)
|
|
406
|
+
|
|
407
|
+
humanoid = armature_extension.vrm1.humanoid
|
|
408
|
+
if humanoid.pose == humanoid.POSE_AUTO_POSE.identifier:
|
|
409
|
+
state.info_messages.append(
|
|
410
|
+
pgettext(
|
|
411
|
+
"Automatic T-Pose Conversion is enabled."
|
|
412
|
+
+ " There is a setting"
|
|
413
|
+
+ ' in "VRM" panel → "Humanoid" → "T-Pose".'
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
if humanoid.pose == humanoid.POSE_CUSTOM_POSE.identifier and not (
|
|
417
|
+
humanoid.pose_library and humanoid.pose_library.name in bpy.data.actions
|
|
418
|
+
):
|
|
419
|
+
state.skippable_warning_messages.append(
|
|
420
|
+
pgettext(
|
|
421
|
+
"The T-Pose is set to a custom pose, but the custom pose action"
|
|
422
|
+
+ " is not set or no longer exists. The armature rest position"
|
|
423
|
+
+ " will be used."
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
human_bones = humanoid.human_bones
|
|
428
|
+
all_required_bones_exist = True
|
|
429
|
+
human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
|
|
430
|
+
for (
|
|
431
|
+
human_bone_name,
|
|
432
|
+
human_bone,
|
|
433
|
+
) in human_bone_name_to_human_bone.items():
|
|
434
|
+
human_bone_specification = vrm1_human_bone.HumanBoneSpecifications.get(
|
|
435
|
+
human_bone_name
|
|
436
|
+
)
|
|
437
|
+
if not human_bone_specification.requirement:
|
|
438
|
+
continue
|
|
439
|
+
if (
|
|
440
|
+
human_bone.node
|
|
441
|
+
and human_bone.node.bone_name
|
|
442
|
+
and human_bone.node.bone_name in armature_data.bones
|
|
443
|
+
):
|
|
444
|
+
continue
|
|
445
|
+
all_required_bones_exist = False
|
|
446
|
+
if not human_bones.allow_non_humanoid_rig:
|
|
447
|
+
state.error_messages.append(
|
|
448
|
+
pgettext(
|
|
449
|
+
'Required VRM Human Bone "{humanoid_name}" is'
|
|
450
|
+
+ " not assigned. Please confirm hierarchy"
|
|
451
|
+
+ " of {humanoid_name} and its children. "
|
|
452
|
+
+ '"VRM" Panel → "Humanoid" → {humanoid_name}'
|
|
453
|
+
+ " will be empty or displayed in red"
|
|
454
|
+
+ " if hierarchy is wrong."
|
|
455
|
+
).format(humanoid_name=human_bone_specification.title)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
if all_required_bones_exist:
|
|
459
|
+
# https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0-beta/humanoid.md#list-of-humanoid-bones
|
|
460
|
+
for (
|
|
461
|
+
human_bone_specification
|
|
462
|
+
) in vrm1_human_bone.HumanBoneSpecifications.all_human_bones:
|
|
463
|
+
if not human_bone_specification.parent_requirement:
|
|
464
|
+
continue
|
|
465
|
+
parent = human_bone_specification.parent
|
|
466
|
+
if parent is None:
|
|
467
|
+
message = (
|
|
468
|
+
f"Fatal: {human_bone_specification.name} has" + " no parent"
|
|
469
|
+
)
|
|
470
|
+
raise ValueError(message)
|
|
471
|
+
child_human_bone = human_bone_name_to_human_bone[
|
|
472
|
+
human_bone_specification.name
|
|
473
|
+
]
|
|
474
|
+
parent_human_bone = human_bone_name_to_human_bone[parent.name]
|
|
475
|
+
if (
|
|
476
|
+
child_human_bone.node.bone_name
|
|
477
|
+
and not parent_human_bone.node.bone_name
|
|
478
|
+
):
|
|
479
|
+
state.error_messages.append(
|
|
480
|
+
pgettext(
|
|
481
|
+
'VRM Human Bone "{child}" needs "{parent}".'
|
|
482
|
+
+ " Please confirm"
|
|
483
|
+
+ ' "VRM" Panel → "Humanoid"'
|
|
484
|
+
+ ' → "Optional VRM Human Bones"'
|
|
485
|
+
+ ' → "{parent}".'
|
|
486
|
+
).format(
|
|
487
|
+
child=human_bone_specification.title,
|
|
488
|
+
parent=parent.title,
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
if human_bones.allow_non_humanoid_rig:
|
|
493
|
+
state.skippable_warning_messages.append(
|
|
494
|
+
pgettext(
|
|
495
|
+
"This armature will be exported but not as a humanoid."
|
|
496
|
+
+ " It cannot have animations applied"
|
|
497
|
+
+ " for humanoid avatars."
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
return all_required_bones_exist
|
|
501
|
+
|
|
502
|
+
@staticmethod
|
|
503
|
+
def validate_vrm0_humanoid(
|
|
504
|
+
armature_extension: VrmAddonArmatureExtensionPropertyGroup,
|
|
505
|
+
armature_data: Armature,
|
|
506
|
+
state: ValidationState,
|
|
507
|
+
) -> bool:
|
|
508
|
+
humanoid = armature_extension.vrm0.humanoid
|
|
509
|
+
if humanoid.pose == humanoid.POSE_AUTO_POSE.identifier:
|
|
510
|
+
state.info_messages.append(
|
|
511
|
+
pgettext(
|
|
512
|
+
"Automatic T-Pose Conversion is enabled."
|
|
513
|
+
+ " There is a setting"
|
|
514
|
+
+ ' in "VRM" panel → "VRM 0.x Humanoid" → "T-Pose".'
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
if humanoid.pose == humanoid.POSE_CUSTOM_POSE.identifier and not (
|
|
518
|
+
humanoid.pose_library and humanoid.pose_library.name in bpy.data.actions
|
|
519
|
+
):
|
|
520
|
+
state.skippable_warning_messages.append(
|
|
521
|
+
pgettext(
|
|
522
|
+
"The T-Pose is set to a custom pose, but the custom pose action"
|
|
523
|
+
+ " is not set or no longer exists. The armature rest position"
|
|
524
|
+
+ " will be used."
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
human_bones = humanoid.human_bones
|
|
529
|
+
all_required_bones_exist = True
|
|
530
|
+
for humanoid_name in vrm0_human_bone.HumanBoneSpecifications.required_names:
|
|
531
|
+
if any(
|
|
532
|
+
human_bone.bone == humanoid_name
|
|
533
|
+
and human_bone.node
|
|
534
|
+
and human_bone.node.bone_name
|
|
535
|
+
and human_bone.node.bone_name in armature_data.bones
|
|
536
|
+
for human_bone in human_bones
|
|
537
|
+
):
|
|
538
|
+
continue
|
|
539
|
+
all_required_bones_exist = False
|
|
540
|
+
state.error_messages.append(
|
|
541
|
+
pgettext(
|
|
542
|
+
'Required VRM Human Bone "{humanoid_name}" is'
|
|
543
|
+
+ " not assigned. Please confirm hierarchy"
|
|
544
|
+
+ " of {humanoid_name} and its children."
|
|
545
|
+
+ ' "VRM" Panel → "VRM 0.x Humanoid" → {humanoid_name}'
|
|
546
|
+
+ " will be empty or displayed in red"
|
|
547
|
+
+ " if hierarchy is wrong."
|
|
548
|
+
).format(humanoid_name=humanoid_name.capitalize())
|
|
549
|
+
)
|
|
550
|
+
return all_required_bones_exist
|
|
551
|
+
|
|
552
|
+
@staticmethod
|
|
553
|
+
def validate_vrm1_additional(
|
|
554
|
+
armature_data: Armature,
|
|
555
|
+
export_objects: list[Object],
|
|
556
|
+
state: ValidationState,
|
|
557
|
+
) -> None:
|
|
558
|
+
# meta URL validation
|
|
559
|
+
armature_extension = get_armature_extension(armature_data)
|
|
560
|
+
if armature_extension.vrm1.look_at.enable_preview:
|
|
561
|
+
state.info_messages.append(
|
|
562
|
+
pgettext(
|
|
563
|
+
"The Look At preview is enabled. It will be disabled during export."
|
|
564
|
+
)
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
meta1 = armature_extension.vrm1.meta
|
|
568
|
+
if not is_valid_url(meta1.other_license_url, allow_empty_str=True):
|
|
569
|
+
state.skippable_warning_messages.append(
|
|
570
|
+
pgettext('"{url}" is not a valid URL.').format(
|
|
571
|
+
url=meta1.other_license_url
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
state.error_messages.extend(
|
|
576
|
+
pgettext(
|
|
577
|
+
'Object "{name}" contains a negative value for the scale;'
|
|
578
|
+
+ " VRM 1.0 does not allow negative values to be specified"
|
|
579
|
+
+ " for the scale."
|
|
580
|
+
).format(name=obj.name)
|
|
581
|
+
for obj in export_objects
|
|
582
|
+
if obj.scale[0] < 0 or obj.scale[1] < 0 or obj.scale[2] < 0
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
joint_chain_bone_names_to_spring_vrm_name: dict[str, str] = {}
|
|
586
|
+
for spring in get_armature_extension(armature_data).spring_bone1.springs:
|
|
587
|
+
joint_bone_names: list[str] = []
|
|
588
|
+
for joint in spring.joints:
|
|
589
|
+
bone_name = joint.node.bone_name
|
|
590
|
+
if bone_name and bone_name not in joint_bone_names:
|
|
591
|
+
joint_bone_names.append(bone_name)
|
|
592
|
+
|
|
593
|
+
joint_chain_bone_names: list[str] = []
|
|
594
|
+
for bone_name in joint_bone_names:
|
|
595
|
+
search_joint_chain_bone_names: list[str] = []
|
|
596
|
+
bone: Optional[Bone] = armature_data.bones.get(bone_name)
|
|
597
|
+
terminated = False
|
|
598
|
+
while bone:
|
|
599
|
+
if bone.name != bone_name and bone.name in joint_bone_names:
|
|
600
|
+
terminated = True
|
|
601
|
+
break
|
|
602
|
+
search_joint_chain_bone_names.append(bone.name)
|
|
603
|
+
bone = bone.parent
|
|
604
|
+
if not terminated:
|
|
605
|
+
if bone_name not in joint_chain_bone_names:
|
|
606
|
+
joint_chain_bone_names.append(bone_name)
|
|
607
|
+
continue
|
|
608
|
+
joint_chain_bone_names.extend(search_joint_chain_bone_names)
|
|
609
|
+
|
|
610
|
+
for joint_chain_bone_name in joint_chain_bone_names:
|
|
611
|
+
spring_vrm_name = joint_chain_bone_names_to_spring_vrm_name.get(
|
|
612
|
+
joint_chain_bone_name
|
|
613
|
+
)
|
|
614
|
+
if not spring_vrm_name:
|
|
615
|
+
joint_chain_bone_names_to_spring_vrm_name[joint_chain_bone_name] = (
|
|
616
|
+
spring.vrm_name
|
|
617
|
+
)
|
|
618
|
+
continue
|
|
619
|
+
state.skippable_warning_messages.append(
|
|
620
|
+
pgettext(
|
|
621
|
+
'Spring "{spring_name1}" and "{spring_name2}" have'
|
|
622
|
+
+ ' common bone "{bone_name}".'
|
|
623
|
+
).format(
|
|
624
|
+
spring_name1=spring_vrm_name,
|
|
625
|
+
spring_name2=spring.vrm_name,
|
|
626
|
+
bone_name=joint_chain_bone_name,
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
@staticmethod
|
|
631
|
+
def validate_vertex_weights(
|
|
632
|
+
export_objects: list[Object],
|
|
633
|
+
armature: Optional[Object],
|
|
634
|
+
armature_data: Optional[Armature],
|
|
635
|
+
*,
|
|
636
|
+
is_vrm1: bool,
|
|
637
|
+
state: ValidationState,
|
|
638
|
+
) -> None:
|
|
639
|
+
bones_names = (
|
|
640
|
+
{b.name for b in armature_data.bones} if armature_data else set[str]()
|
|
641
|
+
)
|
|
642
|
+
vertex_error_count = 0
|
|
643
|
+
|
|
644
|
+
for mesh in (obj for obj in export_objects if obj.type == "MESH"):
|
|
645
|
+
if not isinstance(mesh_data := mesh.data, Mesh):
|
|
646
|
+
continue
|
|
647
|
+
|
|
648
|
+
mesh_vertex_group_names = tuple(g.name for g in mesh.vertex_groups)
|
|
649
|
+
|
|
650
|
+
for v in mesh_data.vertices:
|
|
651
|
+
if not v.groups and mesh.parent_bone == "":
|
|
652
|
+
if vertex_error_count > 5:
|
|
653
|
+
continue
|
|
654
|
+
if is_vrm1:
|
|
655
|
+
continue
|
|
656
|
+
state.info_messages.append(
|
|
657
|
+
pgettext(
|
|
658
|
+
'vertex index "{vertex_index}" is no weight'
|
|
659
|
+
+ ' in "{mesh_name}".'
|
|
660
|
+
+ " Add weight to parent bone automatically."
|
|
661
|
+
).format(vertex_index=v.index, mesh_name=mesh.name)
|
|
662
|
+
)
|
|
663
|
+
vertex_error_count += 1
|
|
664
|
+
elif len(v.groups) >= 5 and armature is not None:
|
|
665
|
+
weight_count = 0
|
|
666
|
+
for g in v.groups:
|
|
667
|
+
if (
|
|
668
|
+
0 <= g.group < len(mesh_vertex_group_names)
|
|
669
|
+
and mesh_vertex_group_names[g.group] in bones_names
|
|
670
|
+
and g.weight < float_info.epsilon
|
|
671
|
+
):
|
|
672
|
+
weight_count += 1
|
|
673
|
+
if weight_count > 4 and vertex_error_count < 5:
|
|
674
|
+
state.info_messages.append(
|
|
675
|
+
pgettext(
|
|
676
|
+
'vertex index "{vertex_index}" has'
|
|
677
|
+
+ ' too many (over 4) weight in "{mesh_name}".'
|
|
678
|
+
+ " It will be truncated to 4 descending"
|
|
679
|
+
+ " order by its weight."
|
|
680
|
+
).format(vertex_index=v.index, mesh_name=mesh.name)
|
|
681
|
+
)
|
|
682
|
+
vertex_error_count += 1
|
|
683
|
+
|
|
684
|
+
@staticmethod
|
|
685
|
+
def validate_materials(
|
|
686
|
+
_context: Context,
|
|
687
|
+
_export_objects: list[Object],
|
|
688
|
+
*,
|
|
689
|
+
is_vrm0: bool,
|
|
690
|
+
state: ValidationState,
|
|
691
|
+
) -> None:
|
|
692
|
+
for material in state.used_materials:
|
|
693
|
+
if (
|
|
694
|
+
not (mat_node_tree := material.node_tree)
|
|
695
|
+
or get_material_extension(material).mtoon1.enabled
|
|
696
|
+
or shader.MmdMaterial.try_parse(material)
|
|
697
|
+
):
|
|
698
|
+
continue
|
|
699
|
+
for node in mat_node_tree.nodes:
|
|
700
|
+
if node.type != "OUTPUT_MATERIAL":
|
|
701
|
+
continue
|
|
702
|
+
|
|
703
|
+
links = node.inputs["Surface"].links
|
|
704
|
+
if links and links[0].from_node.type == "BSDF_PRINCIPLED":
|
|
705
|
+
continue
|
|
706
|
+
|
|
707
|
+
if links:
|
|
708
|
+
from_node = links[0].from_node
|
|
709
|
+
if from_node.type == "GROUP" and isinstance(
|
|
710
|
+
from_node, ShaderNodeGroup
|
|
711
|
+
):
|
|
712
|
+
node_tree = from_node.node_tree
|
|
713
|
+
if (
|
|
714
|
+
node_tree
|
|
715
|
+
and node_tree.get("SHADER") in shader.LEGACY_SHADER_NAMES
|
|
716
|
+
):
|
|
717
|
+
continue
|
|
718
|
+
|
|
719
|
+
state.skippable_warning_messages.append(
|
|
720
|
+
pgettext(
|
|
721
|
+
'"{material_name}" needs to enable'
|
|
722
|
+
+ ' "VRM MToon Material" or connect'
|
|
723
|
+
+ " Principled BSDF/MToon_unversioned/TRANSPARENT_ZWRITE"
|
|
724
|
+
+ ' to "Surface" directly. Empty material will be exported.'
|
|
725
|
+
).format(material_name=material.name)
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
for material in state.used_materials:
|
|
729
|
+
legacy_addon_material = LegacyAddonMaterial.try_parse(material)
|
|
730
|
+
if not legacy_addon_material:
|
|
731
|
+
continue
|
|
732
|
+
# MToon
|
|
733
|
+
if legacy_addon_material.shader_name == "MToon_unversioned":
|
|
734
|
+
for texture_val in MtoonUnversioned.texture_kind_exchange_dict.values():
|
|
735
|
+
suffix = "_alpha" if texture_val == "ReceiveShadow_Texture" else ""
|
|
736
|
+
_node_material_input_check(
|
|
737
|
+
legacy_addon_material.shader_node_group,
|
|
738
|
+
material,
|
|
739
|
+
"TEX_IMAGE",
|
|
740
|
+
texture_val + suffix,
|
|
741
|
+
state.error_messages,
|
|
742
|
+
state.used_images,
|
|
743
|
+
)
|
|
744
|
+
for float_val in MtoonUnversioned.float_props_exchange_dict.values():
|
|
745
|
+
if float_val is None:
|
|
746
|
+
continue
|
|
747
|
+
_node_material_input_check(
|
|
748
|
+
legacy_addon_material.shader_node_group,
|
|
749
|
+
material,
|
|
750
|
+
"VALUE",
|
|
751
|
+
float_val,
|
|
752
|
+
state.error_messages,
|
|
753
|
+
state.used_images,
|
|
754
|
+
)
|
|
755
|
+
for k in ("_Color", "_ShadeColor", "_EmissionColor", "_OutlineColor"):
|
|
756
|
+
_node_material_input_check(
|
|
757
|
+
legacy_addon_material.shader_node_group,
|
|
758
|
+
material,
|
|
759
|
+
"RGB",
|
|
760
|
+
MtoonUnversioned.vector_props_exchange_dict[k],
|
|
761
|
+
state.error_messages,
|
|
762
|
+
state.used_images,
|
|
763
|
+
)
|
|
764
|
+
# GLTF
|
|
765
|
+
elif legacy_addon_material.shader_name == "GLTF":
|
|
766
|
+
for k in TEXTURE_INPUT_NAMES:
|
|
767
|
+
_node_material_input_check(
|
|
768
|
+
legacy_addon_material.shader_node_group,
|
|
769
|
+
material,
|
|
770
|
+
"TEX_IMAGE",
|
|
771
|
+
k,
|
|
772
|
+
state.error_messages,
|
|
773
|
+
state.used_images,
|
|
774
|
+
)
|
|
775
|
+
for k in VAL_INPUT_NAMES:
|
|
776
|
+
_node_material_input_check(
|
|
777
|
+
legacy_addon_material.shader_node_group,
|
|
778
|
+
material,
|
|
779
|
+
"VALUE",
|
|
780
|
+
k,
|
|
781
|
+
state.error_messages,
|
|
782
|
+
state.used_images,
|
|
783
|
+
)
|
|
784
|
+
for k in RGBA_INPUT_NAMES:
|
|
785
|
+
_node_material_input_check(
|
|
786
|
+
legacy_addon_material.shader_node_group,
|
|
787
|
+
material,
|
|
788
|
+
"RGB",
|
|
789
|
+
k,
|
|
790
|
+
state.error_messages,
|
|
791
|
+
state.used_images,
|
|
792
|
+
)
|
|
793
|
+
# Transparent_Zwrite
|
|
794
|
+
elif legacy_addon_material.shader_name == "TRANSPARENT_ZWRITE":
|
|
795
|
+
_node_material_input_check(
|
|
796
|
+
legacy_addon_material.shader_node_group,
|
|
797
|
+
material,
|
|
798
|
+
"TEX_IMAGE",
|
|
799
|
+
"Main_Texture",
|
|
800
|
+
state.error_messages,
|
|
801
|
+
state.used_images,
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
for material in state.used_materials:
|
|
805
|
+
gltf = get_material_extension(material).mtoon1
|
|
806
|
+
if not gltf.enabled:
|
|
807
|
+
continue
|
|
808
|
+
|
|
809
|
+
for texture in gltf.all_textures(downgrade_to_mtoon0=is_vrm0):
|
|
810
|
+
source = texture.get_connected_node_image()
|
|
811
|
+
if not source:
|
|
812
|
+
continue
|
|
813
|
+
if source not in state.used_images:
|
|
814
|
+
state.used_images.append(source)
|
|
815
|
+
if source.colorspace_settings.name == texture.colorspace:
|
|
816
|
+
continue
|
|
817
|
+
state.skippable_warning_messages.append(
|
|
818
|
+
pgettext(
|
|
819
|
+
'It is recommended to set "{colorspace}"'
|
|
820
|
+
+ ' to "{input_colorspace}" for "{texture_label}"'
|
|
821
|
+
+ ' in Material "{name}".'
|
|
822
|
+
).format(
|
|
823
|
+
name=material.name,
|
|
824
|
+
texture_label=source.name,
|
|
825
|
+
colorspace=pgettext(texture.colorspace),
|
|
826
|
+
input_colorspace=pgettext("Input Color Space"),
|
|
827
|
+
)
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
for texture_info in gltf.all_texture_info():
|
|
831
|
+
source = texture_info.index.get_connected_node_image()
|
|
832
|
+
if source and source not in state.used_images:
|
|
833
|
+
state.used_images.append(source)
|
|
834
|
+
|
|
835
|
+
if not is_vrm0 or not source:
|
|
836
|
+
continue
|
|
837
|
+
|
|
838
|
+
base_color_texture = gltf.pbr_metallic_roughness.base_color_texture
|
|
839
|
+
base_extensions = base_color_texture.extensions
|
|
840
|
+
base_scale = base_extensions.khr_texture_transform.scale
|
|
841
|
+
base_offset = base_extensions.khr_texture_transform.offset
|
|
842
|
+
scale = texture_info.extensions.khr_texture_transform.scale
|
|
843
|
+
offset = texture_info.extensions.khr_texture_transform.offset
|
|
844
|
+
if texture_info == gltf.extensions.vrmc_materials_mtoon.matcap_texture:
|
|
845
|
+
if (
|
|
846
|
+
abs(scale[0] - 1) > 0
|
|
847
|
+
or abs(scale[1] - 1) > 0
|
|
848
|
+
or abs(offset[0]) > 0
|
|
849
|
+
or abs(offset[1]) > 0
|
|
850
|
+
):
|
|
851
|
+
state.skippable_warning_messages.append(
|
|
852
|
+
pgettext(
|
|
853
|
+
'Material "{name}" {texture}\'s Offset and Scale are'
|
|
854
|
+
+ " ignored in VRM 0.0."
|
|
855
|
+
).format(
|
|
856
|
+
name=material.name,
|
|
857
|
+
texture=texture_info.index.label,
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
elif (
|
|
861
|
+
abs(base_scale[0] - scale[0]) > 0
|
|
862
|
+
or abs(base_scale[1] - scale[1]) > 0
|
|
863
|
+
or abs(base_offset[0] - offset[0]) > 0
|
|
864
|
+
or abs(base_offset[1] - offset[1]) > 0
|
|
865
|
+
):
|
|
866
|
+
state.skippable_warning_messages.append(
|
|
867
|
+
pgettext(
|
|
868
|
+
'Material "{name}" {texture}\'s Offset and Scale'
|
|
869
|
+
+ " in VRM 0.0 are the values of"
|
|
870
|
+
+ " the Lit Color Texture."
|
|
871
|
+
).format(
|
|
872
|
+
name=material.name,
|
|
873
|
+
texture=texture_info.index.label,
|
|
874
|
+
)
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
@staticmethod
|
|
878
|
+
def validate_vrm0_additional(
|
|
879
|
+
context: Context,
|
|
880
|
+
armature_data: Armature,
|
|
881
|
+
export_objects: list[Object],
|
|
882
|
+
state: ValidationState,
|
|
883
|
+
) -> None:
|
|
884
|
+
# first_person
|
|
885
|
+
first_person = get_armature_extension(armature_data).vrm0.first_person
|
|
886
|
+
if not first_person.first_person_bone.bone_name:
|
|
887
|
+
state.info_messages.append(
|
|
888
|
+
pgettext(
|
|
889
|
+
"firstPersonBone was not found. "
|
|
890
|
+
+ 'VRM HumanBone "head" will be used automatically instead.'
|
|
891
|
+
)
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
# meta URL validation
|
|
895
|
+
meta0 = get_armature_extension(armature_data).vrm0.meta
|
|
896
|
+
state.skippable_warning_messages.extend(
|
|
897
|
+
pgettext('"{url}" is not a valid URL.').format(url=url_value)
|
|
898
|
+
for url_value in (
|
|
899
|
+
meta0.other_permission_url,
|
|
900
|
+
meta0.other_license_url,
|
|
901
|
+
)
|
|
902
|
+
if not is_valid_url(url_value, allow_empty_str=True)
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
# blend_shape_master
|
|
906
|
+
blend_shape_master = get_armature_extension(
|
|
907
|
+
armature_data
|
|
908
|
+
).vrm0.blend_shape_master
|
|
909
|
+
for blend_shape_group in blend_shape_master.blend_shape_groups:
|
|
910
|
+
for material_value in blend_shape_group.material_values:
|
|
911
|
+
if not material_value.material:
|
|
912
|
+
state.info_messages.append(
|
|
913
|
+
pgettext(
|
|
914
|
+
"A material value bind exists in a blend shape group"
|
|
915
|
+
+ ' named "{blend_shape_group_name}" but'
|
|
916
|
+
+ " no material is set."
|
|
917
|
+
).format(blend_shape_group_name=blend_shape_group.name)
|
|
918
|
+
)
|
|
919
|
+
continue
|
|
920
|
+
|
|
921
|
+
if material_value.material not in state.used_materials:
|
|
922
|
+
state.info_messages.append(
|
|
923
|
+
pgettext(
|
|
924
|
+
'A material named "{material_name}" is assigned to a'
|
|
925
|
+
+ ' blend shape group named "{blend_shape_group_name}"'
|
|
926
|
+
+ " but the material will not be exported."
|
|
927
|
+
).format(
|
|
928
|
+
blend_shape_group_name=blend_shape_group.name,
|
|
929
|
+
material_name=material_value.material.name,
|
|
930
|
+
)
|
|
931
|
+
)
|
|
932
|
+
continue
|
|
933
|
+
|
|
934
|
+
if not material_value.property_name:
|
|
935
|
+
state.info_messages.append(
|
|
936
|
+
pgettext(
|
|
937
|
+
'A material named "{material_name}" is assigned to a'
|
|
938
|
+
+ ' blend shape group named "{blend_shape_group_name}"'
|
|
939
|
+
+ " but the property name is empty."
|
|
940
|
+
).format(
|
|
941
|
+
blend_shape_group_name=blend_shape_group.name,
|
|
942
|
+
material_name=material_value.material.name,
|
|
943
|
+
)
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
for bind in blend_shape_group.binds:
|
|
947
|
+
mesh_object_name = bind.mesh.mesh_object_name
|
|
948
|
+
if not bind.index or not mesh_object_name:
|
|
949
|
+
continue
|
|
950
|
+
mesh_object = next(
|
|
951
|
+
iter(obj for obj in export_objects if obj.name == mesh_object_name),
|
|
952
|
+
None,
|
|
953
|
+
)
|
|
954
|
+
if not mesh_object and mesh_object_name in context.blend_data.objects:
|
|
955
|
+
state.info_messages.append(
|
|
956
|
+
pgettext(
|
|
957
|
+
'A mesh named "{mesh_name}" is assigned to a blend'
|
|
958
|
+
+ ' shape group named "{blend_shape_group_name}" but'
|
|
959
|
+
+ " the mesh will not be exported."
|
|
960
|
+
).format(
|
|
961
|
+
blend_shape_group_name=blend_shape_group.name,
|
|
962
|
+
mesh_name=mesh_object_name,
|
|
963
|
+
)
|
|
964
|
+
)
|
|
965
|
+
if not mesh_object:
|
|
966
|
+
continue
|
|
967
|
+
mesh_data = mesh_object.data
|
|
968
|
+
if not isinstance(mesh_data, Mesh):
|
|
969
|
+
continue
|
|
970
|
+
shape_keys = mesh_data.shape_keys
|
|
971
|
+
if not shape_keys or bind.index not in shape_keys.key_blocks:
|
|
972
|
+
state.info_messages.append(
|
|
973
|
+
pgettext(
|
|
974
|
+
'A shape key named "{shape_key_name}" in a mesh'
|
|
975
|
+
+ ' named "{mesh_name}" is assigned to a blend shape'
|
|
976
|
+
+ ' group named "{blend_shape_group_name}" but the'
|
|
977
|
+
+ " shape key doesn't exist."
|
|
978
|
+
).format(
|
|
979
|
+
mesh_name=mesh_object.name,
|
|
980
|
+
shape_key_name=bind.index,
|
|
981
|
+
blend_shape_group_name=blend_shape_group.name,
|
|
982
|
+
)
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
@staticmethod
|
|
986
|
+
def append_image_path_errors(state: ValidationState) -> None:
|
|
987
|
+
state.error_messages.extend(
|
|
988
|
+
pgettext(
|
|
989
|
+
'"{image_name}" was not found at file path "{image_filepath}". '
|
|
990
|
+
+ "Please load the file in Blender."
|
|
991
|
+
).format(image_name=image.name, image_filepath=image.filepath_from_user())
|
|
992
|
+
for image in state.used_images
|
|
993
|
+
if (
|
|
994
|
+
image.source == "FILE"
|
|
995
|
+
and not image.is_dirty
|
|
996
|
+
and image.packed_file is None
|
|
997
|
+
and not Path(image.filepath_from_user()).exists()
|
|
998
|
+
)
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
@staticmethod
|
|
1002
|
+
def set_error_collection(
|
|
1003
|
+
error_collection: Optional[CollectionPropertyProtocol[VrmValidationError]],
|
|
1004
|
+
state: ValidationState,
|
|
1005
|
+
) -> None:
|
|
1006
|
+
if error_collection is None:
|
|
1007
|
+
return
|
|
1008
|
+
|
|
1009
|
+
error_collection.clear()
|
|
1010
|
+
|
|
1011
|
+
for message in state.error_messages:
|
|
1012
|
+
error = error_collection.add()
|
|
1013
|
+
error.name = f"VrmModelError{len(error_collection)}"
|
|
1014
|
+
error.severity = 0
|
|
1015
|
+
error.message = message
|
|
1016
|
+
|
|
1017
|
+
for message in state.warning_messages:
|
|
1018
|
+
error = error_collection.add()
|
|
1019
|
+
error.name = f"VrmModelError{len(error_collection)}"
|
|
1020
|
+
error.severity = 1
|
|
1021
|
+
error.message = message
|
|
1022
|
+
|
|
1023
|
+
for message in state.skippable_warning_messages:
|
|
1024
|
+
error = error_collection.add()
|
|
1025
|
+
error.name = f"VrmModelError{len(error_collection)}"
|
|
1026
|
+
error.severity = 2
|
|
1027
|
+
error.message = message
|
|
1028
|
+
|
|
1029
|
+
for message in state.info_messages:
|
|
1030
|
+
error = error_collection.add()
|
|
1031
|
+
error.name = f"VrmModelError{len(error_collection)}"
|
|
1032
|
+
error.severity = 3
|
|
1033
|
+
error.message = message
|
|
1034
|
+
|
|
1035
|
+
@staticmethod
|
|
1036
|
+
def detect_errors(
|
|
1037
|
+
context: Context,
|
|
1038
|
+
error_collection: Optional[CollectionPropertyProtocol[VrmValidationError]],
|
|
1039
|
+
armature_object_name: str,
|
|
1040
|
+
*,
|
|
1041
|
+
execute_migration: bool = False,
|
|
1042
|
+
) -> bool:
|
|
1043
|
+
state = ValidationState()
|
|
1044
|
+
|
|
1045
|
+
# export object seeking
|
|
1046
|
+
preferences = get_preferences(context)
|
|
1047
|
+
|
|
1048
|
+
version_warning_message = version.validation_warning_message()
|
|
1049
|
+
if version_warning_message is not None:
|
|
1050
|
+
state.warning_messages.append(version_warning_message)
|
|
1051
|
+
|
|
1052
|
+
export_objects = search.export_objects(
|
|
1053
|
+
context,
|
|
1054
|
+
armature_object_name,
|
|
1055
|
+
export_invisibles=preferences.export_invisibles,
|
|
1056
|
+
export_only_selections=preferences.export_only_selections,
|
|
1057
|
+
export_lights=preferences.export_lights,
|
|
1058
|
+
)
|
|
1059
|
+
state.used_materials.extend(search.export_materials(context, export_objects))
|
|
1060
|
+
|
|
1061
|
+
if not any(
|
|
1062
|
+
obj.type in search.MESH_CONVERTIBLE_OBJECT_TYPES for obj in export_objects
|
|
1063
|
+
):
|
|
1064
|
+
if preferences.export_only_selections:
|
|
1065
|
+
state.warning_messages.append(
|
|
1066
|
+
pgettext(
|
|
1067
|
+
'"{export_only_selections}" is enabled'
|
|
1068
|
+
+ ", but no mesh is selected."
|
|
1069
|
+
).format(export_only_selections=pgettext("Export Only Selections"))
|
|
1070
|
+
)
|
|
1071
|
+
else:
|
|
1072
|
+
state.warning_messages.append(pgettext("There is no mesh to export."))
|
|
1073
|
+
|
|
1074
|
+
armature = context.blend_data.objects.get(armature_object_name)
|
|
1075
|
+
if not armature:
|
|
1076
|
+
armature = search.current_armature(context)
|
|
1077
|
+
if armature and isinstance(armature_data := armature.data, Armature):
|
|
1078
|
+
is_vrm0 = get_armature_extension(armature_data).is_vrm0()
|
|
1079
|
+
is_vrm1 = get_armature_extension(armature_data).is_vrm1()
|
|
1080
|
+
else:
|
|
1081
|
+
armature_data = None
|
|
1082
|
+
is_vrm0 = (
|
|
1083
|
+
VrmAddonArmatureExtensionPropertyGroup.SPEC_VERSION_DEFAULT
|
|
1084
|
+
== VrmAddonArmatureExtensionPropertyGroup.SPEC_VERSION_VRM0
|
|
1085
|
+
)
|
|
1086
|
+
is_vrm1 = (
|
|
1087
|
+
VrmAddonArmatureExtensionPropertyGroup.SPEC_VERSION_DEFAULT
|
|
1088
|
+
== VrmAddonArmatureExtensionPropertyGroup.SPEC_VERSION_VRM1
|
|
1089
|
+
)
|
|
1090
|
+
state.info_messages.append(pgettext("No armature exists."))
|
|
1091
|
+
|
|
1092
|
+
WM_OT_vrm_validator.validate_export_objects(
|
|
1093
|
+
context,
|
|
1094
|
+
export_objects,
|
|
1095
|
+
armature,
|
|
1096
|
+
armature_data,
|
|
1097
|
+
is_vrm0=is_vrm0,
|
|
1098
|
+
is_vrm1=is_vrm1,
|
|
1099
|
+
execute_migration=execute_migration,
|
|
1100
|
+
state=state,
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
if is_vrm1 and armature_data:
|
|
1104
|
+
WM_OT_vrm_validator.validate_vrm1_additional(
|
|
1105
|
+
armature_data,
|
|
1106
|
+
export_objects,
|
|
1107
|
+
state,
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
WM_OT_vrm_validator.validate_vertex_weights(
|
|
1111
|
+
export_objects,
|
|
1112
|
+
armature,
|
|
1113
|
+
armature_data,
|
|
1114
|
+
is_vrm1=is_vrm1,
|
|
1115
|
+
state=state,
|
|
1116
|
+
)
|
|
1117
|
+
WM_OT_vrm_validator.validate_materials(
|
|
1118
|
+
context,
|
|
1119
|
+
export_objects,
|
|
1120
|
+
is_vrm0=is_vrm0,
|
|
1121
|
+
state=state,
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
if is_vrm0 and armature_data:
|
|
1125
|
+
WM_OT_vrm_validator.validate_vrm0_additional(
|
|
1126
|
+
context,
|
|
1127
|
+
armature_data,
|
|
1128
|
+
export_objects,
|
|
1129
|
+
state,
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
WM_OT_vrm_validator.append_image_path_errors(state)
|
|
1133
|
+
WM_OT_vrm_validator.set_error_collection(error_collection, state)
|
|
1134
|
+
|
|
1135
|
+
return bool(state.error_messages)
|
|
1136
|
+
|
|
1137
|
+
@staticmethod
|
|
1138
|
+
def draw_errors(
|
|
1139
|
+
layout: UILayout,
|
|
1140
|
+
error_collection: CollectionPropertyProtocol[VrmValidationError],
|
|
1141
|
+
*,
|
|
1142
|
+
show_successful_message: bool,
|
|
1143
|
+
) -> None:
|
|
1144
|
+
error_errors: list[VrmValidationError] = []
|
|
1145
|
+
warning_errors: list[VrmValidationError] = []
|
|
1146
|
+
info_errors: list[VrmValidationError] = []
|
|
1147
|
+
|
|
1148
|
+
for error in error_collection:
|
|
1149
|
+
if error.severity == 0:
|
|
1150
|
+
error_errors.append(error)
|
|
1151
|
+
elif error.severity <= 2:
|
|
1152
|
+
warning_errors.append(error)
|
|
1153
|
+
else:
|
|
1154
|
+
info_errors.append(error)
|
|
1155
|
+
|
|
1156
|
+
if not error_errors and warning_errors:
|
|
1157
|
+
row = layout.row()
|
|
1158
|
+
row.emboss = "NONE"
|
|
1159
|
+
row.box().label(
|
|
1160
|
+
text=pgettext(
|
|
1161
|
+
"No errors found. But there are {warning_count} warning(s)."
|
|
1162
|
+
+ " The output may not be what you expected."
|
|
1163
|
+
).format(
|
|
1164
|
+
warning_count=len(warning_errors),
|
|
1165
|
+
),
|
|
1166
|
+
translate=False,
|
|
1167
|
+
)
|
|
1168
|
+
elif not error_errors and show_successful_message:
|
|
1169
|
+
row = layout.row()
|
|
1170
|
+
row.emboss = "NONE"
|
|
1171
|
+
row.box().label(text="No errors found. Ready to export VRM.")
|
|
1172
|
+
|
|
1173
|
+
if error_errors:
|
|
1174
|
+
layout.label(text="Error", icon="ERROR")
|
|
1175
|
+
column = layout.column(align=True)
|
|
1176
|
+
for error in error_errors:
|
|
1177
|
+
column.prop(
|
|
1178
|
+
error,
|
|
1179
|
+
"message",
|
|
1180
|
+
text="",
|
|
1181
|
+
translate=False,
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
if warning_errors:
|
|
1185
|
+
layout.label(text="Warning", icon="CANCEL")
|
|
1186
|
+
column = layout.column(align=True)
|
|
1187
|
+
for error in warning_errors:
|
|
1188
|
+
column.prop(
|
|
1189
|
+
error,
|
|
1190
|
+
"message",
|
|
1191
|
+
text="",
|
|
1192
|
+
translate=False,
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
if info_errors:
|
|
1196
|
+
layout.label(text="Info", icon="INFO")
|
|
1197
|
+
column = layout.column(align=True)
|
|
1198
|
+
for error in info_errors:
|
|
1199
|
+
column.prop(
|
|
1200
|
+
error,
|
|
1201
|
+
"message",
|
|
1202
|
+
text="",
|
|
1203
|
+
translate=False,
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
if TYPE_CHECKING:
|
|
1207
|
+
# This code is auto generated.
|
|
1208
|
+
# To regenerate, run the `uv run tools/property_typing.py` command.
|
|
1209
|
+
show_successful_message: bool # type: ignore[no-redef]
|
|
1210
|
+
errors: CollectionPropertyProtocol[ # type: ignore[no-redef]
|
|
1211
|
+
VrmValidationError
|
|
1212
|
+
]
|
|
1213
|
+
armature_object_name: str # type: ignore[no-redef]
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def _node_material_input_check(
|
|
1217
|
+
node: ShaderNodeGroup,
|
|
1218
|
+
material: Material,
|
|
1219
|
+
expect_node_type: str,
|
|
1220
|
+
shader_val: str,
|
|
1221
|
+
messages: list[str],
|
|
1222
|
+
used_images: list[Image],
|
|
1223
|
+
) -> None:
|
|
1224
|
+
# Support models that were loaded by earlier versions (1.3.5 or earlier), which had
|
|
1225
|
+
# this typo
|
|
1226
|
+
#
|
|
1227
|
+
# Those models have node.inputs["NomalmapTexture"] instead of "NormalmapTexture".
|
|
1228
|
+
# But 'shader_val', which is come from MaterialMtoon.texture_kind_exchange_dict,
|
|
1229
|
+
# can be "NormalmapTexture". if script reference node.inputs["NormalmapTexture"] in
|
|
1230
|
+
# that situation, it will occur error. So change it to "NomalmapTexture" which is
|
|
1231
|
+
# typo but points to the same thing in those models.
|
|
1232
|
+
if (
|
|
1233
|
+
shader_val == "NormalmapTexture"
|
|
1234
|
+
and "NormalmapTexture" not in node.inputs.keys()
|
|
1235
|
+
):
|
|
1236
|
+
shader_val = "NomalmapTexture"
|
|
1237
|
+
|
|
1238
|
+
if node.inputs[shader_val].links:
|
|
1239
|
+
n = node.inputs[shader_val].links[0].from_node
|
|
1240
|
+
if n.type != expect_node_type:
|
|
1241
|
+
messages.append(
|
|
1242
|
+
pgettext(
|
|
1243
|
+
'need "{expect_node_type}" input'
|
|
1244
|
+
+ ' in "{shader_val}" of "{material_name}".',
|
|
1245
|
+
).format(
|
|
1246
|
+
expect_node_type=expect_node_type,
|
|
1247
|
+
shader_val=shader_val,
|
|
1248
|
+
material_name=material.name,
|
|
1249
|
+
)
|
|
1250
|
+
)
|
|
1251
|
+
elif expect_node_type == "TEX_IMAGE" and isinstance(n, ShaderNodeTexImage):
|
|
1252
|
+
if (image := n.image) is not None:
|
|
1253
|
+
if image not in used_images:
|
|
1254
|
+
used_images.append(image)
|
|
1255
|
+
else:
|
|
1256
|
+
messages.append(
|
|
1257
|
+
pgettext(
|
|
1258
|
+
'Image in material "{material_name}" is not set.'
|
|
1259
|
+
+ " Please add an image.",
|
|
1260
|
+
).format(material_name=material.name)
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def is_valid_url(url_str: str, *, allow_empty_str: bool) -> bool:
|
|
1265
|
+
if not url_str:
|
|
1266
|
+
return allow_empty_str
|
|
1267
|
+
try:
|
|
1268
|
+
url = urlsplit(url_str)
|
|
1269
|
+
except ValueError:
|
|
1270
|
+
return False
|
|
1271
|
+
return bool(url.scheme)
|