@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,1035 @@
1
+ # SPDX-License-Identifier: MIT OR GPL-3.0-or-later
2
+ import itertools
3
+ import struct
4
+ from collections.abc import Mapping, Sequence
5
+ from os import environ
6
+ from pathlib import Path
7
+ from sys import float_info
8
+ from typing import Optional
9
+
10
+ import bpy
11
+ from bpy.types import (
12
+ Action,
13
+ Armature,
14
+ Context,
15
+ FCurve,
16
+ Object,
17
+ PoseBone,
18
+ )
19
+ from mathutils import Matrix, Quaternion, Vector
20
+
21
+ from ..common import version
22
+ from ..common.convert import Json
23
+ from ..common.deep import make_json
24
+ from ..common.gl import GL_FLOAT
25
+ from ..common.gltf import pack_glb
26
+ from ..common.logger import get_logger
27
+ from ..common.vrm1.human_bone import (
28
+ HumanBoneName,
29
+ HumanBoneSpecification,
30
+ HumanBoneSpecifications,
31
+ )
32
+ from ..common.workspace import save_workspace
33
+ from ..editor.extension_accessor import get_armature_extension
34
+ from ..editor.t_pose import setup_humanoid_t_pose
35
+ from ..editor.vrm1.property_group import Vrm1HumanBonesPropertyGroup, Vrm1PropertyGroup
36
+
37
+ _logger = get_logger(__name__)
38
+
39
+
40
+ class VrmAnimationExporter:
41
+ @staticmethod
42
+ def execute(context: Context, path: Path, armature: Object) -> set[str]:
43
+ armature_data = armature.data
44
+ if not isinstance(armature_data, Armature):
45
+ return {"CANCELLED"}
46
+
47
+ with (
48
+ setup_humanoid_t_pose(context, armature),
49
+ save_workspace(context, armature, mode="POSE"),
50
+ ):
51
+ output_bytes = _export_vrm_animation(context, armature)
52
+
53
+ path.write_bytes(output_bytes)
54
+ return {"FINISHED"}
55
+
56
+
57
+ def _connect_humanoid_node_dicts(
58
+ human_bone_specification: HumanBoneSpecification,
59
+ human_bone_name_to_node_dict: dict[HumanBoneName, dict[str, Json]],
60
+ parent_node_dict: Optional[dict[str, Json]],
61
+ node_dicts: list[dict[str, Json]],
62
+ human_bone_name_to_node_index: dict[HumanBoneName, int],
63
+ ) -> None:
64
+ current_node_dict = human_bone_name_to_node_dict.get(human_bone_specification.name)
65
+ if isinstance(current_node_dict, dict):
66
+ node_index = len(node_dicts)
67
+ human_bone_name_to_node_index[human_bone_specification.name] = node_index
68
+ node_dicts.append(current_node_dict)
69
+ if parent_node_dict is not None:
70
+ children = parent_node_dict.get("children")
71
+ if not isinstance(children, list):
72
+ children = []
73
+ parent_node_dict["children"] = children
74
+ children.append(node_index)
75
+ parent_node_dict = current_node_dict
76
+ for child in human_bone_specification.children:
77
+ _connect_humanoid_node_dicts(
78
+ child,
79
+ human_bone_name_to_node_dict,
80
+ parent_node_dict,
81
+ node_dicts,
82
+ human_bone_name_to_node_index,
83
+ )
84
+
85
+
86
+ def _create_node_dicts(
87
+ armature: Object,
88
+ human_bones: Vrm1HumanBonesPropertyGroup,
89
+ human_bone_specification: HumanBoneSpecification,
90
+ parent_bone: Optional[PoseBone],
91
+ node_dicts: list[dict[str, Json]],
92
+ bone_name_to_node_index: dict[str, int],
93
+ ) -> list[int]:
94
+ human_bone = human_bones.human_bone_name_to_human_bone().get(
95
+ human_bone_specification.name
96
+ )
97
+ if not human_bone:
98
+ _logger.error("Failed to find human bone %s", human_bone_specification.name)
99
+ return []
100
+
101
+ bone = armature.pose.bones.get(human_bone.node.bone_name)
102
+ if not bone:
103
+ return sum(
104
+ [
105
+ _create_node_dicts(
106
+ armature,
107
+ human_bones,
108
+ child,
109
+ parent_bone,
110
+ node_dicts,
111
+ bone_name_to_node_index,
112
+ )
113
+ for child in human_bone_specification.children
114
+ ],
115
+ list[int](),
116
+ )
117
+
118
+ node_index = len(node_dicts)
119
+ node_dict: dict[str, Json] = {
120
+ "name": human_bone_specification.title.replace(" ", ""),
121
+ }
122
+ node_dicts.append(node_dict)
123
+ bone_name_to_node_index[bone.name] = node_index
124
+
125
+ parent_bone_matrix = parent_bone.matrix if parent_bone else Matrix()
126
+ matrix = parent_bone_matrix.inverted() @ bone.matrix
127
+ translation = matrix.to_translation()
128
+ rotation = matrix.to_quaternion()
129
+ node_dict["translation"] = [
130
+ translation.x,
131
+ translation.z,
132
+ -translation.y,
133
+ ]
134
+ node_dict["rotation"] = [
135
+ rotation.x,
136
+ rotation.z,
137
+ -rotation.y,
138
+ rotation.w,
139
+ ]
140
+ children = sum(
141
+ [
142
+ _create_node_dicts(
143
+ armature, human_bones, child, bone, node_dicts, bone_name_to_node_index
144
+ )
145
+ for child in human_bone_specification.children
146
+ ],
147
+ list[int](),
148
+ )
149
+ if children:
150
+ node_dict["children"] = make_json(children)
151
+
152
+ return [node_index]
153
+
154
+
155
+ def _export_vrm_animation(context: Context, armature: Object) -> bytes:
156
+ armature_data = armature.data
157
+ if not isinstance(armature_data, Armature):
158
+ message = "Armature data is not an Armature"
159
+ raise TypeError(message)
160
+
161
+ bpy.ops.pose.select_all(action="DESELECT")
162
+
163
+ vrm1 = get_armature_extension(armature_data).vrm1
164
+ human_bones = vrm1.humanoid.human_bones
165
+
166
+ node_dicts: list[dict[str, Json]] = []
167
+ bone_name_to_node_index: dict[str, int] = {}
168
+ scene_node_indices: list[int] = [0]
169
+ root_node_translation = armature.matrix_world.to_translation()
170
+ root_node_rotation = armature.matrix_world.to_quaternion()
171
+ root_node_scale = armature.matrix_world.to_scale()
172
+ root_node_dict: dict[str, Json] = {
173
+ "name": armature.name,
174
+ "translation": [
175
+ root_node_translation.x,
176
+ root_node_translation.z,
177
+ -root_node_translation.y,
178
+ ],
179
+ "rotation": [
180
+ root_node_rotation.x,
181
+ root_node_rotation.z,
182
+ -root_node_rotation.y,
183
+ root_node_rotation.w,
184
+ ],
185
+ "scale": [
186
+ root_node_scale.x,
187
+ root_node_scale.z,
188
+ root_node_scale.y,
189
+ ],
190
+ }
191
+ node_dicts.append(root_node_dict)
192
+
193
+ root_node_dict["children"] = make_json(
194
+ _create_node_dicts(
195
+ armature,
196
+ human_bones,
197
+ HumanBoneSpecifications.HIPS,
198
+ None,
199
+ node_dicts,
200
+ bone_name_to_node_index,
201
+ )
202
+ )
203
+
204
+ if armature_data.pose_position != "POSE":
205
+ armature_data.pose_position = "POSE"
206
+ context.view_layer.update()
207
+
208
+ frame_to_timestamp_factor = context.scene.render.fps_base / float(
209
+ context.scene.render.fps
210
+ )
211
+
212
+ buffer0_bytearray = bytearray()
213
+ accessor_dicts: list[dict[str, Json]] = []
214
+ buffer_view_dicts: list[dict[str, Json]] = []
215
+ animation_sampler_dicts: list[dict[str, Json]] = []
216
+ animation_channel_dicts: list[dict[str, Json]] = []
217
+ preset_expression_dict: dict[str, dict[str, Json]] = {}
218
+ custom_expression_dict: dict[str, dict[str, Json]] = {}
219
+
220
+ frame_start = context.scene.frame_start
221
+ frame_end = context.scene.frame_end
222
+
223
+ _create_expression_animation(
224
+ vrm1,
225
+ frame_start=frame_start,
226
+ frame_end=frame_end,
227
+ frame_to_timestamp_factor=frame_to_timestamp_factor,
228
+ armature_data=armature_data,
229
+ node_dicts=node_dicts,
230
+ accessor_dicts=accessor_dicts,
231
+ buffer_view_dicts=buffer_view_dicts,
232
+ animation_channel_dicts=animation_channel_dicts,
233
+ animation_sampler_dicts=animation_sampler_dicts,
234
+ scene_node_indices=scene_node_indices,
235
+ buffer0_bytearray=buffer0_bytearray,
236
+ preset_expression_dict=preset_expression_dict,
237
+ custom_expression_dict=custom_expression_dict,
238
+ )
239
+
240
+ _create_node_animation(
241
+ context=context,
242
+ frame_start=frame_start,
243
+ frame_end=frame_end,
244
+ frame_to_timestamp_factor=frame_to_timestamp_factor,
245
+ armature=armature,
246
+ human_bones=human_bones,
247
+ bone_name_to_node_index=bone_name_to_node_index,
248
+ buffer0_bytearray=buffer0_bytearray,
249
+ buffer_view_dicts=buffer_view_dicts,
250
+ accessor_dicts=accessor_dicts,
251
+ animation_channel_dicts=animation_channel_dicts,
252
+ animation_sampler_dicts=animation_sampler_dicts,
253
+ )
254
+
255
+ look_at_target_node_index = _create_look_at_animation(
256
+ vrm1,
257
+ frame_start=frame_start,
258
+ frame_end=frame_end,
259
+ frame_to_timestamp_factor=frame_to_timestamp_factor,
260
+ node_dicts=node_dicts,
261
+ accessor_dicts=accessor_dicts,
262
+ buffer_view_dicts=buffer_view_dicts,
263
+ animation_channel_dicts=animation_channel_dicts,
264
+ animation_sampler_dicts=animation_sampler_dicts,
265
+ buffer0_bytearray=buffer0_bytearray,
266
+ )
267
+
268
+ buffer_dicts: list[dict[str, Json]] = [{"byteLength": len(buffer0_bytearray)}]
269
+
270
+ human_bones_dict: dict[str, Json] = {}
271
+ human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
272
+ for human_bone_name, human_bone in human_bone_name_to_human_bone.items():
273
+ bone_name = human_bone.node.bone_name
274
+ node_index = bone_name_to_node_index.get(bone_name)
275
+ if not isinstance(node_index, int):
276
+ continue
277
+ human_bones_dict[human_bone_name.value] = {"node": node_index}
278
+
279
+ addon_version = version.get_addon_version()
280
+ if environ.get("BLENDER_VRM_USE_TEST_EXPORTER_VERSION") == "true":
281
+ addon_version = (999, 999, 999)
282
+
283
+ vrmc_vrm_animation_dict: dict[str, Json] = {
284
+ "specVersion": "1.0",
285
+ "humanoid": {
286
+ "humanBones": human_bones_dict,
287
+ },
288
+ "expressions": make_json(
289
+ {
290
+ "preset": preset_expression_dict,
291
+ "custom": custom_expression_dict,
292
+ }
293
+ ),
294
+ }
295
+ if look_at_target_node_index is not None:
296
+ vrmc_vrm_animation_dict["lookAt"] = {
297
+ "node": look_at_target_node_index,
298
+ "offsetFromHeadBone": list(vrm1.look_at.offset_from_head_bone),
299
+ }
300
+
301
+ vrma_dict: dict[str, Json] = {
302
+ "asset": make_json(
303
+ {
304
+ "version": "2.0",
305
+ "generator": "VRM Add-on for Blender v"
306
+ + ".".join(map(str, addon_version)),
307
+ }
308
+ ),
309
+ "nodes": make_json(node_dicts),
310
+ "scenes": make_json([{"nodes": scene_node_indices}]),
311
+ "extensionsUsed": ["VRMC_vrm_animation"],
312
+ "extensions": {
313
+ "VRMC_vrm_animation": vrmc_vrm_animation_dict,
314
+ },
315
+ }
316
+
317
+ if buffer_dicts:
318
+ vrma_dict["buffers"] = make_json(buffer_dicts)
319
+ if buffer_view_dicts:
320
+ vrma_dict["bufferViews"] = make_json(buffer_view_dicts)
321
+ if accessor_dicts:
322
+ vrma_dict["accessors"] = make_json(accessor_dicts)
323
+ if animation_channel_dicts and animation_sampler_dicts:
324
+ vrma_dict["animations"] = make_json(
325
+ [
326
+ {
327
+ "channels": animation_channel_dicts,
328
+ "samplers": animation_sampler_dicts,
329
+ }
330
+ ]
331
+ )
332
+
333
+ return pack_glb(vrma_dict, buffer0_bytearray)
334
+
335
+
336
+ def _create_look_at_animation(
337
+ vrm1: Vrm1PropertyGroup,
338
+ *,
339
+ frame_start: int,
340
+ frame_end: int,
341
+ frame_to_timestamp_factor: float,
342
+ node_dicts: list[dict[str, Json]],
343
+ accessor_dicts: list[dict[str, Json]],
344
+ buffer_view_dicts: list[dict[str, Json]],
345
+ animation_channel_dicts: list[dict[str, Json]],
346
+ animation_sampler_dicts: list[dict[str, Json]],
347
+ buffer0_bytearray: bytearray,
348
+ ) -> Optional[int]:
349
+ look_at_target_object = vrm1.look_at.preview_target_bpy_object
350
+ if not look_at_target_object:
351
+ return None
352
+
353
+ animation_data = look_at_target_object.animation_data
354
+ if not animation_data:
355
+ return None
356
+
357
+ action = animation_data.action
358
+ if not action:
359
+ return None
360
+
361
+ look_at_translation_offsets: list[Vector] = []
362
+ data_path = look_at_target_object.path_from_id("location")
363
+ for fcurve in _get_action_fcurves(action):
364
+ if fcurve.mute:
365
+ continue
366
+ if not fcurve.is_valid:
367
+ continue
368
+ if fcurve.data_path != data_path:
369
+ continue
370
+ for frame in range(frame_start, frame_end + 1):
371
+ offset = frame - frame_start
372
+ value = float(fcurve.evaluate(frame))
373
+ if offset < len(look_at_translation_offsets):
374
+ translation_offset = look_at_translation_offsets[offset]
375
+ else:
376
+ translation_offset = Vector((0.0, 0.0, 0.0))
377
+ look_at_translation_offsets.append(translation_offset)
378
+ translation_offset[fcurve.array_index] = value
379
+
380
+ parent = look_at_target_object.parent
381
+ parent_world_matrix = parent.matrix_world if parent else Matrix()
382
+
383
+ look_at_translations = [
384
+ parent_world_matrix @ look_at_translation_offset
385
+ for look_at_translation_offset in look_at_translation_offsets
386
+ ]
387
+ if not look_at_translations:
388
+ return None
389
+
390
+ look_at_target_node_index = len(node_dicts)
391
+ look_at_default_node_translation = (
392
+ look_at_target_object.matrix_world.to_translation()
393
+ )
394
+ node_dicts.append(
395
+ {
396
+ "name": look_at_target_object.name,
397
+ "translation": [
398
+ look_at_default_node_translation.x,
399
+ look_at_default_node_translation.z,
400
+ -look_at_default_node_translation.y,
401
+ ],
402
+ }
403
+ )
404
+
405
+ input_byte_offset = len(buffer0_bytearray)
406
+ input_floats = [
407
+ frame * frame_to_timestamp_factor
408
+ for frame, _ in enumerate(look_at_translations)
409
+ ]
410
+ input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
411
+ buffer0_bytearray.extend(input_bytes)
412
+ while len(buffer0_bytearray) % 4:
413
+ buffer0_bytearray.append(0)
414
+ input_buffer_view_index = len(buffer_view_dicts)
415
+ input_buffer_view_dict: dict[str, Json] = {
416
+ "buffer": 0,
417
+ "byteLength": len(input_bytes),
418
+ }
419
+ if input_byte_offset > 0:
420
+ input_buffer_view_dict["byteOffset"] = input_byte_offset
421
+ buffer_view_dicts.append(input_buffer_view_dict)
422
+
423
+ output_byte_offset = len(buffer0_bytearray)
424
+ gltf_translations = [
425
+ (
426
+ translation.x,
427
+ translation.z,
428
+ -translation.y,
429
+ )
430
+ for translation in look_at_translations
431
+ ]
432
+ translation_floats = list(itertools.chain(*gltf_translations))
433
+ translation_bytes = struct.pack(
434
+ "<" + "f" * len(translation_floats), *translation_floats
435
+ )
436
+ buffer0_bytearray.extend(translation_bytes)
437
+ while len(buffer0_bytearray) % 4:
438
+ buffer0_bytearray.append(0)
439
+ output_buffer_view_index = len(buffer_view_dicts)
440
+ output_buffer_view_dict: dict[str, Json] = {
441
+ "buffer": 0,
442
+ "byteLength": len(translation_bytes),
443
+ }
444
+ if output_byte_offset > 0:
445
+ output_buffer_view_dict["byteOffset"] = output_byte_offset
446
+ buffer_view_dicts.append(output_buffer_view_dict)
447
+
448
+ input_accessor_index = len(accessor_dicts)
449
+ accessor_dicts.append(
450
+ {
451
+ "bufferView": input_buffer_view_index,
452
+ "componentType": GL_FLOAT,
453
+ "count": len(input_floats),
454
+ "type": "SCALAR",
455
+ "min": [min(input_floats)],
456
+ "max": [max(input_floats)],
457
+ }
458
+ )
459
+
460
+ output_accessor_index = len(accessor_dicts)
461
+ gltf_translation_x_values = [t[0] for t in gltf_translations]
462
+ gltf_translation_y_values = [t[1] for t in gltf_translations]
463
+ gltf_translation_z_values = [t[2] for t in gltf_translations]
464
+ accessor_dicts.append(
465
+ {
466
+ "bufferView": output_buffer_view_index,
467
+ "componentType": GL_FLOAT,
468
+ "count": len(look_at_translations),
469
+ "type": "VEC3",
470
+ "min": [
471
+ min(gltf_translation_x_values),
472
+ min(gltf_translation_y_values),
473
+ min(gltf_translation_z_values),
474
+ ],
475
+ "max": [
476
+ max(gltf_translation_x_values),
477
+ max(gltf_translation_y_values),
478
+ max(gltf_translation_z_values),
479
+ ],
480
+ }
481
+ )
482
+
483
+ animation_sampler_index = len(animation_sampler_dicts)
484
+ animation_sampler_dicts.append(
485
+ {
486
+ "input": input_accessor_index,
487
+ "output": output_accessor_index,
488
+ }
489
+ )
490
+ animation_channel_dicts.append(
491
+ {
492
+ "sampler": animation_sampler_index,
493
+ "target": {
494
+ "node": look_at_target_node_index,
495
+ "path": "translation",
496
+ },
497
+ }
498
+ )
499
+
500
+ return look_at_target_node_index
501
+
502
+
503
+ def _create_expression_animation(
504
+ vrm1: Vrm1PropertyGroup,
505
+ *,
506
+ frame_start: int,
507
+ frame_end: int,
508
+ frame_to_timestamp_factor: float,
509
+ armature_data: Armature,
510
+ node_dicts: list[dict[str, Json]],
511
+ accessor_dicts: list[dict[str, Json]],
512
+ buffer_view_dicts: list[dict[str, Json]],
513
+ animation_channel_dicts: list[dict[str, Json]],
514
+ animation_sampler_dicts: list[dict[str, Json]],
515
+ scene_node_indices: list[int],
516
+ buffer0_bytearray: bytearray,
517
+ preset_expression_dict: dict[str, dict[str, Json]],
518
+ custom_expression_dict: dict[str, dict[str, Json]],
519
+ ) -> None:
520
+ expression_animation_data = armature_data.animation_data
521
+ if not expression_animation_data:
522
+ return
523
+
524
+ action = expression_animation_data.action
525
+ if not action:
526
+ return
527
+
528
+ data_path_to_expression_name: dict[str, str] = {}
529
+ for (
530
+ expression_name,
531
+ expression,
532
+ ) in vrm1.expressions.all_name_to_expression_dict().items():
533
+ if expression_name in {
534
+ "lookUp",
535
+ "lookDown",
536
+ "lookLeft",
537
+ "lookRight",
538
+ }:
539
+ continue
540
+ data_path_to_expression_name[expression.path_from_id("preview")] = (
541
+ expression_name
542
+ )
543
+
544
+ expression_name_to_expression_values: dict[
545
+ str, list[tuple[float, float, float]]
546
+ ] = {}
547
+
548
+ expression_export_index = 0
549
+ for fcurve in _get_action_fcurves(action):
550
+ if fcurve.mute:
551
+ continue
552
+ if not fcurve.is_valid:
553
+ continue
554
+ expression_name = data_path_to_expression_name.get(fcurve.data_path)
555
+ if not expression_name:
556
+ continue
557
+ for frame in range(frame_start, frame_end + 1):
558
+ expression_values = expression_name_to_expression_values.get(
559
+ expression_name
560
+ )
561
+ if not expression_values:
562
+ expression_values = []
563
+ expression_name_to_expression_values[expression_name] = (
564
+ expression_values
565
+ )
566
+ expression_values.append(
567
+ (
568
+ max(0, min(fcurve.evaluate(frame), 1)),
569
+ 0,
570
+ expression_export_index / 8.0,
571
+ )
572
+ )
573
+ expression_export_index += 1
574
+
575
+ node_index: Optional[int] = None
576
+ for (
577
+ expression_name,
578
+ expression_translations,
579
+ ) in expression_name_to_expression_values.items():
580
+ node_index = len(node_dicts)
581
+ node_dicts.append(
582
+ {
583
+ "name": expression_name,
584
+ }
585
+ )
586
+
587
+ if expression_name in vrm1.expressions.preset.name_to_expression_dict():
588
+ preset_expression_dict[expression_name] = {
589
+ "node": node_index,
590
+ }
591
+ else:
592
+ custom_expression_dict[expression_name] = {
593
+ "node": node_index,
594
+ }
595
+ scene_node_indices.append(node_index)
596
+
597
+ input_byte_offset = len(buffer0_bytearray)
598
+ input_floats = [
599
+ frame * frame_to_timestamp_factor
600
+ for frame, _ in enumerate(expression_translations)
601
+ ]
602
+ input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
603
+ buffer0_bytearray.extend(input_bytes)
604
+ while len(buffer0_bytearray) % 4:
605
+ buffer0_bytearray.append(0)
606
+ input_buffer_view_index = len(buffer_view_dicts)
607
+ input_buffer_view_dict: dict[str, Json] = {
608
+ "buffer": 0,
609
+ "byteLength": len(input_bytes),
610
+ }
611
+ if input_byte_offset > 0:
612
+ input_buffer_view_dict["byteOffset"] = input_byte_offset
613
+ buffer_view_dicts.append(input_buffer_view_dict)
614
+
615
+ output_byte_offset = len(buffer0_bytearray)
616
+ expression_translation_floats: list[float] = list(
617
+ itertools.chain(*expression_translations)
618
+ )
619
+ translation_bytes = struct.pack(
620
+ "<" + "f" * len(expression_translation_floats),
621
+ *expression_translation_floats,
622
+ )
623
+ buffer0_bytearray.extend(translation_bytes)
624
+ while len(buffer0_bytearray) % 4:
625
+ buffer0_bytearray.append(0)
626
+ output_buffer_view_index = len(buffer_view_dicts)
627
+ output_buffer_view_dict: dict[str, Json] = {
628
+ "buffer": 0,
629
+ "byteLength": len(translation_bytes),
630
+ }
631
+ if output_byte_offset > 0:
632
+ output_buffer_view_dict["byteOffset"] = output_byte_offset
633
+ buffer_view_dicts.append(output_buffer_view_dict)
634
+
635
+ input_accessor_index = len(accessor_dicts)
636
+ accessor_dicts.append(
637
+ {
638
+ "bufferView": input_buffer_view_index,
639
+ "componentType": GL_FLOAT,
640
+ "count": len(input_floats),
641
+ "type": "SCALAR",
642
+ "min": [min(input_floats)],
643
+ "max": [max(input_floats)],
644
+ }
645
+ )
646
+
647
+ output_accessor_index = len(accessor_dicts)
648
+ accessor_dicts.append(
649
+ {
650
+ "bufferView": output_buffer_view_index,
651
+ "componentType": GL_FLOAT,
652
+ "count": len(expression_translations),
653
+ "type": "VEC3",
654
+ "min": [
655
+ min(values)
656
+ for values in (
657
+ (
658
+ gltf_translation[i]
659
+ for gltf_translation in expression_translations
660
+ )
661
+ for i in range(3)
662
+ )
663
+ ],
664
+ "max": [
665
+ max(values)
666
+ for values in (
667
+ (
668
+ gltf_translation[i]
669
+ for gltf_translation in expression_translations
670
+ )
671
+ for i in range(3)
672
+ )
673
+ ],
674
+ }
675
+ )
676
+
677
+ animation_sampler_index = len(animation_sampler_dicts)
678
+ animation_sampler_dicts.append(
679
+ {
680
+ "input": input_accessor_index,
681
+ "output": output_accessor_index,
682
+ }
683
+ )
684
+ animation_channel_dicts.append(
685
+ {
686
+ "sampler": animation_sampler_index,
687
+ "target": {"node": node_index, "path": "translation"},
688
+ }
689
+ )
690
+
691
+
692
+ def _set_frame_rotations(
693
+ armature: Object,
694
+ human_bones: Vrm1HumanBonesPropertyGroup,
695
+ human_bone_specification: HumanBoneSpecification,
696
+ parent_current_pose_matrix: Matrix,
697
+ bone_name_to_node_index: Mapping[str, int],
698
+ bone_name_to_quaternions: dict[str, list[Quaternion]],
699
+ ) -> None:
700
+ human_bone = human_bones.human_bone_name_to_human_bone().get(
701
+ human_bone_specification.name
702
+ )
703
+ if not human_bone:
704
+ _logger.error("Failed to find human bone %s", human_bone_specification.name)
705
+ return
706
+
707
+ bone = armature.pose.bones.get(human_bone.node.bone_name)
708
+ if not bone:
709
+ for child in human_bone_specification.children:
710
+ _set_frame_rotations(
711
+ armature,
712
+ human_bones,
713
+ child,
714
+ parent_current_pose_matrix,
715
+ bone_name_to_node_index,
716
+ bone_name_to_quaternions,
717
+ )
718
+ return
719
+
720
+ matrix = parent_current_pose_matrix.inverted() @ bone.matrix
721
+ quaternion = matrix.to_quaternion()
722
+
723
+ quaternions = bone_name_to_quaternions.get(bone.name)
724
+ if quaternions is None:
725
+ quaternions = []
726
+ bone_name_to_quaternions[bone.name] = quaternions
727
+ quaternions.append(quaternion)
728
+
729
+ for child in human_bone_specification.children:
730
+ _set_frame_rotations(
731
+ armature,
732
+ human_bones,
733
+ child,
734
+ bone.matrix,
735
+ bone_name_to_node_index,
736
+ bone_name_to_quaternions,
737
+ )
738
+
739
+
740
+ def _create_node_animation(
741
+ context: Context,
742
+ frame_start: int,
743
+ frame_end: int,
744
+ frame_to_timestamp_factor: float,
745
+ armature: Object,
746
+ human_bones: Vrm1HumanBonesPropertyGroup,
747
+ bone_name_to_node_index: Mapping[str, int],
748
+ buffer0_bytearray: bytearray,
749
+ buffer_view_dicts: list[dict[str, Json]],
750
+ accessor_dicts: list[dict[str, Json]],
751
+ animation_channel_dicts: list[dict[str, Json]],
752
+ animation_sampler_dicts: list[dict[str, Json]],
753
+ ) -> None:
754
+ human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
755
+
756
+ animation_data = armature.animation_data
757
+ if not animation_data:
758
+ return
759
+
760
+ action = animation_data.action
761
+ if not action:
762
+ return
763
+
764
+ bone_name_to_quaternions: dict[str, list[Quaternion]] = {}
765
+ hips_translations: list[Vector] = []
766
+
767
+ hips_bone = human_bones.human_bone_name_to_human_bone().get(HumanBoneName.HIPS)
768
+ if not hips_bone:
769
+ _logger.error("Failed to find hips bone")
770
+ return
771
+ hips_bone_name = hips_bone.node.bone_name
772
+
773
+ for frame in range(frame_start, frame_end + 1):
774
+ armature.pose.apply_pose_from_action(action, evaluation_time=frame)
775
+ context.view_layer.update()
776
+ _set_frame_rotations(
777
+ armature,
778
+ human_bones,
779
+ HumanBoneSpecifications.HIPS,
780
+ Matrix(),
781
+ bone_name_to_node_index,
782
+ bone_name_to_quaternions,
783
+ )
784
+ hips_pose_bone = armature.pose.bones.get(hips_bone_name)
785
+ if not hips_pose_bone:
786
+ _logger.error("Failed to find hips bone %s", hips_bone_name)
787
+ continue
788
+ hips_translations.append(hips_pose_bone.matrix.to_translation())
789
+
790
+ # Export rotation
791
+ for bone_name, quaternions in bone_name_to_quaternions.items():
792
+ if all(
793
+ abs(quaternion.angle) < float_info.epsilon for quaternion in quaternions
794
+ ):
795
+ continue
796
+ human_bone_name = next(
797
+ (
798
+ n
799
+ for n, human_bone in human_bone_name_to_human_bone.items()
800
+ if human_bone.node.bone_name == bone_name
801
+ ),
802
+ None,
803
+ )
804
+ if human_bone_name is None:
805
+ _logger.error("Failed to find human bone name for bone %s", bone_name)
806
+ continue
807
+ if human_bone_name in {HumanBoneName.RIGHT_EYE, HumanBoneName.LEFT_EYE}:
808
+ continue
809
+ node_index = bone_name_to_node_index.get(bone_name)
810
+ if not isinstance(node_index, int):
811
+ _logger.error("Failed to find node index for bone %s", bone_name)
812
+ continue
813
+
814
+ input_byte_offset = len(buffer0_bytearray)
815
+ input_floats = [
816
+ frame * frame_to_timestamp_factor for frame, _ in enumerate(quaternions)
817
+ ]
818
+ input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
819
+ buffer0_bytearray.extend(input_bytes)
820
+ while len(buffer0_bytearray) % 4:
821
+ buffer0_bytearray.append(0)
822
+ input_buffer_view_index = len(buffer_view_dicts)
823
+ input_buffer_view_dict: dict[str, Json] = {
824
+ "buffer": 0,
825
+ "byteLength": len(input_bytes),
826
+ }
827
+ if input_byte_offset > 0:
828
+ input_buffer_view_dict["byteOffset"] = input_byte_offset
829
+ buffer_view_dicts.append(input_buffer_view_dict)
830
+
831
+ output_byte_offset = len(buffer0_bytearray)
832
+ gltf_quaternions = [
833
+ (
834
+ quaternion.x,
835
+ quaternion.z,
836
+ -quaternion.y,
837
+ quaternion.w,
838
+ )
839
+ for quaternion in quaternions
840
+ ]
841
+ quaternion_floats: list[float] = list(itertools.chain(*gltf_quaternions))
842
+ quaternion_bytes = struct.pack(
843
+ "<" + "f" * len(quaternion_floats), *quaternion_floats
844
+ )
845
+ buffer0_bytearray.extend(quaternion_bytes)
846
+ while len(buffer0_bytearray) % 4:
847
+ buffer0_bytearray.append(0)
848
+ output_buffer_view_index = len(buffer_view_dicts)
849
+ output_buffer_view_dict: dict[str, Json] = {
850
+ "buffer": 0,
851
+ "byteLength": len(quaternion_bytes),
852
+ }
853
+ if output_byte_offset > 0:
854
+ output_buffer_view_dict["byteOffset"] = output_byte_offset
855
+ buffer_view_dicts.append(output_buffer_view_dict)
856
+
857
+ input_accessor_index = len(accessor_dicts)
858
+ accessor_dicts.append(
859
+ {
860
+ "bufferView": input_buffer_view_index,
861
+ "componentType": GL_FLOAT,
862
+ "count": len(input_floats),
863
+ "type": "SCALAR",
864
+ "min": [min(input_floats)],
865
+ "max": [max(input_floats)],
866
+ }
867
+ )
868
+
869
+ output_accessor_index = len(accessor_dicts)
870
+ accessor_dicts.append(
871
+ {
872
+ "bufferView": output_buffer_view_index,
873
+ "componentType": GL_FLOAT,
874
+ "count": len(quaternions),
875
+ "type": "VEC4",
876
+ "min": [
877
+ min(values)
878
+ for values in (
879
+ (gltf_quaternion[i] for gltf_quaternion in gltf_quaternions)
880
+ for i in range(4)
881
+ )
882
+ ],
883
+ "max": [
884
+ max(values)
885
+ for values in (
886
+ (gltf_quaternion[i] for gltf_quaternion in gltf_quaternions)
887
+ for i in range(4)
888
+ )
889
+ ],
890
+ }
891
+ )
892
+
893
+ animation_sampler_index = len(animation_sampler_dicts)
894
+ animation_sampler_dicts.append(
895
+ {
896
+ "input": input_accessor_index,
897
+ "output": output_accessor_index,
898
+ }
899
+ )
900
+ animation_channel_dicts.append(
901
+ {
902
+ "sampler": animation_sampler_index,
903
+ "target": {"node": node_index, "path": "rotation"},
904
+ }
905
+ )
906
+
907
+ # Export hips translation
908
+ hips_node_index = bone_name_to_node_index.get(hips_bone_name)
909
+ if hips_node_index is None:
910
+ _logger.error("Failed to find node index for hips bone %s", hips_bone_name)
911
+ return
912
+ if all(
913
+ hips_translation.length_squared < float_info.epsilon
914
+ for hips_translation in hips_translations
915
+ ):
916
+ return
917
+
918
+ input_byte_offset = len(buffer0_bytearray)
919
+ input_floats = [
920
+ frame * frame_to_timestamp_factor for frame, _ in enumerate(hips_translations)
921
+ ]
922
+ input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
923
+ buffer0_bytearray.extend(input_bytes)
924
+ while len(buffer0_bytearray) % 4:
925
+ buffer0_bytearray.append(0)
926
+ input_buffer_view_index = len(buffer_view_dicts)
927
+ input_buffer_view_dict = {
928
+ "buffer": 0,
929
+ "byteLength": len(input_bytes),
930
+ }
931
+ if input_byte_offset > 0:
932
+ input_buffer_view_dict["byteOffset"] = input_byte_offset
933
+ buffer_view_dicts.append(input_buffer_view_dict)
934
+
935
+ output_byte_offset = len(buffer0_bytearray)
936
+ gltf_translations = [
937
+ (
938
+ translation.x,
939
+ translation.z,
940
+ -translation.y,
941
+ )
942
+ for translation in hips_translations
943
+ ]
944
+ translation_floats: list[float] = list(itertools.chain(*gltf_translations))
945
+ translation_bytes = struct.pack(
946
+ "<" + "f" * len(translation_floats), *translation_floats
947
+ )
948
+ buffer0_bytearray.extend(translation_bytes)
949
+ while len(buffer0_bytearray) % 4:
950
+ buffer0_bytearray.append(0)
951
+ output_buffer_view_index = len(buffer_view_dicts)
952
+ output_buffer_view_dict = {
953
+ "buffer": 0,
954
+ "byteLength": len(translation_bytes),
955
+ }
956
+ if output_byte_offset > 0:
957
+ output_buffer_view_dict["byteOffset"] = output_byte_offset
958
+ buffer_view_dicts.append(output_buffer_view_dict)
959
+
960
+ input_accessor_index = len(accessor_dicts)
961
+ accessor_dicts.append(
962
+ {
963
+ "bufferView": input_buffer_view_index,
964
+ "componentType": GL_FLOAT,
965
+ "count": len(input_floats),
966
+ "type": "SCALAR",
967
+ "min": [min(input_floats)],
968
+ "max": [max(input_floats)],
969
+ }
970
+ )
971
+
972
+ output_accessor_index = len(accessor_dicts)
973
+ gltf_translation_x_values = [t[0] for t in gltf_translations]
974
+ gltf_translation_y_values = [t[1] for t in gltf_translations]
975
+ gltf_translation_z_values = [t[2] for t in gltf_translations]
976
+ accessor_dicts.append(
977
+ {
978
+ "bufferView": output_buffer_view_index,
979
+ "componentType": GL_FLOAT,
980
+ "count": len(hips_translations),
981
+ "type": "VEC3",
982
+ "min": [
983
+ min(gltf_translation_x_values),
984
+ min(gltf_translation_y_values),
985
+ min(gltf_translation_z_values),
986
+ ],
987
+ "max": [
988
+ max(gltf_translation_x_values),
989
+ max(gltf_translation_y_values),
990
+ max(gltf_translation_z_values),
991
+ ],
992
+ }
993
+ )
994
+
995
+ animation_sampler_index = len(animation_sampler_dicts)
996
+ animation_sampler_dicts.append(
997
+ {
998
+ "input": input_accessor_index,
999
+ "output": output_accessor_index,
1000
+ }
1001
+ )
1002
+ animation_channel_dicts.append(
1003
+ {
1004
+ "sampler": animation_sampler_index,
1005
+ "target": {"node": hips_node_index, "path": "translation"},
1006
+ }
1007
+ )
1008
+
1009
+
1010
+ def _get_action_fcurves(action: Action) -> Sequence[FCurve]:
1011
+ if bpy.app.version < (4, 4):
1012
+ return list(action.fcurves)
1013
+
1014
+ from bpy.types import ActionKeyframeStrip
1015
+
1016
+ # https://developer.blender.org/docs/release_notes/4.4/python_api/#deprecated
1017
+ layers = action.layers
1018
+ if not layers:
1019
+ return []
1020
+ layer = layers[0]
1021
+
1022
+ strips = layer.strips
1023
+ if not strips:
1024
+ return []
1025
+ strip = strips[0]
1026
+ if not isinstance(strip, ActionKeyframeStrip):
1027
+ return []
1028
+
1029
+ slots = action.slots
1030
+ if not slots:
1031
+ return []
1032
+ slot = slots[0]
1033
+
1034
+ channelbag = strip.channelbag(slot)
1035
+ return list(channelbag.fcurves)