@volter/blender-engine 0.1.148 → 0.1.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/LICENSE +22 -5
  2. package/README.md +4 -11
  3. package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
  4. package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
  5. package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
  6. package/browser/addons/vrm/README.txt +3 -0
  7. package/browser/addons/vrm/__init__.py +41 -0
  8. package/browser/addons/vrm/blender_manifest.toml +16 -0
  9. package/browser/addons/vrm/common/__init__.py +1 -0
  10. package/browser/addons/vrm/common/animation.py +58 -0
  11. package/browser/addons/vrm/common/blender_manifest.py +82 -0
  12. package/browser/addons/vrm/common/char.py +16 -0
  13. package/browser/addons/vrm/common/convert.py +287 -0
  14. package/browser/addons/vrm/common/convert_any.py +48 -0
  15. package/browser/addons/vrm/common/debug.py +40 -0
  16. package/browser/addons/vrm/common/deep.py +162 -0
  17. package/browser/addons/vrm/common/error_dialog.py +274 -0
  18. package/browser/addons/vrm/common/fs.py +71 -0
  19. package/browser/addons/vrm/common/gl.py +20 -0
  20. package/browser/addons/vrm/common/gltf.py +1149 -0
  21. package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
  22. package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
  23. package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
  24. package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
  25. package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
  26. package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
  27. package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
  28. package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
  29. package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
  30. package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
  31. package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
  32. package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
  33. package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
  34. package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
  35. package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
  36. package/browser/addons/vrm/common/legacy_gltf.py +16 -0
  37. package/browser/addons/vrm/common/logger.py +84 -0
  38. package/browser/addons/vrm/common/mtoon0.blend +0 -0
  39. package/browser/addons/vrm/common/mtoon1.blend +0 -0
  40. package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
  41. package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
  42. package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
  43. package/browser/addons/vrm/common/ops/__init__.py +10 -0
  44. package/browser/addons/vrm/common/ops/export_scene.py +67 -0
  45. package/browser/addons/vrm/common/ops/icyp.py +53 -0
  46. package/browser/addons/vrm/common/ops/import_scene.py +57 -0
  47. package/browser/addons/vrm/common/ops/vrm.py +2380 -0
  48. package/browser/addons/vrm/common/ops/wm.py +167 -0
  49. package/browser/addons/vrm/common/preferences.py +362 -0
  50. package/browser/addons/vrm/common/progress.py +87 -0
  51. package/browser/addons/vrm/common/rotation.py +89 -0
  52. package/browser/addons/vrm/common/safe_removal.py +46 -0
  53. package/browser/addons/vrm/common/scene_watcher.py +230 -0
  54. package/browser/addons/vrm/common/shader.py +2243 -0
  55. package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
  56. package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
  57. package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
  58. package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
  59. package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
  60. package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
  61. package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
  62. package/browser/addons/vrm/common/version.py +161 -0
  63. package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
  64. package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
  65. package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
  66. package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
  67. package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
  68. package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
  69. package/browser/addons/vrm/common/workspace.py +227 -0
  70. package/browser/addons/vrm/common/writable_context.py +77 -0
  71. package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
  72. package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
  73. package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
  74. package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
  75. package/browser/addons/vrm/documentation/en-us/index.md +41 -0
  76. package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
  77. package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
  78. package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
  79. package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
  80. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
  81. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
  82. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
  83. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
  84. package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
  85. package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
  86. package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
  87. package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
  88. package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
  89. package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
  90. package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
  91. package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
  92. package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
  93. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
  94. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
  95. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
  96. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
  97. package/browser/addons/vrm/editor/__init__.py +1 -0
  98. package/browser/addons/vrm/editor/extension.py +425 -0
  99. package/browser/addons/vrm/editor/extension_accessor.py +75 -0
  100. package/browser/addons/vrm/editor/handler.py +84 -0
  101. package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
  102. package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
  103. package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
  104. package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
  105. package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
  106. package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
  107. package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
  108. package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
  109. package/browser/addons/vrm/editor/make_armature.py +709 -0
  110. package/browser/addons/vrm/editor/menu.py +147 -0
  111. package/browser/addons/vrm/editor/migration.py +297 -0
  112. package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
  113. package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
  114. package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
  115. package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
  116. package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
  117. package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
  118. package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
  119. package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
  120. package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
  121. package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
  122. package/browser/addons/vrm/editor/ops.py +471 -0
  123. package/browser/addons/vrm/editor/panel.py +315 -0
  124. package/browser/addons/vrm/editor/property_group.py +888 -0
  125. package/browser/addons/vrm/editor/search.py +682 -0
  126. package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
  127. package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
  128. package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
  129. package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
  130. package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
  131. package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
  132. package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
  133. package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
  134. package/browser/addons/vrm/editor/subscription.py +123 -0
  135. package/browser/addons/vrm/editor/t_pose.py +713 -0
  136. package/browser/addons/vrm/editor/validation.py +1271 -0
  137. package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
  138. package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
  139. package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
  140. package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
  141. package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
  142. package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
  143. package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
  144. package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
  145. package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
  146. package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
  147. package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
  148. package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
  149. package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
  150. package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
  151. package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
  152. package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
  153. package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
  154. package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
  155. package/browser/addons/vrm/exporter/__init__.py +1 -0
  156. package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
  157. package/browser/addons/vrm/exporter/export_scene.py +930 -0
  158. package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
  159. package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
  160. package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
  161. package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
  162. package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
  163. package/browser/addons/vrm/external/__init__.py +1 -0
  164. package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
  165. package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
  166. package/browser/addons/vrm/external/rigify_support.py +1 -0
  167. package/browser/addons/vrm/importer/__init__.py +1 -0
  168. package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
  169. package/browser/addons/vrm/importer/file_handler.py +34 -0
  170. package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
  171. package/browser/addons/vrm/importer/import_scene.py +646 -0
  172. package/browser/addons/vrm/importer/license_validation.py +198 -0
  173. package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
  174. package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
  175. package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
  176. package/browser/addons/vrm/importer/vrm_diff.py +316 -0
  177. package/browser/addons/vrm/locale/__init__.py +1 -0
  178. package/browser/addons/vrm/locale/ja_jp.py +826 -0
  179. package/browser/addons/vrm/locale/ruff.toml +8 -0
  180. package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
  181. package/browser/addons/vrm/locale/zh_hans.py +928 -0
  182. package/browser/addons/vrm/registration.py +670 -0
  183. package/browser/addons/vrm.json +18 -0
  184. package/browser/blender-emscripten-engine.mts +66 -129
  185. package/browser/blender-engine.mts +20 -186
  186. package/browser/blender-wali-engine.mts +37 -81
  187. package/browser/describe-thrown.mts +3 -6
  188. package/browser/document-chunks.mts +4 -16
  189. package/browser/gltf_export.py +3161 -0
  190. package/browser/heap-growth.mts +23 -0
  191. package/browser/index.ts +3 -0
  192. package/browser/protocol.ts +130 -43
  193. package/browser/pull-job.mts +2 -10
  194. package/browser/rna.ts +190 -73
  195. package/browser/runtime.ts +271 -106
  196. package/browser/session-addons.mts +33 -0
  197. package/browser/session-frame.mts +5 -24
  198. package/browser/session.py +1389 -1446
  199. package/browser/three/attach-presenter.ts +11 -39
  200. package/browser/three/blender-base64.ts +1 -36
  201. package/browser/three/blender-corner-normals.ts +1 -23
  202. package/browser/three/blender-display-shaders.json +1 -1
  203. package/browser/three/blender-display-shaders.lut +0 -0
  204. package/browser/three/blender-display-transform.ts +2 -1
  205. package/browser/three/blender-frame-columns.ts +5 -13
  206. package/browser/three/blender-graph-material.ts +18 -3
  207. package/browser/three/blender-node-graph.ts +10 -34
  208. package/browser/three/blender-physical-material.ts +2 -31
  209. package/browser/three/blender-runtime-armature.ts +7 -60
  210. package/browser/three/blender-runtime-frame.ts +9 -23
  211. package/browser/three/blender-runtime-geometry.ts +22 -26
  212. package/browser/three/blender-runtime-lighting.ts +48 -178
  213. package/browser/three/blender-runtime-shadows.ts +187 -1
  214. package/browser/three/blender-runtime-skeleton.ts +53 -1
  215. package/browser/three/blender-runtime-view.ts +492 -306
  216. package/browser/three/blender-runtime-weights.ts +9 -72
  217. package/browser/three/blender-sky.ts +13 -33
  218. package/browser/three/blender-triangulate.ts +7 -35
  219. package/browser/three/blender-viewport-sampling.ts +235 -0
  220. package/browser/three/blender-workbench-material.ts +25 -29
  221. package/browser/three/blender-workbench-outline.ts +199 -0
  222. package/browser/three/blender-world-volume.ts +62 -10
  223. package/browser/three/presenter.ts +10 -26
  224. package/browser/three/release.ts +5 -21
  225. package/browser/three/sky-precompute-worker.ts +1 -18
  226. package/browser/three/sky-worker.ts +5 -23
  227. package/browser/three/world-field-sampler.ts +12 -26
  228. package/browser/volter_three.py +9 -105
  229. package/browser/worker.ts +195 -180
  230. package/package.json +5 -2
  231. package/wasm/BUNDLE.json +17 -17
  232. package/wasm/DEPENDENCY-LICENSES.txt +14 -0
  233. package/wasm/blender_browser.data.br +0 -0
  234. package/wasm/blender_browser.js +1 -1
  235. package/wasm/blender_browser.wasm.br +0 -0
  236. package/browser/frame-transfer.mts +0 -19
  237. package/browser/three/agx-base-srgb.lut +0 -0
  238. package/browser/three/agx-look-medium-high-contrast.lut +0 -0
  239. package/browser/three/agx-look-punchy.lut +0 -0
  240. package/browser/three/blender-agx.ts +0 -235
  241. package/browser/three/blender-display-lut.ts +0 -145
  242. package/browser/three/blender-filmic.ts +0 -49
  243. package/browser/three/blender-standard.ts +0 -68
  244. package/browser/three/blender-world-visibility.ts +0 -278
  245. package/browser/three/filmic-srgb.lut +0 -0
@@ -0,0 +1,1548 @@
1
+ # SPDX-License-Identifier: MIT OR GPL-3.0-or-later
2
+ # SPDX-FileCopyrightText: 2018 iCyP
3
+
4
+ import base64
5
+ import contextlib
6
+ import functools
7
+ import math
8
+ import os
9
+ import re
10
+ import secrets
11
+ import shutil
12
+ import string
13
+ import struct
14
+ import tempfile
15
+ import uuid
16
+ from abc import ABC, abstractmethod
17
+ from collections.abc import Generator, Mapping, Sequence
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ import bpy
23
+ from bpy.path import clean_name
24
+ from bpy.types import (
25
+ Armature,
26
+ Context,
27
+ Image,
28
+ Material,
29
+ Mesh,
30
+ Object,
31
+ SpaceView3D,
32
+ )
33
+ from mathutils import Euler, Matrix
34
+
35
+ from ..common import safe_removal, shader
36
+ from ..common.convert import Json, sequence_or_none
37
+ from ..common.deep import make_json
38
+ from ..common.fs import (
39
+ create_unique_indexed_directory_path,
40
+ create_unique_indexed_file_path,
41
+ )
42
+ from ..common.gl import GL_FLOAT, GL_LINEAR, GL_REPEAT, GL_UNSIGNED_SHORT
43
+ from ..common.gltf import (
44
+ FLOAT_NEGATIVE_MAX,
45
+ FLOAT_POSITIVE_MAX,
46
+ merge_duplicate_vertex_skinning_weights,
47
+ pack_glb,
48
+ parse_glb,
49
+ read_buffer_view_as_bytes,
50
+ )
51
+ from ..common.logger import get_logger
52
+ from ..common.preferences import ImportPreferencesProtocol
53
+ from ..common.progress import PartialProgress, create_progress
54
+ from ..common.rotation import (
55
+ get_rotation_as_quaternion,
56
+ set_rotation_without_mode_change,
57
+ )
58
+ from ..common.workspace import save_workspace
59
+ from ..editor.extension_accessor import get_armature_extension
60
+ from ..external.io_scene_gltf2_support import (
61
+ ImportSceneGltfArguments,
62
+ import_scene_gltf,
63
+ )
64
+ from .gltf2_import_user_extension import glTF2ImportUserExtension
65
+ from .license_validation import validate_license
66
+
67
+ _logger = get_logger(__name__)
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class ParseResult:
72
+ filepath: Path
73
+ json_dict: Mapping[str, Json]
74
+ spec_version_number: tuple[int, int]
75
+ spec_version_str: str
76
+ spec_version_is_stable: bool
77
+ vrm0_extension_dict: Mapping[str, Json]
78
+ vrm1_extension_dict: Mapping[str, Json]
79
+ hips_node_index: Optional[int]
80
+ bin_chunk: bytes
81
+
82
+ def load_thumbnail_image(self, context: Context) -> Optional[str]:
83
+ image_index = None
84
+
85
+ if isinstance(meta_dict := self.vrm1_extension_dict.get("meta"), dict):
86
+ image_index = meta_dict.get("thumbnailImage")
87
+
88
+ if (
89
+ not isinstance(image_index, int)
90
+ and isinstance(meta_dict := self.vrm0_extension_dict.get("meta"), dict)
91
+ and isinstance(texture_index := meta_dict.get("texture"), int)
92
+ and isinstance(texture_dicts := self.json_dict.get("textures"), list)
93
+ and 0 <= texture_index < len(texture_dicts)
94
+ and isinstance(texture_dict := texture_dicts[texture_index], dict)
95
+ ):
96
+ image_index = texture_dict.get("source")
97
+
98
+ if not isinstance(image_index, int):
99
+ return None
100
+
101
+ image_dicts = self.json_dict.get("images")
102
+ if not isinstance(image_dicts, list):
103
+ return None
104
+ if not (0 <= image_index < len(image_dicts)):
105
+ return None
106
+
107
+ image_dict = image_dicts[image_index]
108
+ if not isinstance(image_dict, dict):
109
+ return None
110
+
111
+ image_uri = image_dict.get("uri")
112
+ if isinstance(image_uri, str):
113
+ return None
114
+
115
+ buffer_dicts = self.json_dict.get("buffers")
116
+ if not isinstance(buffer_dicts, list):
117
+ return None
118
+
119
+ buffer_view_index = image_dict.get("bufferView")
120
+ if not isinstance(buffer_view_index, int):
121
+ return None
122
+
123
+ buffer_view_dicts = self.json_dict.get("bufferViews")
124
+ if not isinstance(buffer_view_dicts, list):
125
+ return None
126
+ if not (0 <= buffer_view_index < len(buffer_view_dicts)):
127
+ return None
128
+
129
+ buffer_view_dict = buffer_view_dicts[buffer_view_index]
130
+ if not isinstance(buffer_view_dict, dict):
131
+ return None
132
+
133
+ image_bytes = read_buffer_view_as_bytes(
134
+ buffer_view_dict, buffer_dicts, self.bin_chunk
135
+ )
136
+ if not image_bytes:
137
+ return None
138
+
139
+ mime_type = image_dict.get("mimeType")
140
+ if mime_type == "image/jpeg":
141
+ suffix = ".jpg"
142
+ elif mime_type == "image/png":
143
+ suffix = ".png"
144
+ else:
145
+ return None
146
+
147
+ temp_dir = tempfile.TemporaryDirectory()
148
+ try:
149
+ temp_file_path = Path(temp_dir.name) / f"thumbnail{suffix}"
150
+ temp_file_path.write_bytes(image_bytes)
151
+ image = context.blend_data.images.load(str(temp_file_path))
152
+ image.name = "vrm-thumbnail-" + uuid.uuid4().hex
153
+ image.pack()
154
+ finally:
155
+ with contextlib.suppress(OSError):
156
+ temp_dir.cleanup()
157
+ return image.name
158
+
159
+
160
+ class AbstractBaseVrmImporter(ABC):
161
+ def __init__(
162
+ self,
163
+ context: Context,
164
+ parse_result: ParseResult,
165
+ preferences: ImportPreferencesProtocol,
166
+ ) -> None:
167
+ self._context = context
168
+ self._parse_result = parse_result
169
+ self._preferences = preferences
170
+
171
+ self._meshes: dict[int, Mesh] = {}
172
+ self._images: dict[int, Image] = {}
173
+ self._armature: Optional[Object] = None
174
+ self._bone_names: dict[int, str] = {}
175
+ self._materials: dict[int, Material] = {}
176
+ self._bone_child_object_world_matrices: dict[str, Matrix] = {}
177
+ self._import_id = glTF2ImportUserExtension.update_current_import_id()
178
+ self._temp_object_name_count = 0
179
+ self._object_names: dict[int, str] = {}
180
+ self._imported_object_names: Optional[list[str]] = None
181
+
182
+ @abstractmethod
183
+ def load_materials(self, progress: PartialProgress) -> None:
184
+ pass
185
+
186
+ @abstractmethod
187
+ def load_gltf_extensions(self) -> None:
188
+ pass
189
+
190
+ @abstractmethod
191
+ def find_vrm_bone_node_indices(self) -> list[int]:
192
+ pass
193
+
194
+ def import_vrm(self) -> None:
195
+ try:
196
+ with create_progress(self._context) as progress:
197
+ with save_workspace(self._context):
198
+ progress.update(0.1)
199
+ self.import_gltf2_with_indices()
200
+ progress.update(0.4)
201
+ if (
202
+ self._parse_result.vrm1_extension_dict
203
+ or self._parse_result.vrm0_extension_dict
204
+ ):
205
+ self.load_materials(progress.partial_progress(0.9))
206
+ if (
207
+ self._parse_result.vrm1_extension_dict
208
+ or self._parse_result.vrm0_extension_dict
209
+ ):
210
+ self.load_gltf_extensions()
211
+ progress.update(0.92)
212
+ self.setup_viewport()
213
+ progress.update(0.94)
214
+ self._context.view_layer.update()
215
+ progress.update(0.96)
216
+
217
+ # Texture extraction occurs. During this process, .blend file saving
218
+ # may occur and save callbacks may run, so be careful not to apply
219
+ # those callbacks to incompletely imported VRM data
220
+ if self._preferences.extract_textures_into_folder:
221
+ self.extract_textures(repack=False)
222
+ elif bpy.app.version < (3, 1):
223
+ self.extract_textures(repack=True)
224
+ else:
225
+ self.assign_packed_image_filepaths()
226
+
227
+ self.save_t_pose_action()
228
+ progress.update(0.97)
229
+ self.setup_object_selection_and_activation()
230
+ progress.update(0.98)
231
+ finally:
232
+ glTF2ImportUserExtension.clear_current_import_id()
233
+
234
+ self.on_post_import()
235
+
236
+ @property
237
+ def armature_data(self) -> Armature:
238
+ if not self._armature:
239
+ message = "armature is not set"
240
+ raise AssertionError(message)
241
+ armature_data = self._armature.data
242
+ if not isinstance(armature_data, Armature):
243
+ message = f"{type(armature_data)} is not an Armature"
244
+ raise TypeError(message)
245
+ return armature_data
246
+
247
+ @staticmethod
248
+ def enter_save_bone_child_object_transforms(
249
+ context: Context, armature_object: Object
250
+ ) -> Optional[Mapping[str, Matrix]]:
251
+ armature_data = armature_object.data
252
+ if not isinstance(armature_data, Armature):
253
+ return None
254
+
255
+ # Save the world matrices of child objects of bones before editing
256
+ context.view_layer.update()
257
+ bone_child_object_world_matrices: dict[str, Matrix] = {}
258
+ for obj in context.blend_data.objects:
259
+ if (
260
+ obj.parent_type == "BONE"
261
+ and obj.parent == armature_object
262
+ and obj.parent_bone in armature_data.bones
263
+ ):
264
+ bone_child_object_world_matrices[obj.name] = obj.matrix_world.copy()
265
+
266
+ return bone_child_object_world_matrices
267
+
268
+ @staticmethod
269
+ def leave_save_bone_child_object_transforms(
270
+ context: Context,
271
+ armature_object: Object,
272
+ bone_child_object_world_matrices: Mapping[str, Matrix],
273
+ ) -> None:
274
+ armature_data = armature_object.data
275
+ if not isinstance(armature_data, Armature):
276
+ message = f"{type(armature_data)} is not an Armature"
277
+ raise TypeError(message)
278
+ # Restore the world matrices of child objects of bones before editing
279
+ context.view_layer.update()
280
+ for name, matrix_world in reversed(
281
+ list(bone_child_object_world_matrices.items())
282
+ ):
283
+ restore_obj = context.blend_data.objects.get(name)
284
+ if (
285
+ restore_obj
286
+ and restore_obj.parent_type == "BONE"
287
+ and restore_obj.parent == armature_object
288
+ and restore_obj.parent_bone in armature_data.bones
289
+ ):
290
+ restore_obj.matrix_world = matrix_world.copy()
291
+ context.view_layer.update()
292
+
293
+ @staticmethod
294
+ @contextlib.contextmanager
295
+ def save_bone_child_object_transforms(
296
+ context: Context, armature: Object
297
+ ) -> Generator[Armature]:
298
+ bone_child_object_world_matrices = (
299
+ AbstractBaseVrmImporter.enter_save_bone_child_object_transforms(
300
+ context, armature
301
+ )
302
+ )
303
+ try:
304
+ with save_workspace(context, armature, mode="EDIT"):
305
+ armature_data = armature.data
306
+ if not isinstance(armature_data, Armature):
307
+ message = f"{type(armature_data)} is not an Armature"
308
+ raise TypeError(message)
309
+ yield armature_data
310
+ # After yield, bpy native objects may be deleted or frames may advance
311
+ # making them invalid. Accessing them in this state can cause crashes,
312
+ # so be careful not to access such native objects after yield
313
+ finally:
314
+ if bone_child_object_world_matrices is not None:
315
+ AbstractBaseVrmImporter.leave_save_bone_child_object_transforms(
316
+ context, armature, bone_child_object_world_matrices
317
+ )
318
+
319
+ @staticmethod
320
+ def reset_material(material: Material) -> None:
321
+ if bpy.app.version < (5, 0, 0) and not material.use_nodes:
322
+ material.use_nodes = True
323
+ shader.clear_node_tree(material.node_tree)
324
+ if material.alpha_threshold != 0.5:
325
+ material.alpha_threshold = 0.5
326
+ if material.blend_method != "OPAQUE":
327
+ material.blend_method = "OPAQUE"
328
+ if bpy.app.version < (4, 3) and material.shadow_method != "OPAQUE":
329
+ material.shadow_method = "OPAQUE"
330
+ if material.use_backface_culling:
331
+ material.use_backface_culling = False
332
+ if material.show_transparent_back:
333
+ material.show_transparent_back = False
334
+ node_tree = material.node_tree
335
+ if node_tree:
336
+ node_tree.nodes.new("ShaderNodeOutputMaterial")
337
+ else:
338
+ _logger.error("No node tree for material %s", material.name)
339
+
340
+ def assign_packed_image_filepaths(self) -> None:
341
+ # Assign image filepath for fbx export
342
+ for image in self._images.values():
343
+ if image.packed_file is None:
344
+ continue
345
+ if image.filepath:
346
+ continue
347
+ image_name = Path(image.filepath_from_user()).stem
348
+ if not image_name:
349
+ image_name = clean_name(image.name)
350
+ image_type = image.file_format.lower()
351
+ if bpy.app.version >= (3, 4):
352
+ image.filepath_raw = f"//textures{os.sep}{image_name}.{image_type}"
353
+ else:
354
+ image.filepath_raw = f"//{image_name}.{image_type}"
355
+
356
+ def extract_textures(self, *, repack: bool) -> None:
357
+ """Extract textures to a folder as files.
358
+
359
+ In Blender 3.1 and later, texture extraction requires saving the .blend file.
360
+ Therefore, the file is saved, but be careful that file save callbacks may run
361
+ during this process.
362
+ """
363
+ dir_path = self._parse_result.filepath.with_suffix(".vrm.textures").absolute()
364
+ if self._preferences.make_new_texture_folder or repack:
365
+ dir_path = create_unique_indexed_directory_path(dir_path)
366
+ dir_path.mkdir(parents=True, exist_ok=True)
367
+
368
+ if bpy.app.version >= (3, 1) and not self._context.blend_data.filepath:
369
+ temp_blend_path = None
370
+ for _ in range(10000):
371
+ image_suffix = (
372
+ ".temp"
373
+ + "".join(secrets.choice(string.digits) for _ in range(15))
374
+ + ".blend"
375
+ )
376
+ temp_blend_path = self._parse_result.filepath.with_suffix(image_suffix)
377
+ if not temp_blend_path.exists():
378
+ break
379
+ if temp_blend_path is not None:
380
+ bpy.ops.wm.save_as_mainfile(filepath=str(temp_blend_path))
381
+
382
+ for image_index, image in self._images.items():
383
+ image_original_file_path = Path(image.filepath_from_user())
384
+ image_path_stem = image_original_file_path.stem
385
+ if image_path_stem:
386
+ legacy_image_name_prefix = self._import_id + "Image"
387
+ if image_path_stem.startswith(legacy_image_name_prefix):
388
+ image_path_stem = re.sub(
389
+ r"^\d+_",
390
+ "",
391
+ image_path_stem[len(legacy_image_name_prefix) :],
392
+ )
393
+ if not image_path_stem:
394
+ image_path_stem = image.name
395
+ image_path_stem = Path(clean_name(image_path_stem[:100])).stem
396
+ if not image_path_stem:
397
+ image_path_stem = f"Image_{image_index}"
398
+
399
+ original_image_path_suffix = image_original_file_path.suffix
400
+ detected_image_path_suffix = "." + image.file_format.lower()
401
+ if original_image_path_suffix.lower() != detected_image_path_suffix and (
402
+ original_image_path_suffix.lower(),
403
+ detected_image_path_suffix,
404
+ ) != (".jpg", ".jpeg"):
405
+ image_path_suffix = detected_image_path_suffix
406
+ else:
407
+ image_path_suffix = original_image_path_suffix
408
+
409
+ image_path = dir_path / (image_path_stem + image_path_suffix)
410
+
411
+ try:
412
+ image.unpack(method="WRITE_ORIGINAL")
413
+ except RuntimeError:
414
+ _logger.exception("Failed to unpack %s", image.name)
415
+ continue
416
+
417
+ image_unpacked_path_str = image.filepath_from_user()
418
+ if not image_unpacked_path_str:
419
+ continue
420
+ image_unpacked_file_path = Path(image_unpacked_path_str)
421
+ if not image_unpacked_file_path.exists():
422
+ continue
423
+
424
+ image_bytes = image_unpacked_file_path.read_bytes()
425
+ with contextlib.suppress(OSError):
426
+ image_unpacked_file_path.unlink()
427
+ image_path = create_unique_indexed_file_path(image_path, image_bytes)
428
+ if image.filepath != str(image_path):
429
+ image.filepath = str(image_path)
430
+ image.reload()
431
+ if repack:
432
+ image.pack()
433
+
434
+ if repack:
435
+ shutil.rmtree(dir_path, ignore_errors=True)
436
+
437
+ # Be careful not to let bones proliferate by repeating VRM re-import.
438
+ # Things to pay special attention to:
439
+ # - Root bones
440
+ # - Bones that have meshes parented to them
441
+ def find_retain_node_indices(self, scene_dict: dict[str, Json]) -> list[int]:
442
+ scene_node_index_jsons = scene_dict.get("nodes")
443
+ if not isinstance(scene_node_index_jsons, list):
444
+ return []
445
+ scene_node_indices = [
446
+ index for index in scene_node_index_jsons if isinstance(index, int)
447
+ ]
448
+ json_dict = self._parse_result.json_dict
449
+ node_dict_jsons = json_dict.get("nodes")
450
+ if not isinstance(node_dict_jsons, list):
451
+ return []
452
+ node_dicts = [
453
+ node_dict for node_dict in node_dict_jsons if isinstance(node_dict, dict)
454
+ ]
455
+ skin_dict_jsons = json_dict.get("skins")
456
+ if not isinstance(skin_dict_jsons, list):
457
+ skin_dict_jsons = []
458
+ skin_dicts = [
459
+ skin_dict for skin_dict in skin_dict_jsons if isinstance(skin_dict, dict)
460
+ ]
461
+
462
+ bone_node_indices = self.find_vrm_bone_node_indices()
463
+
464
+ # Collect all nodes in the scene node tree where the hips bone exists.
465
+ # Also treat the root node of that tree as a bone.
466
+ all_scene_node_indices: list[int] = []
467
+ hips_found = False
468
+ for scene_node_index in scene_node_indices:
469
+ all_scene_node_indices.clear()
470
+
471
+ search_scene_node_indices = [scene_node_index]
472
+ while search_scene_node_indices:
473
+ search_scene_node_index = search_scene_node_indices.pop()
474
+ if search_scene_node_index == self._parse_result.hips_node_index:
475
+ bone_node_indices.append(scene_node_index)
476
+ hips_found = True
477
+ if not (0 <= search_scene_node_index < len(node_dicts)):
478
+ continue
479
+ node_dict = node_dicts[search_scene_node_index]
480
+ all_scene_node_indices.append(search_scene_node_index)
481
+ child_indices = node_dict.get("children")
482
+ if not isinstance(child_indices, list):
483
+ continue
484
+ for child_index in child_indices:
485
+ if not isinstance(child_index, int):
486
+ continue
487
+ if child_index in all_scene_node_indices:
488
+ # Avoid recursive nodes
489
+ continue
490
+ search_scene_node_indices.append(child_index)
491
+ if hips_found:
492
+ break
493
+ if not hips_found:
494
+ return []
495
+
496
+ all_scene_node_indices = list(dict.fromkeys(all_scene_node_indices)) # Distinct
497
+
498
+ # Also treat indices registered in skin as bones
499
+ for node_index in all_scene_node_indices:
500
+ if not (0 <= node_index < len(node_dicts)):
501
+ continue
502
+ node_dict = node_dicts[node_index]
503
+ skin_index = node_dict.get("skin")
504
+ if not isinstance(skin_index, int) or not (
505
+ 0 <= skin_index < len(skin_dicts)
506
+ ):
507
+ continue
508
+ skin_dict = skin_dicts[skin_index]
509
+ skeleton_index = skin_dict.get("skeleton")
510
+ if isinstance(skeleton_index, int):
511
+ bone_node_indices.append(skeleton_index)
512
+ joint_indices = skin_dict.get("joints")
513
+ if isinstance(joint_indices, list):
514
+ for joint_index in joint_indices:
515
+ if isinstance(joint_index, int):
516
+ bone_node_indices.append(joint_index)
517
+
518
+ # Remove bone indices that are not in the scene node index
519
+ for bone_node_index in list(bone_node_indices):
520
+ if bone_node_index not in all_scene_node_indices:
521
+ bone_node_indices.remove(bone_node_index)
522
+
523
+ # Add children from currently found bone nodes until hitting mesh nodes
524
+ search_bone_node_indices = list(bone_node_indices)
525
+ while search_bone_node_indices:
526
+ search_bone_node_index = search_bone_node_indices.pop()
527
+ if not (0 <= search_bone_node_index < len(node_dicts)):
528
+ continue
529
+ node_dict = node_dicts[search_bone_node_index]
530
+ if isinstance(node_dict.get("mesh"), int):
531
+ continue
532
+
533
+ bone_node_indices.append(search_bone_node_index)
534
+
535
+ child_indices = node_dict.get("children")
536
+ if not isinstance(child_indices, list):
537
+ continue
538
+ for child_index in child_indices:
539
+ if not isinstance(child_index, int):
540
+ continue
541
+ if child_index in bone_node_indices:
542
+ continue
543
+ search_bone_node_indices.append(child_index)
544
+
545
+ # If a mesh node has bone nodes as children,
546
+ # treat that mesh node as a bone too
547
+ bone_node_indices.extend(
548
+ functools.reduce(
549
+ lambda left, right: left + right,
550
+ [
551
+ self.find_middle_bone_indices(
552
+ node_dicts, bone_node_indices, bone_node_index, []
553
+ )
554
+ for bone_node_index in bone_node_indices
555
+ ],
556
+ list[int](),
557
+ )
558
+ )
559
+
560
+ return list(dict.fromkeys(bone_node_indices)) # Distinct
561
+
562
+ @classmethod
563
+ def find_middle_bone_indices(
564
+ cls,
565
+ node_dicts: list[dict[str, Json]],
566
+ bone_node_indices: list[int],
567
+ bone_node_index: int,
568
+ middle_bone_node_indices: list[int],
569
+ ) -> list[int]:
570
+ if not (0 <= bone_node_index < len(node_dicts)):
571
+ return []
572
+ if bone_node_index in middle_bone_node_indices:
573
+ return []
574
+ node_dict = node_dicts[bone_node_index]
575
+ child_indices = node_dict.get("children")
576
+ if not isinstance(child_indices, list):
577
+ return []
578
+
579
+ result: list[int] = []
580
+ for child_index in child_indices:
581
+ if not isinstance(child_index, int):
582
+ continue
583
+ if not (0 <= child_index < len(node_dicts)):
584
+ continue
585
+ if child_index in bone_node_indices:
586
+ result.extend(middle_bone_node_indices)
587
+ continue
588
+ result.extend(
589
+ cls.find_middle_bone_indices(
590
+ node_dicts,
591
+ bone_node_indices,
592
+ child_index,
593
+ [*middle_bone_node_indices, bone_node_index],
594
+ )
595
+ )
596
+ return result
597
+
598
+ def import_gltf2_with_indices(self) -> None:
599
+ json_dict, buffer0_bytes = parse_glb(self._parse_result.filepath.read_bytes())
600
+
601
+ node_dicts = json_dict.get("nodes")
602
+ if not isinstance(node_dicts, list):
603
+ node_dicts = []
604
+ json_dict["nodes"] = node_dicts
605
+
606
+ material_dicts = json_dict.get("materials")
607
+ if not isinstance(material_dicts, list):
608
+ material_dicts = []
609
+ json_dict["materials"] = material_dicts
610
+
611
+ mesh_dicts = json_dict.get("meshes")
612
+ if not isinstance(mesh_dicts, list):
613
+ mesh_dicts = []
614
+ json_dict["meshes"] = mesh_dicts
615
+
616
+ scene_dicts = json_dict.get("scenes")
617
+ if not isinstance(scene_dicts, list):
618
+ scene_dicts = []
619
+ json_dict["scenes"] = scene_dicts
620
+
621
+ accessor_dicts = json_dict.get("accessors")
622
+ if not isinstance(accessor_dicts, list):
623
+ accessor_dicts = []
624
+ json_dict["accessors"] = accessor_dicts
625
+
626
+ extras_node_index_key = self._import_id + "Nodes"
627
+ extras_material_index_key = self._import_id + "Materials"
628
+ extras_mesh_index_key = self._import_id + "Meshes"
629
+ extras_mesh_node_indices_key = self._import_id + "MeshNodes"
630
+
631
+ for value_dicts, extras_index_key in (
632
+ (node_dicts, extras_node_index_key),
633
+ (material_dicts, extras_material_index_key),
634
+ (mesh_dicts, extras_mesh_index_key),
635
+ ):
636
+ for index, value_dict in enumerate(value_dicts):
637
+ if not isinstance(value_dict, dict):
638
+ continue
639
+
640
+ extras_dict = value_dict.get("extras")
641
+ if not isinstance(extras_dict, dict):
642
+ extras_dict = {}
643
+ value_dict["extras"] = extras_dict
644
+ extras_dict.update({extras_index_key: index})
645
+
646
+ for mesh_index, mesh_dict in enumerate(mesh_dicts):
647
+ if not isinstance(mesh_dict, dict):
648
+ continue
649
+ node_indices = [
650
+ node_index
651
+ for node_index, node_dict in enumerate(node_dicts)
652
+ if isinstance(node_dict, dict) and node_dict.get("mesh") == mesh_index
653
+ ]
654
+ if not node_indices:
655
+ continue
656
+ extras_dict = mesh_dict.get("extras")
657
+ if not isinstance(extras_dict, dict):
658
+ extras_dict = {}
659
+ mesh_dict["extras"] = extras_dict
660
+ extras_dict.update({extras_mesh_node_indices_key: make_json(node_indices)})
661
+
662
+ legacy_image_name_prefix = self._import_id + "Image"
663
+ image_dicts = json_dict.get("images")
664
+ if isinstance(image_dicts, list):
665
+ for image_index, image_dict in enumerate(image_dicts):
666
+ texture_dicts = json_dict.get("textures")
667
+ if not isinstance(texture_dicts, list) or not [
668
+ True
669
+ for texture_dict in texture_dicts
670
+ if isinstance(texture_dict, dict)
671
+ and texture_dict.get("source") == image_index
672
+ ]:
673
+ sampler_dicts = json_dict.get("samplers")
674
+ if not isinstance(sampler_dicts, list):
675
+ sampler_dicts = []
676
+ json_dict["samplers"] = sampler_dicts
677
+ sampler_index = len(sampler_dicts)
678
+ sampler_dicts.append(
679
+ {
680
+ "magFilter": GL_LINEAR,
681
+ "minFilter": GL_LINEAR,
682
+ "wrapS": GL_REPEAT,
683
+ "wrapT": GL_REPEAT,
684
+ }
685
+ )
686
+
687
+ if not isinstance(texture_dicts, list):
688
+ texture_dicts = []
689
+ json_dict["textures"] = texture_dicts
690
+ texture_dicts.append(
691
+ {
692
+ "sampler": sampler_index,
693
+ "source": image_index,
694
+ }
695
+ )
696
+
697
+ if bpy.app.version < (3, 1):
698
+ if not isinstance(image_dict, dict):
699
+ continue
700
+ name = image_dict.get("name")
701
+ if not isinstance(name, str) or not name:
702
+ # https://github.com/KhronosGroup/glTF-Blender-IO/blob/709630548cdc184af6ea50b2ff3ddc5450bc0af3/addons/io_scene_gltf2/blender/imp/gltf2_blender_image.py#L54
703
+ name = f"Image_{image_index}"
704
+ image_dict["name"] = (
705
+ legacy_image_name_prefix + str(image_index) + "_" + name
706
+ )
707
+
708
+ for mesh_dict in mesh_dicts:
709
+ if not isinstance(mesh_dict, dict):
710
+ continue
711
+ mesh_extras_dict = mesh_dict.get("extras")
712
+ if not isinstance(mesh_extras_dict, dict):
713
+ mesh_extras_dict = {}
714
+ mesh_dict["extras"] = mesh_extras_dict
715
+ mesh_target_names = mesh_extras_dict.get("targetNames")
716
+ if isinstance(mesh_target_names, list):
717
+ continue
718
+ primitive_dicts = mesh_dict.get("primitives")
719
+ if not isinstance(primitive_dicts, list):
720
+ continue
721
+ for primitive_dict in primitive_dicts:
722
+ if not isinstance(primitive_dict, dict):
723
+ continue
724
+ primitive_extras_dict = primitive_dict.get("extras")
725
+ if not isinstance(primitive_extras_dict, dict):
726
+ continue
727
+ primitive_target_names = primitive_extras_dict.get("targetNames")
728
+ if not isinstance(primitive_target_names, list):
729
+ continue
730
+ mesh_extras_dict["targetNames"] = primitive_target_names
731
+ break
732
+
733
+ texture_dicts = json_dict.get("textures")
734
+ if isinstance(texture_dicts, list) and texture_dicts:
735
+ primitive_dicts = list[Json]()
736
+
737
+ for texture_index, _ in enumerate(texture_dicts):
738
+ buffer_dicts = json_dict.get("buffers")
739
+ if not isinstance(buffer_dicts, list):
740
+ buffer_dicts = []
741
+ json_dict["buffers"] = buffer_dicts
742
+ position_buffer_index = len(buffer_dicts)
743
+ position_buffer_bytes = struct.pack(
744
+ "<9f", 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0
745
+ )
746
+ buffer_dicts.append(
747
+ {
748
+ "uri": "data:application/gltf-buffer;base64,"
749
+ + base64.b64encode(position_buffer_bytes).decode("ascii"),
750
+ "byteLength": len(position_buffer_bytes),
751
+ }
752
+ )
753
+ texcoord_buffer_index = len(buffer_dicts)
754
+ texcoord_buffer_bytes = struct.pack("<6f", 0.0, 0.0, 1.0, 0.0, 0.0, 1.0)
755
+ buffer_dicts.append(
756
+ {
757
+ "uri": "data:application/gltf-buffer;base64,"
758
+ + base64.b64encode(texcoord_buffer_bytes).decode("ascii"),
759
+ "byteLength": len(texcoord_buffer_bytes),
760
+ }
761
+ )
762
+
763
+ buffer_view_dicts = json_dict.get("bufferViews")
764
+ if not isinstance(buffer_view_dicts, list):
765
+ buffer_view_dicts = []
766
+ json_dict["bufferViews"] = buffer_view_dicts
767
+ position_buffer_view_index = len(buffer_view_dicts)
768
+ buffer_view_dicts.append(
769
+ {
770
+ "buffer": position_buffer_index,
771
+ "byteLength": len(position_buffer_bytes),
772
+ }
773
+ )
774
+ texcoord_buffer_view_index = len(buffer_view_dicts)
775
+ buffer_view_dicts.append(
776
+ {
777
+ "buffer": texcoord_buffer_index,
778
+ "byteLength": len(texcoord_buffer_bytes),
779
+ }
780
+ )
781
+
782
+ accessor_dicts = json_dict.get("accessors")
783
+ if not isinstance(accessor_dicts, list):
784
+ accessor_dicts = []
785
+ json_dict["accessors"] = accessor_dicts
786
+ position_accessors_index = len(accessor_dicts)
787
+ accessor_dicts.append(
788
+ {
789
+ "bufferView": position_buffer_view_index,
790
+ "type": "VEC3",
791
+ "componentType": GL_FLOAT,
792
+ "count": 3,
793
+ "min": [0, 0, 0],
794
+ "max": [1, 1, 0],
795
+ }
796
+ )
797
+ texcoord_accessors_index = len(accessor_dicts)
798
+ accessor_dicts.append(
799
+ {
800
+ "bufferView": texcoord_buffer_view_index,
801
+ "type": "VEC2",
802
+ "componentType": GL_FLOAT,
803
+ "count": 3,
804
+ }
805
+ )
806
+
807
+ tex_material_index = len(material_dicts)
808
+ material_dicts.append(
809
+ {
810
+ "name": self.temp_object_name(),
811
+ "emissiveTexture": {"index": texture_index},
812
+ }
813
+ )
814
+ primitive_dicts.append(
815
+ {
816
+ "attributes": {
817
+ "POSITION": position_accessors_index,
818
+ "TEXCOORD_0": texcoord_accessors_index,
819
+ },
820
+ "material": tex_material_index,
821
+ }
822
+ )
823
+
824
+ tex_mesh_index = len(mesh_dicts)
825
+ mesh_dicts.append(
826
+ {"name": self.temp_object_name(), "primitives": primitive_dicts}
827
+ )
828
+
829
+ tex_node_index = len(node_dicts)
830
+ node_dicts.append({"name": self.temp_object_name(), "mesh": tex_mesh_index})
831
+
832
+ scene_dicts.append(
833
+ {"name": self.temp_object_name(), "nodes": [tex_node_index]}
834
+ )
835
+
836
+ for scene_dict in scene_dicts:
837
+ if not isinstance(scene_dict, dict):
838
+ continue
839
+ retain_node_indices = self.find_retain_node_indices(scene_dict)
840
+ if not retain_node_indices:
841
+ continue
842
+
843
+ buffer_dicts = json_dict.get("buffers")
844
+ if not isinstance(buffer_dicts, list):
845
+ buffer_dicts = []
846
+ json_dict["buffers"] = buffer_dicts
847
+ position_buffer_index = len(buffer_dicts)
848
+ position_buffer_bytes = struct.pack(
849
+ "<9f", 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0
850
+ )
851
+ buffer_dicts.append(
852
+ {
853
+ "uri": "data:application/gltf-buffer;base64,"
854
+ + base64.b64encode(position_buffer_bytes).decode("ascii"),
855
+ "byteLength": len(position_buffer_bytes),
856
+ }
857
+ )
858
+ joints_buffer_index = len(buffer_dicts)
859
+ joints_buffer_bytes = struct.pack(
860
+ "<12H", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
861
+ )
862
+ buffer_dicts.append(
863
+ {
864
+ "uri": "data:application/gltf-buffer;base64,"
865
+ + base64.b64encode(joints_buffer_bytes).decode("ascii"),
866
+ "byteLength": len(joints_buffer_bytes),
867
+ }
868
+ )
869
+ weights_buffer_index = len(buffer_dicts)
870
+ weights_buffer_bytes = struct.pack(
871
+ "<12f", 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0
872
+ )
873
+ buffer_dicts.append(
874
+ {
875
+ "uri": "data:application/gltf-buffer;base64,"
876
+ + base64.b64encode(weights_buffer_bytes).decode("ascii"),
877
+ "byteLength": len(weights_buffer_bytes),
878
+ }
879
+ )
880
+
881
+ buffer_view_dicts = json_dict.get("bufferViews")
882
+ if not isinstance(buffer_view_dicts, list):
883
+ buffer_view_dicts = []
884
+ json_dict["bufferViews"] = buffer_view_dicts
885
+ position_buffer_view_index = len(buffer_view_dicts)
886
+ buffer_view_dicts.append(
887
+ {
888
+ "buffer": position_buffer_index,
889
+ "byteLength": len(position_buffer_bytes),
890
+ }
891
+ )
892
+ joints_buffer_view_index = len(buffer_view_dicts)
893
+ buffer_view_dicts.append(
894
+ {
895
+ "buffer": joints_buffer_index,
896
+ "byteLength": len(joints_buffer_bytes),
897
+ }
898
+ )
899
+ weights_buffer_view_index = len(buffer_view_dicts)
900
+ buffer_view_dicts.append(
901
+ {
902
+ "buffer": weights_buffer_index,
903
+ "byteLength": len(weights_buffer_bytes),
904
+ }
905
+ )
906
+
907
+ accessor_dicts = json_dict.get("accessors")
908
+ if not isinstance(accessor_dicts, list):
909
+ accessor_dicts = []
910
+ json_dict["accessors"] = accessor_dicts
911
+ position_accessors_index = len(accessor_dicts)
912
+ accessor_dicts.append(
913
+ {
914
+ "bufferView": position_buffer_view_index,
915
+ "type": "VEC3",
916
+ "componentType": GL_FLOAT,
917
+ "count": 3,
918
+ "min": [0, 0, 0],
919
+ "max": [1, 1, 0],
920
+ }
921
+ )
922
+ joints_accessors_index = len(accessor_dicts)
923
+ accessor_dicts.append(
924
+ {
925
+ "bufferView": joints_buffer_view_index,
926
+ "type": "VEC4",
927
+ "componentType": GL_UNSIGNED_SHORT,
928
+ "count": 3,
929
+ }
930
+ )
931
+ weights_accessors_index = len(accessor_dicts)
932
+ accessor_dicts.append(
933
+ {
934
+ "bufferView": weights_buffer_view_index,
935
+ "type": "VEC4",
936
+ "componentType": GL_FLOAT,
937
+ "count": 3,
938
+ }
939
+ )
940
+
941
+ primitive_dicts = [
942
+ {
943
+ "attributes": {
944
+ "POSITION": position_accessors_index,
945
+ "JOINTS_0": joints_accessors_index,
946
+ "WEIGHTS_0": weights_accessors_index,
947
+ }
948
+ }
949
+ ]
950
+
951
+ mesh_dicts = json_dict.get("meshes")
952
+ if not isinstance(mesh_dicts, list):
953
+ mesh_dicts = []
954
+ json_dict["meshes"] = mesh_dicts
955
+ skin_mesh_index = len(mesh_dicts)
956
+ mesh_dicts.append(
957
+ make_json(
958
+ {"name": self.temp_object_name(), "primitives": primitive_dicts}
959
+ )
960
+ )
961
+
962
+ skin_dicts = json_dict.get("skins")
963
+ if not isinstance(skin_dicts, list):
964
+ skin_dicts = []
965
+ json_dict["skins"] = skin_dicts
966
+ skin_index = len(skin_dicts)
967
+ skin_dicts.append({"joints": list(retain_node_indices)})
968
+
969
+ node_dicts = json_dict.get("nodes")
970
+ if not isinstance(node_dicts, list):
971
+ node_dicts = []
972
+ json_dict["nodes"] = node_dicts
973
+ skin_node_index = len(node_dicts)
974
+ node_dicts.append(
975
+ {
976
+ "name": self.temp_object_name(),
977
+ "mesh": skin_mesh_index,
978
+ "skin": skin_index,
979
+ }
980
+ )
981
+
982
+ scene_nodes = scene_dict.get("nodes")
983
+ if not isinstance(scene_nodes, list):
984
+ scene_nodes = []
985
+ scene_dict["nodes"] = scene_nodes
986
+
987
+ scene_nodes.append(skin_node_index)
988
+
989
+ # Prevent errors when extensions not supported by the glTF 2.0 add-on
990
+ # are included in "extensionsRequired"
991
+ extensions_required = json_dict.get("extensionsRequired")
992
+ if isinstance(extensions_required, list):
993
+ for supported_extension in (
994
+ "VRM",
995
+ "VRMC_vrm",
996
+ "VRMC_springBone",
997
+ "VRMC_node_constraint",
998
+ "VRMC_materials_mtoon",
999
+ "VRMC_materials_hdr_emissiveMultiplier",
1000
+ ):
1001
+ while supported_extension in extensions_required:
1002
+ extensions_required.remove(supported_extension)
1003
+
1004
+ # Unfortunately such VRMs exist.
1005
+ for accessor_dict in accessor_dicts:
1006
+ if not isinstance(accessor_dict, dict):
1007
+ continue
1008
+
1009
+ max_values = accessor_dict.get("max")
1010
+ if isinstance(max_values, list):
1011
+ for i, max_value in enumerate(list(max_values)):
1012
+ if not isinstance(max_value, (float, int)) or math.isnan(max_value):
1013
+ max_values[i] = FLOAT_POSITIVE_MAX
1014
+ elif math.isinf(max_value):
1015
+ max_values[i] = (
1016
+ FLOAT_POSITIVE_MAX if max_value > 0 else FLOAT_NEGATIVE_MAX
1017
+ )
1018
+
1019
+ min_values = accessor_dict.get("min")
1020
+ if isinstance(min_values, list):
1021
+ for i, min_value in enumerate(list(min_values)):
1022
+ if not isinstance(min_value, (float, int)) or math.isnan(min_value):
1023
+ min_values[i] = FLOAT_NEGATIVE_MAX
1024
+ elif math.isinf(min_value):
1025
+ min_values[i] = (
1026
+ FLOAT_POSITIVE_MAX if min_value > 0 else FLOAT_NEGATIVE_MAX
1027
+ )
1028
+
1029
+ merge_duplicate_vertex_skinning_weights(json_dict, buffer0_bytes)
1030
+
1031
+ if self._parse_result.spec_version_number < (1, 0):
1032
+ bone_heuristic = "FORTUNE"
1033
+ else:
1034
+ bone_heuristic = "BLENDER"
1035
+ full_vrm_import_success = False
1036
+ with tempfile.TemporaryDirectory() as temp_dir:
1037
+ indexed_vrm_filepath = Path(temp_dir, "indexed.vrm")
1038
+ indexed_vrm_filepath.write_bytes(pack_glb(json_dict, buffer0_bytes))
1039
+ try:
1040
+ import_scene_gltf(
1041
+ ImportSceneGltfArguments(
1042
+ filepath=str(indexed_vrm_filepath),
1043
+ import_pack_images=True,
1044
+ bone_heuristic=bone_heuristic,
1045
+ guess_original_bind_pose=False,
1046
+ disable_bone_shape=True,
1047
+ import_scene_as_collection=False,
1048
+ )
1049
+ )
1050
+ full_vrm_import_success = True
1051
+ except RuntimeError:
1052
+ _logger.exception(
1053
+ 'Failed to import "%s" generated from "%s" using glTF 2.0 Add-on',
1054
+ indexed_vrm_filepath,
1055
+ self._parse_result.filepath,
1056
+ )
1057
+ self.cleanup_gltf2_with_indices()
1058
+ if not full_vrm_import_success:
1059
+ # Some VRMs have broken animations.
1060
+ # https://github.com/vrm-c/UniVRM/issues/1522
1061
+ # https://github.com/saturday06/VRM-Addon-for-Blender/issues/58
1062
+ json_dict.pop("animations", None)
1063
+ with tempfile.TemporaryDirectory() as temp_dir:
1064
+ indexed_vrm_filepath = Path(temp_dir, "indexed.vrm")
1065
+ indexed_vrm_filepath.write_bytes(pack_glb(json_dict, buffer0_bytes))
1066
+ try:
1067
+ import_scene_gltf(
1068
+ ImportSceneGltfArguments(
1069
+ filepath=str(indexed_vrm_filepath),
1070
+ import_pack_images=True,
1071
+ bone_heuristic=bone_heuristic,
1072
+ guess_original_bind_pose=False,
1073
+ disable_bone_shape=True,
1074
+ import_scene_as_collection=False,
1075
+ )
1076
+ )
1077
+ except RuntimeError:
1078
+ _logger.exception(
1079
+ 'Failed to import "%s" generated from "%s"'
1080
+ " using glTF 2.0 Add-on without animations key",
1081
+ indexed_vrm_filepath,
1082
+ self._parse_result.filepath,
1083
+ )
1084
+ self.cleanup_gltf2_with_indices()
1085
+ raise
1086
+ # Save the selection state of objects immediately after glTF import
1087
+ self._imported_object_names = [
1088
+ selected_object.name for selected_object in self._context.selected_objects
1089
+ ]
1090
+
1091
+ for obj in self._context.blend_data.objects:
1092
+ node_index = obj.pop(extras_node_index_key, None)
1093
+ if isinstance(node_index, int):
1094
+ self._object_names[node_index] = obj.name
1095
+ data = obj.data
1096
+ if isinstance(data, Mesh):
1097
+ data.pop(extras_node_index_key, None)
1098
+
1099
+ if not isinstance(data, Armature):
1100
+ continue
1101
+
1102
+ for pose_bone in obj.pose.bones:
1103
+ pose_bone.pop(extras_node_index_key, None)
1104
+
1105
+ for bone_name, bone in data.bones.items():
1106
+ bone_node_index = bone.pop(extras_node_index_key, None)
1107
+ if not isinstance(bone_node_index, int):
1108
+ continue
1109
+ if 0 <= bone_node_index < len(node_dicts):
1110
+ node_dict = node_dicts[bone_node_index]
1111
+ if isinstance(node_dict, dict):
1112
+ node_dict["name"] = bone_name
1113
+ self._bone_names[bone_node_index] = bone_name
1114
+ if (
1115
+ self._armature is not None
1116
+ or bone_node_index != self._parse_result.hips_node_index
1117
+ ):
1118
+ continue
1119
+
1120
+ ext = get_armature_extension(data)
1121
+ vrm0 = ext.vrm0
1122
+ vrm1 = ext.vrm1
1123
+ spring_bone1 = ext.spring_bone1
1124
+ vrm0.humanoid.initial_automatic_bone_assignment = False
1125
+ vrm1.humanoid.human_bones.initial_automatic_bone_assignment = False
1126
+ if self._parse_result.spec_version_number >= (1, 0):
1127
+ vrm1.expressions.initial_automatic_expression_assignment = False
1128
+ spring_bone1.initial_automatic_spring_bone_assignment = False
1129
+ vrm0.humanoid.pose = vrm0.humanoid.POSE_CURRENT_POSE.identifier
1130
+ vrm1.humanoid.pose = vrm1.humanoid.POSE_CURRENT_POSE.identifier
1131
+ self._armature = obj
1132
+
1133
+ if (
1134
+ (armature := self._armature) is not None
1135
+ and isinstance(armature.data, Armature)
1136
+ and self._parse_result.spec_version_number < (1, 0)
1137
+ ):
1138
+ bone_name_to_roll: dict[str, float] = {}
1139
+ with save_workspace(self._context, armature, mode="EDIT"):
1140
+ for edit_bone in armature.data.edit_bones:
1141
+ bone_name_to_roll[edit_bone.name] = edit_bone.roll
1142
+
1143
+ with save_workspace(self._context, armature):
1144
+ armature_rotation = get_rotation_as_quaternion(armature)
1145
+ armature_rotation.rotate(Euler((0.0, 0.0, math.pi), "XYZ"))
1146
+ set_rotation_without_mode_change(armature, armature_rotation)
1147
+
1148
+ bpy.ops.object.select_all(action="DESELECT")
1149
+ armature.select_set(True)
1150
+ bpy.ops.object.transform_apply(
1151
+ location=False, rotation=True, scale=False, properties=False
1152
+ )
1153
+
1154
+ with self.save_bone_child_object_transforms(self._context, armature):
1155
+ edit_bones = [
1156
+ edit_bone
1157
+ for edit_bone in armature.data.edit_bones
1158
+ if not edit_bone.parent
1159
+ ]
1160
+ while edit_bones:
1161
+ edit_bone = edit_bones.pop(0)
1162
+ roll = bone_name_to_roll.get(edit_bone.name)
1163
+ if roll is not None:
1164
+ edit_bone.roll = roll
1165
+ edit_bones.extend(edit_bone.children)
1166
+
1167
+ for material in self._context.blend_data.materials:
1168
+ if self.is_temp_object_name(material.name):
1169
+ continue
1170
+ material_index = material.pop(extras_material_index_key, None)
1171
+ if isinstance(material_index, int):
1172
+ self._materials[material_index] = material
1173
+
1174
+ for mesh in self._context.blend_data.meshes:
1175
+ custom_mesh_index = mesh.pop(extras_mesh_index_key, None)
1176
+ custom_mesh_node_indices: Optional[Sequence[object]] = sequence_or_none(
1177
+ mesh.pop(extras_mesh_node_indices_key, None)
1178
+ )
1179
+ if not isinstance(custom_mesh_index, int):
1180
+ continue
1181
+ self._meshes[custom_mesh_index] = mesh
1182
+
1183
+ # In some cases, it may not be possible to retrieve the node index
1184
+ # corresponding to an object. If the mesh object that owns the mesh data
1185
+ # does not have a node index assigned to it, assign the array of node
1186
+ # indices registered in the mesh data.
1187
+ if not custom_mesh_node_indices:
1188
+ continue
1189
+ self._restore_object_names_from_mesh_node_indices(
1190
+ mesh, custom_mesh_node_indices, len(node_dicts)
1191
+ )
1192
+
1193
+ for image in list(self._context.blend_data.images):
1194
+ custom_image_index = image.get(self._import_id)
1195
+ if not isinstance(custom_image_index, int) and image.name.startswith(
1196
+ legacy_image_name_prefix
1197
+ ):
1198
+ custom_image_index_str = "".join(
1199
+ image.name.split(legacy_image_name_prefix)[1:]
1200
+ ).split("_", maxsplit=1)[0]
1201
+ with contextlib.suppress(ValueError):
1202
+ custom_image_index = int(custom_image_index_str)
1203
+ if not isinstance(custom_image_index, int):
1204
+ continue
1205
+ image_dicts = json_dict.get("images")
1206
+ if isinstance(image_dicts, list) and 0 <= custom_image_index < len(
1207
+ image_dicts
1208
+ ):
1209
+ # image.name may be automatically shortened during import, so
1210
+ # restore it from the json value
1211
+ image_dict = image_dicts[custom_image_index]
1212
+ indexed_image_name = None
1213
+
1214
+ if isinstance(image_dict, dict):
1215
+ indexed_image_name = image_dict.get("name")
1216
+
1217
+ if isinstance(indexed_image_name, str):
1218
+ if indexed_image_name.startswith(legacy_image_name_prefix):
1219
+ indexed_image_name = "_".join(indexed_image_name.split("_")[1:])
1220
+ else:
1221
+ indexed_image_name = None
1222
+
1223
+ if indexed_image_name:
1224
+ image.name = indexed_image_name
1225
+ else:
1226
+ # https://github.com/KhronosGroup/glTF-Blender-IO/blob/709630548cdc184af6ea50b2ff3ddc5450bc0af3/addons/io_scene_gltf2/blender/imp/gltf2_blender_image.py#L54
1227
+ image.name = f"Image_{custom_image_index}"
1228
+
1229
+ else:
1230
+ image.name = "_".join(image.name.split("_")[1:])
1231
+
1232
+ self._images[custom_image_index] = image
1233
+
1234
+ if self._context.object is not None and self._context.object.mode == "EDIT":
1235
+ bpy.ops.object.mode_set(mode="OBJECT")
1236
+
1237
+ for scene in list(self._context.blend_data.scenes):
1238
+ if not self.is_temp_object_name(scene.name):
1239
+ continue
1240
+ if bpy.app.version >= (3, 2) and scene.use_extra_user:
1241
+ scene.use_extra_user = False
1242
+ if scene.users:
1243
+ _logger.warning(
1244
+ 'Failed to remove "%s" with %d users while removing temp scenes',
1245
+ scene.name,
1246
+ scene.users,
1247
+ )
1248
+ else:
1249
+ self._context.blend_data.scenes.remove(scene)
1250
+
1251
+ for obj in list(self._context.blend_data.objects):
1252
+ if not self.is_temp_object_name(obj.name):
1253
+ continue
1254
+ if not safe_removal.remove_object(self._context, obj):
1255
+ _logger.warning(
1256
+ 'Failed to remove "%s" with %d users while removing temp objects',
1257
+ obj.name,
1258
+ obj.users,
1259
+ )
1260
+
1261
+ for mesh in list(self._context.blend_data.meshes):
1262
+ if not self.is_temp_object_name(mesh.name):
1263
+ continue
1264
+ if mesh.users:
1265
+ _logger.warning(
1266
+ 'Failed to remove "%s" with %d users while removing temp meshes',
1267
+ mesh.name,
1268
+ mesh.users,
1269
+ )
1270
+ else:
1271
+ self._context.blend_data.meshes.remove(mesh)
1272
+
1273
+ for material in list(self._context.blend_data.materials):
1274
+ if not self.is_temp_object_name(material.name):
1275
+ continue
1276
+ if material.users:
1277
+ _logger.warning(
1278
+ 'Failed to remove "%s" with %d users while removing temp materials',
1279
+ material.name,
1280
+ material.users,
1281
+ )
1282
+ else:
1283
+ self._context.blend_data.materials.remove(material)
1284
+
1285
+ if self._armature is None:
1286
+ _logger.warning("Failed to read VRM Humanoid")
1287
+
1288
+ def _restore_object_names_from_mesh_node_indices(
1289
+ self,
1290
+ mesh: Mesh,
1291
+ node_indices: Sequence[object],
1292
+ node_count: int,
1293
+ ) -> None:
1294
+ unassigned_object_names: list[str] = [
1295
+ obj.name
1296
+ for obj in self._context.blend_data.objects
1297
+ if obj.type == "MESH" and obj.data == mesh
1298
+ ]
1299
+ unassigned_node_indices: list[int] = []
1300
+ for node_index in node_indices:
1301
+ if not isinstance(node_index, int):
1302
+ continue
1303
+ if not (0 <= node_index < node_count):
1304
+ continue
1305
+ if (
1306
+ (object_name := self._object_names.get(node_index))
1307
+ and (obj := self._context.blend_data.objects.get(object_name))
1308
+ and obj.name in unassigned_object_names
1309
+ ):
1310
+ unassigned_object_names.remove(obj.name)
1311
+ else:
1312
+ unassigned_node_indices.append(node_index)
1313
+ if not unassigned_object_names:
1314
+ return
1315
+ # Link unassigned items to one another. However, it is very difficult to
1316
+ # establish completely accurate links.
1317
+ for index, node_index in enumerate(unassigned_node_indices):
1318
+ self._object_names[node_index] = unassigned_object_names[
1319
+ index % len(unassigned_object_names)
1320
+ ]
1321
+
1322
+ def cleanup_gltf2_with_indices(self) -> None:
1323
+ with save_workspace(self._context):
1324
+ meshes_key = self._import_id + "Meshes"
1325
+ nodes_key = self._import_id + "Nodes"
1326
+ remove_objs: list[Object] = []
1327
+ for obj in list(self._context.scene.collection.objects):
1328
+ if isinstance(armature_data := obj.data, Armature):
1329
+ for bone in armature_data.bones:
1330
+ if nodes_key in bone:
1331
+ remove_objs.append(obj)
1332
+ break
1333
+ continue
1334
+
1335
+ if isinstance(mesh_data := obj.data, Mesh) and (
1336
+ nodes_key in mesh_data
1337
+ or meshes_key in mesh_data
1338
+ or self.is_temp_object_name(mesh_data.name)
1339
+ ):
1340
+ remove_objs.append(obj)
1341
+ continue
1342
+
1343
+ if (
1344
+ nodes_key in obj
1345
+ or meshes_key in obj
1346
+ or self.is_temp_object_name(obj.name)
1347
+ ):
1348
+ remove_objs.append(obj)
1349
+
1350
+ for obj in remove_objs:
1351
+ safe_removal.remove_object(self._context, obj)
1352
+
1353
+ def temp_object_name(self) -> str:
1354
+ self._temp_object_name_count += 1
1355
+ return f"{self._import_id}Temp_{self._temp_object_name_count}_"
1356
+
1357
+ def is_temp_object_name(self, name: str) -> bool:
1358
+ return name.startswith(f"{self._import_id}Temp_")
1359
+
1360
+ def setup_viewport(self) -> None:
1361
+ if self._armature:
1362
+ if self._preferences.set_armature_display_to_wire:
1363
+ self._armature.display_type = "WIRE"
1364
+ if self._preferences.set_armature_display_to_show_in_front:
1365
+ self._armature.show_in_front = True
1366
+ if self._preferences.set_armature_bone_shape_to_default:
1367
+ for bone in self._armature.pose.bones:
1368
+ bone.custom_shape = None
1369
+
1370
+ if self._preferences.set_view_transform_to_standard_on_import:
1371
+ # https://github.com/saturday06/VRM-Addon-for-Blender/issues/336#issuecomment-1760729404
1372
+ view_settings = self._context.scene.view_settings
1373
+ try:
1374
+ view_settings.view_transform = "Standard"
1375
+ except TypeError:
1376
+ _logger.exception(
1377
+ 'scene.view_settings.view_transform doesn\'t support "Standard".'
1378
+ )
1379
+
1380
+ if self._preferences.set_shading_type_to_material_on_import:
1381
+ screen = self._context.screen
1382
+ if screen:
1383
+ for area in screen.areas:
1384
+ for space in area.spaces:
1385
+ if space.type == "VIEW_3D" and isinstance(space, SpaceView3D):
1386
+ space.shading.type = "MATERIAL"
1387
+
1388
+ def save_t_pose_action(self) -> None:
1389
+ if self._armature is None:
1390
+ return
1391
+
1392
+ t_pose_action = self._context.blend_data.actions.new(name="T-Pose")
1393
+ t_pose_action.use_fake_user = True
1394
+ animation_data_created = False
1395
+ if not self._armature.animation_data:
1396
+ self._armature.animation_data_create()
1397
+ animation_data_created = True
1398
+ if not self._armature.animation_data:
1399
+ message = "armature.animation_data is None"
1400
+ raise ValueError(message)
1401
+ original_action = self._armature.animation_data.action
1402
+ self._armature.animation_data.action = t_pose_action
1403
+
1404
+ for bone in self._armature.pose.bones:
1405
+ for data_path in (
1406
+ "location",
1407
+ "rotation_quaternion",
1408
+ "rotation_axis_angle",
1409
+ "rotation_euler",
1410
+ "scale",
1411
+ ):
1412
+ bone.keyframe_insert(data_path=data_path, frame=1)
1413
+
1414
+ ext = get_armature_extension(self.armature_data)
1415
+ ext.vrm0.humanoid.pose = ext.vrm0.humanoid.POSE_CUSTOM_POSE.identifier
1416
+ ext.vrm1.humanoid.pose = ext.vrm1.humanoid.POSE_CUSTOM_POSE.identifier
1417
+ ext.vrm0.humanoid.pose_library = t_pose_action
1418
+ ext.vrm1.humanoid.pose_library = t_pose_action
1419
+ if animation_data_created:
1420
+ self._armature.animation_data_clear()
1421
+ else:
1422
+ self._armature.animation_data.action = original_action
1423
+
1424
+ def setup_object_selection_and_activation(self) -> None:
1425
+ active_object = self._context.view_layer.objects.active
1426
+ if active_object and active_object.mode != "OBJECT":
1427
+ bpy.ops.object.mode_set(mode="OBJECT")
1428
+ if self._imported_object_names is not None:
1429
+ bpy.ops.object.select_all(action="DESELECT")
1430
+ for obj in self._context.selectable_objects:
1431
+ if obj.name in self._imported_object_names:
1432
+ obj.select_set(True)
1433
+ if self._armature is not None:
1434
+ self._context.view_layer.objects.active = self._armature
1435
+
1436
+ @abstractmethod
1437
+ def on_post_import(self) -> None:
1438
+ pass
1439
+
1440
+
1441
+ def parse_vrm_json(filepath: Path, *, license_validation: bool) -> ParseResult:
1442
+ json_dict, bin_chunk = parse_glb(filepath.read_bytes())
1443
+
1444
+ extensions_dict = json_dict.get("extensions")
1445
+ if isinstance(extensions_dict, dict):
1446
+ vrm1_extension_dict = extensions_dict.get("VRMC_vrm")
1447
+ vrm0_extension_dict = extensions_dict.get("VRM")
1448
+ else:
1449
+ vrm1_extension_dict = None
1450
+ vrm0_extension_dict = None
1451
+
1452
+ if isinstance(vrm1_extension_dict, dict):
1453
+ (
1454
+ spec_version_number,
1455
+ spec_version_str,
1456
+ spec_version_is_stable,
1457
+ hips_node_index,
1458
+ ) = _read_vrm1_extension(vrm1_extension_dict)
1459
+ vrm0_extension_dict = {}
1460
+ elif isinstance(vrm0_extension_dict, dict):
1461
+ (
1462
+ spec_version_number,
1463
+ spec_version_str,
1464
+ spec_version_is_stable,
1465
+ hips_node_index,
1466
+ ) = _read_vrm0_extension(vrm0_extension_dict)
1467
+ vrm1_extension_dict = {}
1468
+ else:
1469
+ spec_version_number = (0, 0)
1470
+ spec_version_str = "0.0"
1471
+ spec_version_is_stable = True
1472
+ hips_node_index = None
1473
+ vrm0_extension_dict = {}
1474
+ vrm1_extension_dict = {}
1475
+
1476
+ if license_validation:
1477
+ validate_license(json_dict, spec_version_number)
1478
+
1479
+ return ParseResult(
1480
+ filepath=filepath,
1481
+ json_dict=json_dict,
1482
+ spec_version_number=spec_version_number,
1483
+ spec_version_str=spec_version_str,
1484
+ spec_version_is_stable=spec_version_is_stable,
1485
+ vrm0_extension_dict=vrm0_extension_dict,
1486
+ vrm1_extension_dict=vrm1_extension_dict,
1487
+ hips_node_index=hips_node_index,
1488
+ bin_chunk=bin_chunk,
1489
+ )
1490
+
1491
+
1492
+ def _read_vrm0_extension(
1493
+ vrm0_dict: dict[str, Json],
1494
+ ) -> tuple[tuple[int, int], str, bool, Optional[int]]:
1495
+ spec_version_number = (0, 0)
1496
+ spec_version_str = "0.0"
1497
+ spec_version_is_stable = True
1498
+ hips_node_index = None
1499
+
1500
+ spec_version = vrm0_dict.get("specVersion")
1501
+ if isinstance(spec_version, str):
1502
+ spec_version_str = spec_version
1503
+
1504
+ humanoid_dict = vrm0_dict.get("humanoid")
1505
+ if isinstance(humanoid_dict, dict):
1506
+ human_bone_dicts = humanoid_dict.get("humanBones")
1507
+ if isinstance(human_bone_dicts, list):
1508
+ for human_bone_dict in human_bone_dicts:
1509
+ if not isinstance(human_bone_dict, dict):
1510
+ continue
1511
+ if human_bone_dict.get("bone") != "hips":
1512
+ continue
1513
+ node_index = human_bone_dict.get("node")
1514
+ if isinstance(node_index, int):
1515
+ hips_node_index = node_index
1516
+
1517
+ return (
1518
+ spec_version_number,
1519
+ spec_version_str,
1520
+ spec_version_is_stable,
1521
+ hips_node_index,
1522
+ )
1523
+
1524
+
1525
+ def _read_vrm1_extension(
1526
+ vrm1_dict: dict[str, Json],
1527
+ ) -> tuple[tuple[int, int], str, bool, Optional[int]]:
1528
+ spec_version_number = (1, 0)
1529
+ spec_version_str = "1.0"
1530
+ spec_version_is_stable = True
1531
+ hips_node_index = None
1532
+
1533
+ humanoid_dict = vrm1_dict.get("humanoid")
1534
+ if isinstance(humanoid_dict, dict):
1535
+ human_bones_dict = humanoid_dict.get("humanBones")
1536
+ if isinstance(human_bones_dict, dict):
1537
+ hips_dict = human_bones_dict.get("hips")
1538
+ if isinstance(hips_dict, dict):
1539
+ node_index = hips_dict.get("node")
1540
+ if isinstance(node_index, int):
1541
+ hips_node_index = node_index
1542
+
1543
+ return (
1544
+ spec_version_number,
1545
+ spec_version_str,
1546
+ spec_version_is_stable,
1547
+ hips_node_index,
1548
+ )