@volter/blender-engine 0.1.148 → 0.1.150

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.
Files changed (245) hide show
  1. package/LICENSE +22 -5
  2. package/README.md +4 -11
  3. package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
  4. package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
  5. package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
  6. package/browser/addons/vrm/README.txt +3 -0
  7. package/browser/addons/vrm/__init__.py +41 -0
  8. package/browser/addons/vrm/blender_manifest.toml +16 -0
  9. package/browser/addons/vrm/common/__init__.py +1 -0
  10. package/browser/addons/vrm/common/animation.py +58 -0
  11. package/browser/addons/vrm/common/blender_manifest.py +82 -0
  12. package/browser/addons/vrm/common/char.py +16 -0
  13. package/browser/addons/vrm/common/convert.py +287 -0
  14. package/browser/addons/vrm/common/convert_any.py +48 -0
  15. package/browser/addons/vrm/common/debug.py +40 -0
  16. package/browser/addons/vrm/common/deep.py +162 -0
  17. package/browser/addons/vrm/common/error_dialog.py +274 -0
  18. package/browser/addons/vrm/common/fs.py +71 -0
  19. package/browser/addons/vrm/common/gl.py +20 -0
  20. package/browser/addons/vrm/common/gltf.py +1149 -0
  21. package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
  22. package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
  23. package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
  24. package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
  25. package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
  26. package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
  27. package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
  28. package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
  29. package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
  30. package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
  31. package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
  32. package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
  33. package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
  34. package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
  35. package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
  36. package/browser/addons/vrm/common/legacy_gltf.py +16 -0
  37. package/browser/addons/vrm/common/logger.py +84 -0
  38. package/browser/addons/vrm/common/mtoon0.blend +0 -0
  39. package/browser/addons/vrm/common/mtoon1.blend +0 -0
  40. package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
  41. package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
  42. package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
  43. package/browser/addons/vrm/common/ops/__init__.py +10 -0
  44. package/browser/addons/vrm/common/ops/export_scene.py +67 -0
  45. package/browser/addons/vrm/common/ops/icyp.py +53 -0
  46. package/browser/addons/vrm/common/ops/import_scene.py +57 -0
  47. package/browser/addons/vrm/common/ops/vrm.py +2380 -0
  48. package/browser/addons/vrm/common/ops/wm.py +167 -0
  49. package/browser/addons/vrm/common/preferences.py +362 -0
  50. package/browser/addons/vrm/common/progress.py +87 -0
  51. package/browser/addons/vrm/common/rotation.py +89 -0
  52. package/browser/addons/vrm/common/safe_removal.py +46 -0
  53. package/browser/addons/vrm/common/scene_watcher.py +230 -0
  54. package/browser/addons/vrm/common/shader.py +2243 -0
  55. package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
  56. package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
  57. package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
  58. package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
  59. package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
  60. package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
  61. package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
  62. package/browser/addons/vrm/common/version.py +161 -0
  63. package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
  64. package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
  65. package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
  66. package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
  67. package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
  68. package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
  69. package/browser/addons/vrm/common/workspace.py +227 -0
  70. package/browser/addons/vrm/common/writable_context.py +77 -0
  71. package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
  72. package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
  73. package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
  74. package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
  75. package/browser/addons/vrm/documentation/en-us/index.md +41 -0
  76. package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
  77. package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
  78. package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
  79. package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
  80. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
  81. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
  82. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
  83. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
  84. package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
  85. package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
  86. package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
  87. package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
  88. package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
  89. package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
  90. package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
  91. package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
  92. package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
  93. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
  94. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
  95. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
  96. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
  97. package/browser/addons/vrm/editor/__init__.py +1 -0
  98. package/browser/addons/vrm/editor/extension.py +425 -0
  99. package/browser/addons/vrm/editor/extension_accessor.py +75 -0
  100. package/browser/addons/vrm/editor/handler.py +84 -0
  101. package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
  102. package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
  103. package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
  104. package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
  105. package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
  106. package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
  107. package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
  108. package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
  109. package/browser/addons/vrm/editor/make_armature.py +709 -0
  110. package/browser/addons/vrm/editor/menu.py +147 -0
  111. package/browser/addons/vrm/editor/migration.py +297 -0
  112. package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
  113. package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
  114. package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
  115. package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
  116. package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
  117. package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
  118. package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
  119. package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
  120. package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
  121. package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
  122. package/browser/addons/vrm/editor/ops.py +471 -0
  123. package/browser/addons/vrm/editor/panel.py +315 -0
  124. package/browser/addons/vrm/editor/property_group.py +888 -0
  125. package/browser/addons/vrm/editor/search.py +682 -0
  126. package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
  127. package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
  128. package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
  129. package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
  130. package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
  131. package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
  132. package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
  133. package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
  134. package/browser/addons/vrm/editor/subscription.py +123 -0
  135. package/browser/addons/vrm/editor/t_pose.py +713 -0
  136. package/browser/addons/vrm/editor/validation.py +1271 -0
  137. package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
  138. package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
  139. package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
  140. package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
  141. package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
  142. package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
  143. package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
  144. package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
  145. package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
  146. package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
  147. package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
  148. package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
  149. package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
  150. package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
  151. package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
  152. package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
  153. package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
  154. package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
  155. package/browser/addons/vrm/exporter/__init__.py +1 -0
  156. package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
  157. package/browser/addons/vrm/exporter/export_scene.py +930 -0
  158. package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
  159. package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
  160. package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
  161. package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
  162. package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
  163. package/browser/addons/vrm/external/__init__.py +1 -0
  164. package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
  165. package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
  166. package/browser/addons/vrm/external/rigify_support.py +1 -0
  167. package/browser/addons/vrm/importer/__init__.py +1 -0
  168. package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
  169. package/browser/addons/vrm/importer/file_handler.py +34 -0
  170. package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
  171. package/browser/addons/vrm/importer/import_scene.py +646 -0
  172. package/browser/addons/vrm/importer/license_validation.py +198 -0
  173. package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
  174. package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
  175. package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
  176. package/browser/addons/vrm/importer/vrm_diff.py +316 -0
  177. package/browser/addons/vrm/locale/__init__.py +1 -0
  178. package/browser/addons/vrm/locale/ja_jp.py +826 -0
  179. package/browser/addons/vrm/locale/ruff.toml +8 -0
  180. package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
  181. package/browser/addons/vrm/locale/zh_hans.py +928 -0
  182. package/browser/addons/vrm/registration.py +670 -0
  183. package/browser/addons/vrm.json +18 -0
  184. package/browser/blender-emscripten-engine.mts +66 -129
  185. package/browser/blender-engine.mts +20 -186
  186. package/browser/blender-wali-engine.mts +37 -81
  187. package/browser/describe-thrown.mts +3 -6
  188. package/browser/document-chunks.mts +4 -16
  189. package/browser/gltf_export.py +3161 -0
  190. package/browser/heap-growth.mts +23 -0
  191. package/browser/index.ts +3 -0
  192. package/browser/protocol.ts +130 -43
  193. package/browser/pull-job.mts +2 -10
  194. package/browser/rna.ts +190 -73
  195. package/browser/runtime.ts +271 -106
  196. package/browser/session-addons.mts +33 -0
  197. package/browser/session-frame.mts +5 -24
  198. package/browser/session.py +1389 -1446
  199. package/browser/three/attach-presenter.ts +11 -39
  200. package/browser/three/blender-base64.ts +1 -36
  201. package/browser/three/blender-corner-normals.ts +1 -23
  202. package/browser/three/blender-display-shaders.json +1 -1
  203. package/browser/three/blender-display-shaders.lut +0 -0
  204. package/browser/three/blender-display-transform.ts +2 -1
  205. package/browser/three/blender-frame-columns.ts +5 -13
  206. package/browser/three/blender-graph-material.ts +18 -3
  207. package/browser/three/blender-node-graph.ts +10 -34
  208. package/browser/three/blender-physical-material.ts +2 -31
  209. package/browser/three/blender-runtime-armature.ts +7 -60
  210. package/browser/three/blender-runtime-frame.ts +9 -23
  211. package/browser/three/blender-runtime-geometry.ts +22 -26
  212. package/browser/three/blender-runtime-lighting.ts +48 -178
  213. package/browser/three/blender-runtime-shadows.ts +187 -1
  214. package/browser/three/blender-runtime-skeleton.ts +53 -1
  215. package/browser/three/blender-runtime-view.ts +492 -306
  216. package/browser/three/blender-runtime-weights.ts +9 -72
  217. package/browser/three/blender-sky.ts +13 -33
  218. package/browser/three/blender-triangulate.ts +7 -35
  219. package/browser/three/blender-viewport-sampling.ts +235 -0
  220. package/browser/three/blender-workbench-material.ts +25 -29
  221. package/browser/three/blender-workbench-outline.ts +199 -0
  222. package/browser/three/blender-world-volume.ts +62 -10
  223. package/browser/three/presenter.ts +10 -26
  224. package/browser/three/release.ts +5 -21
  225. package/browser/three/sky-precompute-worker.ts +1 -18
  226. package/browser/three/sky-worker.ts +5 -23
  227. package/browser/three/world-field-sampler.ts +12 -26
  228. package/browser/volter_three.py +9 -105
  229. package/browser/worker.ts +195 -180
  230. package/package.json +5 -2
  231. package/wasm/BUNDLE.json +17 -17
  232. package/wasm/DEPENDENCY-LICENSES.txt +14 -0
  233. package/wasm/blender_browser.data.br +0 -0
  234. package/wasm/blender_browser.js +1 -1
  235. package/wasm/blender_browser.wasm.br +0 -0
  236. package/browser/frame-transfer.mts +0 -19
  237. package/browser/three/agx-base-srgb.lut +0 -0
  238. package/browser/three/agx-look-medium-high-contrast.lut +0 -0
  239. package/browser/three/agx-look-punchy.lut +0 -0
  240. package/browser/three/blender-agx.ts +0 -235
  241. package/browser/three/blender-display-lut.ts +0 -145
  242. package/browser/three/blender-filmic.ts +0 -49
  243. package/browser/three/blender-standard.ts +0 -68
  244. package/browser/three/blender-world-visibility.ts +0 -278
  245. 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)