@volter/blender-engine 0.1.147 → 0.1.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +4 -11
  3. package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
  4. package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
  5. package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
  6. package/browser/addons/vrm/README.txt +3 -0
  7. package/browser/addons/vrm/__init__.py +41 -0
  8. package/browser/addons/vrm/blender_manifest.toml +16 -0
  9. package/browser/addons/vrm/common/__init__.py +1 -0
  10. package/browser/addons/vrm/common/animation.py +58 -0
  11. package/browser/addons/vrm/common/blender_manifest.py +82 -0
  12. package/browser/addons/vrm/common/char.py +16 -0
  13. package/browser/addons/vrm/common/convert.py +287 -0
  14. package/browser/addons/vrm/common/convert_any.py +48 -0
  15. package/browser/addons/vrm/common/debug.py +40 -0
  16. package/browser/addons/vrm/common/deep.py +162 -0
  17. package/browser/addons/vrm/common/error_dialog.py +274 -0
  18. package/browser/addons/vrm/common/fs.py +71 -0
  19. package/browser/addons/vrm/common/gl.py +20 -0
  20. package/browser/addons/vrm/common/gltf.py +1149 -0
  21. package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
  22. package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
  23. package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
  24. package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
  25. package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
  26. package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
  27. package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
  28. package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
  29. package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
  30. package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
  31. package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
  32. package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
  33. package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
  34. package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
  35. package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
  36. package/browser/addons/vrm/common/legacy_gltf.py +16 -0
  37. package/browser/addons/vrm/common/logger.py +84 -0
  38. package/browser/addons/vrm/common/mtoon0.blend +0 -0
  39. package/browser/addons/vrm/common/mtoon1.blend +0 -0
  40. package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
  41. package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
  42. package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
  43. package/browser/addons/vrm/common/ops/__init__.py +10 -0
  44. package/browser/addons/vrm/common/ops/export_scene.py +67 -0
  45. package/browser/addons/vrm/common/ops/icyp.py +53 -0
  46. package/browser/addons/vrm/common/ops/import_scene.py +57 -0
  47. package/browser/addons/vrm/common/ops/vrm.py +2380 -0
  48. package/browser/addons/vrm/common/ops/wm.py +167 -0
  49. package/browser/addons/vrm/common/preferences.py +362 -0
  50. package/browser/addons/vrm/common/progress.py +87 -0
  51. package/browser/addons/vrm/common/rotation.py +89 -0
  52. package/browser/addons/vrm/common/safe_removal.py +46 -0
  53. package/browser/addons/vrm/common/scene_watcher.py +230 -0
  54. package/browser/addons/vrm/common/shader.py +2243 -0
  55. package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
  56. package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
  57. package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
  58. package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
  59. package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
  60. package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
  61. package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
  62. package/browser/addons/vrm/common/version.py +161 -0
  63. package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
  64. package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
  65. package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
  66. package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
  67. package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
  68. package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
  69. package/browser/addons/vrm/common/workspace.py +227 -0
  70. package/browser/addons/vrm/common/writable_context.py +77 -0
  71. package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
  72. package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
  73. package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
  74. package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
  75. package/browser/addons/vrm/documentation/en-us/index.md +41 -0
  76. package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
  77. package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
  78. package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
  79. package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
  80. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
  81. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
  82. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
  83. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
  84. package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
  85. package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
  86. package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
  87. package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
  88. package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
  89. package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
  90. package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
  91. package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
  92. package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
  93. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
  94. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
  95. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
  96. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
  97. package/browser/addons/vrm/editor/__init__.py +1 -0
  98. package/browser/addons/vrm/editor/extension.py +425 -0
  99. package/browser/addons/vrm/editor/extension_accessor.py +75 -0
  100. package/browser/addons/vrm/editor/handler.py +84 -0
  101. package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
  102. package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
  103. package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
  104. package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
  105. package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
  106. package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
  107. package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
  108. package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
  109. package/browser/addons/vrm/editor/make_armature.py +709 -0
  110. package/browser/addons/vrm/editor/menu.py +147 -0
  111. package/browser/addons/vrm/editor/migration.py +297 -0
  112. package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
  113. package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
  114. package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
  115. package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
  116. package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
  117. package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
  118. package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
  119. package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
  120. package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
  121. package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
  122. package/browser/addons/vrm/editor/ops.py +471 -0
  123. package/browser/addons/vrm/editor/panel.py +315 -0
  124. package/browser/addons/vrm/editor/property_group.py +888 -0
  125. package/browser/addons/vrm/editor/search.py +682 -0
  126. package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
  127. package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
  128. package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
  129. package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
  130. package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
  131. package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
  132. package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
  133. package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
  134. package/browser/addons/vrm/editor/subscription.py +123 -0
  135. package/browser/addons/vrm/editor/t_pose.py +713 -0
  136. package/browser/addons/vrm/editor/validation.py +1271 -0
  137. package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
  138. package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
  139. package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
  140. package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
  141. package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
  142. package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
  143. package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
  144. package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
  145. package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
  146. package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
  147. package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
  148. package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
  149. package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
  150. package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
  151. package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
  152. package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
  153. package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
  154. package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
  155. package/browser/addons/vrm/exporter/__init__.py +1 -0
  156. package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
  157. package/browser/addons/vrm/exporter/export_scene.py +930 -0
  158. package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
  159. package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
  160. package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
  161. package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
  162. package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
  163. package/browser/addons/vrm/external/__init__.py +1 -0
  164. package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
  165. package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
  166. package/browser/addons/vrm/external/rigify_support.py +1 -0
  167. package/browser/addons/vrm/importer/__init__.py +1 -0
  168. package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
  169. package/browser/addons/vrm/importer/file_handler.py +34 -0
  170. package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
  171. package/browser/addons/vrm/importer/import_scene.py +646 -0
  172. package/browser/addons/vrm/importer/license_validation.py +198 -0
  173. package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
  174. package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
  175. package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
  176. package/browser/addons/vrm/importer/vrm_diff.py +316 -0
  177. package/browser/addons/vrm/locale/__init__.py +1 -0
  178. package/browser/addons/vrm/locale/ja_jp.py +826 -0
  179. package/browser/addons/vrm/locale/ruff.toml +8 -0
  180. package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
  181. package/browser/addons/vrm/locale/zh_hans.py +928 -0
  182. package/browser/addons/vrm/registration.py +670 -0
  183. package/browser/addons/vrm.json +18 -0
  184. package/browser/blender-emscripten-engine.mts +60 -128
  185. package/browser/blender-engine.mts +20 -186
  186. package/browser/blender-wali-engine.mts +35 -81
  187. package/browser/describe-thrown.mts +3 -6
  188. package/browser/document-chunks.mts +4 -16
  189. package/browser/gltf_export.py +3161 -0
  190. package/browser/heap-growth.mts +23 -0
  191. package/browser/index.ts +2 -0
  192. package/browser/protocol.ts +119 -35
  193. package/browser/pull-job.mts +2 -10
  194. package/browser/rna.ts +190 -73
  195. package/browser/runtime.ts +197 -82
  196. package/browser/session-addons.mts +33 -0
  197. package/browser/session-frame.mts +5 -24
  198. package/browser/session.py +1338 -1427
  199. package/browser/three/attach-presenter.ts +11 -39
  200. package/browser/three/blender-base64.ts +1 -36
  201. package/browser/three/blender-corner-normals.ts +1 -23
  202. package/browser/three/blender-display-shaders.json +1 -1
  203. package/browser/three/blender-display-shaders.lut +0 -0
  204. package/browser/three/blender-display-transform.ts +2 -1
  205. package/browser/three/blender-frame-columns.ts +5 -13
  206. package/browser/three/blender-graph-material.ts +18 -3
  207. package/browser/three/blender-node-graph.ts +10 -34
  208. package/browser/three/blender-physical-material.ts +2 -31
  209. package/browser/three/blender-runtime-armature.ts +7 -60
  210. package/browser/three/blender-runtime-frame.ts +9 -23
  211. package/browser/three/blender-runtime-geometry.ts +22 -26
  212. package/browser/three/blender-runtime-lighting.ts +48 -178
  213. package/browser/three/blender-runtime-shadows.ts +187 -1
  214. package/browser/three/blender-runtime-skeleton.ts +53 -1
  215. package/browser/three/blender-runtime-view.ts +505 -283
  216. package/browser/three/blender-runtime-weights.ts +9 -72
  217. package/browser/three/blender-sky.ts +13 -33
  218. package/browser/three/blender-triangulate.ts +7 -35
  219. package/browser/three/blender-viewport-sampling.ts +235 -0
  220. package/browser/three/blender-workbench-material.ts +25 -29
  221. package/browser/three/blender-workbench-outline.ts +199 -0
  222. package/browser/three/blender-world-volume.ts +62 -10
  223. package/browser/three/presenter.ts +10 -26
  224. package/browser/three/release.ts +5 -21
  225. package/browser/three/sky-precompute-worker.ts +1 -18
  226. package/browser/three/sky-worker.ts +5 -23
  227. package/browser/three/world-field-sampler.ts +12 -26
  228. package/browser/volter_three.py +9 -105
  229. package/browser/worker.ts +156 -151
  230. package/package.json +5 -2
  231. package/wasm/BUNDLE.json +17 -17
  232. package/wasm/DEPENDENCY-LICENSES.txt +14 -0
  233. package/wasm/blender_browser.data.br +0 -0
  234. package/wasm/blender_browser.js +1 -1
  235. package/wasm/blender_browser.wasm.br +0 -0
  236. package/browser/frame-transfer.mts +0 -19
  237. package/browser/three/agx-base-srgb.lut +0 -0
  238. package/browser/three/agx-look-medium-high-contrast.lut +0 -0
  239. package/browser/three/agx-look-punchy.lut +0 -0
  240. package/browser/three/blender-agx.ts +0 -235
  241. package/browser/three/blender-display-lut.ts +0 -145
  242. package/browser/three/blender-filmic.ts +0 -49
  243. package/browser/three/blender-standard.ts +0 -68
  244. package/browser/three/blender-world-visibility.ts +0 -278
  245. package/browser/three/filmic-srgb.lut +0 -0
@@ -0,0 +1,994 @@
1
+ # SPDX-License-Identifier: MIT OR GPL-3.0-or-later
2
+
3
+ import math
4
+ from collections.abc import Iterable, Sequence
5
+ from dataclasses import dataclass
6
+ from decimal import Decimal
7
+ from sys import float_info
8
+ from typing import Final, Optional, Union
9
+
10
+ import bpy
11
+ from bpy.app.handlers import persistent
12
+ from bpy.types import Armature, Context, Depsgraph, Object, PoseBone, Scene
13
+ from mathutils import Matrix, Quaternion, Vector
14
+
15
+ from ...common.rotation import (
16
+ get_rotation_as_quaternion,
17
+ set_rotation_without_mode_change,
18
+ )
19
+ from ..extension_accessor import get_armature_extension
20
+ from ..property_group import CollectionPropertyProtocol
21
+ from .property_group import (
22
+ SpringBone1JointPropertyGroup,
23
+ SpringBone1SpringPropertyGroup,
24
+ SpringBone1VrmcSpringBoneLimitPropertyGroup,
25
+ )
26
+
27
+
28
+ @dataclass
29
+ class State:
30
+ frame_count: Decimal = Decimal()
31
+ spring_bone_60_fps_update_count: Decimal = Decimal()
32
+ last_fps: Optional[Decimal] = None
33
+ last_fps_base: Optional[Decimal] = None
34
+
35
+ def reset(self, context: Context) -> None:
36
+ self.frame_count = Decimal()
37
+ self.spring_bone_60_fps_update_count = Decimal()
38
+ self.last_fps_base = Decimal(context.scene.render.fps_base)
39
+ self.last_fps = Decimal(context.scene.render.fps)
40
+
41
+
42
+ _state: Final = State()
43
+
44
+
45
+ def _apply_spring_bone_limit_local_direction(
46
+ direction: Vector,
47
+ limit: SpringBone1VrmcSpringBoneLimitPropertyGroup,
48
+ ) -> Vector:
49
+ direction = direction.normalized()
50
+
51
+ if limit.limit_type == limit.LIMIT_TYPE_CONE.identifier:
52
+ limit_angle = max(0.0, min(math.pi, limit.cone_angle))
53
+ cos_limit_angle = math.cos(limit_angle)
54
+ if direction.y < cos_limit_angle:
55
+ # mathutils vectors use single precision. Computing this from y
56
+ # loses the horizontal components near either pole.
57
+ horizontal_length_squared = (
58
+ direction.x * direction.x + direction.z * direction.z
59
+ )
60
+ sin_limit_angle = math.sqrt(
61
+ max(0.0, 1.0 - cos_limit_angle * cos_limit_angle)
62
+ )
63
+ if horizontal_length_squared < float_info.epsilon:
64
+ direction.x = 0.0
65
+ direction.z = sin_limit_angle
66
+ else:
67
+ scale = sin_limit_angle / math.sqrt(horizontal_length_squared)
68
+ direction.x *= scale
69
+ direction.z *= scale
70
+ direction.y = cos_limit_angle
71
+ return direction
72
+
73
+ if limit.limit_type == limit.LIMIT_TYPE_HINGE.identifier:
74
+ projected_length_squared = direction.y * direction.y + direction.z * direction.z
75
+ if projected_length_squared < float_info.epsilon:
76
+ return Vector((0.0, 1.0, 0.0))
77
+
78
+ projected_length = math.sqrt(projected_length_squared)
79
+ direction = Vector(
80
+ (0.0, direction.y / projected_length, direction.z / projected_length)
81
+ )
82
+ limit_angle = max(0.0, min(math.pi, limit.hinge_angle))
83
+ cos_limit_angle = math.cos(limit_angle)
84
+ if direction.y < cos_limit_angle:
85
+ sin_limit_angle = math.sqrt(
86
+ max(0.0, 1.0 - cos_limit_angle * cos_limit_angle)
87
+ )
88
+ direction.y = cos_limit_angle
89
+ direction.z = -sin_limit_angle if direction.z < 0.0 else sin_limit_angle
90
+ return direction
91
+
92
+ if limit.limit_type == limit.LIMIT_TYPE_SPHERICAL.identifier:
93
+ x = max(-1.0, min(1.0, direction.x))
94
+ y = max(-1.0, min(1.0, direction.y))
95
+ if x * x + direction.z * direction.z < float_info.epsilon and y < 0.0:
96
+ pitch = math.pi
97
+ elif y * y + direction.z * direction.z < float_info.epsilon:
98
+ pitch = 0.0
99
+ else:
100
+ pitch = math.atan2(direction.z, y)
101
+ yaw = math.asin(x)
102
+
103
+ limit_pitch = max(0.0, min(math.pi, limit.spherical_pitch))
104
+ limit_yaw = max(0.0, min(math.pi / 2.0, limit.spherical_yaw))
105
+ pitch = max(-limit_pitch, min(limit_pitch, pitch))
106
+ yaw = max(-limit_yaw, min(limit_yaw, yaw))
107
+ cos_yaw = math.cos(yaw)
108
+ return Vector(
109
+ (
110
+ math.sin(yaw),
111
+ cos_yaw * math.cos(pitch),
112
+ cos_yaw * math.sin(pitch),
113
+ )
114
+ )
115
+
116
+ return direction
117
+
118
+
119
+ def _apply_spring_bone_limit(
120
+ next_tail_world_translation: Vector,
121
+ next_head_world_translation: Vector,
122
+ head_to_tail_world_distance: float,
123
+ obj_matrix_world_quaternion: Quaternion,
124
+ next_head_parent_pose_bone_object_rotation: Quaternion,
125
+ bone_axis: Vector,
126
+ limit: SpringBone1VrmcSpringBoneLimitPropertyGroup,
127
+ ) -> Vector:
128
+ if limit.limit_type == limit.LIMIT_TYPE_NONE.identifier:
129
+ return next_tail_world_translation
130
+ if bone_axis.length_squared < float_info.epsilon:
131
+ return next_tail_world_translation
132
+
133
+ axis_rotation = limit.default_rotation(bone_axis)
134
+
135
+ limit_world_rotation = (
136
+ obj_matrix_world_quaternion
137
+ @ next_head_parent_pose_bone_object_rotation
138
+ @ axis_rotation
139
+ @ limit.rotation_quaternion()
140
+ ).normalized()
141
+ tail_direction = next_tail_world_translation - next_head_world_translation
142
+ if tail_direction.length_squared < float_info.epsilon:
143
+ tail_direction = limit_world_rotation @ Vector((0.0, 1.0, 0.0))
144
+ else:
145
+ tail_direction.normalize()
146
+ local_direction = limit_world_rotation.inverted() @ tail_direction
147
+ limited_local_direction = _apply_spring_bone_limit_local_direction(
148
+ local_direction, limit
149
+ )
150
+ return (
151
+ next_head_world_translation
152
+ + (limit_world_rotation @ limited_local_direction) * head_to_tail_world_distance
153
+ )
154
+
155
+
156
+ def reset_state(context: Context) -> None:
157
+ _state.reset(context)
158
+
159
+
160
+ @dataclass(frozen=True)
161
+ class SphereWorldCollider:
162
+ offset: Vector
163
+ radius: float
164
+
165
+ def calculate_collision(
166
+ self, target: Vector, target_radius: float
167
+ ) -> tuple[Vector, float]:
168
+ diff = target - self.offset
169
+ diff_length = diff.length
170
+ if diff_length < float_info.epsilon:
171
+ return Vector((0, 0, -1)), -0.01
172
+ return diff / diff_length, diff_length - target_radius - self.radius
173
+
174
+
175
+ @dataclass(frozen=True)
176
+ class CapsuleWorldCollider:
177
+ offset: Vector
178
+ radius: float
179
+ tail: Vector
180
+ offset_to_tail_diff: Vector # Must be non-zero vector
181
+ offset_to_tail_diff_length_squared: float # Must be non-negative value
182
+
183
+ def calculate_collision(
184
+ self, target: Vector, target_radius: float
185
+ ) -> tuple[Vector, float]:
186
+ offset_to_target_diff = target - self.offset
187
+
188
+ # Find the shortest point on the line containing offset and tail to the target
189
+ # self.offset + (self.tail - self.offset) * offset_to_tail_ratio_for_nearest
190
+ # Calculate offset_to_tail_ratio_for_nearest to express it as the above formula
191
+ offset_to_tail_ratio_for_nearest = (
192
+ self.offset_to_tail_diff.dot(offset_to_target_diff)
193
+ / self.offset_to_tail_diff_length_squared
194
+ )
195
+
196
+ # The line segment from offset to tail has start point 0 and end point 1,
197
+ # so clamp outside ranges
198
+ offset_to_tail_ratio_for_nearest = max(
199
+ 0.0, min(1.0, offset_to_tail_ratio_for_nearest)
200
+ )
201
+
202
+ # Calculate the shortest point to the target
203
+ nearest = (
204
+ self.offset + self.offset_to_tail_diff * offset_to_tail_ratio_for_nearest
205
+ )
206
+
207
+ # Collision detection
208
+ diff = target - nearest
209
+ diff_length = diff.length
210
+ if diff_length < float_info.epsilon:
211
+ return Vector((0, 0, -1)), -0.01
212
+ return diff / diff_length, diff_length - target_radius - self.radius
213
+
214
+
215
+ @dataclass(frozen=True)
216
+ class SphereInsideWorldCollider:
217
+ offset: Vector
218
+ radius: float
219
+
220
+ def calculate_collision(
221
+ self, target: Vector, target_radius: float
222
+ ) -> tuple[Vector, float]:
223
+ diff = self.offset - target
224
+ diff_length = diff.length
225
+ if diff_length < float_info.epsilon:
226
+ return Vector((0, 0, -1)), -0.01
227
+ return diff / diff_length, -diff_length - target_radius + self.radius
228
+
229
+
230
+ @dataclass(frozen=True)
231
+ class CapsuleInsideWorldCollider:
232
+ offset: Vector
233
+ radius: float
234
+ tail: Vector
235
+ offset_to_tail_diff: Vector # Must be non-zero vector
236
+ offset_to_tail_diff_length_squared: float # Must be non-negative value
237
+
238
+ def calculate_collision(
239
+ self, target: Vector, target_radius: float
240
+ ) -> tuple[Vector, float]:
241
+ offset_to_target_diff = target - self.offset
242
+
243
+ # Find the shortest point on the line containing offset and tail to the target
244
+ # self.offset + (self.tail - self.offset) * offset_to_tail_ratio_for_nearest
245
+ # Calculate offset_to_tail_ratio_for_nearest to express it as the above formula
246
+ offset_to_tail_ratio_for_nearest = (
247
+ self.offset_to_tail_diff.dot(offset_to_target_diff)
248
+ / self.offset_to_tail_diff_length_squared
249
+ )
250
+
251
+ # The line segment from offset to tail has start point 0 and end point 1,
252
+ # so clamp outside ranges
253
+ offset_to_tail_ratio_for_nearest = max(
254
+ 0.0, min(1.0, offset_to_tail_ratio_for_nearest)
255
+ )
256
+
257
+ # Calculate the shortest point to the target
258
+ nearest = (
259
+ self.offset + self.offset_to_tail_diff * offset_to_tail_ratio_for_nearest
260
+ )
261
+
262
+ # Collision detection
263
+ diff = nearest - target
264
+ diff_length = diff.length
265
+ if diff_length < float_info.epsilon:
266
+ return Vector((0, 0, -1)), -0.01
267
+ return diff / diff_length, -diff_length - target_radius + self.radius
268
+
269
+
270
+ @dataclass(frozen=True)
271
+ class PlaneWorldCollider:
272
+ offset: Vector
273
+ normal: Vector
274
+
275
+ def calculate_collision(
276
+ self, target: Vector, target_radius: float
277
+ ) -> tuple[Vector, float]:
278
+ distance = (target - self.offset).dot(self.normal) - target_radius
279
+ return self.normal, distance
280
+
281
+
282
+ # https://github.com/vrm-c/vrm-specification/tree/993a90a5bda9025f3d9e2923ad6dea7506f88553/specification/VRMC_springBone-1.0#update-procedure
283
+ def update_pose_bone_rotations(context: Context, delta_time: float) -> None:
284
+ pose_bone_and_rotations: list[tuple[PoseBone, Quaternion]] = []
285
+
286
+ for obj in context.blend_data.objects:
287
+ _calculate_object_pose_bone_rotations(delta_time, obj, pose_bone_and_rotations)
288
+
289
+ for pose_bone, pose_bone_rotation in pose_bone_and_rotations:
290
+ # Assigning rotation to pose_bone is expensive, so avoid it as much as possible
291
+ angle_diff = pose_bone_rotation.rotation_difference(
292
+ get_rotation_as_quaternion(pose_bone)
293
+ ).angle
294
+ if abs(angle_diff) < float_info.epsilon:
295
+ continue
296
+ set_rotation_without_mode_change(pose_bone, pose_bone_rotation)
297
+
298
+
299
+ def _calculate_object_pose_bone_rotations(
300
+ delta_time: float,
301
+ obj: Object,
302
+ pose_bone_and_rotations: list[tuple[PoseBone, Quaternion]],
303
+ ) -> None:
304
+ if obj.type != "ARMATURE":
305
+ return
306
+ armature_data = obj.data
307
+ if not isinstance(armature_data, Armature):
308
+ return
309
+ ext = get_armature_extension(armature_data)
310
+ if not ext.is_vrm1():
311
+ return
312
+ spring_bone1 = ext.spring_bone1
313
+ if not spring_bone1.enable_animation:
314
+ return
315
+
316
+ obj_matrix_world = obj.matrix_world
317
+ obj_matrix_world_inverted = obj_matrix_world.inverted_safe()
318
+ obj_matrix_world_quaternion = obj_matrix_world.to_quaternion()
319
+
320
+ collider_uuid_to_world_collider: dict[
321
+ str,
322
+ Union[
323
+ SphereWorldCollider,
324
+ CapsuleWorldCollider,
325
+ SphereInsideWorldCollider,
326
+ CapsuleInsideWorldCollider,
327
+ PlaneWorldCollider,
328
+ ],
329
+ ] = {}
330
+ for collider in spring_bone1.colliders:
331
+ if not collider.uuid:
332
+ continue
333
+ pose_bone = obj.pose.bones.get(collider.node.bone_name)
334
+ if not pose_bone:
335
+ continue
336
+ pose_bone_world_matrix = obj_matrix_world @ pose_bone.matrix
337
+
338
+ extended_collider = collider.extensions.vrmc_spring_bone_extended_collider
339
+ world_collider: Union[
340
+ SphereWorldCollider,
341
+ CapsuleWorldCollider,
342
+ SphereInsideWorldCollider,
343
+ CapsuleInsideWorldCollider,
344
+ PlaneWorldCollider,
345
+ None,
346
+ ] = None
347
+ if extended_collider.enabled:
348
+ if (
349
+ extended_collider.shape_type
350
+ == extended_collider.SHAPE_TYPE_EXTENDED_SPHERE.identifier
351
+ ):
352
+ offset = pose_bone_world_matrix @ Vector(
353
+ extended_collider.shape.sphere.offset
354
+ )
355
+ radius = extended_collider.shape.sphere.radius
356
+ if extended_collider.shape.sphere.inside:
357
+ world_collider = SphereInsideWorldCollider(
358
+ offset=offset, radius=radius
359
+ )
360
+ else:
361
+ world_collider = SphereWorldCollider(offset=offset, radius=radius)
362
+ elif (
363
+ extended_collider.shape_type
364
+ == extended_collider.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
365
+ ):
366
+ offset = pose_bone_world_matrix @ Vector(
367
+ extended_collider.shape.capsule.offset
368
+ )
369
+ tail = pose_bone_world_matrix @ Vector(
370
+ extended_collider.shape.capsule.tail
371
+ )
372
+ radius = extended_collider.shape.capsule.radius
373
+ offset_to_tail_diff = tail - offset
374
+ offset_to_tail_diff_length_squared = offset_to_tail_diff.length_squared
375
+ if offset_to_tail_diff_length_squared < float_info.epsilon:
376
+ # If offset and tail positions are the same, use as sphere collider
377
+ if extended_collider.shape.capsule.inside:
378
+ world_collider = SphereInsideWorldCollider(
379
+ offset=offset, radius=radius
380
+ )
381
+ else:
382
+ world_collider = SphereWorldCollider(
383
+ offset=offset, radius=radius
384
+ )
385
+ elif extended_collider.shape.capsule.inside:
386
+ world_collider = CapsuleInsideWorldCollider(
387
+ offset=offset,
388
+ radius=radius,
389
+ tail=tail,
390
+ offset_to_tail_diff=offset_to_tail_diff,
391
+ offset_to_tail_diff_length_squared=offset_to_tail_diff_length_squared,
392
+ )
393
+ else:
394
+ world_collider = CapsuleWorldCollider(
395
+ offset=offset,
396
+ radius=radius,
397
+ tail=tail,
398
+ offset_to_tail_diff=offset_to_tail_diff,
399
+ offset_to_tail_diff_length_squared=offset_to_tail_diff_length_squared,
400
+ )
401
+ elif (
402
+ extended_collider.shape_type
403
+ == extended_collider.SHAPE_TYPE_EXTENDED_PLANE.identifier
404
+ ):
405
+ offset = pose_bone_world_matrix @ Vector(
406
+ extended_collider.shape.plane.offset
407
+ )
408
+ normal = pose_bone_world_matrix.to_quaternion() @ Vector(
409
+ extended_collider.shape.plane.normal
410
+ )
411
+ world_collider = PlaneWorldCollider(
412
+ offset=offset,
413
+ normal=normal,
414
+ )
415
+ elif collider.shape_type == collider.SHAPE_TYPE_SPHERE.identifier:
416
+ offset = pose_bone_world_matrix @ Vector(collider.shape.sphere.offset)
417
+ radius = collider.shape.sphere.radius
418
+ world_collider = SphereWorldCollider(
419
+ offset=offset,
420
+ radius=radius,
421
+ )
422
+ elif collider.shape_type == collider.SHAPE_TYPE_CAPSULE.identifier:
423
+ offset = pose_bone_world_matrix @ Vector(collider.shape.capsule.offset)
424
+ tail = pose_bone_world_matrix @ Vector(collider.shape.capsule.tail)
425
+ radius = collider.shape.capsule.radius
426
+ offset_to_tail_diff = tail - offset
427
+ offset_to_tail_diff_length_squared = offset_to_tail_diff.length_squared
428
+ if offset_to_tail_diff_length_squared < float_info.epsilon:
429
+ # If offset and tail positions are the same, use as sphere collider
430
+ world_collider = SphereWorldCollider(
431
+ offset=offset,
432
+ radius=radius,
433
+ )
434
+ else:
435
+ world_collider = CapsuleWorldCollider(
436
+ offset=offset,
437
+ radius=radius,
438
+ tail=tail,
439
+ offset_to_tail_diff=offset_to_tail_diff,
440
+ offset_to_tail_diff_length_squared=offset_to_tail_diff_length_squared,
441
+ )
442
+
443
+ if world_collider:
444
+ collider_uuid_to_world_collider[collider.uuid] = world_collider
445
+
446
+ collider_group_uuid_to_world_colliders: dict[
447
+ str,
448
+ list[
449
+ Union[
450
+ SphereWorldCollider,
451
+ CapsuleWorldCollider,
452
+ SphereInsideWorldCollider,
453
+ CapsuleInsideWorldCollider,
454
+ PlaneWorldCollider,
455
+ ]
456
+ ],
457
+ ] = {}
458
+ for collider_group in spring_bone1.collider_groups:
459
+ for collider_reference in collider_group.colliders:
460
+ world_collider = collider_uuid_to_world_collider.get(
461
+ collider_reference.collider_uuid
462
+ )
463
+ if world_collider is None:
464
+ continue
465
+ world_colliders = collider_group_uuid_to_world_colliders.get(
466
+ collider_group.uuid
467
+ )
468
+ if world_colliders is None:
469
+ world_colliders = []
470
+ collider_group_uuid_to_world_colliders[collider_group.uuid] = (
471
+ world_colliders
472
+ )
473
+ world_colliders.append(world_collider)
474
+
475
+ for spring in spring_bone1.springs:
476
+ joints = spring.joints
477
+ if not joints:
478
+ continue
479
+
480
+ _calculate_spring_pose_bone_rotations(
481
+ delta_time,
482
+ obj,
483
+ obj_matrix_world,
484
+ obj_matrix_world_inverted,
485
+ obj_matrix_world_quaternion,
486
+ spring,
487
+ pose_bone_and_rotations,
488
+ collider_group_uuid_to_world_colliders,
489
+ )
490
+
491
+
492
+ def _calculate_spring_pose_bone_rotations(
493
+ delta_time: float,
494
+ obj: Object,
495
+ obj_matrix_world: Matrix,
496
+ obj_matrix_world_inverted: Matrix,
497
+ obj_matrix_world_quaternion: Quaternion,
498
+ spring: SpringBone1SpringPropertyGroup,
499
+ pose_bone_and_rotations: list[tuple[PoseBone, Quaternion]],
500
+ collider_group_uuid_to_world_colliders: dict[
501
+ str,
502
+ list[
503
+ Union[
504
+ SphereWorldCollider,
505
+ CapsuleWorldCollider,
506
+ SphereInsideWorldCollider,
507
+ CapsuleInsideWorldCollider,
508
+ PlaneWorldCollider,
509
+ ]
510
+ ],
511
+ ],
512
+ ) -> None:
513
+ world_collider_groups: Sequence[
514
+ Sequence[
515
+ Union[
516
+ SphereWorldCollider,
517
+ CapsuleWorldCollider,
518
+ SphereInsideWorldCollider,
519
+ CapsuleInsideWorldCollider,
520
+ PlaneWorldCollider,
521
+ ]
522
+ ]
523
+ ] = [
524
+ collider_group_world_colliders
525
+ for collider_group_reference in spring.collider_groups
526
+ if (
527
+ collider_group_world_colliders
528
+ := collider_group_uuid_to_world_colliders.get(
529
+ collider_group_reference.collider_group_uuid
530
+ )
531
+ )
532
+ and collider_group_world_colliders
533
+ ]
534
+
535
+ center_pose_bone = obj.pose.bones.get(spring.center.bone_name)
536
+ if center_pose_bone:
537
+ current_center_world_translation = (
538
+ obj_matrix_world @ center_pose_bone.matrix
539
+ ).to_translation()
540
+ previous_center_world_translation = Vector(
541
+ spring.animation_state.previous_center_world_translation
542
+ )
543
+ previous_to_current_center_world_translation = (
544
+ current_center_world_translation - previous_center_world_translation
545
+ )
546
+ if not spring.animation_state.use_center_space:
547
+ spring.animation_state.previous_center_world_translation = (
548
+ current_center_world_translation.copy()
549
+ )
550
+ spring.animation_state.use_center_space = True
551
+ else:
552
+ current_center_world_translation = Vector((0, 0, 0))
553
+ previous_to_current_center_world_translation = Vector((0, 0, 0))
554
+ if spring.animation_state.use_center_space:
555
+ spring.animation_state.use_center_space = False
556
+
557
+ for sorted_joint_and_bones in sort_spring_bone_joints(obj, spring.joints):
558
+ joints: list[
559
+ tuple[
560
+ SpringBone1JointPropertyGroup,
561
+ PoseBone,
562
+ Matrix,
563
+ ]
564
+ ] = [
565
+ (
566
+ joint,
567
+ pose_bone,
568
+ pose_bone.bone.convert_local_to_pose(
569
+ Matrix(), pose_bone.bone.matrix_local
570
+ ),
571
+ )
572
+ for joint, pose_bone in sorted_joint_and_bones
573
+ ]
574
+
575
+ # https://github.com/vrm-c/vrm-specification/blob/7279e169ac0dcf37e7d81b2adcad9107101d7e25/specification/VRMC_springBone-1.0/README.md#center-space
576
+ enable_center_space = False
577
+ if center_pose_bone:
578
+ first_pose_bone = next((pose_bone for (_, pose_bone, _) in joints), None)
579
+ ancestor_of_first_pose_bone: Optional[PoseBone] = first_pose_bone
580
+ while ancestor_of_first_pose_bone:
581
+ if center_pose_bone == ancestor_of_first_pose_bone:
582
+ enable_center_space = True
583
+ break
584
+ ancestor_of_first_pose_bone = ancestor_of_first_pose_bone.parent
585
+
586
+ next_head_pose_bone_before_rotation_matrix = None
587
+ for (
588
+ head_joint,
589
+ head_pose_bone,
590
+ head_rest_object_matrix,
591
+ ), (
592
+ tail_joint,
593
+ tail_pose_bone,
594
+ tail_rest_object_matrix,
595
+ ) in zip(joints, joints[1:]):
596
+ head_tail_parented = False
597
+ searching_tail_parent = tail_pose_bone.parent
598
+ while searching_tail_parent:
599
+ if searching_tail_parent.name == head_pose_bone.name:
600
+ head_tail_parented = True
601
+ break
602
+ searching_tail_parent = searching_tail_parent.parent
603
+ if not head_tail_parented:
604
+ break
605
+
606
+ (
607
+ head_pose_bone_rotation,
608
+ next_head_pose_bone_before_rotation_matrix,
609
+ ) = _calculate_joint_pair_head_pose_bone_rotations(
610
+ delta_time,
611
+ obj_matrix_world,
612
+ obj_matrix_world_inverted,
613
+ obj_matrix_world_quaternion,
614
+ head_joint,
615
+ head_pose_bone,
616
+ head_rest_object_matrix,
617
+ tail_joint,
618
+ tail_pose_bone,
619
+ tail_rest_object_matrix,
620
+ next_head_pose_bone_before_rotation_matrix,
621
+ world_collider_groups,
622
+ previous_to_current_center_world_translation
623
+ if enable_center_space
624
+ else Vector((0, 0, 0)),
625
+ )
626
+ pose_bone_and_rotations.append((head_pose_bone, head_pose_bone_rotation))
627
+
628
+ spring.animation_state.previous_center_world_translation = (
629
+ current_center_world_translation
630
+ )
631
+
632
+
633
+ def _calculate_joint_pair_head_pose_bone_rotations(
634
+ delta_time: float,
635
+ obj_matrix_world: Matrix,
636
+ obj_matrix_world_inverted: Matrix,
637
+ obj_matrix_world_quaternion: Quaternion,
638
+ head_joint: SpringBone1JointPropertyGroup,
639
+ head_pose_bone: PoseBone,
640
+ current_head_rest_object_matrix: Matrix,
641
+ tail_joint: SpringBone1JointPropertyGroup,
642
+ tail_pose_bone: PoseBone,
643
+ current_tail_rest_object_matrix: Matrix,
644
+ next_head_pose_bone_before_rotation_matrix: Optional[Matrix],
645
+ world_collider_groups: Sequence[
646
+ Sequence[
647
+ Union[
648
+ SphereWorldCollider,
649
+ CapsuleWorldCollider,
650
+ SphereInsideWorldCollider,
651
+ CapsuleInsideWorldCollider,
652
+ PlaneWorldCollider,
653
+ ]
654
+ ]
655
+ ],
656
+ previous_to_current_center_world_translation: Vector,
657
+ ) -> tuple[Quaternion, Matrix]:
658
+ current_head_pose_bone_matrix = head_pose_bone.matrix
659
+ current_tail_pose_bone_matrix = tail_pose_bone.matrix
660
+
661
+ if next_head_pose_bone_before_rotation_matrix is None:
662
+ if head_pose_bone_parent := head_pose_bone.parent:
663
+ current_head_parent_matrix = head_pose_bone_parent.matrix
664
+ current_head_parent_rest_object_matrix = (
665
+ head_pose_bone_parent.bone.convert_local_to_pose(
666
+ Matrix(), head_pose_bone_parent.bone.matrix_local
667
+ )
668
+ )
669
+ next_head_pose_bone_before_rotation_matrix = current_head_parent_matrix @ (
670
+ current_head_parent_rest_object_matrix.inverted_safe()
671
+ @ current_head_rest_object_matrix
672
+ )
673
+ else:
674
+ next_head_pose_bone_before_rotation_matrix = (
675
+ current_head_rest_object_matrix.copy()
676
+ )
677
+ (
678
+ next_head_pose_bone_translation,
679
+ next_head_parent_pose_bone_object_rotation,
680
+ next_head_pose_bone_scale,
681
+ ) = next_head_pose_bone_before_rotation_matrix.decompose()
682
+
683
+ next_head_world_translation = obj_matrix_world @ next_head_pose_bone_translation
684
+
685
+ if not tail_joint.animation_state.initialized_as_tail:
686
+ initial_tail_world_translation = (
687
+ obj_matrix_world @ current_tail_pose_bone_matrix
688
+ ).to_translation()
689
+ tail_joint.animation_state.initialized_as_tail = True
690
+ tail_joint.animation_state.previous_world_translation = list(
691
+ initial_tail_world_translation
692
+ )
693
+ tail_joint.animation_state.current_world_translation = list(
694
+ initial_tail_world_translation
695
+ )
696
+
697
+ previous_tail_world_translation = (
698
+ Vector(tail_joint.animation_state.previous_world_translation)
699
+ + previous_to_current_center_world_translation
700
+ )
701
+ current_tail_world_translation = (
702
+ Vector(tail_joint.animation_state.current_world_translation)
703
+ + previous_to_current_center_world_translation
704
+ )
705
+
706
+ inertia = (current_tail_world_translation - previous_tail_world_translation) * (
707
+ 1.0 - head_joint.drag_force
708
+ )
709
+
710
+ current_head_rest_object_matrix_inverted = (
711
+ current_head_rest_object_matrix.inverted_safe()
712
+ )
713
+ next_head_rotation_start_target_local_translation = (
714
+ current_head_rest_object_matrix_inverted
715
+ @ current_tail_rest_object_matrix.to_translation()
716
+ )
717
+ stiffness_direction = (
718
+ obj_matrix_world_quaternion
719
+ @ next_head_parent_pose_bone_object_rotation
720
+ @ next_head_rotation_start_target_local_translation
721
+ ).normalized()
722
+ stiffness = stiffness_direction * delta_time * head_joint.stiffness
723
+ external = Vector(head_joint.gravity_dir) * delta_time * head_joint.gravity_power
724
+
725
+ next_tail_world_translation = (
726
+ current_tail_world_translation + inertia + stiffness + external
727
+ )
728
+
729
+ head_to_tail_world_distance = (
730
+ obj_matrix_world @ current_head_pose_bone_matrix.to_translation()
731
+ - (obj_matrix_world @ current_tail_pose_bone_matrix.to_translation())
732
+ ).length
733
+
734
+ # Apply distance constraint to next Tail
735
+ next_tail_world_translation = (
736
+ next_head_world_translation
737
+ + (next_tail_world_translation - next_head_world_translation).normalized()
738
+ * head_to_tail_world_distance
739
+ )
740
+ next_tail_world_translation = _apply_spring_bone_limit(
741
+ next_tail_world_translation,
742
+ next_head_world_translation,
743
+ head_to_tail_world_distance,
744
+ obj_matrix_world_quaternion,
745
+ next_head_parent_pose_bone_object_rotation,
746
+ next_head_rotation_start_target_local_translation,
747
+ head_joint.vrmc_spring_bone_limit,
748
+ )
749
+ # Calculate collider collision
750
+ for world_colliders in world_collider_groups:
751
+ for world_collider in world_colliders:
752
+ direction, distance = world_collider.calculate_collision(
753
+ next_tail_world_translation,
754
+ head_joint.hit_radius,
755
+ )
756
+ if distance >= 0:
757
+ continue
758
+ # Push away
759
+ next_tail_world_translation = (
760
+ next_tail_world_translation - direction * distance
761
+ )
762
+ # Apply distance constraint to next Tail
763
+ next_tail_world_translation = (
764
+ next_head_world_translation
765
+ + (
766
+ next_tail_world_translation - next_head_world_translation
767
+ ).normalized()
768
+ * head_to_tail_world_distance
769
+ )
770
+ next_tail_world_translation = _apply_spring_bone_limit(
771
+ next_tail_world_translation,
772
+ next_head_world_translation,
773
+ head_to_tail_world_distance,
774
+ obj_matrix_world_quaternion,
775
+ next_head_parent_pose_bone_object_rotation,
776
+ next_head_rotation_start_target_local_translation,
777
+ head_joint.vrmc_spring_bone_limit,
778
+ )
779
+
780
+ next_tail_object_local_translation = (
781
+ obj_matrix_world_inverted @ next_tail_world_translation
782
+ )
783
+ next_head_rotation_end_target_local_translation = (
784
+ next_head_pose_bone_before_rotation_matrix.inverted_safe()
785
+ @ next_tail_object_local_translation
786
+ )
787
+
788
+ next_head_pose_bone_rotation = Quaternion(
789
+ next_head_rotation_start_target_local_translation.cross(
790
+ next_head_rotation_end_target_local_translation
791
+ ),
792
+ next_head_rotation_start_target_local_translation.angle(
793
+ next_head_rotation_end_target_local_translation, 0
794
+ ),
795
+ )
796
+
797
+ next_head_pose_bone_object_rotation = (
798
+ next_head_parent_pose_bone_object_rotation @ next_head_pose_bone_rotation
799
+ )
800
+ next_head_pose_bone_matrix = (
801
+ Matrix.Translation(next_head_pose_bone_translation)
802
+ @ next_head_pose_bone_object_rotation.to_matrix().to_4x4()
803
+ @ Matrix.Diagonal(next_head_pose_bone_scale).to_4x4()
804
+ )
805
+
806
+ next_tail_pose_bone_before_rotation_matrix = (
807
+ next_head_pose_bone_matrix
808
+ @ current_head_rest_object_matrix_inverted
809
+ @ current_tail_rest_object_matrix
810
+ )
811
+
812
+ tail_joint.animation_state.previous_world_translation = list(
813
+ current_tail_world_translation
814
+ )
815
+ tail_joint.animation_state.current_world_translation = list(
816
+ next_tail_world_translation
817
+ )
818
+
819
+ return (
820
+ next_head_pose_bone_rotation
821
+ if head_pose_bone.bone.use_inherit_rotation
822
+ else next_head_pose_bone_object_rotation,
823
+ next_tail_pose_bone_before_rotation_matrix,
824
+ )
825
+
826
+
827
+ @persistent
828
+ def depsgraph_update_pre(_scene: Scene, _depsgraph: Depsgraph) -> None:
829
+ context = bpy.context
830
+
831
+ _state.reset(context)
832
+
833
+
834
+ @persistent
835
+ def frame_change_pre(_unused: object) -> None:
836
+ context = bpy.context
837
+
838
+ fps = Decimal(context.scene.render.fps)
839
+ last_fps = _state.last_fps
840
+ fps_base = Decimal(context.scene.render.fps_base)
841
+ last_fps_base = _state.last_fps_base
842
+ if (
843
+ last_fps_base is None
844
+ or (fps_base - last_fps_base).copy_abs() > 0.00001
845
+ or fps != last_fps
846
+ ):
847
+ _state.reset(context)
848
+
849
+ _state.frame_count += 1
850
+
851
+ # If the current time is future than the next SpringBone calculation
852
+ # time, move the SpringBone
853
+ # To minimize floating-point rounding errors, multiply numerator by
854
+ # common denominator to minimize decimal handling
855
+ frame_time_x_60_x_fps = _state.frame_count * Decimal(60) * fps_base
856
+ while True:
857
+ next_spring_bone_60_fps_update_count = (
858
+ _state.spring_bone_60_fps_update_count + Decimal(1)
859
+ )
860
+
861
+ next_spring_bone_update_time_x_60_x_fps = (
862
+ next_spring_bone_60_fps_update_count * fps
863
+ )
864
+ if next_spring_bone_update_time_x_60_x_fps > frame_time_x_60_x_fps:
865
+ break
866
+
867
+ # To accumulate float rounding errors, don't hardcode delta_time as 1.0/60.0
868
+ # Use the difference between previous and next times
869
+ next_spring_bone_update_time = next_spring_bone_60_fps_update_count / Decimal(
870
+ 60
871
+ )
872
+ current_spring_bone_update_time = (
873
+ _state.spring_bone_60_fps_update_count / Decimal(60)
874
+ )
875
+ delta_time = float(next_spring_bone_update_time) - float(
876
+ current_spring_bone_update_time
877
+ )
878
+ update_pose_bone_rotations(context, delta_time)
879
+
880
+ _state.spring_bone_60_fps_update_count += 1
881
+
882
+
883
+ def sort_spring_bone_joints(
884
+ obj: Object, joints: CollectionPropertyProtocol[SpringBone1JointPropertyGroup]
885
+ ) -> Sequence[Iterable[tuple[SpringBone1JointPropertyGroup, PoseBone]]]:
886
+ bones = obj.pose.bones
887
+
888
+ # Check if it's sorted and return as-is if already sorted.
889
+ # This is logically unnecessary but done for simulation efficiency.
890
+ sorted_pose_bones: list[PoseBone] = []
891
+ for joint in joints:
892
+ joint_bone = bones.get(joint.node.bone_name)
893
+ if not joint_bone:
894
+ break
895
+ if not sorted_pose_bones:
896
+ sorted_pose_bones.append(joint_bone)
897
+ continue
898
+ parent_bone = sorted_pose_bones[-1]
899
+ sorted_pose_bones.append(joint_bone)
900
+
901
+ traversing_bone = joint_bone.parent
902
+ while traversing_bone:
903
+ if traversing_bone == parent_bone:
904
+ break
905
+ traversing_bone = traversing_bone.parent
906
+ else:
907
+ break
908
+ else:
909
+ return [zip(joints, sorted_pose_bones)]
910
+
911
+ # Perform sorting
912
+ chains = list[list[tuple[SpringBone1JointPropertyGroup, PoseBone]]]()
913
+ for joint in joints:
914
+ joint_bone = bones.get(joint.node.bone_name)
915
+ if not joint_bone:
916
+ continue
917
+
918
+ if not chains:
919
+ chains.append([(joint, joint_bone)])
920
+ continue
921
+
922
+ # Skip if already registered in chain
923
+ if any(joint_bone == bone for chain in chains for _, bone in chain):
924
+ continue
925
+
926
+ # If ancestor of chain head, or descendant of chain tail,
927
+ # or descendant of chain head and ancestor of chain tail,
928
+ # add to that chain
929
+ # Otherwise, create a new chain
930
+ assigned = False
931
+ for chain in chains:
932
+ if not chain:
933
+ # This should not happen
934
+ continue
935
+
936
+ # Check if it's an ancestor of the chain head
937
+ _, chain_head_bone = chain[0]
938
+ traversing_bone = chain_head_bone.parent
939
+ assigned = False
940
+ while traversing_bone:
941
+ if traversing_bone == joint_bone:
942
+ chain.insert(0, (joint, joint_bone))
943
+ assigned = True
944
+ break
945
+ traversing_bone = traversing_bone.parent
946
+ if assigned:
947
+ break
948
+
949
+ # Check if it's an ancestor of the chain tail
950
+ _, chain_tail_bone = chain[-1]
951
+ traversing_bone = joint_bone.parent
952
+ assigned = False
953
+ while traversing_bone:
954
+ if traversing_bone == chain_tail_bone:
955
+ chain.append((joint, joint_bone))
956
+ assigned = True
957
+ break
958
+ traversing_bone = traversing_bone.parent
959
+ if assigned:
960
+ break
961
+
962
+ # Check if it's a descendant of the chain head and ancestor of
963
+ # the chain tail
964
+ assigned = False
965
+ for i in range(len(chain) - 1):
966
+ _, chain_parent_bone = chain[i]
967
+ _, chain_child_bone = chain[i + 1]
968
+
969
+ traversing_bone = chain_child_bone.parent
970
+ while traversing_bone:
971
+ if traversing_bone == joint_bone:
972
+ chain.insert(i + 1, (joint, joint_bone))
973
+ assigned = True
974
+ break
975
+ if traversing_bone == chain_parent_bone:
976
+ break
977
+ traversing_bone = traversing_bone.parent
978
+ if assigned:
979
+ break
980
+ if assigned:
981
+ break
982
+
983
+ if not assigned:
984
+ chains.append([(joint, joint_bone)])
985
+
986
+ return chains
987
+
988
+
989
+ @persistent
990
+ def save_pre(_unused: object) -> None:
991
+ context = bpy.context
992
+ for armature_data in context.blend_data.armatures:
993
+ spring_bone1 = get_armature_extension(armature_data).spring_bone1
994
+ spring_bone1.fixup()