@volter/blender-engine 0.1.147 → 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.
Files changed (245) hide show
  1. package/LICENSE +16 -0
  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 +60 -128
  185. package/browser/blender-engine.mts +20 -186
  186. package/browser/blender-wali-engine.mts +35 -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 +2 -0
  192. package/browser/protocol.ts +119 -35
  193. package/browser/pull-job.mts +2 -10
  194. package/browser/rna.ts +190 -73
  195. package/browser/runtime.ts +197 -82
  196. package/browser/session-addons.mts +33 -0
  197. package/browser/session-frame.mts +5 -24
  198. package/browser/session.py +1338 -1427
  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 +505 -283
  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 +156 -151
  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,3682 @@
1
+ # SPDX-License-Identifier: MIT OR GPL-3.0-or-later
2
+ import math
3
+ import tempfile
4
+ from collections.abc import Generator, Mapping, Sequence
5
+ from contextlib import contextmanager
6
+ from copy import deepcopy
7
+ from os import environ
8
+ from pathlib import Path
9
+ from sys import float_info
10
+ from types import MappingProxyType
11
+ from typing import Optional, Union
12
+ from uuid import uuid4
13
+
14
+ import bpy
15
+ from bpy.types import (
16
+ ID,
17
+ Armature,
18
+ ArmatureModifier,
19
+ Bone,
20
+ Constraint,
21
+ Context,
22
+ CopyRotationConstraint,
23
+ Curve,
24
+ DampedTrackConstraint,
25
+ Image,
26
+ Material,
27
+ Mesh,
28
+ Node,
29
+ Object,
30
+ PoseBone,
31
+ ShaderNodeGroup,
32
+ VectorFont,
33
+ )
34
+ from mathutils import Matrix, Quaternion, Vector
35
+
36
+ from ..common import convert, safe_removal, shader
37
+ from ..common.char import INTERNAL_NAME_PREFIX
38
+ from ..common.convert import Json
39
+ from ..common.deep import make_json
40
+ from ..common.gl import GL_LINEAR, GL_REPEAT
41
+ from ..common.gltf import (
42
+ pack_glb,
43
+ parse_glb,
44
+ parse_gltf_node_matrix,
45
+ remove_empty_root_arrays,
46
+ )
47
+ from ..common.logger import get_logger
48
+ from ..common.preferences import ExportPreferencesProtocol
49
+ from ..common.rotation import (
50
+ ROTATION_MODE_EULER,
51
+ get_rotation_as_quaternion,
52
+ set_rotation_without_mode_change,
53
+ )
54
+ from ..common.shader import LegacyAddonMaterial, MmdMaterial
55
+ from ..common.third_party_user_extension import (
56
+ collect_third_party_user_extensions,
57
+ trigger_third_party_user_extension_hook,
58
+ )
59
+ from ..common.version import get_addon_version
60
+ from ..common.vrm1.human_bone import (
61
+ HumanBoneName,
62
+ HumanBoneSpecification,
63
+ HumanBoneSpecifications,
64
+ )
65
+ from ..common.workspace import save_workspace
66
+ from ..editor import search
67
+ from ..editor.extension_accessor import get_armature_extension, get_material_extension
68
+ from ..editor.mtoon1.property_group import (
69
+ Mtoon1SamplerPropertyGroup,
70
+ Mtoon1TextureInfoPropertyGroup,
71
+ link_or_unlink_gltf_material_nodes,
72
+ )
73
+ from ..editor.spring_bone1.handler import sort_spring_bone_joints
74
+ from ..editor.spring_bone1.property_group import (
75
+ SpringBone1JointPropertyGroup,
76
+ SpringBone1SpringBonePropertyGroup,
77
+ SpringBone1SpringPropertyGroup,
78
+ )
79
+ from ..editor.t_pose import setup_humanoid_t_pose
80
+ from ..editor.vrm1.property_group import (
81
+ Vrm1ExpressionPropertyGroup,
82
+ Vrm1ExpressionsPropertyGroup,
83
+ Vrm1FirstPersonPropertyGroup,
84
+ Vrm1HumanBonesPropertyGroup,
85
+ Vrm1HumanoidPropertyGroup,
86
+ Vrm1LookAtPropertyGroup,
87
+ Vrm1MetaPropertyGroup,
88
+ )
89
+ from ..external.io_scene_gltf2_support import (
90
+ ExportSceneGltfArguments,
91
+ export_scene_gltf,
92
+ image_to_image_bytes,
93
+ init_extras_export,
94
+ )
95
+ from .abstract_base_vrm_exporter import (
96
+ AbstractBaseVrmExporter,
97
+ FlexibleHierarchyBoneSetup,
98
+ assign_dict,
99
+ force_apply_modifiers,
100
+ )
101
+
102
+ _logger = get_logger(__name__)
103
+
104
+
105
+ class Vrm1Exporter(AbstractBaseVrmExporter):
106
+ def __init__(
107
+ self,
108
+ context: Context,
109
+ export_objects: Sequence[Object],
110
+ armature: Object,
111
+ export_preferences: ExportPreferencesProtocol,
112
+ ) -> None:
113
+ super().__init__(context, export_objects, armature)
114
+
115
+ self._export_all_influences = (
116
+ export_preferences.enable_advanced_preferences
117
+ and export_preferences.export_all_influences
118
+ )
119
+ self._export_lights = (
120
+ export_preferences.enable_advanced_preferences
121
+ and export_preferences.export_lights
122
+ )
123
+ self._export_gltf_animations = (
124
+ export_preferences.enable_advanced_preferences
125
+ and export_preferences.export_gltf_animations
126
+ )
127
+ self._export_try_sparse_sk = (
128
+ export_preferences.enable_advanced_preferences
129
+ and export_preferences.export_try_sparse_sk
130
+ )
131
+
132
+ self._extras_main_armature_key = (
133
+ INTERNAL_NAME_PREFIX + self._export_id + "MainArmature"
134
+ )
135
+ self._extras_bone_name_key = INTERNAL_NAME_PREFIX + self._export_id + "BoneName"
136
+ self._extras_object_name_key = (
137
+ INTERNAL_NAME_PREFIX + self._export_id + "ObjectName"
138
+ )
139
+ self._extras_material_name_key = (
140
+ INTERNAL_NAME_PREFIX + self._export_id + "MaterialName"
141
+ )
142
+ self._extras_mesh_name_key = INTERNAL_NAME_PREFIX + self._export_id + "MeshName"
143
+
144
+ self._third_party_user_extensions: list[object] = []
145
+ self.generator_postfix = ""
146
+ for (
147
+ third_party_user_extension_name,
148
+ third_party_user_extension,
149
+ ) in collect_third_party_user_extensions(context, "Vrm1ExportUserExtension"):
150
+ self._third_party_user_extensions.append(third_party_user_extension)
151
+ self.generator_postfix += " + " + third_party_user_extension_name
152
+
153
+ @staticmethod
154
+ def enter_overwrite_object_visibility_and_selection(
155
+ context: Context, export_objects: Sequence[Object]
156
+ ) -> Mapping[str, tuple[bool, bool]]:
157
+ object_visibility_and_selection: dict[str, tuple[bool, bool]] = {}
158
+ # https://projects.blender.org/blender/blender/issues/113378
159
+ context.view_layer.update()
160
+ for obj in context.view_layer.objects:
161
+ object_visibility_and_selection[obj.name] = (
162
+ obj.hide_get(),
163
+ obj.select_get(),
164
+ )
165
+ enabled = obj in export_objects
166
+ obj.hide_set(not enabled)
167
+ obj.select_set(enabled)
168
+ return object_visibility_and_selection
169
+
170
+ @staticmethod
171
+ def leave_overwrite_object_visibility_and_selection(
172
+ context: Context,
173
+ object_visibility_and_selection: Mapping[str, tuple[bool, bool]],
174
+ ) -> None:
175
+ for object_name, (
176
+ hidden,
177
+ selection,
178
+ ) in reversed(list(object_visibility_and_selection.items())):
179
+ restore_obj = context.blend_data.objects.get(object_name)
180
+ if restore_obj:
181
+ restore_obj.hide_set(hidden)
182
+ restore_obj.select_set(selection)
183
+
184
+ @contextmanager
185
+ def overwrite_object_visibility_and_selection(self) -> Generator[None]:
186
+ object_visibility_and_selection = (
187
+ self.enter_overwrite_object_visibility_and_selection(
188
+ self._context, self._export_objects
189
+ )
190
+ )
191
+ try:
192
+ yield
193
+ # After yield, bpy's native objects may be deleted or frames may advance
194
+ # and become invalid. Accessing them in that state can cause crashes, so
195
+ # be careful not to access such potentially invalid native objects
196
+ # after yield
197
+ finally:
198
+ self.leave_overwrite_object_visibility_and_selection(
199
+ self._context, object_visibility_and_selection
200
+ )
201
+
202
+ @staticmethod
203
+ def enter_save_selected_mesh_compat_objects(
204
+ context: Context, armature: Object, original_export_objects: Sequence[Object]
205
+ ) -> tuple[Mapping[str, str], Mapping[str, str]]:
206
+ backup_obj_name_to_original_obj_name: dict[str, str] = {}
207
+ backup_data_name_to_original_data_name: dict[str, str] = {}
208
+
209
+ # https://projects.blender.org/blender/blender/issues/113378
210
+ context.view_layer.update()
211
+
212
+ export_objects: list[Object] = []
213
+
214
+ # Duplicate objects.
215
+ # New objects are used for export
216
+ # Old objects are given backup names and restored after export
217
+ # Also, object data is given backup names
218
+ for obj in context.view_layer.objects:
219
+ if obj not in original_export_objects:
220
+ continue
221
+ if obj.type not in search.MESH_CONVERTIBLE_OBJECT_TYPES:
222
+ continue
223
+ if not obj.select_get():
224
+ continue
225
+
226
+ backup_obj_name = "Backup-Object-" + uuid4().hex
227
+ original_obj_name = obj.name
228
+ backup_obj_name_to_original_obj_name[backup_obj_name] = original_obj_name
229
+
230
+ export_obj = obj.copy()
231
+ obj.name = backup_obj_name
232
+ export_obj.name = original_obj_name
233
+
234
+ export_objects.append(export_obj)
235
+ obj.select_set(False)
236
+ scene_collection_objects = context.scene.collection.objects
237
+ scene_collection_objects.link(export_obj)
238
+ export_obj.select_set(True)
239
+
240
+ obj_data = obj.data
241
+ if not obj_data:
242
+ continue
243
+ if obj_data.name in backup_data_name_to_original_data_name:
244
+ continue
245
+ backup_data_name = (
246
+ "Backup-Data-" + uuid4().hex + "-" + type(obj_data).__name__
247
+ )
248
+ backup_data_name_to_original_data_name[backup_data_name] = obj_data.name
249
+ obj_data.name = backup_data_name
250
+
251
+ # Duplicate object data
252
+ # However, if active modifiers other than Armature modifiers are attached,
253
+ # share the object data
254
+ data_to_non_sharing_objects: dict[ID, list[Object]] = {}
255
+ data_to_sharing_objects: dict[ID, list[Object]] = {}
256
+ for export_object in export_objects:
257
+ data = export_object.data
258
+ if not data:
259
+ continue
260
+ sharing = True
261
+ for modifier in export_object.modifiers:
262
+ if not modifier.show_viewport:
263
+ continue
264
+ if (
265
+ isinstance(modifier, ArmatureModifier)
266
+ and modifier.object == armature
267
+ ):
268
+ continue
269
+ sharing = False
270
+ break
271
+ mapping = (
272
+ data_to_sharing_objects if sharing else data_to_non_sharing_objects
273
+ )
274
+ objs = mapping.get(data)
275
+ if objs is None:
276
+ objs = list[Object]()
277
+ mapping[data] = objs
278
+ objs.append(export_object)
279
+
280
+ for data, sharing_export_objects in data_to_sharing_objects.items():
281
+ original_data_name = backup_data_name_to_original_data_name.get(data.name)
282
+ if not original_data_name:
283
+ continue # This should not happen
284
+ export_data = data.copy()
285
+ export_data.name = original_data_name
286
+ for sharing_export_object in sharing_export_objects:
287
+ sharing_export_object.data = export_data
288
+
289
+ for data, non_sharing_export_objects in data_to_non_sharing_objects.items():
290
+ original_data_name = backup_data_name_to_original_data_name.get(data.name)
291
+ if not original_data_name:
292
+ continue # This should not happen
293
+ for non_sharing_export_object in non_sharing_export_objects:
294
+ export_data = data.copy()
295
+ export_data.name = original_data_name
296
+ non_sharing_export_object.data = export_data
297
+
298
+ return (
299
+ backup_obj_name_to_original_obj_name,
300
+ backup_data_name_to_original_data_name,
301
+ )
302
+
303
+ @staticmethod
304
+ def leave_save_selected_mesh_compat_objects(
305
+ context: Context,
306
+ backup_obj_name_to_original_obj_name: Mapping[str, str],
307
+ backup_data_name_to_original_data_name: Mapping[str, str],
308
+ ) -> None:
309
+ removing_object_datum: list[ID] = []
310
+
311
+ # Delete export objects
312
+ for original_obj_name in reversed(
313
+ list(backup_obj_name_to_original_obj_name.values())
314
+ ):
315
+ restored_export_obj = context.blend_data.objects.get(original_obj_name)
316
+ if not restored_export_obj:
317
+ continue
318
+
319
+ # Data may be referenced by multiple objects, so
320
+ # collect them here and delete them all at once later
321
+ restored_export_obj_data = restored_export_obj.data
322
+ if (
323
+ restored_export_obj_data
324
+ and restored_export_obj_data not in removing_object_datum
325
+ ):
326
+ removing_object_datum.append(restored_export_obj_data)
327
+
328
+ # Even if deletion fails, randomize the name to avoid
329
+ # confusion with the original object
330
+ restored_export_obj.name = "Export-Object-" + uuid4().hex
331
+ if not safe_removal.remove_object(context, restored_export_obj):
332
+ _logger.warning(
333
+ 'Failed to remove "%s" with %d users (temp object for "%s")',
334
+ restored_export_obj.name,
335
+ restored_export_obj.users,
336
+ original_obj_name,
337
+ )
338
+
339
+ # Delete export object data
340
+ for removing_object_data in removing_object_datum:
341
+ # Even if deletion fails, randomize the name to avoid
342
+ # confusion with the original object data
343
+ removing_object_data.name = (
344
+ "Export-Data-" + uuid4().hex + "-" + type(removing_object_data).__name__
345
+ )
346
+
347
+ if removing_object_data.users:
348
+ _logger.warning(
349
+ 'Failed to remove "%s" with %d users',
350
+ removing_object_data.name,
351
+ removing_object_data.users,
352
+ )
353
+ elif isinstance(removing_object_data, Mesh):
354
+ context.blend_data.meshes.remove(removing_object_data)
355
+ elif isinstance(removing_object_data, Curve):
356
+ context.blend_data.curves.remove(removing_object_data)
357
+ elif isinstance(removing_object_data, VectorFont):
358
+ context.blend_data.fonts.remove(removing_object_data)
359
+ else:
360
+ _logger.warning(
361
+ 'Failed to remove "%s" with %d users. Not implemented.',
362
+ removing_object_data.name,
363
+ removing_object_data.users,
364
+ )
365
+
366
+ # Restore the names of backed up objects
367
+ for (
368
+ backup_obj_name,
369
+ original_obj_name,
370
+ ) in reversed(list(backup_obj_name_to_original_obj_name.items())):
371
+ restored_original_data_name = None
372
+ restored_obj = context.blend_data.objects.get(backup_obj_name)
373
+ if not restored_obj:
374
+ continue
375
+ restored_obj.name = original_obj_name
376
+ restored_obj.select_set(True)
377
+
378
+ # Restore the names of backed up object data
379
+ restored_obj_data = restored_obj.data
380
+ if not restored_obj_data:
381
+ continue
382
+ restored_original_data_name = backup_data_name_to_original_data_name.get(
383
+ restored_obj_data.name
384
+ )
385
+ if not restored_original_data_name:
386
+ continue
387
+ restored_obj_data.name = restored_original_data_name
388
+
389
+ @contextmanager
390
+ def save_selected_mesh_compat_objects(self) -> Generator[Sequence[str]]:
391
+ (
392
+ backup_obj_name_to_original_obj_name,
393
+ backup_data_name_to_original_data_name,
394
+ ) = self.enter_save_selected_mesh_compat_objects(
395
+ self._context, self._armature, self._export_objects
396
+ )
397
+ try:
398
+ yield list(backup_obj_name_to_original_obj_name.values())
399
+ # After yield, bpy's native objects may be deleted or frames may advance
400
+ # and become invalid. Accessing them in that state can cause crashes, so
401
+ # be careful not to access such potentially invalid native objects
402
+ # after yield
403
+ finally:
404
+ self.leave_save_selected_mesh_compat_objects(
405
+ self._context,
406
+ backup_obj_name_to_original_obj_name,
407
+ backup_data_name_to_original_data_name,
408
+ )
409
+
410
+ def enter_mount_skinned_mesh_parent(self, armature: Object) -> Sequence[str]:
411
+ mounted_object_names: list[str] = []
412
+
413
+ for obj in self._export_objects:
414
+ if obj.type != "MESH" or not [
415
+ True
416
+ for m in obj.modifiers
417
+ if isinstance(m, ArmatureModifier) and m.object == armature
418
+ ]:
419
+ continue
420
+
421
+ search_obj = obj
422
+ while search_obj != armature:
423
+ if search_obj.parent:
424
+ search_obj = search_obj.parent
425
+ continue
426
+ mounted_object_names.append(search_obj.name)
427
+ matrix_world = search_obj.matrix_world.copy()
428
+ search_obj.parent = armature
429
+ search_obj.matrix_world = matrix_world
430
+ break
431
+ return mounted_object_names
432
+
433
+ def leave_mount_skinned_mesh_parent(
434
+ self, mounted_object_names: Sequence[str]
435
+ ) -> None:
436
+ for mounted_object_name in reversed(mounted_object_names):
437
+ restore_obj = self._context.blend_data.objects.get(mounted_object_name)
438
+ if not restore_obj:
439
+ continue
440
+ matrix_world = restore_obj.matrix_world.copy()
441
+ restore_obj.parent = None
442
+ restore_obj.matrix_world = matrix_world
443
+
444
+ @contextmanager
445
+ def mount_skinned_mesh_parent(self) -> Generator[None]:
446
+ # The glTF 2.0 export processing of the addon included with Blender 3.1.2
447
+ # outputs invalid glb with incorrect inverseBindMatrices when all of the
448
+ # following conditions are met:
449
+ # - A mesh that is not a descendant of the armature is skinned to bones
450
+ # of that armature
451
+ # - Another mesh exists as a child of the skinned bone
452
+ # Therefore, make the ancestor parent of meshes that are not descendants
453
+ # of the armature as the armature, and restore it later
454
+
455
+ armature = self._armature
456
+ if not armature:
457
+ yield
458
+ return
459
+
460
+ if bpy.app.version >= (3, 2, 1):
461
+ yield
462
+ return
463
+
464
+ mounted_object_names = self.enter_mount_skinned_mesh_parent(armature)
465
+ try:
466
+ yield
467
+ # After yield, bpy's native objects may be deleted or frames may advance
468
+ # and become invalid. Accessing them in that state can cause crashes, so
469
+ # be careful not to access such potentially invalid native objects
470
+ # after yield
471
+ finally:
472
+ self.leave_mount_skinned_mesh_parent(mounted_object_names)
473
+
474
+ @classmethod
475
+ def create_meta_dict(
476
+ cls,
477
+ meta: Vrm1MetaPropertyGroup,
478
+ json_dict: dict[str, Json],
479
+ buffer0: bytearray,
480
+ image_name_to_index_dict: dict[str, int],
481
+ gltf2_addon_export_settings: dict[str, object],
482
+ ) -> dict[str, Json]:
483
+ meta_dict: dict[str, Json] = {
484
+ "licenseUrl": "https://vrm.dev/licenses/1.0/",
485
+ "name": meta.vrm_name or "undefined",
486
+ "version": meta.version or "undefined",
487
+ "avatarPermission": meta.avatar_permission,
488
+ "allowExcessivelyViolentUsage": meta.allow_excessively_violent_usage,
489
+ "allowExcessivelySexualUsage": meta.allow_excessively_sexual_usage,
490
+ "commercialUsage": meta.commercial_usage,
491
+ "allowPoliticalOrReligiousUsage": meta.allow_political_or_religious_usage,
492
+ "allowAntisocialOrHateUsage": meta.allow_antisocial_or_hate_usage,
493
+ "creditNotation": meta.credit_notation,
494
+ "allowRedistribution": meta.allow_redistribution,
495
+ "modification": meta.modification,
496
+ }
497
+
498
+ authors = [author.value for author in meta.authors if author.value]
499
+ if not authors:
500
+ authors = ["undefined"]
501
+ meta_dict["authors"] = make_json(authors)
502
+
503
+ if meta.copyright_information:
504
+ meta_dict["copyrightInformation"] = meta.copyright_information
505
+
506
+ references: list[Json] = [
507
+ reference.value for reference in meta.references if reference.value
508
+ ]
509
+ if references:
510
+ meta_dict["references"] = references
511
+
512
+ if meta.third_party_licenses:
513
+ meta_dict["thirdPartyLicenses"] = meta.third_party_licenses
514
+
515
+ if meta.other_license_url:
516
+ meta_dict["otherLicenseUrl"] = meta.other_license_url
517
+
518
+ if meta.thumbnail_image:
519
+ meta_dict["thumbnailImage"] = cls.find_or_create_image(
520
+ json_dict,
521
+ buffer0,
522
+ image_name_to_index_dict,
523
+ meta.thumbnail_image,
524
+ gltf2_addon_export_settings,
525
+ )
526
+
527
+ return meta_dict
528
+
529
+ @classmethod
530
+ def create_humanoid_dict(
531
+ cls,
532
+ humanoid: Vrm1HumanoidPropertyGroup,
533
+ bone_name_to_index_dict: Mapping[str, int],
534
+ ) -> dict[str, Json]:
535
+ human_bones_dict: dict[str, Json] = {}
536
+ for (
537
+ human_bone_name,
538
+ human_bone,
539
+ ) in humanoid.human_bones.human_bone_name_to_human_bone().items():
540
+ index = bone_name_to_index_dict.get(human_bone.node.bone_name)
541
+ if isinstance(index, int):
542
+ human_bones_dict[human_bone_name.value] = {"node": index}
543
+
544
+ return {
545
+ "humanBones": human_bones_dict,
546
+ }
547
+
548
+ @classmethod
549
+ def create_first_person_dict(
550
+ cls,
551
+ first_person: Vrm1FirstPersonPropertyGroup,
552
+ mesh_object_name_to_node_index_dict: Mapping[str, int],
553
+ ) -> dict[str, Json]:
554
+ mesh_annotation_dicts: list[Json] = []
555
+ for mesh_annotation in first_person.mesh_annotations:
556
+ if not mesh_annotation.node or not mesh_annotation.node.mesh_object_name:
557
+ continue
558
+ node_index = mesh_object_name_to_node_index_dict.get(
559
+ mesh_annotation.node.mesh_object_name
560
+ )
561
+ if not isinstance(node_index, int):
562
+ continue
563
+ mesh_annotation_dicts.append(
564
+ {
565
+ "node": node_index,
566
+ "type": mesh_annotation.type,
567
+ }
568
+ )
569
+ if not mesh_annotation_dicts:
570
+ return {}
571
+ return {"meshAnnotations": mesh_annotation_dicts}
572
+
573
+ @classmethod
574
+ def create_look_at_dict(
575
+ cls,
576
+ look_at: Vrm1LookAtPropertyGroup,
577
+ ) -> dict[str, Json]:
578
+ return {
579
+ "offsetFromHeadBone": list(look_at.offset_from_head_bone),
580
+ "type": look_at.type,
581
+ "rangeMapHorizontalInner": {
582
+ "inputMaxValue": look_at.range_map_horizontal_inner.input_max_value,
583
+ "outputScale": look_at.range_map_horizontal_inner.output_scale,
584
+ },
585
+ "rangeMapHorizontalOuter": {
586
+ "inputMaxValue": look_at.range_map_horizontal_outer.input_max_value,
587
+ "outputScale": look_at.range_map_horizontal_outer.output_scale,
588
+ },
589
+ "rangeMapVerticalDown": {
590
+ "inputMaxValue": look_at.range_map_vertical_down.input_max_value,
591
+ "outputScale": look_at.range_map_vertical_down.output_scale,
592
+ },
593
+ "rangeMapVerticalUp": {
594
+ "inputMaxValue": look_at.range_map_vertical_up.input_max_value,
595
+ "outputScale": look_at.range_map_vertical_up.output_scale,
596
+ },
597
+ }
598
+
599
+ @classmethod
600
+ def create_expression_dict(
601
+ cls,
602
+ expression: Vrm1ExpressionPropertyGroup,
603
+ mesh_object_name_to_node_index_dict: Mapping[str, int],
604
+ mesh_object_name_to_morph_target_names_dict: Mapping[str, list[str]],
605
+ material_name_to_index_dict: Mapping[str, int],
606
+ ) -> dict[str, Json]:
607
+ expression_dict: dict[str, Json] = {
608
+ "isBinary": expression.is_binary,
609
+ "overrideBlink": expression.override_blink,
610
+ "overrideLookAt": expression.override_look_at,
611
+ "overrideMouth": expression.override_mouth,
612
+ }
613
+ morph_target_bind_dicts: list[Json] = []
614
+ for morph_target_bind in expression.morph_target_binds:
615
+ if (
616
+ not morph_target_bind.node
617
+ or not morph_target_bind.node.mesh_object_name
618
+ ):
619
+ continue
620
+ node_index = mesh_object_name_to_node_index_dict.get(
621
+ morph_target_bind.node.mesh_object_name
622
+ )
623
+ if not isinstance(node_index, int):
624
+ continue
625
+ morph_targets = mesh_object_name_to_morph_target_names_dict.get(
626
+ morph_target_bind.node.mesh_object_name
627
+ )
628
+ if not isinstance(morph_targets, list):
629
+ continue
630
+ if morph_target_bind.index not in morph_targets:
631
+ continue
632
+ morph_target_bind_dicts.append(
633
+ {
634
+ "node": node_index,
635
+ "index": morph_targets.index(morph_target_bind.index),
636
+ "weight": morph_target_bind.weight,
637
+ }
638
+ )
639
+ if morph_target_bind_dicts:
640
+ expression_dict["morphTargetBinds"] = morph_target_bind_dicts
641
+
642
+ material_color_bind_dicts: list[Json] = []
643
+ for material_color_bind in expression.material_color_binds:
644
+ material_color_bind_material = material_color_bind.material
645
+ if (
646
+ not material_color_bind_material
647
+ or not material_color_bind_material.name
648
+ ):
649
+ continue
650
+ material_index = material_name_to_index_dict.get(
651
+ material_color_bind_material.name
652
+ )
653
+ if not isinstance(material_index, int):
654
+ continue
655
+ if material_color_bind.type == "color":
656
+ target_value: Json = list(material_color_bind.target_value)
657
+ else:
658
+ rgb = material_color_bind.target_value_as_rgb
659
+ target_value = [rgb[0], rgb[1], rgb[2], 1.0]
660
+ material_color_bind_dicts.append(
661
+ {
662
+ "material": material_index,
663
+ "type": material_color_bind.type,
664
+ "targetValue": target_value,
665
+ }
666
+ )
667
+ if material_color_bind_dicts:
668
+ expression_dict["materialColorBinds"] = material_color_bind_dicts
669
+
670
+ texture_transform_binds: list[Json] = []
671
+ for texture_transform_bind in expression.texture_transform_binds:
672
+ texture_transform_bind_material = texture_transform_bind.material
673
+ if (
674
+ not texture_transform_bind_material
675
+ or not texture_transform_bind_material.name
676
+ ):
677
+ continue
678
+ material_index = material_name_to_index_dict.get(
679
+ texture_transform_bind_material.name
680
+ )
681
+ if not isinstance(material_index, int):
682
+ continue
683
+ texture_transform_binds.append(
684
+ {
685
+ "material": material_index,
686
+ "scale": list(texture_transform_bind.scale),
687
+ "offset": list(texture_transform_bind.offset),
688
+ }
689
+ )
690
+ if texture_transform_binds:
691
+ expression_dict["textureTransformBinds"] = texture_transform_binds
692
+
693
+ return expression_dict
694
+
695
+ @classmethod
696
+ def create_expressions_dict(
697
+ cls,
698
+ expressions: Vrm1ExpressionsPropertyGroup,
699
+ mesh_object_name_to_node_index_dict: Mapping[str, int],
700
+ mesh_object_name_to_morph_target_names_dict: Mapping[str, list[str]],
701
+ material_name_to_index_dict: dict[str, int],
702
+ ) -> dict[str, Json]:
703
+ preset_dict: dict[str, Json] = {}
704
+ for (
705
+ preset_name,
706
+ expression,
707
+ ) in expressions.preset.name_to_expression_dict().items():
708
+ preset_dict[preset_name] = cls.create_expression_dict(
709
+ expression,
710
+ mesh_object_name_to_node_index_dict,
711
+ mesh_object_name_to_morph_target_names_dict,
712
+ material_name_to_index_dict,
713
+ )
714
+ custom_dict: dict[str, Json] = {}
715
+ for custom_expression in expressions.custom:
716
+ custom_dict[custom_expression.custom_name] = cls.create_expression_dict(
717
+ custom_expression,
718
+ mesh_object_name_to_node_index_dict,
719
+ mesh_object_name_to_morph_target_names_dict,
720
+ material_name_to_index_dict,
721
+ )
722
+ return {
723
+ "preset": preset_dict,
724
+ "custom": custom_dict,
725
+ }
726
+
727
+ @classmethod
728
+ def create_spring_bone_collider_dicts(
729
+ cls,
730
+ extensions_used: list[Json],
731
+ spring_bone: SpringBone1SpringBonePropertyGroup,
732
+ bone_name_to_index_dict: Mapping[str, int],
733
+ ) -> tuple[list[Json], dict[str, int]]:
734
+ collider_dicts: list[Json] = []
735
+ collider_uuid_to_index_dict: dict[str, int] = {}
736
+ for collider in spring_bone.colliders:
737
+ if not collider.uuid:
738
+ continue
739
+ collider_dict: dict[str, Json] = {}
740
+ node_index = bone_name_to_index_dict.get(collider.node.bone_name)
741
+ if not isinstance(node_index, int):
742
+ continue
743
+ collider_dict["node"] = node_index
744
+
745
+ extended_collider = collider.extensions.vrmc_spring_bone_extended_collider
746
+ if collider.shape_type == collider.SHAPE_TYPE_SPHERE.identifier:
747
+ if not extended_collider.enabled:
748
+ sphere_offset: Json = list(collider.shape.sphere.offset)
749
+ sphere_radius = collider.shape.sphere.radius
750
+ elif not extended_collider.automatic_fallback_generation:
751
+ sphere_offset = list(collider.shape.sphere.fallback_offset)
752
+ sphere_radius = collider.shape.sphere.fallback_radius
753
+ elif (
754
+ extended_collider.shape_type
755
+ == extended_collider.SHAPE_TYPE_EXTENDED_SPHERE.identifier
756
+ and not extended_collider.shape.sphere.inside
757
+ ):
758
+ sphere_offset = list(extended_collider.shape.sphere.offset)
759
+ sphere_radius = extended_collider.shape.sphere.radius
760
+ else:
761
+ sphere_offset = [0, -10000, 0]
762
+ sphere_radius = 0.00001
763
+ sphere_dict: dict[str, Json] = {
764
+ "offset": sphere_offset,
765
+ "radius": sphere_radius,
766
+ }
767
+ shape_dict: dict[str, Json] = {"sphere": sphere_dict}
768
+ elif collider.shape_type == collider.SHAPE_TYPE_CAPSULE.identifier:
769
+ if not extended_collider.enabled:
770
+ capsule_offset: Json = list(collider.shape.capsule.offset)
771
+ capsule_radius = collider.shape.capsule.radius
772
+ capsule_tail: Json = list(collider.shape.capsule.tail)
773
+ elif not extended_collider.automatic_fallback_generation:
774
+ capsule_offset = list(collider.shape.capsule.fallback_offset)
775
+ capsule_radius = collider.shape.capsule.fallback_radius
776
+ capsule_tail = list(collider.shape.capsule.fallback_tail)
777
+ elif (
778
+ extended_collider.shape_type
779
+ == extended_collider.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
780
+ and not extended_collider.shape.capsule.inside
781
+ ):
782
+ capsule_offset = list(extended_collider.shape.capsule.offset)
783
+ capsule_radius = extended_collider.shape.capsule.radius
784
+ capsule_tail = list(extended_collider.shape.capsule.tail)
785
+ else:
786
+ capsule_offset = [0, -10000, 0]
787
+ capsule_radius = 0.00001
788
+ capsule_tail = [0, -10001, 0]
789
+ capsule_dict: dict[str, Json] = {
790
+ "offset": capsule_offset,
791
+ "radius": capsule_radius,
792
+ "tail": capsule_tail,
793
+ }
794
+ shape_dict = {"capsule": capsule_dict}
795
+ else:
796
+ continue
797
+
798
+ collider_dict["shape"] = shape_dict
799
+ collider_uuid_to_index_dict[collider.uuid] = len(collider_dicts)
800
+ collider_dicts.append(collider_dict)
801
+
802
+ if not extended_collider.enabled:
803
+ continue
804
+
805
+ if (
806
+ extended_collider.shape_type
807
+ == extended_collider.SHAPE_TYPE_EXTENDED_SPHERE.identifier
808
+ ):
809
+ extended_shape_dict: dict[str, Json] = {
810
+ "sphere": {
811
+ "offset": list(extended_collider.shape.sphere.offset),
812
+ "radius": extended_collider.shape.sphere.radius,
813
+ "inside": extended_collider.shape.sphere.inside,
814
+ }
815
+ }
816
+ elif (
817
+ extended_collider.shape_type
818
+ == extended_collider.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
819
+ ):
820
+ extended_shape_dict = {
821
+ "capsule": {
822
+ "offset": list(extended_collider.shape.capsule.offset),
823
+ "radius": extended_collider.shape.capsule.radius,
824
+ "tail": list(extended_collider.shape.capsule.tail),
825
+ "inside": extended_collider.shape.capsule.inside,
826
+ }
827
+ }
828
+ elif (
829
+ extended_collider.shape_type
830
+ == extended_collider.SHAPE_TYPE_EXTENDED_PLANE.identifier
831
+ ):
832
+ extended_shape_dict = {
833
+ "plane": {
834
+ "offset": list(extended_collider.shape.plane.offset),
835
+ "normal": list(extended_collider.shape.plane.normal),
836
+ }
837
+ }
838
+ else:
839
+ continue
840
+
841
+ collider_dict["extensions"] = {
842
+ "VRMC_springBone_extended_collider": {
843
+ "specVersion": "1.0",
844
+ "shape": extended_shape_dict,
845
+ }
846
+ }
847
+
848
+ if "VRMC_springBone_extended_collider" not in extensions_used:
849
+ extensions_used.append("VRMC_springBone_extended_collider")
850
+
851
+ return collider_dicts, collider_uuid_to_index_dict
852
+
853
+ @classmethod
854
+ def create_spring_bone_collider_group_dicts(
855
+ cls,
856
+ spring_bone: SpringBone1SpringBonePropertyGroup,
857
+ collider_uuid_to_index_dict: Mapping[str, int],
858
+ ) -> tuple[list[Json], dict[str, int]]:
859
+ collider_group_dicts: list[Json] = []
860
+ collider_group_uuid_to_index_dict: dict[str, int] = {}
861
+ for collider_group in spring_bone.collider_groups:
862
+ collider_group_dict: dict[str, Json] = {"name": collider_group.vrm_name}
863
+ collider_indices: list[Json] = []
864
+ for collider_reference in collider_group.colliders:
865
+ collider_index = collider_uuid_to_index_dict.get(
866
+ collider_reference.collider_uuid
867
+ )
868
+ if not isinstance(collider_index, int):
869
+ continue
870
+ if collider_index in collider_indices:
871
+ continue
872
+ collider_indices.append(collider_index)
873
+ if not collider_indices:
874
+ continue
875
+ collider_group_dict["colliders"] = collider_indices
876
+ collider_group_uuid_to_index_dict[collider_group.uuid] = len(
877
+ collider_group_dicts
878
+ )
879
+ collider_group_dicts.append(collider_group_dict)
880
+
881
+ return collider_group_dicts, collider_group_uuid_to_index_dict
882
+
883
+ @classmethod
884
+ def create_spring_bone_spring_dicts(
885
+ cls,
886
+ extensions_used: list[Json],
887
+ spring_bone: SpringBone1SpringBonePropertyGroup,
888
+ bone_name_to_index_dict: Mapping[str, int],
889
+ collider_group_uuid_to_index_dict: Mapping[str, int],
890
+ armature: Object,
891
+ ) -> list[Json]:
892
+ spring_dicts: list[Json] = []
893
+ armature_data = armature.data
894
+ if not isinstance(armature_data, Armature):
895
+ _logger.error("%s is not an Armature", type(armature_data))
896
+ return []
897
+ sorted_spring_and_joints: list[
898
+ tuple[
899
+ SpringBone1SpringPropertyGroup, Sequence[SpringBone1JointPropertyGroup]
900
+ ]
901
+ ] = [
902
+ (
903
+ spring,
904
+ [
905
+ joint
906
+ for joint, _ in joint_and_bones
907
+ if isinstance(
908
+ bone_name_to_index_dict.get(joint.node.bone_name), int
909
+ )
910
+ ],
911
+ )
912
+ for spring in spring_bone.springs
913
+ for joint_and_bones in sort_spring_bone_joints(armature, spring.joints)
914
+ ]
915
+ for spring, joints in sorted_spring_and_joints:
916
+ spring_dict: dict[str, Json] = {"name": spring.vrm_name}
917
+
918
+ first_bone: Optional[Bone] = None
919
+ joint_dicts: list[Json] = []
920
+ for joint in joints:
921
+ bone = armature_data.bones.get(joint.node.bone_name)
922
+ if not bone:
923
+ continue
924
+ if first_bone is None:
925
+ first_bone = bone
926
+ node_index = bone_name_to_index_dict.get(joint.node.bone_name)
927
+ if not isinstance(node_index, int):
928
+ continue
929
+ joint_dict: dict[str, Json] = {
930
+ "node": node_index,
931
+ "hitRadius": joint.hit_radius,
932
+ "stiffness": joint.stiffness,
933
+ "gravityPower": joint.gravity_power,
934
+ "gravityDir": [
935
+ joint.gravity_dir[0],
936
+ joint.gravity_dir[2],
937
+ -joint.gravity_dir[1],
938
+ ],
939
+ "dragForce": joint.drag_force,
940
+ }
941
+
942
+ limit = joint.vrmc_spring_bone_limit
943
+ limit_parameter_dict: Optional[dict[str, Json]] = None
944
+ limit_type = ""
945
+ if limit.limit_type == limit.LIMIT_TYPE_CONE.identifier:
946
+ limit_parameter_dict = {"angle": limit.cone_angle}
947
+ limit_type = "cone"
948
+ elif limit.limit_type == limit.LIMIT_TYPE_HINGE.identifier:
949
+ limit_parameter_dict = {"angle": limit.hinge_angle}
950
+ limit_type = "hinge"
951
+ elif limit.limit_type == limit.LIMIT_TYPE_SPHERICAL.identifier:
952
+ limit_parameter_dict = {
953
+ "pitch": limit.spherical_pitch,
954
+ "yaw": limit.spherical_yaw,
955
+ }
956
+ limit_type = "spherical"
957
+
958
+ if limit_parameter_dict is not None and joint is not joints[-1]:
959
+ rotation = limit.rotation_quaternion()
960
+ limit_parameter_dict["rotation"] = [
961
+ rotation.x,
962
+ rotation.y,
963
+ rotation.z,
964
+ rotation.w,
965
+ ]
966
+ joint_dict["extensions"] = {
967
+ "VRMC_springBone_limit": {
968
+ "specVersion": "1.0",
969
+ "limit": {limit_type: limit_parameter_dict},
970
+ }
971
+ }
972
+ if "VRMC_springBone_limit" not in extensions_used:
973
+ extensions_used.append("VRMC_springBone_limit")
974
+
975
+ joint_dicts.append(joint_dict)
976
+
977
+ if not joint_dicts:
978
+ continue
979
+ spring_dict["joints"] = joint_dicts
980
+
981
+ center_bone = armature_data.bones.get(spring.center.bone_name)
982
+ if center_bone:
983
+ center_bone_is_ancestor_of_first_bone = False
984
+ ancestor_of_first_bone = first_bone
985
+ while ancestor_of_first_bone:
986
+ if center_bone == ancestor_of_first_bone:
987
+ center_bone_is_ancestor_of_first_bone = True
988
+ break
989
+ ancestor_of_first_bone = ancestor_of_first_bone.parent
990
+ if center_bone_is_ancestor_of_first_bone:
991
+ center_index = bone_name_to_index_dict.get(spring.center.bone_name)
992
+ if isinstance(center_index, int):
993
+ spring_dict["center"] = center_index
994
+
995
+ collider_group_indices: list[Json] = []
996
+ for collider_group_reference in spring.collider_groups:
997
+ collider_group_index = collider_group_uuid_to_index_dict.get(
998
+ collider_group_reference.collider_group_uuid
999
+ )
1000
+ if isinstance(collider_group_index, int):
1001
+ collider_group_indices.append(collider_group_index)
1002
+
1003
+ if collider_group_indices:
1004
+ spring_dict["colliderGroups"] = collider_group_indices
1005
+
1006
+ spring_dicts.append(spring_dict)
1007
+ return spring_dicts
1008
+
1009
+ @classmethod
1010
+ def search_constraint_target_index(
1011
+ cls,
1012
+ constraint: Union[CopyRotationConstraint, DampedTrackConstraint],
1013
+ object_name_to_index_dict: Mapping[str, int],
1014
+ bone_name_to_index_dict: Mapping[str, int],
1015
+ ) -> Optional[int]:
1016
+ target = constraint.target
1017
+ if not target:
1018
+ return None
1019
+ if target.type == "ARMATURE" and constraint.subtarget:
1020
+ return bone_name_to_index_dict.get(constraint.subtarget)
1021
+ return object_name_to_index_dict.get(target.name)
1022
+
1023
+ @classmethod
1024
+ def create_constraint_dict(
1025
+ cls,
1026
+ name: str,
1027
+ constraints: search.ExportConstraint,
1028
+ object_name_to_index_dict: Mapping[str, int],
1029
+ bone_name_to_index_dict: Mapping[str, int],
1030
+ ) -> Optional[dict[str, Json]]:
1031
+ roll_constraint = constraints.roll_constraints.get(name)
1032
+ aim_constraint = constraints.aim_constraints.get(name)
1033
+ rotation_constraint = constraints.rotation_constraints.get(name)
1034
+ if roll_constraint:
1035
+ source_index = cls.search_constraint_target_index(
1036
+ roll_constraint,
1037
+ object_name_to_index_dict,
1038
+ bone_name_to_index_dict,
1039
+ )
1040
+ if isinstance(source_index, int):
1041
+ if roll_constraint.use_x:
1042
+ roll_axis = "X"
1043
+ elif roll_constraint.use_y:
1044
+ roll_axis = "Y"
1045
+ elif roll_constraint.use_z:
1046
+ roll_axis = "Z"
1047
+ else:
1048
+ message = "Unsupported roll axis"
1049
+ raise ValueError(message)
1050
+ return {
1051
+ "roll": {
1052
+ "source": source_index,
1053
+ "rollAxis": roll_axis,
1054
+ "weight": max(0.0, min(1.0, roll_constraint.influence)),
1055
+ }
1056
+ }
1057
+ elif aim_constraint:
1058
+ source_index = cls.search_constraint_target_index(
1059
+ aim_constraint,
1060
+ object_name_to_index_dict,
1061
+ bone_name_to_index_dict,
1062
+ )
1063
+ if isinstance(source_index, int):
1064
+ return {
1065
+ "aim": {
1066
+ "source": source_index,
1067
+ "aimAxis": convert.BPY_TRACK_AXIS_TO_VRM_AIM_AXIS[
1068
+ aim_constraint.track_axis
1069
+ ],
1070
+ "weight": max(0.0, min(1.0, aim_constraint.influence)),
1071
+ }
1072
+ }
1073
+ elif rotation_constraint:
1074
+ source_index = cls.search_constraint_target_index(
1075
+ rotation_constraint,
1076
+ object_name_to_index_dict,
1077
+ bone_name_to_index_dict,
1078
+ )
1079
+ if isinstance(source_index, int):
1080
+ return {
1081
+ "rotation": {
1082
+ "source": source_index,
1083
+ "weight": max(0.0, min(1.0, rotation_constraint.influence)),
1084
+ }
1085
+ }
1086
+ return None
1087
+
1088
+ @classmethod
1089
+ def create_mtoon0_khr_texture_transform(
1090
+ cls, node: Node, texture_input_name: str
1091
+ ) -> Optional[dict[str, Json]]:
1092
+ texture_input = node.inputs.get(texture_input_name)
1093
+ if not texture_input:
1094
+ return None
1095
+
1096
+ texture_input_links = texture_input.links
1097
+ if not texture_input_links:
1098
+ return None
1099
+
1100
+ x_from_node = texture_input_links[0].from_node
1101
+ if not x_from_node:
1102
+ return None
1103
+
1104
+ x_inputs = x_from_node.inputs
1105
+ if not x_inputs:
1106
+ return None
1107
+
1108
+ x_links = x_inputs[0].links
1109
+ if not x_links:
1110
+ return None
1111
+
1112
+ uv_offset_scaling_node = x_links[0].from_node
1113
+ if not uv_offset_scaling_node or uv_offset_scaling_node.type != "MAPPING":
1114
+ return None
1115
+
1116
+ location_input = uv_offset_scaling_node.inputs.get("Location")
1117
+ if isinstance(location_input, shader.VECTOR_SOCKET_CLASSES):
1118
+ offset_x = location_input.default_value[0]
1119
+ offset_y = location_input.default_value[1]
1120
+ else:
1121
+ offset_x = shader.UV_GROUP_UV_OFFSET_X_DEFAULT
1122
+ offset_y = shader.UV_GROUP_UV_OFFSET_Y_DEFAULT
1123
+
1124
+ scale_input = uv_offset_scaling_node.inputs.get("Scale")
1125
+ if isinstance(scale_input, shader.VECTOR_SOCKET_CLASSES):
1126
+ scale_x = scale_input.default_value[0]
1127
+ scale_y = scale_input.default_value[1]
1128
+ else:
1129
+ scale_x = shader.UV_GROUP_UV_SCALE_X_DEFAULT
1130
+ scale_y = shader.UV_GROUP_UV_SCALE_Y_DEFAULT
1131
+
1132
+ if (
1133
+ abs(offset_x - shader.UV_GROUP_UV_OFFSET_X_DEFAULT) < float_info.epsilon
1134
+ and abs(offset_y - shader.UV_GROUP_UV_OFFSET_Y_DEFAULT) < float_info.epsilon
1135
+ and abs(scale_x - shader.UV_GROUP_UV_SCALE_X_DEFAULT) < float_info.epsilon
1136
+ and abs(scale_y - shader.UV_GROUP_UV_SCALE_Y_DEFAULT) < float_info.epsilon
1137
+ ):
1138
+ return None
1139
+
1140
+ return {
1141
+ "offset": [offset_x, offset_y],
1142
+ "scale": [scale_x, scale_y],
1143
+ }
1144
+
1145
+ @classmethod
1146
+ def find_or_create_image(
1147
+ cls,
1148
+ json_dict: dict[str, Json],
1149
+ buffer0: bytearray,
1150
+ image_name_to_index_dict: dict[str, int],
1151
+ image: Image,
1152
+ gltf2_addon_export_settings: dict[str, object],
1153
+ ) -> int:
1154
+ # Check if image already exists - return early to avoid duplicating buffer data
1155
+ image_index = image_name_to_index_dict.get(image.name)
1156
+ image_dicts = json_dict.get("images")
1157
+ if not isinstance(image_dicts, list):
1158
+ image_dicts = []
1159
+ json_dict["images"] = image_dicts
1160
+
1161
+ if isinstance(image_index, int):
1162
+ if 0 <= image_index < len(image_dicts):
1163
+ # Image already exists, return its index without duplicating data
1164
+ return image_index
1165
+ _logger.error(
1166
+ "Bug: not (0 <= %d < len(images)) for %s", image_index, image.name
1167
+ )
1168
+ image_index = None
1169
+
1170
+ # Image doesn't exist yet - create new buffer view and image entry
1171
+ while len(buffer0) % 4:
1172
+ buffer0.append(0)
1173
+
1174
+ image_bytes, mime = image_to_image_bytes(image, gltf2_addon_export_settings)
1175
+ buffer_view_dicts = json_dict.get("bufferViews")
1176
+ if not isinstance(buffer_view_dicts, list):
1177
+ buffer_view_dicts = []
1178
+ json_dict["bufferViews"] = buffer_view_dicts
1179
+ image_buffer_view_index = len(buffer_view_dicts)
1180
+ buffer_view_dicts.append(
1181
+ {
1182
+ "buffer": 0,
1183
+ "byteOffset": len(buffer0),
1184
+ "byteLength": len(image_bytes),
1185
+ }
1186
+ )
1187
+
1188
+ image_index = len(image_dicts)
1189
+ image_dicts.append(
1190
+ {
1191
+ "name": image.name,
1192
+ "bufferView": image_buffer_view_index,
1193
+ "mimeType": mime,
1194
+ }
1195
+ )
1196
+ image_name_to_index_dict[image.name] = image_index
1197
+
1198
+ buffer0.extend(image_bytes)
1199
+ while len(buffer0) % 4:
1200
+ buffer0.append(0)
1201
+
1202
+ return image_index
1203
+
1204
+ @classmethod
1205
+ def create_mtoon1_texture_info_dict(
1206
+ cls,
1207
+ json_dict: dict[str, Json],
1208
+ buffer0: bytearray,
1209
+ texture_info: Mtoon1TextureInfoPropertyGroup,
1210
+ image_name_to_index_dict: dict[str, int],
1211
+ gltf2_addon_export_settings: dict[str, object],
1212
+ ) -> Optional[dict[str, Json]]:
1213
+ image = texture_info.index.get_connected_node_image()
1214
+ if not image:
1215
+ return None
1216
+
1217
+ image_index = cls.find_or_create_image(
1218
+ json_dict,
1219
+ buffer0,
1220
+ image_name_to_index_dict,
1221
+ image,
1222
+ gltf2_addon_export_settings,
1223
+ )
1224
+
1225
+ mag_filter = Mtoon1SamplerPropertyGroup.mag_filter_enum.identifier_to_value(
1226
+ texture_info.index.sampler.mag_filter,
1227
+ Mtoon1SamplerPropertyGroup.MAG_FILTER_DEFAULT.value,
1228
+ )
1229
+
1230
+ min_filter = Mtoon1SamplerPropertyGroup.min_filter_enum.identifier_to_value(
1231
+ texture_info.index.sampler.min_filter,
1232
+ Mtoon1SamplerPropertyGroup.MIN_FILTER_DEFAULT.value,
1233
+ )
1234
+
1235
+ wrap_s = Mtoon1SamplerPropertyGroup.wrap_enum.identifier_to_value(
1236
+ texture_info.index.sampler.wrap_s,
1237
+ Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.value,
1238
+ )
1239
+
1240
+ wrap_t = Mtoon1SamplerPropertyGroup.wrap_enum.identifier_to_value(
1241
+ texture_info.index.sampler.wrap_t,
1242
+ Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.value,
1243
+ )
1244
+
1245
+ sampler_dict: dict[str, Json] = {
1246
+ "magFilter": mag_filter,
1247
+ "minFilter": min_filter,
1248
+ "wrapS": wrap_s,
1249
+ "wrapT": wrap_t,
1250
+ }
1251
+
1252
+ sampler_dicts = json_dict.get("samplers")
1253
+ if not isinstance(sampler_dicts, list):
1254
+ sampler_dicts = []
1255
+ json_dict["samplers"] = sampler_dicts
1256
+
1257
+ if sampler_dict in sampler_dicts:
1258
+ sampler_index = sampler_dicts.index(sampler_dict)
1259
+ else:
1260
+ sampler_index = len(sampler_dicts)
1261
+ sampler_dicts.append(sampler_dict)
1262
+
1263
+ texture_dicts = json_dict.get("textures")
1264
+ if not isinstance(texture_dicts, list):
1265
+ texture_dicts = []
1266
+ json_dict["textures"] = texture_dicts
1267
+
1268
+ texture_dict: dict[str, Json] = {
1269
+ "sampler": sampler_index,
1270
+ "source": image_index,
1271
+ }
1272
+
1273
+ if texture_dict in texture_dicts:
1274
+ texture_index = texture_dicts.index(texture_dict)
1275
+ else:
1276
+ texture_index = len(texture_dicts)
1277
+ texture_dicts.append(texture_dict)
1278
+
1279
+ khr_texture_transform = texture_info.extensions.khr_texture_transform
1280
+ if khr_texture_transform.is_default():
1281
+ return {"index": texture_index}
1282
+
1283
+ extensions_used = json_dict.get("extensionsUsed")
1284
+ if not isinstance(extensions_used, list):
1285
+ extensions_used = []
1286
+ json_dict["extensionsUsed"] = extensions_used
1287
+
1288
+ if "KHR_texture_transform" not in extensions_used:
1289
+ extensions_used.append("KHR_texture_transform")
1290
+ json_dict["extensionsUsed"] = extensions_used
1291
+
1292
+ khr_texture_transform_dict: dict[str, Json] = {
1293
+ "offset": list(khr_texture_transform.offset),
1294
+ "scale": list(khr_texture_transform.scale),
1295
+ }
1296
+
1297
+ return {
1298
+ "index": texture_index,
1299
+ "extensions": {"KHR_texture_transform": khr_texture_transform_dict},
1300
+ }
1301
+
1302
+ @classmethod
1303
+ def create_mtoon0_texture_info_dict(
1304
+ cls,
1305
+ context: Context,
1306
+ json_dict: dict[str, Json],
1307
+ buffer0: bytearray,
1308
+ node: Node,
1309
+ texture_input_name: str,
1310
+ image_name_to_index_dict: dict[str, int],
1311
+ gltf2_addon_export_settings: dict[str, object],
1312
+ ) -> Optional[dict[str, Json]]:
1313
+ image_name_and_sampler_type = shader.get_image_name_and_sampler_type(
1314
+ node, texture_input_name
1315
+ )
1316
+ if image_name_and_sampler_type is None:
1317
+ return None
1318
+
1319
+ image_name, wrap_type, filter_type = image_name_and_sampler_type
1320
+ image_index = cls.find_or_create_image(
1321
+ json_dict,
1322
+ buffer0,
1323
+ image_name_to_index_dict,
1324
+ context.blend_data.images[image_name],
1325
+ gltf2_addon_export_settings,
1326
+ )
1327
+
1328
+ sampler_dict: dict[str, Json] = {
1329
+ "magFilter": filter_type,
1330
+ "minFilter": filter_type,
1331
+ "wrapS": wrap_type,
1332
+ "wrapT": wrap_type,
1333
+ }
1334
+ sampler_dicts = json_dict.get("samplers")
1335
+ if not isinstance(sampler_dicts, list):
1336
+ sampler_dicts = []
1337
+ json_dict["samplers"] = sampler_dicts
1338
+ if sampler_dict in sampler_dicts:
1339
+ sampler_index = sampler_dicts.index(sampler_dict)
1340
+ else:
1341
+ sampler_index = len(sampler_dicts)
1342
+ sampler_dicts.append(sampler_dict)
1343
+
1344
+ texture_dicts = json_dict.get("textures")
1345
+ if not isinstance(texture_dicts, list):
1346
+ texture_dicts = []
1347
+ json_dict["textures"] = texture_dicts
1348
+
1349
+ texture_dict: dict[str, Json] = {
1350
+ "sampler": sampler_index,
1351
+ "source": image_index,
1352
+ }
1353
+
1354
+ if texture_dict in texture_dicts:
1355
+ texture_index = texture_dicts.index(texture_dict)
1356
+ else:
1357
+ texture_index = len(texture_dicts)
1358
+ texture_dicts.append(texture_dict)
1359
+
1360
+ khr_texture_transform_dict = cls.create_mtoon0_khr_texture_transform(
1361
+ node, texture_input_name
1362
+ )
1363
+ if not khr_texture_transform_dict:
1364
+ return {"index": texture_index}
1365
+
1366
+ extensions_used = json_dict.get("extensionsUsed")
1367
+ if not isinstance(extensions_used, list):
1368
+ extensions_used = []
1369
+ json_dict["extensionsUsed"] = extensions_used
1370
+
1371
+ if "KHR_texture_transform" not in extensions_used:
1372
+ extensions_used.append("KHR_texture_transform")
1373
+ json_dict["extensionsUsed"] = extensions_used
1374
+
1375
+ return {
1376
+ "index": texture_index,
1377
+ "extensions": {"KHR_texture_transform": khr_texture_transform_dict},
1378
+ }
1379
+
1380
+ @classmethod
1381
+ def create_mtoon1_material_dict(
1382
+ cls,
1383
+ json_dict: dict[str, Json],
1384
+ buffer0: bytearray,
1385
+ material: Material,
1386
+ image_name_to_index_dict: dict[str, int],
1387
+ gltf2_addon_export_settings: dict[str, object],
1388
+ ) -> dict[str, Json]:
1389
+ extensions_used = json_dict.get("extensionsUsed")
1390
+ if not isinstance(extensions_used, list):
1391
+ extensions_used = []
1392
+ json_dict["extensionsUsed"] = extensions_used
1393
+
1394
+ if "KHR_materials_unlit" not in extensions_used:
1395
+ extensions_used.append("KHR_materials_unlit")
1396
+ if "KHR_materials_emissive_strength" not in extensions_used:
1397
+ extensions_used.append("KHR_materials_emissive_strength")
1398
+ if "VRMC_materials_mtoon" not in extensions_used:
1399
+ extensions_used.append("VRMC_materials_mtoon")
1400
+
1401
+ # https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs
1402
+ mtoon_dict: dict[str, Json] = {
1403
+ "specVersion": "1.0",
1404
+ }
1405
+
1406
+ gltf = get_material_extension(material).mtoon1
1407
+ mtoon = gltf.extensions.vrmc_materials_mtoon
1408
+
1409
+ extensions_dict: dict[str, Json] = {
1410
+ "KHR_materials_unlit": {},
1411
+ "KHR_materials_emissive_strength": {
1412
+ "emissiveStrength": (
1413
+ gltf.extensions.khr_materials_emissive_strength.emissive_strength
1414
+ ),
1415
+ },
1416
+ "VRMC_materials_mtoon": mtoon_dict,
1417
+ }
1418
+
1419
+ material_dict: dict[str, Json] = {"name": material.name}
1420
+ pbr_metallic_roughness_dict: dict[str, Json] = {}
1421
+
1422
+ material_dict["alphaMode"] = gltf.alpha_mode
1423
+ mtoon_dict["transparentWithZWrite"] = mtoon.transparent_with_z_write
1424
+ mtoon_dict["renderQueueOffsetNumber"] = mtoon.render_queue_offset_number
1425
+ if gltf.alpha_mode == "MASK":
1426
+ material_dict["alphaCutoff"] = gltf.alpha_cutoff
1427
+ material_dict["doubleSided"] = gltf.double_sided
1428
+ pbr_metallic_roughness_dict["baseColorFactor"] = list(
1429
+ gltf.pbr_metallic_roughness.base_color_factor
1430
+ )
1431
+ assign_dict(
1432
+ pbr_metallic_roughness_dict,
1433
+ "baseColorTexture",
1434
+ cls.create_mtoon1_texture_info_dict(
1435
+ json_dict,
1436
+ buffer0,
1437
+ gltf.pbr_metallic_roughness.base_color_texture,
1438
+ image_name_to_index_dict,
1439
+ gltf2_addon_export_settings,
1440
+ ),
1441
+ )
1442
+ mtoon_dict["shadeColorFactor"] = list(mtoon.shade_color_factor)
1443
+ assign_dict(
1444
+ mtoon_dict,
1445
+ "shadeMultiplyTexture",
1446
+ cls.create_mtoon1_texture_info_dict(
1447
+ json_dict,
1448
+ buffer0,
1449
+ mtoon.shade_multiply_texture,
1450
+ image_name_to_index_dict,
1451
+ gltf2_addon_export_settings,
1452
+ ),
1453
+ )
1454
+ assign_dict(
1455
+ material_dict,
1456
+ "normalTexture",
1457
+ cls.create_mtoon1_texture_info_dict(
1458
+ json_dict,
1459
+ buffer0,
1460
+ gltf.normal_texture,
1461
+ image_name_to_index_dict,
1462
+ gltf2_addon_export_settings,
1463
+ ),
1464
+ )
1465
+ normal_texture_dict = material_dict.get("normalTexture")
1466
+ if isinstance(normal_texture_dict, dict):
1467
+ normal_texture_dict["scale"] = gltf.normal_texture.scale
1468
+ mtoon_dict["shadingShiftFactor"] = mtoon.shading_shift_factor
1469
+ assign_dict(
1470
+ mtoon_dict,
1471
+ "shadingShiftTexture",
1472
+ cls.create_mtoon1_texture_info_dict(
1473
+ json_dict,
1474
+ buffer0,
1475
+ mtoon.shading_shift_texture,
1476
+ image_name_to_index_dict,
1477
+ gltf2_addon_export_settings,
1478
+ ),
1479
+ )
1480
+ shading_shift_texture_dict = mtoon_dict.get("shadingShiftTexture")
1481
+ if isinstance(shading_shift_texture_dict, dict):
1482
+ shading_shift_texture_dict["scale"] = mtoon.shading_shift_texture.scale
1483
+ mtoon_dict["shadingToonyFactor"] = mtoon.shading_toony_factor
1484
+ mtoon_dict["giEqualizationFactor"] = mtoon.gi_equalization_factor
1485
+ material_dict["emissiveFactor"] = list(gltf.emissive_factor)
1486
+ assign_dict(
1487
+ material_dict,
1488
+ "emissiveTexture",
1489
+ cls.create_mtoon1_texture_info_dict(
1490
+ json_dict,
1491
+ buffer0,
1492
+ gltf.emissive_texture,
1493
+ image_name_to_index_dict,
1494
+ gltf2_addon_export_settings,
1495
+ ),
1496
+ )
1497
+ if assign_dict(
1498
+ mtoon_dict,
1499
+ "matcapTexture",
1500
+ cls.create_mtoon1_texture_info_dict(
1501
+ json_dict,
1502
+ buffer0,
1503
+ mtoon.matcap_texture,
1504
+ image_name_to_index_dict,
1505
+ gltf2_addon_export_settings,
1506
+ ),
1507
+ ):
1508
+ mtoon_dict["matcapFactor"] = list(mtoon.matcap_factor)
1509
+
1510
+ mtoon_dict["parametricRimColorFactor"] = list(mtoon.parametric_rim_color_factor)
1511
+ mtoon_dict["parametricRimFresnelPowerFactor"] = (
1512
+ mtoon.parametric_rim_fresnel_power_factor
1513
+ )
1514
+ mtoon_dict["parametricRimLiftFactor"] = mtoon.parametric_rim_lift_factor
1515
+ assign_dict(
1516
+ mtoon_dict,
1517
+ "rimMultiplyTexture",
1518
+ cls.create_mtoon1_texture_info_dict(
1519
+ json_dict,
1520
+ buffer0,
1521
+ mtoon.rim_multiply_texture,
1522
+ image_name_to_index_dict,
1523
+ gltf2_addon_export_settings,
1524
+ ),
1525
+ )
1526
+ mtoon_dict["rimLightingMixFactor"] = mtoon.rim_lighting_mix_factor
1527
+ mtoon_dict["outlineWidthMode"] = mtoon.outline_width_mode
1528
+ mtoon_dict["outlineWidthFactor"] = mtoon.outline_width_factor
1529
+ assign_dict(
1530
+ mtoon_dict,
1531
+ "outlineWidthMultiplyTexture",
1532
+ cls.create_mtoon1_texture_info_dict(
1533
+ json_dict,
1534
+ buffer0,
1535
+ mtoon.outline_width_multiply_texture,
1536
+ image_name_to_index_dict,
1537
+ gltf2_addon_export_settings,
1538
+ ),
1539
+ )
1540
+ mtoon_dict["outlineColorFactor"] = list(mtoon.outline_color_factor)
1541
+ mtoon_dict["outlineLightingMixFactor"] = mtoon.outline_lighting_mix_factor
1542
+ assign_dict(
1543
+ mtoon_dict,
1544
+ "uvAnimationMaskTexture",
1545
+ cls.create_mtoon1_texture_info_dict(
1546
+ json_dict,
1547
+ buffer0,
1548
+ mtoon.uv_animation_mask_texture,
1549
+ image_name_to_index_dict,
1550
+ gltf2_addon_export_settings,
1551
+ ),
1552
+ )
1553
+ mtoon_dict["uvAnimationRotationSpeedFactor"] = (
1554
+ mtoon.uv_animation_rotation_speed_factor
1555
+ )
1556
+ mtoon_dict["uvAnimationScrollXSpeedFactor"] = (
1557
+ mtoon.uv_animation_scroll_x_speed_factor
1558
+ )
1559
+ mtoon_dict["uvAnimationScrollYSpeedFactor"] = (
1560
+ mtoon.uv_animation_scroll_y_speed_factor
1561
+ )
1562
+
1563
+ if pbr_metallic_roughness_dict:
1564
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
1565
+
1566
+ extensions_dict["VRMC_materials_mtoon"] = mtoon_dict
1567
+ material_dict["extensions"] = extensions_dict
1568
+
1569
+ return material_dict
1570
+
1571
+ @classmethod
1572
+ def create_legacy_gltf_material_dict(
1573
+ cls,
1574
+ context: Context,
1575
+ json_dict: dict[str, Json],
1576
+ buffer0: bytearray,
1577
+ material: Material,
1578
+ node: Node,
1579
+ image_name_to_index_dict: dict[str, int],
1580
+ gltf2_addon_export_settings: dict[str, object],
1581
+ ) -> dict[str, Json]:
1582
+ material_dict: dict[str, Json] = {"name": material.name}
1583
+ pbr_metallic_roughness_dict: dict[str, Json] = {}
1584
+
1585
+ if material.blend_method == "OPAQUE":
1586
+ material_dict["alphaMode"] = "OPAQUE"
1587
+ elif material.blend_method == "CLIP":
1588
+ material_dict["alphaCutoff"] = max(0.0, min(1.0, material.alpha_threshold))
1589
+ material_dict["alphaMode"] = "MASK"
1590
+ else:
1591
+ material_dict["alphaMode"] = "BLEND"
1592
+ assign_dict(
1593
+ material_dict, "doubleSided", not material.use_backface_culling, False
1594
+ )
1595
+ assign_dict(
1596
+ pbr_metallic_roughness_dict,
1597
+ "baseColorFactor",
1598
+ shader.get_rgba_value(node, "base_Color", 0.0, 1.0),
1599
+ )
1600
+ base_color_texture_dict = cls.create_mtoon0_texture_info_dict(
1601
+ context,
1602
+ json_dict,
1603
+ buffer0,
1604
+ node,
1605
+ "color_texture",
1606
+ image_name_to_index_dict,
1607
+ gltf2_addon_export_settings,
1608
+ )
1609
+ assign_dict(
1610
+ pbr_metallic_roughness_dict, "baseColorTexture", base_color_texture_dict
1611
+ )
1612
+
1613
+ assign_dict(
1614
+ pbr_metallic_roughness_dict,
1615
+ "metallicFactor",
1616
+ shader.get_float_value(node, "metallic", 0.0, 1.0),
1617
+ default_value=1.0,
1618
+ )
1619
+ assign_dict(
1620
+ pbr_metallic_roughness_dict,
1621
+ "roughnessFactor",
1622
+ shader.get_float_value(node, "roughness", 0.0, 1.0),
1623
+ default_value=1.0,
1624
+ )
1625
+ assign_dict(
1626
+ pbr_metallic_roughness_dict,
1627
+ "metallicRoughnessTexture",
1628
+ cls.create_mtoon0_texture_info_dict(
1629
+ context,
1630
+ json_dict,
1631
+ buffer0,
1632
+ node,
1633
+ "emissive_texture",
1634
+ image_name_to_index_dict,
1635
+ gltf2_addon_export_settings,
1636
+ ),
1637
+ )
1638
+
1639
+ normal_texture_dict = cls.create_mtoon0_texture_info_dict(
1640
+ context,
1641
+ json_dict,
1642
+ buffer0,
1643
+ node,
1644
+ "normal",
1645
+ image_name_to_index_dict,
1646
+ gltf2_addon_export_settings,
1647
+ )
1648
+ assign_dict(material_dict, "normalTexture", normal_texture_dict)
1649
+
1650
+ assign_dict(
1651
+ material_dict,
1652
+ "emissiveFactor",
1653
+ shader.get_rgb_value(node, "emissive_color", 0.0, 1.0),
1654
+ )
1655
+ assign_dict(
1656
+ material_dict,
1657
+ "emissiveTexture",
1658
+ cls.create_mtoon0_texture_info_dict(
1659
+ context,
1660
+ json_dict,
1661
+ buffer0,
1662
+ node,
1663
+ "emissive_texture",
1664
+ image_name_to_index_dict,
1665
+ gltf2_addon_export_settings,
1666
+ ),
1667
+ )
1668
+
1669
+ assign_dict(
1670
+ material_dict,
1671
+ "occlusionTexture",
1672
+ cls.create_mtoon0_texture_info_dict(
1673
+ context,
1674
+ json_dict,
1675
+ buffer0,
1676
+ node,
1677
+ "occlusion_texture",
1678
+ image_name_to_index_dict,
1679
+ gltf2_addon_export_settings,
1680
+ ),
1681
+ )
1682
+
1683
+ if pbr_metallic_roughness_dict:
1684
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
1685
+
1686
+ unlit_value = shader.get_float_value(node, "unlit")
1687
+ if unlit_value is None or unlit_value > 0.5:
1688
+ extensions_used = json_dict.get("extensionsUsed")
1689
+ if not isinstance(extensions_used, list):
1690
+ extensions_used = []
1691
+ json_dict["extensionsUsed"] = extensions_used
1692
+
1693
+ if "KHR_materials_unlit" not in extensions_used:
1694
+ extensions_used.append("KHR_materials_unlit")
1695
+
1696
+ material_dict["extensions"] = {"KHR_materials_unlit": {}}
1697
+
1698
+ return material_dict
1699
+
1700
+ @classmethod
1701
+ def create_legacy_transparent_zwrite_material_dict(
1702
+ cls,
1703
+ context: Context,
1704
+ json_dict: dict[str, Json],
1705
+ buffer0: bytearray,
1706
+ material: Material,
1707
+ node: Node,
1708
+ image_name_to_index_dict: dict[str, int],
1709
+ gltf2_addon_export_settings: dict[str, object],
1710
+ ) -> dict[str, Json]:
1711
+ # https://vrm-c.github.io/UniVRM/en/implementation/transparent_zwrite.html#mtoon-unlit
1712
+ extensions_used = json_dict.get("extensionsUsed")
1713
+ if not isinstance(extensions_used, list):
1714
+ extensions_used = []
1715
+ json_dict["extensionsUsed"] = extensions_used
1716
+
1717
+ if "KHR_materials_unlit" not in extensions_used:
1718
+ extensions_used.append("KHR_materials_unlit")
1719
+ if "VRMC_materials_mtoon" not in extensions_used:
1720
+ extensions_used.append("VRMC_materials_mtoon")
1721
+
1722
+ mtoon_dict: dict[str, Json] = {
1723
+ "specVersion": "1.0",
1724
+ }
1725
+ extensions_dict: dict[str, Json] = {
1726
+ "KHR_materials_unlit": {},
1727
+ "VRMC_materials_mtoon": mtoon_dict,
1728
+ }
1729
+
1730
+ material_dict: dict[str, Json] = {
1731
+ "name": material.name,
1732
+ "emissiveFactor": [1, 1, 1],
1733
+ }
1734
+ pbr_metallic_roughness_dict: dict[str, Json] = {
1735
+ "baseColorFactor": [0, 0, 0, 1],
1736
+ }
1737
+
1738
+ material_dict["alphaMode"] = "BLEND"
1739
+ mtoon_dict["transparentWithZWrite"] = True
1740
+ mtoon_dict["renderQueueOffsetNumber"] = 0
1741
+ assign_dict(
1742
+ material_dict, "doubleSided", not material.use_backface_culling, False
1743
+ )
1744
+ base_color_texture_dict = cls.create_mtoon0_texture_info_dict(
1745
+ context,
1746
+ json_dict,
1747
+ buffer0,
1748
+ node,
1749
+ "Main_Texture",
1750
+ image_name_to_index_dict,
1751
+ gltf2_addon_export_settings,
1752
+ )
1753
+ assign_dict(
1754
+ pbr_metallic_roughness_dict, "baseColorTexture", base_color_texture_dict
1755
+ )
1756
+ if base_color_texture_dict is not None:
1757
+ mtoon_dict["shadeMultiplyTexture"] = base_color_texture_dict
1758
+ material_dict["emissiveTexture"] = base_color_texture_dict
1759
+
1760
+ if pbr_metallic_roughness_dict:
1761
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
1762
+
1763
+ extensions_dict["VRMC_materials_mtoon"] = mtoon_dict
1764
+ material_dict["extensions"] = extensions_dict
1765
+
1766
+ return material_dict
1767
+
1768
+ @classmethod
1769
+ def create_mmd_material_dict(
1770
+ cls,
1771
+ _context: Context,
1772
+ json_dict: dict[str, Json],
1773
+ buffer0: bytearray,
1774
+ material: Material,
1775
+ mmd_material: MmdMaterial,
1776
+ image_name_to_index_dict: dict[str, int],
1777
+ gltf2_addon_export_settings: dict[str, object],
1778
+ ) -> dict[str, Json]:
1779
+ material_dict: dict[str, Json] = {
1780
+ "name": material.name,
1781
+ }
1782
+ if mmd_material.double_sided:
1783
+ material_dict["doubleSided"] = True
1784
+ if mmd_material.alpha < 1.0:
1785
+ material_dict["alphaMode"] = "BLEND"
1786
+
1787
+ if texture_image := mmd_material.texture:
1788
+ pbr_metallic_roughness_dict: dict[str, Json] = {
1789
+ "baseColorFactor": [0, 0, 0, mmd_material.alpha],
1790
+ }
1791
+ image_index = cls.find_or_create_image(
1792
+ json_dict,
1793
+ buffer0,
1794
+ image_name_to_index_dict,
1795
+ texture_image,
1796
+ gltf2_addon_export_settings,
1797
+ )
1798
+
1799
+ sampler_dict: dict[str, Json] = {
1800
+ "magFilter": GL_LINEAR,
1801
+ "minFilter": GL_LINEAR,
1802
+ "wrapS": GL_REPEAT,
1803
+ "wrapT": GL_REPEAT,
1804
+ }
1805
+ sampler_dicts = json_dict.get("samplers")
1806
+ if not isinstance(sampler_dicts, list):
1807
+ sampler_dicts = []
1808
+ json_dict["samplers"] = sampler_dicts
1809
+ if sampler_dict in sampler_dicts:
1810
+ sampler_index = sampler_dicts.index(sampler_dict)
1811
+ else:
1812
+ sampler_index = len(sampler_dicts)
1813
+ sampler_dicts.append(sampler_dict)
1814
+
1815
+ texture_dicts = json_dict.get("textures")
1816
+ if not isinstance(texture_dicts, list):
1817
+ texture_dicts = []
1818
+ json_dict["textures"] = texture_dicts
1819
+
1820
+ texture_dict: dict[str, Json] = {
1821
+ "sampler": sampler_index,
1822
+ "source": image_index,
1823
+ }
1824
+
1825
+ if texture_dict in texture_dicts:
1826
+ texture_index = texture_dicts.index(texture_dict)
1827
+ else:
1828
+ texture_index = len(texture_dicts)
1829
+ texture_dicts.append(texture_dict)
1830
+
1831
+ texture_info: dict[str, Json] = {"index": texture_index}
1832
+ material_dict["emissiveTexture"] = texture_info
1833
+ material_dict["emissiveFactor"] = [1.0, 1.0, 1.0]
1834
+ else:
1835
+ pbr_metallic_roughness_dict = {
1836
+ "baseColorFactor": [*mmd_material.diffuse_color, mmd_material.alpha]
1837
+ }
1838
+
1839
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
1840
+ return material_dict
1841
+
1842
+ @classmethod
1843
+ def create_mtoon_unversioned_material_dict(
1844
+ cls,
1845
+ context: Context,
1846
+ json_dict: dict[str, Json],
1847
+ buffer0: bytearray,
1848
+ material: Material,
1849
+ node: ShaderNodeGroup,
1850
+ image_name_to_index_dict: dict[str, int],
1851
+ gltf2_addon_export_settings: dict[str, object],
1852
+ ) -> dict[str, Json]:
1853
+ extensions_used = json_dict.get("extensionsUsed")
1854
+ if not isinstance(extensions_used, list):
1855
+ extensions_used = []
1856
+ json_dict["extensionsUsed"] = extensions_used
1857
+
1858
+ if "KHR_materials_unlit" not in extensions_used:
1859
+ extensions_used.append("KHR_materials_unlit")
1860
+ if "VRMC_materials_mtoon" not in extensions_used:
1861
+ extensions_used.append("VRMC_materials_mtoon")
1862
+
1863
+ # https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs
1864
+ mtoon_dict: dict[str, Json] = {
1865
+ "specVersion": "1.0",
1866
+ }
1867
+ extensions_dict: dict[str, Json] = {
1868
+ "KHR_materials_unlit": {},
1869
+ "VRMC_materials_mtoon": mtoon_dict,
1870
+ }
1871
+
1872
+ material_dict: dict[str, Json] = {"name": material.name}
1873
+ pbr_metallic_roughness_dict: dict[str, Json] = {}
1874
+
1875
+ if material.blend_method == "OPAQUE":
1876
+ material_dict["alphaMode"] = "OPAQUE"
1877
+ mtoon_dict["transparentWithZWrite"] = False
1878
+ mtoon_dict["renderQueueOffsetNumber"] = 0
1879
+ elif material.blend_method == "CLIP":
1880
+ alpha_cutoff = shader.get_float_value(node, "CutoffRate", 0, float_info.max)
1881
+ if alpha_cutoff is not None:
1882
+ material_dict["alphaCutoff"] = alpha_cutoff
1883
+ else:
1884
+ material_dict["alphaCutoff"] = 0.5
1885
+ material_dict["alphaMode"] = "MASK"
1886
+ mtoon_dict["transparentWithZWrite"] = False
1887
+ mtoon_dict["renderQueueOffsetNumber"] = 0
1888
+ else:
1889
+ material_dict["alphaMode"] = "BLEND"
1890
+ transparent_with_z_write = shader.get_float_value(
1891
+ node, "TransparentWithZWrite"
1892
+ )
1893
+ if (
1894
+ transparent_with_z_write is None
1895
+ or math.fabs(transparent_with_z_write) < float_info.epsilon
1896
+ ):
1897
+ mtoon_dict["transparentWithZWrite"] = False
1898
+ else:
1899
+ mtoon_dict["transparentWithZWrite"] = True
1900
+ mtoon_dict["renderQueueOffsetNumber"] = 0
1901
+ assign_dict(
1902
+ material_dict, "doubleSided", not material.use_backface_culling, False
1903
+ )
1904
+ assign_dict(
1905
+ pbr_metallic_roughness_dict,
1906
+ "baseColorFactor",
1907
+ shader.get_rgba_value(node, "DiffuseColor", 0.0, 1.0),
1908
+ )
1909
+ base_color_texture_dict = cls.create_mtoon0_texture_info_dict(
1910
+ context,
1911
+ json_dict,
1912
+ buffer0,
1913
+ node,
1914
+ "MainTexture",
1915
+ image_name_to_index_dict,
1916
+ gltf2_addon_export_settings,
1917
+ )
1918
+ assign_dict(
1919
+ pbr_metallic_roughness_dict, "baseColorTexture", base_color_texture_dict
1920
+ )
1921
+ assign_dict(
1922
+ mtoon_dict,
1923
+ "shadeColorFactor",
1924
+ shader.get_rgb_value(node, "ShadeColor", 0.0, 1.0),
1925
+ )
1926
+ shade_multiply_texture_dict = cls.create_mtoon0_texture_info_dict(
1927
+ context,
1928
+ json_dict,
1929
+ buffer0,
1930
+ node,
1931
+ "ShadeTexture",
1932
+ image_name_to_index_dict,
1933
+ gltf2_addon_export_settings,
1934
+ )
1935
+ if shade_multiply_texture_dict is not None:
1936
+ mtoon_dict["shadeMultiplyTexture"] = shade_multiply_texture_dict
1937
+ elif base_color_texture_dict is not None:
1938
+ # https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs#L185-L204
1939
+ mtoon_dict["shadeMultiplyTexture"] = base_color_texture_dict
1940
+ normal_texture_dict = cls.create_mtoon0_texture_info_dict(
1941
+ context,
1942
+ json_dict,
1943
+ buffer0,
1944
+ node,
1945
+ "NormalmapTexture",
1946
+ image_name_to_index_dict,
1947
+ gltf2_addon_export_settings,
1948
+ )
1949
+ if not normal_texture_dict:
1950
+ normal_texture_dict = cls.create_mtoon0_texture_info_dict(
1951
+ context,
1952
+ json_dict,
1953
+ buffer0,
1954
+ node,
1955
+ "NomalmapTexture",
1956
+ image_name_to_index_dict,
1957
+ gltf2_addon_export_settings,
1958
+ )
1959
+ if assign_dict(
1960
+ material_dict, "normalTexture", normal_texture_dict
1961
+ ) and isinstance(normal_texture_dict, dict):
1962
+ assign_dict(
1963
+ normal_texture_dict, "scale", shader.get_float_value(node, "BumpScale")
1964
+ )
1965
+
1966
+ shading_shift_0x = shader.get_float_value(node, "ShadeShift")
1967
+ if shading_shift_0x is None:
1968
+ shading_shift_0x = 0.0
1969
+
1970
+ shading_toony_0x = shader.get_float_value(node, "ShadeToony")
1971
+ if shading_toony_0x is None:
1972
+ shading_toony_0x = 0.0
1973
+
1974
+ mtoon_dict["shadingShiftFactor"] = convert.mtoon_shading_shift_0_to_1(
1975
+ shading_toony_0x, shading_shift_0x
1976
+ )
1977
+
1978
+ mtoon_dict["shadingToonyFactor"] = convert.mtoon_shading_toony_0_to_1(
1979
+ shading_toony_0x, shading_shift_0x
1980
+ )
1981
+
1982
+ gi_equalization_0x = shader.get_float_value(node, "IndirectLightIntensity")
1983
+ if gi_equalization_0x is not None:
1984
+ mtoon_dict["giEqualizationFactor"] = (
1985
+ convert.mtoon_intensity_to_gi_equalization(gi_equalization_0x)
1986
+ )
1987
+
1988
+ assign_dict(
1989
+ material_dict,
1990
+ "emissiveFactor",
1991
+ shader.get_rgb_value(node, "EmissionColor", 0.0, 1.0),
1992
+ )
1993
+ assign_dict(
1994
+ material_dict,
1995
+ "emissiveTexture",
1996
+ cls.create_mtoon0_texture_info_dict(
1997
+ context,
1998
+ json_dict,
1999
+ buffer0,
2000
+ node,
2001
+ "Emission_Texture",
2002
+ image_name_to_index_dict,
2003
+ gltf2_addon_export_settings,
2004
+ ),
2005
+ )
2006
+ if assign_dict(
2007
+ mtoon_dict,
2008
+ "matcapTexture",
2009
+ cls.create_mtoon0_texture_info_dict(
2010
+ context,
2011
+ json_dict,
2012
+ buffer0,
2013
+ node,
2014
+ "SphereAddTexture",
2015
+ image_name_to_index_dict,
2016
+ gltf2_addon_export_settings,
2017
+ ),
2018
+ ):
2019
+ mtoon_dict["matcapFactor"] = [1, 1, 1]
2020
+
2021
+ assign_dict(
2022
+ mtoon_dict,
2023
+ "parametricRimColorFactor",
2024
+ shader.get_rgb_value(node, "RimColor", 0.0, 1.0),
2025
+ )
2026
+ assign_dict(
2027
+ mtoon_dict,
2028
+ "parametricRimFresnelPowerFactor",
2029
+ shader.get_float_value(node, "RimFresnelPower", 0.0, float_info.max),
2030
+ )
2031
+ assign_dict(
2032
+ mtoon_dict,
2033
+ "parametricRimLiftFactor",
2034
+ shader.get_float_value(node, "RimLift"),
2035
+ )
2036
+ assign_dict(
2037
+ mtoon_dict,
2038
+ "rimMultiplyTexture",
2039
+ cls.create_mtoon0_texture_info_dict(
2040
+ context,
2041
+ json_dict,
2042
+ buffer0,
2043
+ node,
2044
+ "RimTexture",
2045
+ image_name_to_index_dict,
2046
+ gltf2_addon_export_settings,
2047
+ ),
2048
+ )
2049
+
2050
+ # https://github.com/vrm-c/UniVRM/blob/7c9919ef47a25c04100a2dcbe6a75dff49ef4857/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs#L287-L290
2051
+ mtoon_dict["rimLightingMixFactor"] = 1.0
2052
+
2053
+ centimeter_to_meter = 0.01
2054
+ one_hundredth = 0.01
2055
+
2056
+ outline_width_mode = shader.get_float_value(node, "OutlineWidthMode")
2057
+ if outline_width_mode is not None:
2058
+ outline_width_mode = round(outline_width_mode)
2059
+ else:
2060
+ outline_width_mode = 0
2061
+
2062
+ outline_width = shader.get_float_value(node, "OutlineWidth")
2063
+ if outline_width is None:
2064
+ outline_width = 0.0
2065
+
2066
+ if outline_width_mode == 1:
2067
+ mtoon_dict["outlineWidthMode"] = "worldCoordinates"
2068
+ mtoon_dict["outlineWidthFactor"] = max(
2069
+ 0.0, outline_width * centimeter_to_meter
2070
+ )
2071
+ elif outline_width_mode == 2:
2072
+ mtoon_dict["outlineWidthMode"] = "screenCoordinates"
2073
+ mtoon_dict["outlineWidthFactor"] = max(
2074
+ 0.0, outline_width * one_hundredth * 0.5
2075
+ )
2076
+ else:
2077
+ mtoon_dict["outlineWidthMode"] = "none"
2078
+
2079
+ assign_dict(
2080
+ mtoon_dict,
2081
+ "outlineWidthMultiplyTexture",
2082
+ cls.create_mtoon0_texture_info_dict(
2083
+ context,
2084
+ json_dict,
2085
+ buffer0,
2086
+ node,
2087
+ "OutlineWidthTexture",
2088
+ image_name_to_index_dict,
2089
+ gltf2_addon_export_settings,
2090
+ ),
2091
+ )
2092
+ assign_dict(
2093
+ mtoon_dict,
2094
+ "outlineColorFactor",
2095
+ shader.get_rgb_value(node, "OutlineColor", 0.0, 1.0),
2096
+ )
2097
+
2098
+ outline_color_mode = shader.get_float_value(node, "OutlineColorMode")
2099
+ if outline_color_mode is not None:
2100
+ outline_color_mode = round(outline_color_mode)
2101
+ else:
2102
+ outline_color_mode = 0
2103
+
2104
+ mtoon_dict["outlineLightingMixFactor"] = 0.0
2105
+ if outline_color_mode == 1:
2106
+ assign_dict(
2107
+ mtoon_dict,
2108
+ "outlineLightingMixFactor",
2109
+ shader.get_float_value(node, "OutlineLightingMix"),
2110
+ )
2111
+
2112
+ assign_dict(
2113
+ mtoon_dict,
2114
+ "uvAnimationMaskTexture",
2115
+ cls.create_mtoon0_texture_info_dict(
2116
+ context,
2117
+ json_dict,
2118
+ buffer0,
2119
+ node,
2120
+ "UV_Animation_Mask_Texture",
2121
+ image_name_to_index_dict,
2122
+ gltf2_addon_export_settings,
2123
+ ),
2124
+ )
2125
+ assign_dict(
2126
+ mtoon_dict,
2127
+ "uvAnimationRotationSpeedFactor",
2128
+ shader.get_float_value(node, "UV_Scroll_Rotation"),
2129
+ )
2130
+ assign_dict(
2131
+ mtoon_dict,
2132
+ "uvAnimationScrollXSpeedFactor",
2133
+ shader.get_float_value(node, "UV_Scroll_X"),
2134
+ )
2135
+
2136
+ invert_y = -1
2137
+ uv_animation_scroll_y_speed_factor = shader.get_float_value(node, "UV_Scroll_Y")
2138
+ if uv_animation_scroll_y_speed_factor is not None:
2139
+ mtoon_dict["uvAnimationScrollYSpeedFactor"] = (
2140
+ uv_animation_scroll_y_speed_factor * invert_y
2141
+ )
2142
+
2143
+ if pbr_metallic_roughness_dict:
2144
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
2145
+
2146
+ extensions_dict["VRMC_materials_mtoon"] = mtoon_dict
2147
+ material_dict["extensions"] = extensions_dict
2148
+
2149
+ return material_dict
2150
+
2151
+ @classmethod
2152
+ def save_vrm_materials(
2153
+ cls,
2154
+ context: Context,
2155
+ json_dict: dict[str, Json],
2156
+ buffer0: bytearray,
2157
+ material_name_to_index_dict: Mapping[str, int],
2158
+ image_name_to_index_dict: dict[str, int],
2159
+ gltf2_addon_export_settings: dict[str, object],
2160
+ ) -> None:
2161
+ material_dicts = json_dict.get("materials")
2162
+ if not isinstance(material_dicts, list):
2163
+ material_dicts = []
2164
+ json_dict["materials"] = material_dicts
2165
+
2166
+ for material_name, index in material_name_to_index_dict.items():
2167
+ material = context.blend_data.materials.get(material_name)
2168
+ if not isinstance(material, Material) or not (
2169
+ 0 <= index < len(material_dicts)
2170
+ ):
2171
+ continue
2172
+
2173
+ if get_material_extension(material).mtoon1.enabled:
2174
+ material_dicts[index] = cls.create_mtoon1_material_dict(
2175
+ json_dict,
2176
+ buffer0,
2177
+ material,
2178
+ image_name_to_index_dict,
2179
+ gltf2_addon_export_settings,
2180
+ )
2181
+ continue
2182
+
2183
+ mmd_material = MmdMaterial.try_parse(material)
2184
+ if mmd_material:
2185
+ material_dicts[index] = cls.create_mmd_material_dict(
2186
+ context,
2187
+ json_dict,
2188
+ buffer0,
2189
+ material,
2190
+ mmd_material,
2191
+ image_name_to_index_dict,
2192
+ gltf2_addon_export_settings,
2193
+ )
2194
+ continue
2195
+
2196
+ # MToon_unversioned (MToon for VRM 0.0)
2197
+ if not (legacy_addon_material := LegacyAddonMaterial.try_parse(material)):
2198
+ continue
2199
+ if legacy_addon_material.shader_name == "MToon_unversioned":
2200
+ material_dicts[index] = cls.create_mtoon_unversioned_material_dict(
2201
+ context,
2202
+ json_dict,
2203
+ buffer0,
2204
+ material,
2205
+ legacy_addon_material.shader_node_group,
2206
+ image_name_to_index_dict,
2207
+ gltf2_addon_export_settings,
2208
+ )
2209
+ elif legacy_addon_material.shader_name == "GLTF":
2210
+ material_dicts[index] = cls.create_legacy_gltf_material_dict(
2211
+ context,
2212
+ json_dict,
2213
+ buffer0,
2214
+ material,
2215
+ legacy_addon_material.shader_node_group,
2216
+ image_name_to_index_dict,
2217
+ gltf2_addon_export_settings,
2218
+ )
2219
+ elif legacy_addon_material.shader_name == "TRANSPARENT_ZWRITE":
2220
+ material_dicts[index] = (
2221
+ cls.create_legacy_transparent_zwrite_material_dict(
2222
+ context,
2223
+ json_dict,
2224
+ buffer0,
2225
+ material,
2226
+ legacy_addon_material.shader_node_group,
2227
+ image_name_to_index_dict,
2228
+ gltf2_addon_export_settings,
2229
+ )
2230
+ )
2231
+
2232
+ if material_dicts:
2233
+ json_dict["materials"] = material_dicts
2234
+
2235
+ @staticmethod
2236
+ def enter_disable_mtoon1_material_nodes(context: Context) -> Sequence[str]:
2237
+ disabled_material_names: list[str] = []
2238
+ for material in context.blend_data.materials:
2239
+ if not material:
2240
+ continue
2241
+ if not get_material_extension(material).mtoon1.enabled:
2242
+ continue
2243
+ link_or_unlink_gltf_material_nodes(context, material, link=False)
2244
+ disabled_material_names.append(material.name)
2245
+ return disabled_material_names
2246
+
2247
+ @staticmethod
2248
+ def leave_disable_mtoon1_material_nodes(
2249
+ context: Context, disabled_material_names: Sequence[str]
2250
+ ) -> None:
2251
+ for disabled_material_name in reversed(disabled_material_names):
2252
+ disabled_material = context.blend_data.materials.get(disabled_material_name)
2253
+ if not disabled_material:
2254
+ continue
2255
+ link_or_unlink_gltf_material_nodes(context, disabled_material, link=True)
2256
+
2257
+ @classmethod
2258
+ @contextmanager
2259
+ def disable_mtoon1_material_nodes(cls, context: Context) -> Generator[None]:
2260
+ disabled_material_names = cls.enter_disable_mtoon1_material_nodes(context)
2261
+ try:
2262
+ yield
2263
+ # After yield, bpy's native objects may be deleted or frames may advance
2264
+ # and become invalid. Accessing them in that state can cause crashes, so
2265
+ # be careful not to access such potentially invalid native objects
2266
+ # after yield
2267
+ finally:
2268
+ cls.leave_disable_mtoon1_material_nodes(context, disabled_material_names)
2269
+
2270
+ @classmethod
2271
+ def unassign_normal_from_mtoon_primitive_morph_target(
2272
+ cls,
2273
+ context: Context,
2274
+ json_dict: dict[str, Json],
2275
+ material_name_to_index_dict: Mapping[str, int],
2276
+ ) -> None:
2277
+ mesh_dicts = json_dict.get("meshes")
2278
+ if not isinstance(mesh_dicts, list):
2279
+ return
2280
+ for mesh_dict in mesh_dicts:
2281
+ if not isinstance(mesh_dict, dict):
2282
+ continue
2283
+ primitive_dicts = mesh_dict.get("primitives")
2284
+ if not isinstance(primitive_dicts, list):
2285
+ continue
2286
+ for primitive_dict in primitive_dicts:
2287
+ if not isinstance(primitive_dict, dict):
2288
+ continue
2289
+ material_index = primitive_dict.get("material")
2290
+ if not isinstance(material_index, int):
2291
+ continue
2292
+
2293
+ skip = True
2294
+ for (
2295
+ search_material_name,
2296
+ search_material_index,
2297
+ ) in material_name_to_index_dict.items():
2298
+ if material_index != search_material_index:
2299
+ continue
2300
+ material = context.blend_data.materials.get(search_material_name)
2301
+ if not material:
2302
+ continue
2303
+ mtoon1 = get_material_extension(material).mtoon1
2304
+ if mtoon1.export_shape_key_normals:
2305
+ continue
2306
+ if mtoon1.enabled or (
2307
+ (
2308
+ legacy_addon_material := LegacyAddonMaterial.try_parse(
2309
+ material
2310
+ )
2311
+ )
2312
+ and legacy_addon_material.shader_name == "MToon_unversioned"
2313
+ ):
2314
+ skip = False
2315
+ break
2316
+ if skip:
2317
+ continue
2318
+
2319
+ target_dicts = primitive_dict.get("targets")
2320
+ if not isinstance(target_dicts, list):
2321
+ continue
2322
+ for target_dict in target_dicts:
2323
+ if not isinstance(target_dict, dict):
2324
+ continue
2325
+ target_dict.pop("NORMAL", None)
2326
+
2327
+ @staticmethod
2328
+ def enter_setup_flexible_hierarchy_bones(
2329
+ context: Context,
2330
+ armature_object: Object,
2331
+ export_objects: Sequence[Object],
2332
+ ) -> Optional[FlexibleHierarchyBoneSetup]:
2333
+ armature_data = armature_object.data
2334
+ if not isinstance(armature_data, Armature):
2335
+ return None
2336
+ ext = get_armature_extension(armature_data)
2337
+ human_bones = ext.vrm1.humanoid.human_bones
2338
+ if human_bones.filter_by_human_bone_hierarchy:
2339
+ return None
2340
+
2341
+ assigned_human_bone_specification_to_bone_name: dict[
2342
+ HumanBoneSpecification, str
2343
+ ] = {}
2344
+ for (
2345
+ human_bone_name,
2346
+ human_bone,
2347
+ ) in human_bones.human_bone_name_to_human_bone().items():
2348
+ if not human_bone.node.bone_name:
2349
+ continue
2350
+ if human_bone.node.bone_name not in armature_data.bones:
2351
+ continue
2352
+ specification = HumanBoneSpecifications.get(human_bone_name)
2353
+ assigned_human_bone_specification_to_bone_name[specification] = (
2354
+ human_bone.node.bone_name
2355
+ )
2356
+
2357
+ if not assigned_human_bone_specification_to_bone_name:
2358
+ return None
2359
+
2360
+ child_bone_name_to_parent_bone_name = (
2361
+ Vrm1Exporter.create_child_bone_name_to_parent_bone_name(
2362
+ armature_data=armature_data,
2363
+ assigned_human_bone_specification_to_bone_name=(
2364
+ assigned_human_bone_specification_to_bone_name
2365
+ ),
2366
+ all_human_bone_specifications=HumanBoneSpecifications.all_human_bones,
2367
+ )
2368
+ )
2369
+ if not child_bone_name_to_parent_bone_name:
2370
+ return None
2371
+
2372
+ bone_name_and_muted_constraint_name: list[tuple[str, str]] = []
2373
+ _, bone_constraints, _ = search.export_constraints(
2374
+ export_objects, armature_object
2375
+ )
2376
+ vrm_constraints = {
2377
+ vrm_constraint for _, vrm_constraint in bone_constraints.all_constraints
2378
+ }
2379
+ for pose_bone in armature_object.pose.bones:
2380
+ for constraint in pose_bone.constraints:
2381
+ if not isinstance(constraint, Constraint):
2382
+ continue
2383
+ if constraint.mute:
2384
+ continue
2385
+ if constraint in vrm_constraints:
2386
+ continue
2387
+ bone_name_and_muted_constraint_name.append(
2388
+ (pose_bone.name, constraint.name)
2389
+ )
2390
+ constraint.mute = True
2391
+
2392
+ return FlexibleHierarchyBoneSetup(
2393
+ original_bone_settings=Vrm1Exporter.reparent_edit_bones(
2394
+ context,
2395
+ armature_object,
2396
+ assigned_human_bone_specification_to_bone_name,
2397
+ child_bone_name_to_parent_bone_name,
2398
+ ),
2399
+ pose_bone_name_and_muted_constraint_name=bone_name_and_muted_constraint_name,
2400
+ )
2401
+
2402
+ @staticmethod
2403
+ def enter_setup_dummy_human_bones(
2404
+ context: Context, armature_object: Object
2405
+ ) -> Optional[Mapping[HumanBoneName, str]]:
2406
+ armature_data = armature_object.data
2407
+ if not isinstance(armature_data, Armature):
2408
+ return None
2409
+ ext = get_armature_extension(armature_data)
2410
+ human_bones = ext.vrm1.humanoid.human_bones
2411
+ if (
2412
+ not human_bones.allow_non_humanoid_rig
2413
+ and human_bones.bones_are_correctly_assigned()
2414
+ ):
2415
+ return None
2416
+
2417
+ human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
2418
+ human_bone_name_to_bone_name: dict[HumanBoneName, str] = {}
2419
+ for human_bone_name, human_bone in human_bone_name_to_human_bone.items():
2420
+ if not human_bone.node.bone_name:
2421
+ continue
2422
+ human_bone_name_to_bone_name[human_bone_name] = human_bone.node.bone_name
2423
+ human_bone.node.bone_name = ""
2424
+
2425
+ with save_workspace(context, armature_object, mode="EDIT"):
2426
+ hips_bone = armature_data.edit_bones.new(HumanBoneName.HIPS.value)
2427
+ hips_bone.head = Vector((0, 0, 0.5))
2428
+ hips_bone.tail = Vector((0, 1, 0.5))
2429
+ hips_bone_name = deepcopy(hips_bone.name)
2430
+
2431
+ right_upper_leg_bone = armature_data.edit_bones.new(
2432
+ HumanBoneName.RIGHT_UPPER_LEG.value
2433
+ )
2434
+ right_upper_leg_bone.head = Vector((-0.125, 0, 0.5))
2435
+ right_upper_leg_bone.tail = Vector((-0.125, 1, 0.5))
2436
+ right_upper_leg_bone.parent = hips_bone
2437
+ right_upper_leg_bone_name = deepcopy(right_upper_leg_bone.name)
2438
+
2439
+ right_lower_leg_bone = armature_data.edit_bones.new(
2440
+ HumanBoneName.RIGHT_LOWER_LEG.value
2441
+ )
2442
+ right_lower_leg_bone.head = Vector((-0.125, 0, 0.25))
2443
+ right_lower_leg_bone.tail = Vector((-0.125, 1, 0.25))
2444
+ right_lower_leg_bone.parent = right_upper_leg_bone
2445
+ right_lower_leg_bone_name = deepcopy(right_lower_leg_bone.name)
2446
+
2447
+ right_foot_bone = armature_data.edit_bones.new(
2448
+ HumanBoneName.RIGHT_FOOT.value
2449
+ )
2450
+ right_foot_bone.head = Vector((-0.125, 0, 0))
2451
+ right_foot_bone.tail = Vector((-0.125, 1, 0))
2452
+ right_foot_bone.parent = right_lower_leg_bone
2453
+ right_foot_bone_name = deepcopy(right_foot_bone.name)
2454
+
2455
+ left_upper_leg_bone = armature_data.edit_bones.new(
2456
+ HumanBoneName.LEFT_UPPER_LEG.value
2457
+ )
2458
+ left_upper_leg_bone.head = Vector((0.125, 0, 0.5))
2459
+ left_upper_leg_bone.tail = Vector((0.125, 1, 0.5))
2460
+ left_upper_leg_bone.parent = hips_bone
2461
+ left_upper_leg_bone_name = deepcopy(left_upper_leg_bone.name)
2462
+
2463
+ left_lower_leg_bone = armature_data.edit_bones.new(
2464
+ HumanBoneName.LEFT_LOWER_LEG.value
2465
+ )
2466
+ left_lower_leg_bone.head = Vector((0.125, 0, 0.25))
2467
+ left_lower_leg_bone.tail = Vector((0.125, 1, 0.25))
2468
+ left_lower_leg_bone.parent = left_upper_leg_bone
2469
+ left_lower_leg_bone_name = deepcopy(left_lower_leg_bone.name)
2470
+
2471
+ left_foot_bone = armature_data.edit_bones.new(HumanBoneName.LEFT_FOOT.value)
2472
+ left_foot_bone.head = Vector((0.125, 0, 0))
2473
+ left_foot_bone.tail = Vector((0.125, 1, 0))
2474
+ left_foot_bone.parent = left_lower_leg_bone
2475
+ left_foot_bone_name = deepcopy(left_foot_bone.name)
2476
+
2477
+ spine_bone = armature_data.edit_bones.new(HumanBoneName.SPINE.value)
2478
+ spine_bone.head = Vector((0, 0, 0.625))
2479
+ spine_bone.tail = Vector((0, 1, 0.625))
2480
+ spine_bone.parent = hips_bone
2481
+ spine_bone_name = deepcopy(spine_bone.name)
2482
+
2483
+ right_upper_arm_bone = armature_data.edit_bones.new(
2484
+ HumanBoneName.RIGHT_UPPER_ARM.value
2485
+ )
2486
+ right_upper_arm_bone.head = Vector((-0.125, 0, 0.75))
2487
+ right_upper_arm_bone.tail = Vector((-0.125, 1, 0.75))
2488
+ right_upper_arm_bone.parent = spine_bone
2489
+ right_upper_arm_bone_name = deepcopy(right_upper_arm_bone.name)
2490
+
2491
+ right_lower_arm_bone = armature_data.edit_bones.new(
2492
+ HumanBoneName.RIGHT_LOWER_ARM.value
2493
+ )
2494
+ right_lower_arm_bone.head = Vector((-0.25, 0, 0.75))
2495
+ right_lower_arm_bone.tail = Vector((-0.25, 1, 0.75))
2496
+ right_lower_arm_bone.parent = right_upper_arm_bone
2497
+ right_lower_arm_bone_name = deepcopy(right_lower_arm_bone.name)
2498
+
2499
+ right_hand_bone = armature_data.edit_bones.new(
2500
+ HumanBoneName.RIGHT_HAND.value
2501
+ )
2502
+ right_hand_bone.head = Vector((-0.375, 0, 0.75))
2503
+ right_hand_bone.tail = Vector((-0.375, 1, 0.75))
2504
+ right_hand_bone.parent = right_lower_arm_bone
2505
+ right_hand_bone_name = deepcopy(right_hand_bone.name)
2506
+
2507
+ left_upper_arm_bone = armature_data.edit_bones.new(
2508
+ HumanBoneName.LEFT_UPPER_ARM.value
2509
+ )
2510
+ left_upper_arm_bone.head = Vector((0.125, 0, 0.75))
2511
+ left_upper_arm_bone.tail = Vector((0.125, 1, 0.75))
2512
+ left_upper_arm_bone.parent = spine_bone
2513
+ left_upper_arm_bone_name = deepcopy(left_upper_arm_bone.name)
2514
+
2515
+ left_lower_arm_bone = armature_data.edit_bones.new(
2516
+ HumanBoneName.LEFT_LOWER_ARM.value
2517
+ )
2518
+ left_lower_arm_bone.head = Vector((0.25, 0, 0.75))
2519
+ left_lower_arm_bone.tail = Vector((0.25, 1, 0.75))
2520
+ left_lower_arm_bone.parent = left_upper_arm_bone
2521
+ left_lower_arm_bone_name = deepcopy(left_lower_arm_bone.name)
2522
+
2523
+ left_hand_bone = armature_data.edit_bones.new(HumanBoneName.LEFT_HAND.value)
2524
+ left_hand_bone.head = Vector((0.375, 0, 0.75))
2525
+ left_hand_bone.tail = Vector((0.375, 1, 0.75))
2526
+ left_hand_bone.parent = left_lower_arm_bone
2527
+ left_hand_bone_name = deepcopy(left_hand_bone.name)
2528
+
2529
+ head_bone = armature_data.edit_bones.new(HumanBoneName.HEAD.value)
2530
+ head_bone.head = Vector((0, 0, 0.75))
2531
+ head_bone.tail = Vector((0, 1, 0.75))
2532
+ head_bone.parent = spine_bone
2533
+ head_bone_name = deepcopy(head_bone.name)
2534
+
2535
+ Vrm1HumanBonesPropertyGroup.update_all_bone_name_candidates(
2536
+ context, armature_data.name
2537
+ )
2538
+
2539
+ human_bones.head.node.bone_name = head_bone_name
2540
+ human_bones.spine.node.bone_name = spine_bone_name
2541
+ human_bones.hips.node.bone_name = hips_bone_name
2542
+
2543
+ human_bones.right_upper_arm.node.bone_name = right_upper_arm_bone_name
2544
+ human_bones.right_lower_arm.node.bone_name = right_lower_arm_bone_name
2545
+ human_bones.right_hand.node.bone_name = right_hand_bone_name
2546
+
2547
+ human_bones.left_upper_arm.node.bone_name = left_upper_arm_bone_name
2548
+ human_bones.left_lower_arm.node.bone_name = left_lower_arm_bone_name
2549
+ human_bones.left_hand.node.bone_name = left_hand_bone_name
2550
+
2551
+ human_bones.right_upper_leg.node.bone_name = right_upper_leg_bone_name
2552
+ human_bones.right_lower_leg.node.bone_name = right_lower_leg_bone_name
2553
+ human_bones.right_foot.node.bone_name = right_foot_bone_name
2554
+
2555
+ human_bones.left_upper_leg.node.bone_name = left_upper_leg_bone_name
2556
+ human_bones.left_lower_leg.node.bone_name = left_lower_leg_bone_name
2557
+ human_bones.left_foot.node.bone_name = left_foot_bone_name
2558
+
2559
+ return human_bone_name_to_bone_name
2560
+
2561
+ @staticmethod
2562
+ def leave_setup_dummy_human_bones(
2563
+ context: Context,
2564
+ armature_object: Object,
2565
+ human_bone_name_to_bone_name: Mapping[HumanBoneName, str],
2566
+ ) -> None:
2567
+ armature_data = armature_object.data
2568
+ if not isinstance(armature_data, Armature):
2569
+ return
2570
+
2571
+ human_bones = get_armature_extension(armature_data).vrm1.humanoid.human_bones
2572
+
2573
+ human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
2574
+ dummy_bone_names = deepcopy(
2575
+ [
2576
+ human_bone.node.bone_name
2577
+ for human_bone in human_bone_name_to_human_bone.values()
2578
+ if human_bone.node.bone_name
2579
+ ]
2580
+ )
2581
+
2582
+ for human_bone_name, human_bone in reversed(
2583
+ list(human_bone_name_to_human_bone.items())
2584
+ ):
2585
+ bone_name = human_bone_name_to_bone_name.get(human_bone_name)
2586
+ if bone_name:
2587
+ human_bone.node.bone_name = bone_name
2588
+ else:
2589
+ human_bone.node.bone_name = ""
2590
+ Vrm1HumanBonesPropertyGroup.update_all_bone_name_candidates(
2591
+ context, armature_data.name
2592
+ )
2593
+ with save_workspace(context, armature_object, mode="EDIT"):
2594
+ for dummy_bone_name in reversed(dummy_bone_names):
2595
+ dummy_edit_bone = armature_data.edit_bones.get(dummy_bone_name)
2596
+ if dummy_edit_bone:
2597
+ armature_data.edit_bones.remove(dummy_edit_bone)
2598
+
2599
+ @classmethod
2600
+ @contextmanager
2601
+ def setup_dummy_human_bones(
2602
+ cls,
2603
+ context: Context,
2604
+ armature: Object,
2605
+ ) -> Generator[None]:
2606
+ human_bone_name_to_bone_name = cls.enter_setup_dummy_human_bones(
2607
+ context,
2608
+ armature,
2609
+ )
2610
+ try:
2611
+ yield
2612
+ # After yield, bpy's native objects may be deleted or frames may advance
2613
+ # and become invalid. Accessing them in that state can cause crashes, so
2614
+ # be careful not to access such potentially invalid native objects
2615
+ # after yield
2616
+ finally:
2617
+ if human_bone_name_to_bone_name is not None:
2618
+ cls.leave_setup_dummy_human_bones(
2619
+ context,
2620
+ armature,
2621
+ human_bone_name_to_bone_name,
2622
+ )
2623
+
2624
+ @staticmethod
2625
+ def clear_constrainted_rotation(
2626
+ constraint: CopyRotationConstraint, object_or_bone: Union[Object, PoseBone]
2627
+ ) -> None:
2628
+ euler_order = constraint.euler_order
2629
+ if euler_order == "AUTO":
2630
+ if object_or_bone.rotation_mode in ROTATION_MODE_EULER:
2631
+ euler_order = object_or_bone.rotation_mode
2632
+ else:
2633
+ euler_order = "XYZ"
2634
+ euler = get_rotation_as_quaternion(object_or_bone).to_euler(euler_order)
2635
+ if constraint.use_x:
2636
+ euler.x = 0
2637
+ if constraint.use_y:
2638
+ euler.y = 0
2639
+ if constraint.use_z:
2640
+ euler.z = 0
2641
+ set_rotation_without_mode_change(object_or_bone, euler.to_quaternion())
2642
+
2643
+ @staticmethod
2644
+ def enter_disable_constraints(
2645
+ context: Context, armature: Object, export_objects: Sequence[Object]
2646
+ ) -> tuple[Sequence[tuple[str, str]], Sequence[tuple[str, str]]]:
2647
+ constraint: Optional[Constraint] = None
2648
+
2649
+ object_constraints, bone_constraints, _ = search.export_constraints(
2650
+ export_objects, armature
2651
+ )
2652
+ object_name_and_constraint_name: list[tuple[str, str]] = []
2653
+ for object_name, constraint in object_constraints.all_constraints:
2654
+ obj = context.blend_data.objects.get(object_name)
2655
+ if not obj:
2656
+ continue
2657
+ if isinstance(constraint, CopyRotationConstraint):
2658
+ Vrm1Exporter.clear_constrainted_rotation(constraint, obj)
2659
+ else:
2660
+ set_rotation_without_mode_change(obj, Quaternion())
2661
+ constraint.mute = True
2662
+ object_name_and_constraint_name.append((object_name, constraint.name))
2663
+
2664
+ bone_name_and_constraint_name: list[tuple[str, str]] = []
2665
+ for bone_name, constraint in bone_constraints.all_constraints:
2666
+ bone = armature.pose.bones.get(bone_name)
2667
+ if not bone:
2668
+ continue
2669
+ if isinstance(constraint, CopyRotationConstraint):
2670
+ Vrm1Exporter.clear_constrainted_rotation(constraint, bone)
2671
+ else:
2672
+ set_rotation_without_mode_change(bone, Quaternion())
2673
+ constraint.mute = True
2674
+ bone_name_and_constraint_name.append((bone_name, constraint.name))
2675
+
2676
+ return (object_name_and_constraint_name, bone_name_and_constraint_name)
2677
+
2678
+ @staticmethod
2679
+ def leave_disable_constraints(
2680
+ context: Context,
2681
+ armature: Object,
2682
+ object_name_and_constraint_name: Sequence[tuple[str, str]],
2683
+ bone_name_and_constraint_name: Sequence[tuple[str, str]],
2684
+ ) -> None:
2685
+ # Consider the possibility that Object or Constraint may have been
2686
+ # deleted, and retrieve them again
2687
+
2688
+ for bone_name, constraint_name in reversed(list(bone_name_and_constraint_name)):
2689
+ bone = armature.pose.bones.get(bone_name)
2690
+ if not bone:
2691
+ continue
2692
+ constraint = bone.constraints.get(constraint_name)
2693
+ if not isinstance(constraint, Constraint):
2694
+ continue
2695
+ constraint.mute = False
2696
+
2697
+ for object_name, constraint_name in reversed(
2698
+ list(object_name_and_constraint_name)
2699
+ ):
2700
+ obj = context.blend_data.objects.get(object_name)
2701
+ if not obj:
2702
+ continue
2703
+ constraint = obj.constraints.get(constraint_name)
2704
+ if not isinstance(constraint, Constraint):
2705
+ continue
2706
+ constraint.mute = False
2707
+
2708
+ @contextmanager
2709
+ def disable_constraints(self, context: Context) -> Generator[None]:
2710
+ object_name_and_constraint_name, bone_name_and_constraint_name = (
2711
+ self.enter_disable_constraints(
2712
+ context, self._armature, self._export_objects
2713
+ )
2714
+ )
2715
+ try:
2716
+ yield
2717
+ # After yield, bpy's native objects may be deleted or frames may advance
2718
+ # and become invalid. Accessing them in that state can cause crashes, so
2719
+ # be careful not to access such potentially invalid native objects
2720
+ # after yield
2721
+ finally:
2722
+ self.leave_disable_constraints(
2723
+ context,
2724
+ self._armature,
2725
+ object_name_and_constraint_name,
2726
+ bone_name_and_constraint_name,
2727
+ )
2728
+
2729
+ def enter_assign_export_custom_properties(self) -> None:
2730
+ self._armature[self._extras_main_armature_key] = True
2731
+
2732
+ armature_data = self._armature.data
2733
+ if not isinstance(armature_data, Armature):
2734
+ return
2735
+
2736
+ # To minimize the impact of other glTF2ExportUserExtensions,
2737
+ # use custom properties that seem to have less impact
2738
+ # to establish correspondence between Blender objects and indices.
2739
+ for obj in self._context.blend_data.objects:
2740
+ obj[self._extras_object_name_key] = obj.name
2741
+ for material in self._context.blend_data.materials:
2742
+ material[self._extras_material_name_key] = material.name
2743
+ for mesh in self._context.blend_data.meshes:
2744
+ mesh[self._extras_mesh_name_key] = mesh.name
2745
+
2746
+ # The glTF 2.0 addon comment states that it saves custom properties with
2747
+ # PoseBone, but it actually references custom properties of Bone.
2748
+ # Therefore, write to both just in case
2749
+ for pose_bone in self._armature.pose.bones:
2750
+ pose_bone[self._extras_bone_name_key] = pose_bone.name
2751
+ for bone in armature_data.bones:
2752
+ bone[self._extras_bone_name_key] = bone.name
2753
+
2754
+ def leave_assign_export_custom_properties(self) -> None:
2755
+ armature_data = self._armature.data
2756
+ if not isinstance(armature_data, Armature):
2757
+ return
2758
+
2759
+ for bone in reversed(armature_data.bones):
2760
+ bone.pop(self._extras_bone_name_key, None)
2761
+ for pose_bone in reversed(self._armature.pose.bones):
2762
+ pose_bone.pop(self._extras_bone_name_key, None)
2763
+
2764
+ for mesh in reversed(self._context.blend_data.meshes):
2765
+ mesh.pop(self._extras_mesh_name_key, None)
2766
+ for material in reversed(self._context.blend_data.materials):
2767
+ material.pop(self._extras_material_name_key, None)
2768
+ for obj in reversed(self._context.blend_data.objects):
2769
+ obj.pop(self._extras_object_name_key, None)
2770
+
2771
+ self._armature.pop(self._extras_main_armature_key, None)
2772
+
2773
+ @contextmanager
2774
+ def assign_export_custom_properties(self) -> Generator[None]:
2775
+ self.enter_assign_export_custom_properties()
2776
+ try:
2777
+ yield
2778
+ # After yield, bpy's native objects may be deleted or frames may advance
2779
+ # and become invalid. Accessing them in that state can cause crashes, so
2780
+ # be careful not to access such potentially invalid native objects
2781
+ # after yield
2782
+ finally:
2783
+ self.leave_assign_export_custom_properties()
2784
+
2785
+ @staticmethod
2786
+ def gltf_export_armature_object_remove(
2787
+ context: Context, mesh_object_names: Sequence[str]
2788
+ ) -> bool:
2789
+ """Return whether to enable export_armature_object_remove.
2790
+
2791
+ export_armature_object_remove is very useful and we'd like to use it,
2792
+ but it has many bugs, so enable it only when not affected by those bugs.
2793
+ """
2794
+ if bpy.app.version < (4, 2):
2795
+ return False
2796
+
2797
+ if bpy.app.version < (4, 4):
2798
+ # Root bone with non-deform bones returns False
2799
+ # https://github.com/KhronosGroup/glTF-Blender-IO/issues/2394
2800
+ for selected_object in context.selected_objects:
2801
+ if selected_object.type != "ARMATURE":
2802
+ continue
2803
+ armature = selected_object.data
2804
+ if not isinstance(armature, Armature):
2805
+ continue
2806
+ for bone in armature.bones:
2807
+ if bone.parent:
2808
+ continue
2809
+ if not bone.use_deform:
2810
+ return False
2811
+
2812
+ # Return False if there are vertices with Armature modifier but no weights
2813
+ # https://github.com/KhronosGroup/glTF-Blender-IO/issues/2436
2814
+ armature_data_name_to_deform_bone_names: dict[str, set[str]] = {}
2815
+ for mesh_object_name in mesh_object_names:
2816
+ mesh_object = context.blend_data.objects.get(mesh_object_name)
2817
+ if not mesh_object:
2818
+ continue
2819
+ if mesh_object.type != "MESH":
2820
+ continue
2821
+ mesh = mesh_object.data
2822
+ if not isinstance(mesh, Mesh):
2823
+ continue
2824
+
2825
+ vertex_group_names_set: Optional[set[tuple[str, ...]]] = None
2826
+
2827
+ for modifier in mesh_object.modifiers:
2828
+ if modifier.type != "ARMATURE":
2829
+ continue
2830
+ if not isinstance(modifier, ArmatureModifier):
2831
+ continue
2832
+ if not modifier.show_viewport:
2833
+ continue
2834
+ armature_object = modifier.object
2835
+ if not armature_object:
2836
+ continue
2837
+ armature_data = armature_object.data
2838
+ if not isinstance(armature_data, Armature):
2839
+ continue
2840
+ deform_bone_names = armature_data_name_to_deform_bone_names.get(
2841
+ armature_data.name
2842
+ )
2843
+ if deform_bone_names is None:
2844
+ deform_bone_names = {
2845
+ bone.name for bone in armature_data.bones if bone.use_deform
2846
+ }
2847
+ armature_data_name_to_deform_bone_names[armature_data.name] = (
2848
+ deform_bone_names
2849
+ )
2850
+
2851
+ if vertex_group_names_set is None:
2852
+ vertex_groups = mesh_object.vertex_groups
2853
+ vertex_group_names_set = {
2854
+ tuple(
2855
+ vertex_groups[vertex_group_index].name
2856
+ for vertex_group_index in vertex_group_indices
2857
+ if 0 <= vertex_group_index < len(vertex_groups)
2858
+ )
2859
+ for vertex_group_indices in {
2860
+ tuple(
2861
+ sorted(
2862
+ {
2863
+ group.group
2864
+ for group in vertex.groups
2865
+ if group.weight > 0
2866
+ }
2867
+ )
2868
+ )
2869
+ for vertex in mesh.vertices
2870
+ }
2871
+ }
2872
+
2873
+ for vertex_group_names in vertex_group_names_set:
2874
+ if all(
2875
+ vertex_group_name not in deform_bone_names
2876
+ for vertex_group_name in vertex_group_names
2877
+ ):
2878
+ return False
2879
+
2880
+ return True
2881
+
2882
+ def export(self) -> Optional[bytes]:
2883
+ init_extras_export()
2884
+
2885
+ armature_data = self._armature.data
2886
+ if not isinstance(armature_data, Armature):
2887
+ message = f"{type(armature_data)} is not an Armature"
2888
+ raise TypeError(message)
2889
+
2890
+ self.setup_mtoon_gltf_fallback_nodes(self._context, is_vrm0=False)
2891
+
2892
+ with (
2893
+ save_workspace(self._context),
2894
+ self.setup_flexible_hierarchy_bones(
2895
+ self._context, self._armature, self._export_objects
2896
+ ),
2897
+ self.setup_dummy_human_bones(self._context, self._armature),
2898
+ self.clear_blend_shape_proxy_previews(self._context, armature_data),
2899
+ self.enable_deform_for_all_referenced_bones(armature_data),
2900
+ setup_humanoid_t_pose(self._context, self._armature),
2901
+ self.overwrite_object_visibility_and_selection(),
2902
+ ):
2903
+ with (
2904
+ self.disable_constraints(self._context),
2905
+ self.hide_mtoon1_outline_geometry_nodes(self._context),
2906
+ self.disable_mtoon1_material_nodes(self._context),
2907
+ self.mount_skinned_mesh_parent(),
2908
+ self.save_selected_mesh_compat_objects() as mesh_compat_object_names,
2909
+ self.assign_export_custom_properties(),
2910
+ tempfile.TemporaryDirectory() as temp_dir,
2911
+ ):
2912
+ _force_apply_modifiers_to_objects(
2913
+ self._context, self._armature, mesh_compat_object_names
2914
+ )
2915
+ _remove_inactive_uv_maps(self._context, mesh_compat_object_names)
2916
+
2917
+ filepath = Path(temp_dir, "out.glb")
2918
+ export_scene_gltf_result = export_scene_gltf(
2919
+ ExportSceneGltfArguments(
2920
+ filepath=str(filepath),
2921
+ check_existing=False,
2922
+ export_format="GLB",
2923
+ export_extras=True,
2924
+ export_def_bones=True,
2925
+ export_current_frame=True,
2926
+ use_selection=True,
2927
+ use_active_scene=True,
2928
+ export_animations=self._export_gltf_animations,
2929
+ export_armature_object_remove=(
2930
+ self.gltf_export_armature_object_remove(
2931
+ self._context, mesh_compat_object_names
2932
+ )
2933
+ ),
2934
+ export_rest_position_armature=False,
2935
+ export_apply=False,
2936
+ # Models may appear incorrectly in many viewers
2937
+ export_all_influences=self._export_all_influences,
2938
+ export_lights=self._export_lights,
2939
+ export_try_sparse_sk=self._export_try_sparse_sk,
2940
+ export_vertex_color="MATERIAL",
2941
+ )
2942
+ )
2943
+ if export_scene_gltf_result == {"CANCELLED"}:
2944
+ return None
2945
+ if export_scene_gltf_result != {"FINISHED"}:
2946
+ message = (
2947
+ 'The glTF2 Exporter has not been {"FINISHED"}'
2948
+ f" but {export_scene_gltf_result}"
2949
+ )
2950
+ raise AssertionError(message)
2951
+ extra_name_assigned_glb = filepath.read_bytes()
2952
+ vrm_bytes = self.add_vrm_extension_to_glb(extra_name_assigned_glb)
2953
+ if vrm_bytes is None:
2954
+ return None
2955
+ _logger.info("Generated VRM size: %s bytes", len(vrm_bytes))
2956
+ return vrm_bytes
2957
+
2958
+ def remove_exported_armature_object_before_4_2(
2959
+ self,
2960
+ json_dict: dict[str, Json],
2961
+ node_dicts: list[Json],
2962
+ armature_node_index: int,
2963
+ ) -> None:
2964
+ """Delete scene armature object if it can be deleted in Blender < 4.2.
2965
+
2966
+ This is to prevent the Armature object from being treated as a bone
2967
+ when re-importing in Blender. TODO: Should be handled more intelligently
2968
+ using skin.skeleton etc.
2969
+
2970
+ When the main armature has transforms, the current method doesn't work
2971
+ well with Blender 4.2.1, UniVRM 0.126.0, etc., so we don't do it.
2972
+
2973
+ When there are multiple root bones with Skin Joints registered,
2974
+ skin.joints requires a common root bone constraint, so we don't delete it.
2975
+ """
2976
+ if bpy.app.version >= (4, 2):
2977
+ return
2978
+
2979
+ armature_world_matrix = (
2980
+ _find_node_world_matrix(node_dicts, armature_node_index, None) or Matrix()
2981
+ )
2982
+
2983
+ if not (0 <= armature_node_index < len(node_dicts)):
2984
+ return
2985
+ armature_node_dict = node_dicts[armature_node_index]
2986
+ if not isinstance(armature_node_dict, dict):
2987
+ return
2988
+
2989
+ if not is_identity_matrix(armature_world_matrix):
2990
+ return
2991
+
2992
+ scene_dicts = json_dict.get("scenes")
2993
+ if not isinstance(scene_dicts, list):
2994
+ return
2995
+
2996
+ all_joint_node_indices: list[Sequence[int]] = []
2997
+
2998
+ skin_dicts = json_dict.get("skins")
2999
+ if isinstance(skin_dicts, list):
3000
+ for skin_dict in skin_dicts:
3001
+ if not isinstance(skin_dict, dict):
3002
+ continue
3003
+ joints = skin_dict.get("joints")
3004
+ if not isinstance(joints, list):
3005
+ continue
3006
+ all_joint_node_indices.append(
3007
+ [
3008
+ joint_node_index
3009
+ for joint_node_index in joints
3010
+ if isinstance(joint_node_index, int)
3011
+ ]
3012
+ )
3013
+
3014
+ armature_child_indices = (
3015
+ [child for child in children if isinstance(child, int)]
3016
+ if isinstance(children := armature_node_dict.get("children"), Sequence)
3017
+ else []
3018
+ )
3019
+
3020
+ for joint_node_indices in all_joint_node_indices:
3021
+ if len(set(joint_node_indices).intersection(armature_child_indices)) >= 2:
3022
+ return
3023
+
3024
+ armature_replaced = False
3025
+ for scene_dict in scene_dicts:
3026
+ if not isinstance(scene_dict, dict):
3027
+ continue
3028
+ scene_node_indices = scene_dict.get("nodes")
3029
+ if not isinstance(scene_node_indices, list):
3030
+ continue
3031
+
3032
+ # Remove ancestor nodes of that armature among nodes belonging to the scene
3033
+ for scene_node_index in list(scene_node_indices):
3034
+ if not isinstance(scene_node_index, int):
3035
+ continue
3036
+ search_scene_node_indices = [scene_node_index]
3037
+ while search_scene_node_indices:
3038
+ search_scene_node_index = search_scene_node_indices.pop()
3039
+ if search_scene_node_index == armature_node_index:
3040
+ scene_node_indices.remove(scene_node_index)
3041
+ break
3042
+ if not (0 <= search_scene_node_index < len(node_dicts)):
3043
+ continue
3044
+ search_scene_node_dict = node_dicts[search_scene_node_index]
3045
+ if not isinstance(search_scene_node_dict, dict):
3046
+ continue
3047
+ child_indices = search_scene_node_dict.get("children")
3048
+ if not isinstance(child_indices, list):
3049
+ continue
3050
+ for child_index in child_indices:
3051
+ if not isinstance(child_index, int):
3052
+ continue
3053
+ search_scene_node_indices.append(child_index)
3054
+
3055
+ for armature_child_index in armature_child_indices:
3056
+ if (
3057
+ not (0 <= armature_child_index < len(node_dicts))
3058
+ or armature_child_index in scene_node_indices
3059
+ ):
3060
+ continue
3061
+ scene_node_indices.append(armature_child_index)
3062
+ if armature_replaced:
3063
+ continue
3064
+ # Apply world matrix up to main armature to its children
3065
+ child_node_dict = node_dicts[armature_child_index]
3066
+ if not isinstance(child_node_dict, dict):
3067
+ continue
3068
+ child_matrix = parse_gltf_node_matrix(child_node_dict)
3069
+ _set_node_matrix(
3070
+ child_node_dict,
3071
+ armature_world_matrix @ child_matrix,
3072
+ )
3073
+ armature_replaced = True
3074
+
3075
+ if armature_replaced:
3076
+ armature_node_dict.pop("children", None)
3077
+ armature_node_dict["name"] = "secondary" # Assign dummy name
3078
+
3079
+ def add_vrm_extension_to_glb(
3080
+ self, extra_name_assigned_glb: bytes
3081
+ ) -> Optional[bytes]:
3082
+ armature_data = self._armature.data
3083
+ if not isinstance(armature_data, Armature):
3084
+ message = f"{type(armature_data)} is not an Armature"
3085
+ raise TypeError(message)
3086
+
3087
+ vrm = get_armature_extension(armature_data).vrm1
3088
+
3089
+ json_dict, buffer0_bytes = parse_glb(extra_name_assigned_glb)
3090
+ buffer0 = bytearray(buffer0_bytes)
3091
+
3092
+ bone_name_to_index_dict: dict[str, int] = {}
3093
+ object_name_to_index_dict: dict[str, int] = {}
3094
+ image_name_to_index_dict: dict[str, int] = {}
3095
+ mesh_object_name_to_node_index_dict: dict[str, int] = {}
3096
+ mesh_object_name_to_morph_target_names_dict: dict[str, list[str]] = {}
3097
+
3098
+ node_dicts = json_dict.get("nodes")
3099
+ if not isinstance(node_dicts, list):
3100
+ node_dicts = []
3101
+ json_dict["nodes"] = node_dicts
3102
+
3103
+ mesh_dicts = json_dict.get("meshes")
3104
+ if not isinstance(mesh_dicts, list):
3105
+ mesh_dicts = []
3106
+ json_dict["meshes"] = mesh_dicts
3107
+
3108
+ for node_index, node_dict in enumerate(node_dicts):
3109
+ if not isinstance(node_dict, dict):
3110
+ continue
3111
+ extras_dict = node_dict.get("extras")
3112
+ if not isinstance(extras_dict, dict):
3113
+ continue
3114
+
3115
+ bone_name = extras_dict.pop(self._extras_bone_name_key, None)
3116
+ if isinstance(bone_name, str):
3117
+ bone_name_to_index_dict[bone_name] = node_index
3118
+
3119
+ is_main_armature = extras_dict.pop(self._extras_main_armature_key, None)
3120
+
3121
+ object_name = extras_dict.pop(self._extras_object_name_key, None)
3122
+ if isinstance(object_name, str):
3123
+ object_name_to_index_dict[object_name] = node_index
3124
+ if bpy.app.version < (3, 3):
3125
+ is_main_armature = object_name == self._armature.name
3126
+
3127
+ if is_main_armature:
3128
+ if not extras_dict:
3129
+ node_dict.pop("extras", None)
3130
+ self.remove_exported_armature_object_before_4_2(
3131
+ json_dict, node_dicts, node_index
3132
+ )
3133
+
3134
+ mesh_index = node_dict.get("mesh")
3135
+ if (
3136
+ isinstance(object_name, str)
3137
+ and isinstance(mesh_index, int)
3138
+ and 0 <= mesh_index < len(mesh_dicts)
3139
+ ):
3140
+ mesh_object_name_to_node_index_dict[object_name] = node_index
3141
+ mesh_dict = mesh_dicts[mesh_index]
3142
+ if isinstance(mesh_dict, dict):
3143
+ mesh_extras_dict = mesh_dict.get("extras")
3144
+ if isinstance(mesh_extras_dict, dict):
3145
+ target_names = mesh_extras_dict.get("targetNames")
3146
+ if isinstance(target_names, list):
3147
+ mesh_object_name_to_morph_target_names_dict[object_name] = [
3148
+ str(target_name) for target_name in target_names
3149
+ ]
3150
+
3151
+ if isinstance(object_name, str) and (
3152
+ object_name.startswith(INTERNAL_NAME_PREFIX + "VrmAddonLinkTo")
3153
+ # or object_name == dummy_skinned_mesh_object_name
3154
+ ):
3155
+ node_dict.clear()
3156
+ for child_removing_node_dict in node_dicts:
3157
+ if not isinstance(child_removing_node_dict, dict):
3158
+ continue
3159
+ children = child_removing_node_dict.pop("children", None)
3160
+ if not isinstance(children, list):
3161
+ continue
3162
+ children = [child for child in children if child != node_index]
3163
+ if children:
3164
+ child_removing_node_dict["children"] = children
3165
+
3166
+ # TODO: remove from scenes, skin joints ...
3167
+
3168
+ if not extras_dict:
3169
+ node_dict.pop("extras", None)
3170
+
3171
+ node_constraint_spec_version = "1.0"
3172
+ use_node_constraint = False
3173
+ object_constraints, bone_constraints, _ = search.export_constraints(
3174
+ self._export_objects, self._armature
3175
+ )
3176
+
3177
+ for object_name, node_index in object_name_to_index_dict.items():
3178
+ if not (0 <= node_index < len(node_dicts)):
3179
+ continue
3180
+ node_dict = node_dicts[node_index]
3181
+ if not isinstance(node_dict, dict):
3182
+ _logger.error(
3183
+ "gltf.nodes[%d] is invalid value: %s", node_index, node_dict
3184
+ )
3185
+ node_dict = {}
3186
+ node_dicts[node_index] = node_dict
3187
+ constraint_dict = self.create_constraint_dict(
3188
+ object_name,
3189
+ object_constraints,
3190
+ object_name_to_index_dict,
3191
+ bone_name_to_index_dict,
3192
+ )
3193
+ if constraint_dict:
3194
+ extensions = node_dict.get("extensions")
3195
+ if not isinstance(extensions, dict):
3196
+ node_dict["extensions"] = extensions = {}
3197
+ extensions["VRMC_node_constraint"] = {
3198
+ "specVersion": node_constraint_spec_version,
3199
+ "constraint": constraint_dict,
3200
+ }
3201
+ use_node_constraint = True
3202
+
3203
+ for bone_name, node_index in bone_name_to_index_dict.items():
3204
+ if not (0 <= node_index < len(node_dicts)):
3205
+ continue
3206
+ node_dict = node_dicts[node_index]
3207
+ if not isinstance(node_dict, dict):
3208
+ _logger.error(
3209
+ "gltf.nodes[%d] is invalid value: %s", node_index, node_dict
3210
+ )
3211
+ node_dict = {}
3212
+ node_dicts[node_index] = node_dicts
3213
+ constraint_dict = self.create_constraint_dict(
3214
+ bone_name,
3215
+ bone_constraints,
3216
+ object_name_to_index_dict,
3217
+ bone_name_to_index_dict,
3218
+ )
3219
+ if constraint_dict:
3220
+ extensions = node_dict.get("extensions")
3221
+ if not isinstance(extensions, dict):
3222
+ node_dict["extensions"] = extensions = {}
3223
+ extensions["VRMC_node_constraint"] = {
3224
+ "specVersion": node_constraint_spec_version,
3225
+ "constraint": constraint_dict,
3226
+ }
3227
+ use_node_constraint = True
3228
+
3229
+ material_name_to_index_dict: dict[str, int] = {}
3230
+ material_dicts = json_dict.get("materials")
3231
+ if not isinstance(material_dicts, list):
3232
+ material_dicts = []
3233
+ json_dict["materials"] = material_dicts
3234
+ for material_index, material_dict in enumerate(material_dicts):
3235
+ if not isinstance(material_dict, dict):
3236
+ continue
3237
+ extras_dict = material_dict.get("extras")
3238
+ if not isinstance(extras_dict, dict):
3239
+ continue
3240
+ material_name = extras_dict.pop(self._extras_material_name_key, None)
3241
+ if not isinstance(material_name, str):
3242
+ continue
3243
+ material_name_to_index_dict[material_name] = material_index
3244
+ if not extras_dict:
3245
+ material_dict.pop("extras", None)
3246
+
3247
+ mesh_name_to_index_dict: dict[str, int] = {}
3248
+ for mesh_index, mesh_dict in enumerate(mesh_dicts):
3249
+ if not isinstance(mesh_dict, dict):
3250
+ continue
3251
+ extras_dict = mesh_dict.get("extras")
3252
+ if not isinstance(extras_dict, dict):
3253
+ continue
3254
+ mesh_name = extras_dict.pop(self._extras_mesh_name_key, None)
3255
+ if not isinstance(mesh_name, str):
3256
+ continue
3257
+ mesh_name_to_index_dict[mesh_name] = mesh_index
3258
+ if not extras_dict:
3259
+ mesh_dict.pop("extras", None)
3260
+
3261
+ self.save_vrm_materials(
3262
+ self._context,
3263
+ json_dict,
3264
+ buffer0,
3265
+ material_name_to_index_dict,
3266
+ image_name_to_index_dict,
3267
+ self._gltf2_addon_export_settings,
3268
+ )
3269
+ self.unassign_normal_from_mtoon_primitive_morph_target(
3270
+ self._context, json_dict, material_name_to_index_dict
3271
+ )
3272
+
3273
+ extensions_used = json_dict.get("extensionsUsed")
3274
+ if not isinstance(extensions_used, list):
3275
+ extensions_used = []
3276
+ json_dict["extensionsUsed"] = extensions_used
3277
+
3278
+ if use_node_constraint:
3279
+ extensions_used.append("VRMC_node_constraint")
3280
+
3281
+ extensions = json_dict.get("extensions")
3282
+ if not isinstance(extensions, dict):
3283
+ json_dict["extensions"] = extensions = {}
3284
+
3285
+ extensions_used.append("VRMC_vrm")
3286
+ extensions["VRMC_vrm"] = {
3287
+ "specVersion": get_armature_extension(armature_data).spec_version,
3288
+ "meta": self.create_meta_dict(
3289
+ vrm.meta,
3290
+ json_dict,
3291
+ buffer0,
3292
+ image_name_to_index_dict,
3293
+ self._gltf2_addon_export_settings,
3294
+ ),
3295
+ "humanoid": self.create_humanoid_dict(
3296
+ vrm.humanoid, bone_name_to_index_dict
3297
+ ),
3298
+ "firstPerson": self.create_first_person_dict(
3299
+ vrm.first_person, mesh_object_name_to_node_index_dict
3300
+ ),
3301
+ "lookAt": self.create_look_at_dict(vrm.look_at),
3302
+ "expressions": self.create_expressions_dict(
3303
+ vrm.expressions,
3304
+ mesh_object_name_to_node_index_dict,
3305
+ mesh_object_name_to_morph_target_names_dict,
3306
+ material_name_to_index_dict,
3307
+ ),
3308
+ }
3309
+
3310
+ spring_bone = get_armature_extension(armature_data).spring_bone1
3311
+ spring_bone_dict: dict[str, Json] = {}
3312
+
3313
+ (
3314
+ spring_bone_collider_dicts,
3315
+ collider_uuid_to_index_dict,
3316
+ ) = self.create_spring_bone_collider_dicts(
3317
+ extensions_used, spring_bone, bone_name_to_index_dict
3318
+ )
3319
+ if spring_bone_collider_dicts:
3320
+ spring_bone_dict["colliders"] = spring_bone_collider_dicts
3321
+
3322
+ (
3323
+ spring_bone_collider_group_dicts,
3324
+ collider_group_uuid_to_index_dict,
3325
+ ) = self.create_spring_bone_collider_group_dicts(
3326
+ spring_bone, collider_uuid_to_index_dict
3327
+ )
3328
+ if spring_bone_collider_group_dicts:
3329
+ spring_bone_dict["colliderGroups"] = spring_bone_collider_group_dicts
3330
+
3331
+ spring_bone_spring_dicts = self.create_spring_bone_spring_dicts(
3332
+ extensions_used,
3333
+ spring_bone,
3334
+ bone_name_to_index_dict,
3335
+ collider_group_uuid_to_index_dict,
3336
+ self._armature,
3337
+ )
3338
+ if spring_bone_spring_dicts:
3339
+ spring_bone_dict["springs"] = spring_bone_spring_dicts
3340
+
3341
+ if spring_bone_dict:
3342
+ extensions_used.append("VRMC_springBone")
3343
+ spring_bone_dict["specVersion"] = "1.0"
3344
+ extensions["VRMC_springBone"] = spring_bone_dict
3345
+
3346
+ json_dict["extensions"] = extensions
3347
+ json_dict["extensionsUsed"] = extensions_used
3348
+
3349
+ v = get_addon_version()
3350
+ if environ.get("BLENDER_VRM_USE_TEST_EXPORTER_VERSION") == "true":
3351
+ v = (999, 999, 999)
3352
+
3353
+ generator = "VRM Add-on for Blender v" + ".".join(map(str, v))
3354
+
3355
+ asset_dict = json_dict.get("asset")
3356
+ if not isinstance(asset_dict, dict):
3357
+ asset_dict = {}
3358
+ json_dict["asset"] = asset_dict
3359
+
3360
+ base_generator = asset_dict.get("generator")
3361
+ if isinstance(base_generator, str):
3362
+ generator += " with " + base_generator
3363
+
3364
+ generator += self.generator_postfix
3365
+
3366
+ asset_dict["generator"] = generator
3367
+
3368
+ if buffer0:
3369
+ buffer_dicts = json_dict.get("buffers")
3370
+ if not isinstance(buffer_dicts, list) or not buffer_dicts:
3371
+ buffer_dicts = []
3372
+ json_dict["buffers"] = buffer_dicts
3373
+ if not buffer_dicts:
3374
+ buffer_dicts.append({})
3375
+ buffer_dict = buffer_dicts[0]
3376
+ if not isinstance(buffer_dict, dict):
3377
+ buffer_dict = {}
3378
+ buffer_dicts[0] = buffer_dict
3379
+ buffer_dict["byteLength"] = len(buffer0)
3380
+
3381
+ remove_empty_root_arrays(json_dict)
3382
+
3383
+ self.trigger_pre_save_hook(
3384
+ json_dict,
3385
+ buffer0,
3386
+ self._armature,
3387
+ MappingProxyType(
3388
+ {
3389
+ index: obj
3390
+ for object_name, index in object_name_to_index_dict.items()
3391
+ if (obj := self._context.blend_data.objects.get(object_name))
3392
+ }
3393
+ ),
3394
+ MappingProxyType(
3395
+ {
3396
+ index: bone
3397
+ for bone_name, index in bone_name_to_index_dict.items()
3398
+ if (bone := armature_data.bones.get(bone_name))
3399
+ }
3400
+ ),
3401
+ MappingProxyType(
3402
+ {
3403
+ index: image
3404
+ for image_name, index in image_name_to_index_dict.items()
3405
+ if (image := self._context.blend_data.images.get(image_name))
3406
+ }
3407
+ ),
3408
+ MappingProxyType(
3409
+ {
3410
+ index: material
3411
+ for material_name, index in material_name_to_index_dict.items()
3412
+ if (
3413
+ material := self._context.blend_data.materials.get(
3414
+ material_name
3415
+ )
3416
+ )
3417
+ }
3418
+ ),
3419
+ MappingProxyType(
3420
+ {
3421
+ index: mesh
3422
+ for mesh_name, index in mesh_name_to_index_dict.items()
3423
+ if (mesh := self._context.blend_data.meshes.get(mesh_name))
3424
+ }
3425
+ ),
3426
+ )
3427
+
3428
+ json_dict = {str(k): make_json(v) for k, v in json_dict.items()}
3429
+
3430
+ asset_dict = json_dict.get("asset")
3431
+ if not isinstance(asset_dict, dict):
3432
+ asset_dict = {}
3433
+ json_dict["asset"] = asset_dict
3434
+ asset_dict["generator"] = generator
3435
+
3436
+ if buffer0:
3437
+ buffer_dicts = json_dict.get("buffers")
3438
+ if not isinstance(buffer_dicts, list) or not buffer_dicts:
3439
+ buffer_dicts = []
3440
+ json_dict["buffers"] = buffer_dicts
3441
+ if not buffer_dicts:
3442
+ buffer_dicts.append({})
3443
+ buffer_dict = buffer_dicts[0]
3444
+ if not isinstance(buffer_dict, dict):
3445
+ buffer_dict = {}
3446
+ buffer_dicts[0] = buffer_dict
3447
+ buffer_dict["byteLength"] = len(buffer0)
3448
+
3449
+ return pack_glb(json_dict, buffer0)
3450
+
3451
+ def trigger_pre_save_hook(
3452
+ self,
3453
+ json_dict: dict[str, Json],
3454
+ buffer0: bytearray,
3455
+ armature_object: Object,
3456
+ node_index_to_object: Mapping[int, Object],
3457
+ node_index_to_bone: Mapping[int, Bone],
3458
+ image_index_to_image: Mapping[int, Image],
3459
+ material_index_to_material: Mapping[int, Material],
3460
+ mesh_index_to_mesh: Mapping[int, Mesh],
3461
+ ) -> None:
3462
+ trigger_third_party_user_extension_hook(
3463
+ self._third_party_user_extensions,
3464
+ "pre_save_hook",
3465
+ json_dict,
3466
+ buffer0,
3467
+ armature_object,
3468
+ node_index_to_object,
3469
+ node_index_to_bone,
3470
+ image_index_to_image,
3471
+ material_index_to_material,
3472
+ mesh_index_to_mesh,
3473
+ )
3474
+
3475
+
3476
+ def _remove_inactive_uv_maps(
3477
+ context: Context, mesh_object_names: Sequence[str]
3478
+ ) -> None:
3479
+ """Remove UV maps that are not active for rendering from meshes.
3480
+
3481
+ As of UniVRM v0.131.0, only one UV map is supported, so UV maps not used for
3482
+ rendering are removed.
3483
+ """
3484
+ for mesh_object_name in mesh_object_names:
3485
+ if not (obj := context.blend_data.objects.get(mesh_object_name)):
3486
+ continue
3487
+ if not isinstance(mesh := obj.data, Mesh):
3488
+ continue
3489
+ uv_layers = mesh.uv_layers
3490
+ for uv_layer_index in reversed(range(len(uv_layers))):
3491
+ if len(uv_layers) <= 1:
3492
+ break
3493
+ if not (0 <= uv_layer_index < len(uv_layers)):
3494
+ continue
3495
+ uv_layer = uv_layers[uv_layer_index]
3496
+ if uv_layer.active_render:
3497
+ continue
3498
+ uv_layers.remove(uv_layer)
3499
+
3500
+
3501
+ def _find_node_world_matrix(
3502
+ node_dicts: list[Json],
3503
+ target_node_index: int,
3504
+ parent_node_index: Optional[int],
3505
+ ) -> Optional[Matrix]:
3506
+ if parent_node_index is None:
3507
+ all_child_indices: list[int] = []
3508
+ for node_dict in node_dicts:
3509
+ if not isinstance(node_dict, dict):
3510
+ continue
3511
+ child_node_indices = node_dict.get("children")
3512
+ if isinstance(child_node_indices, list):
3513
+ all_child_indices.extend(
3514
+ child_node_index
3515
+ for child_node_index in child_node_indices
3516
+ if isinstance(child_node_index, int)
3517
+ )
3518
+ for node_index in range(len(node_dicts)):
3519
+ if node_index in all_child_indices:
3520
+ continue
3521
+ matrix = _find_node_world_matrix(node_dicts, target_node_index, node_index)
3522
+ if matrix is not None:
3523
+ return matrix
3524
+ return Matrix()
3525
+
3526
+ if not (0 <= parent_node_index < len(node_dicts)):
3527
+ return None
3528
+
3529
+ node_dict = node_dicts[parent_node_index]
3530
+ if not isinstance(node_dict, dict):
3531
+ return None
3532
+
3533
+ parent_node_matrix = parse_gltf_node_matrix(node_dict)
3534
+
3535
+ if parent_node_index == target_node_index:
3536
+ return parent_node_matrix
3537
+
3538
+ child_node_indices = node_dict.get("children")
3539
+ if not isinstance(child_node_indices, list):
3540
+ return None
3541
+
3542
+ for child_node_index in child_node_indices:
3543
+ if not isinstance(child_node_index, int):
3544
+ continue
3545
+ child_node_matrix = _find_node_world_matrix(
3546
+ node_dicts, target_node_index, child_node_index
3547
+ )
3548
+ if child_node_matrix is not None:
3549
+ return parent_node_matrix @ child_node_matrix
3550
+
3551
+ return None
3552
+
3553
+
3554
+ def _set_node_matrix(node_dict: dict[str, Json], matrix: Matrix) -> None:
3555
+ node_dict.pop("matrix", None)
3556
+ location, rotation, scale = matrix.decompose()
3557
+ node_dict["translation"] = list(location)
3558
+ node_dict["rotation"] = [
3559
+ rotation.x,
3560
+ rotation.y,
3561
+ rotation.z,
3562
+ rotation.w,
3563
+ ]
3564
+ node_dict["scale"] = list(scale)
3565
+
3566
+
3567
+ def _force_apply_modifiers_to_objects(
3568
+ context: Context,
3569
+ armature_object: Object,
3570
+ mesh_compatible_object_names: Sequence[str],
3571
+ ) -> None:
3572
+ selected_object_names: list[str] = [
3573
+ obj.name for obj in context.selectable_objects if obj.select_get()
3574
+ ]
3575
+ for mesh_compatible_object_name in mesh_compatible_object_names:
3576
+ _force_apply_modifiers_to_object(
3577
+ context, armature_object, mesh_compatible_object_name
3578
+ )
3579
+ for obj in context.selectable_objects:
3580
+ obj.select_set(obj.name in selected_object_names)
3581
+
3582
+
3583
+ def _force_apply_modifiers_to_object(
3584
+ context: Context,
3585
+ armature_object: Object,
3586
+ mesh_compatible_object_name: str,
3587
+ ) -> None:
3588
+ mesh_compatible_object = context.blend_data.objects.get(mesh_compatible_object_name)
3589
+ if not mesh_compatible_object:
3590
+ return
3591
+ if mesh_compatible_object.type == "MESH":
3592
+ mesh_object: Optional[Object] = mesh_compatible_object
3593
+ elif mesh_compatible_object.type in search.MESH_CONVERTIBLE_OBJECT_TYPES:
3594
+ with save_workspace(context, mesh_compatible_object):
3595
+ convert_result = bpy.ops.object.convert(target="MESH")
3596
+ if convert_result != {"FINISHED"}:
3597
+ _logger.warning(
3598
+ "Failed to convert %s to MESH: %s",
3599
+ mesh_compatible_object.name,
3600
+ convert_result,
3601
+ )
3602
+ return
3603
+ mesh_object = context.blend_data.objects.get(mesh_compatible_object_name)
3604
+ else:
3605
+ return
3606
+
3607
+ if not mesh_object:
3608
+ return
3609
+
3610
+ original_mesh_data = mesh_object.data
3611
+ if not isinstance(original_mesh_data, Mesh):
3612
+ return
3613
+
3614
+ armature_modifier_name_to_show_render_and_show_viewport: dict[
3615
+ str, tuple[bool, bool]
3616
+ ] = {}
3617
+ need_apply_modifiers = False
3618
+ for modifier in list(mesh_object.modifiers):
3619
+ if (
3620
+ not isinstance(modifier, ArmatureModifier)
3621
+ or modifier.object != armature_object
3622
+ ):
3623
+ if modifier.show_render or modifier.show_viewport:
3624
+ need_apply_modifiers = True
3625
+ continue
3626
+ armature_modifier_name_to_show_render_and_show_viewport[modifier.name] = (
3627
+ modifier.show_render,
3628
+ modifier.show_viewport,
3629
+ )
3630
+ modifier.show_render = False
3631
+ modifier.show_viewport = False
3632
+
3633
+ try:
3634
+ if not need_apply_modifiers:
3635
+ return
3636
+
3637
+ mesh_data_name = original_mesh_data.name
3638
+ original_mesh_data.name = "Backup-Apply-Data-" + uuid4().hex
3639
+
3640
+ mesh_data: Optional[ID] = force_apply_modifiers(context, mesh_object)
3641
+ if not mesh_data:
3642
+ return
3643
+
3644
+ original_mesh_data.user_remap(mesh_data)
3645
+ if original_mesh_data.users:
3646
+ _logger.warning(
3647
+ 'Failed to remove "%s" with %d users while applying modifiers',
3648
+ original_mesh_data.name,
3649
+ original_mesh_data.users,
3650
+ )
3651
+ else:
3652
+ context.blend_data.meshes.remove(original_mesh_data)
3653
+
3654
+ mesh_data.name = mesh_data_name
3655
+ finally:
3656
+ for modifier in list(mesh_object.modifiers):
3657
+ if (
3658
+ not isinstance(modifier, ArmatureModifier)
3659
+ or modifier.object != armature_object
3660
+ ):
3661
+ mesh_object.modifiers.remove(modifier)
3662
+ continue
3663
+ show_render_and_show_viewport = (
3664
+ armature_modifier_name_to_show_render_and_show_viewport.get(
3665
+ modifier.name
3666
+ )
3667
+ )
3668
+ if show_render_and_show_viewport is None:
3669
+ continue
3670
+ modifier.show_render, modifier.show_viewport = show_render_and_show_viewport
3671
+
3672
+
3673
+ def is_identity_matrix(matrix: Matrix) -> bool:
3674
+ for row_index, row in enumerate(matrix):
3675
+ for column_index, value in enumerate(row):
3676
+ if row_index == column_index:
3677
+ if abs(value - 1) < float_info.epsilon:
3678
+ continue
3679
+ elif abs(value) < float_info.epsilon:
3680
+ continue
3681
+ return False
3682
+ return True