@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,4020 @@
1
+ # SPDX-License-Identifier: MIT OR GPL-3.0-or-later
2
+ # SPDX-FileCopyrightText: 2018 iCyP
3
+
4
+ import itertools
5
+ import math
6
+ import re
7
+ import statistics
8
+ import struct
9
+ from collections.abc import Mapping, MutableSequence, Sequence
10
+ from dataclasses import dataclass, field
11
+ from os import environ
12
+ from sys import float_info
13
+ from typing import Final, Optional, Union
14
+
15
+ import bpy
16
+ from bpy.types import (
17
+ Armature,
18
+ Constraint,
19
+ Context,
20
+ Curve,
21
+ Image,
22
+ Key,
23
+ Material,
24
+ Mesh,
25
+ Node,
26
+ Object,
27
+ PoseBone,
28
+ ShaderNodeGroup,
29
+ )
30
+ from mathutils import Matrix, Vector
31
+
32
+ from ..common import convert, gltf, shader
33
+ from ..common.convert import Json
34
+ from ..common.deep import make_json, make_json_dict
35
+ from ..common.gl import (
36
+ GL_FLOAT,
37
+ GL_LINEAR,
38
+ GL_REPEAT,
39
+ GL_UNSIGNED_INT,
40
+ GL_UNSIGNED_SHORT,
41
+ )
42
+ from ..common.gltf import (
43
+ FLOAT_NEGATIVE_MAX,
44
+ FLOAT_POSITIVE_MAX,
45
+ )
46
+ from ..common.logger import get_logger
47
+ from ..common.mtoon_unversioned import MtoonUnversioned
48
+ from ..common.progress import Progress, create_progress
49
+ from ..common.shader import LegacyAddonMaterial, MmdMaterial
50
+ from ..common.version import get_addon_version
51
+ from ..common.vrm0.human_bone import (
52
+ HumanBoneName,
53
+ HumanBoneSpecification,
54
+ HumanBoneSpecifications,
55
+ )
56
+ from ..common.workspace import save_workspace
57
+ from ..editor import search
58
+ from ..editor.extension_accessor import get_armature_extension, get_material_extension
59
+ from ..editor.mtoon1.property_group import (
60
+ Mtoon0TexturePropertyGroup,
61
+ Mtoon1KhrTextureTransformPropertyGroup,
62
+ Mtoon1SamplerPropertyGroup,
63
+ Mtoon1TextureInfoPropertyGroup,
64
+ Mtoon1TexturePropertyGroup,
65
+ )
66
+ from ..editor.search import MESH_CONVERTIBLE_OBJECT_TYPES
67
+ from ..editor.t_pose import setup_humanoid_t_pose
68
+ from ..editor.vrm0.property_group import Vrm0BlendShapeGroupPropertyGroup
69
+ from ..external.io_scene_gltf2_support import (
70
+ gather_gltf2_io_material,
71
+ image_to_image_bytes,
72
+ init_extras_export,
73
+ )
74
+ from .abstract_base_vrm_exporter import (
75
+ AbstractBaseVrmExporter,
76
+ FlexibleHierarchyBoneSetup,
77
+ assign_dict,
78
+ generate_evaluated_mesh,
79
+ )
80
+
81
+ _logger = get_logger(__name__)
82
+
83
+
84
+ class Vrm0Exporter(AbstractBaseVrmExporter):
85
+ @dataclass(frozen=True)
86
+ class Gltf2IoTextureImage:
87
+ name: str
88
+ mime_type: str
89
+ image_bytes: bytes
90
+ export_image_index: int
91
+
92
+ @dataclass
93
+ class PrimitiveTarget:
94
+ name: str
95
+
96
+ position: Final[bytearray] = field(default_factory=bytearray)
97
+ position_max_x = FLOAT_NEGATIVE_MAX
98
+ position_min_x = FLOAT_POSITIVE_MAX
99
+ position_max_y = FLOAT_NEGATIVE_MAX
100
+ position_min_y = FLOAT_POSITIVE_MAX
101
+ position_max_z = FLOAT_NEGATIVE_MAX
102
+ position_min_z = FLOAT_POSITIVE_MAX
103
+
104
+ normal: Final[bytearray] = field(default_factory=bytearray)
105
+
106
+ class VertexAttributesCollector:
107
+ POSITION_STRUCT: Final = struct.Struct("<fff")
108
+ NORMAL_STRUCT: Final = struct.Struct("<fff")
109
+ TEXCOORD_STRUCT: Final = struct.Struct("<ff")
110
+ WEIGHTS_STRUCT: Final = struct.Struct("<ffff")
111
+ JOINTS_STRUCT: Final = struct.Struct("<HHHH")
112
+
113
+ IndexSearchKey = tuple[
114
+ int, tuple[float, float, float], Optional[tuple[float, float]]
115
+ ]
116
+
117
+ def __init__(self) -> None:
118
+ self.count = 0
119
+
120
+ self.position = bytearray()
121
+ self.position_max_x = FLOAT_NEGATIVE_MAX
122
+ self.position_min_x = FLOAT_POSITIVE_MAX
123
+ self.position_max_y = FLOAT_NEGATIVE_MAX
124
+ self.position_min_y = FLOAT_POSITIVE_MAX
125
+ self.position_max_z = FLOAT_NEGATIVE_MAX
126
+ self.position_min_z = FLOAT_POSITIVE_MAX
127
+
128
+ self.normal = bytearray()
129
+ self.texcoord = bytearray()
130
+ self.weights = bytearray()
131
+ self.joints = bytearray()
132
+
133
+ self._index_search_dict: dict[
134
+ tuple[int, tuple[float, float, float], Optional[tuple[float, float]]],
135
+ int,
136
+ ] = {}
137
+
138
+ @staticmethod
139
+ def create_index_search_key(
140
+ *,
141
+ blender_vertex_index: int,
142
+ normal: tuple[float, float, float],
143
+ texcoord: Optional[tuple[float, float]],
144
+ ) -> IndexSearchKey:
145
+ return (
146
+ blender_vertex_index,
147
+ normal,
148
+ texcoord,
149
+ )
150
+
151
+ def find_added_vertex_index(
152
+ self,
153
+ blender_vertex_index: int,
154
+ normal: tuple[float, float, float],
155
+ texcoord: Optional[tuple[float, float]],
156
+ ) -> Optional[int]:
157
+ index_search_key = self.create_index_search_key(
158
+ blender_vertex_index=blender_vertex_index,
159
+ normal=normal,
160
+ texcoord=texcoord,
161
+ )
162
+ return self._index_search_dict.get(index_search_key)
163
+
164
+ def add_vertex(
165
+ self,
166
+ *,
167
+ blender_vertex_index: int,
168
+ position: tuple[float, float, float],
169
+ normal: tuple[float, float, float],
170
+ texcoord: Optional[tuple[float, float]],
171
+ weights: Optional[tuple[float, float, float, float]],
172
+ joints: Optional[tuple[int, int, int, int]],
173
+ ) -> int:
174
+ index = self.count
175
+ self.count += 1
176
+
177
+ index_search_key = self.create_index_search_key(
178
+ blender_vertex_index=blender_vertex_index,
179
+ normal=normal,
180
+ texcoord=texcoord,
181
+ )
182
+ self._index_search_dict[index_search_key] = index
183
+
184
+ self.position.extend(self.POSITION_STRUCT.pack(*position))
185
+ position_x, position_y, position_z = position
186
+ self.position_max_x = max(self.position_max_x, position_x)
187
+ self.position_min_x = min(self.position_min_x, position_x)
188
+ self.position_max_y = max(self.position_max_y, position_y)
189
+ self.position_min_y = min(self.position_min_y, position_y)
190
+ self.position_max_z = max(self.position_max_z, position_z)
191
+ self.position_min_z = min(self.position_min_z, position_z)
192
+
193
+ self.normal.extend(self.NORMAL_STRUCT.pack(*normal))
194
+ if texcoord is not None:
195
+ self.texcoord.extend(self.TEXCOORD_STRUCT.pack(*texcoord))
196
+ if weights is not None:
197
+ self.weights.extend(self.WEIGHTS_STRUCT.pack(*weights))
198
+ if joints is not None:
199
+ self.joints.extend(self.JOINTS_STRUCT.pack(*joints))
200
+
201
+ return index
202
+
203
+ class VertexMorphTargetCollector:
204
+ def __init__(self, count: int) -> None:
205
+ self.count = count
206
+
207
+ self.POSITION_STRUCT = (
208
+ Vrm0Exporter.VertexAttributesCollector.POSITION_STRUCT
209
+ )
210
+ self.position = bytearray(self.POSITION_STRUCT.size * count)
211
+ self.position_max_x = FLOAT_NEGATIVE_MAX
212
+ self.position_min_x = FLOAT_POSITIVE_MAX
213
+ self.position_max_y = FLOAT_NEGATIVE_MAX
214
+ self.position_min_y = FLOAT_POSITIVE_MAX
215
+ self.position_max_z = FLOAT_NEGATIVE_MAX
216
+ self.position_min_z = FLOAT_POSITIVE_MAX
217
+
218
+ self.NORMAL_STRUCT = Vrm0Exporter.VertexAttributesCollector.NORMAL_STRUCT
219
+ self.normal = bytearray(self.NORMAL_STRUCT.size * count)
220
+
221
+ def add_vertex(
222
+ self,
223
+ *,
224
+ vertex_index: int,
225
+ target_position: tuple[float, float, float],
226
+ target_normal: tuple[float, float, float],
227
+ ) -> None:
228
+ if not (0 <= vertex_index < self.count):
229
+ _logger.error(
230
+ "invalid vertex_index: %d, count %d", vertex_index, self.count
231
+ )
232
+ return
233
+
234
+ target_position_x, target_position_y, target_position_z = target_position
235
+
236
+ position_start_index = vertex_index * self.POSITION_STRUCT.size
237
+ self.position[
238
+ position_start_index : position_start_index + self.POSITION_STRUCT.size
239
+ ] = self.POSITION_STRUCT.pack(
240
+ target_position_x, target_position_y, target_position_z
241
+ )
242
+
243
+ self.position_max_x = max(self.position_max_x, target_position_x)
244
+ self.position_min_x = min(self.position_min_x, target_position_x)
245
+ self.position_max_y = max(self.position_max_y, target_position_y)
246
+ self.position_min_y = min(self.position_min_y, target_position_y)
247
+ self.position_max_z = max(self.position_max_z, target_position_z)
248
+ self.position_min_z = min(self.position_min_z, target_position_z)
249
+
250
+ normal_start_index = vertex_index * self.NORMAL_STRUCT.size
251
+ self.normal[
252
+ normal_start_index : normal_start_index + self.NORMAL_STRUCT.size
253
+ ] = self.NORMAL_STRUCT.pack(*target_normal)
254
+
255
+ def export(self) -> Optional[bytes]:
256
+ init_extras_export()
257
+
258
+ with (
259
+ save_workspace(self._context),
260
+ self.setup_flexible_hierarchy_bones(
261
+ self._context, self._armature, self._export_objects
262
+ ),
263
+ self.clear_blend_shape_proxy_previews(self._context, self.armature_data),
264
+ self.enable_deform_for_all_referenced_bones(self.armature_data),
265
+ setup_humanoid_t_pose(self._context, self._armature),
266
+ self.hide_mtoon1_outline_geometry_nodes(self._context),
267
+ create_progress(self._context) as progress,
268
+ ):
269
+ json_dict: dict[str, Json] = {}
270
+ buffer0 = bytearray()
271
+ self.write_glb_structure(progress, json_dict, buffer0)
272
+ return gltf.pack_glb(json_dict, buffer0)
273
+
274
+ @staticmethod
275
+ def enter_setup_flexible_hierarchy_bones(
276
+ context: Context,
277
+ armature_object: Object,
278
+ _export_objects: Sequence[Object],
279
+ ) -> Optional[FlexibleHierarchyBoneSetup]:
280
+ armature_data = armature_object.data
281
+ if not isinstance(armature_data, Armature):
282
+ return None
283
+ humanoid = get_armature_extension(armature_data).vrm0.humanoid
284
+ if humanoid.filter_by_human_bone_hierarchy:
285
+ return None
286
+
287
+ assigned_human_bone_specification_to_bone_name: dict[
288
+ HumanBoneSpecification, str
289
+ ] = {}
290
+ for human_bone in humanoid.human_bones:
291
+ human_bone_name = HumanBoneName.from_str(human_bone.bone)
292
+ if human_bone_name is None:
293
+ continue
294
+ if not human_bone.node.bone_name:
295
+ continue
296
+ if human_bone.node.bone_name not in armature_data.bones:
297
+ continue
298
+ specification = HumanBoneSpecifications.get(human_bone_name)
299
+ assigned_human_bone_specification_to_bone_name[specification] = (
300
+ human_bone.node.bone_name
301
+ )
302
+
303
+ if not assigned_human_bone_specification_to_bone_name:
304
+ return None
305
+
306
+ child_bone_name_to_parent_bone_name = (
307
+ Vrm0Exporter.create_child_bone_name_to_parent_bone_name(
308
+ armature_data=armature_data,
309
+ assigned_human_bone_specification_to_bone_name=(
310
+ assigned_human_bone_specification_to_bone_name
311
+ ),
312
+ all_human_bone_specifications=HumanBoneSpecifications.all_human_bones,
313
+ )
314
+ )
315
+ if not child_bone_name_to_parent_bone_name:
316
+ return None
317
+
318
+ bone_name_and_muted_constraint_name: list[tuple[str, str]] = []
319
+ for pose_bone in armature_object.pose.bones:
320
+ for constraint in pose_bone.constraints:
321
+ if not isinstance(constraint, Constraint):
322
+ continue
323
+ if constraint.mute:
324
+ continue
325
+ bone_name_and_muted_constraint_name.append(
326
+ (pose_bone.name, constraint.name)
327
+ )
328
+ constraint.mute = True
329
+
330
+ return FlexibleHierarchyBoneSetup(
331
+ original_bone_settings=Vrm0Exporter.reparent_edit_bones(
332
+ context,
333
+ armature_object,
334
+ assigned_human_bone_specification_to_bone_name,
335
+ child_bone_name_to_parent_bone_name,
336
+ ),
337
+ pose_bone_name_and_muted_constraint_name=bone_name_and_muted_constraint_name,
338
+ )
339
+
340
+ def write_glb_structure(
341
+ self, progress: Progress, json_dict: dict[str, Json], buffer0: bytearray
342
+ ) -> None:
343
+ json_dict["asset"] = {"generator": self.get_asset_generator(), "version": "2.0"}
344
+
345
+ scene_dicts: list[dict[str, Json]] = []
346
+ node_dicts: list[dict[str, Json]] = []
347
+ mesh_dicts: list[dict[str, Json]] = []
348
+ skin_dicts: list[dict[str, Json]] = []
349
+ material_dicts: list[dict[str, Json]] = []
350
+ texture_dicts: list[dict[str, Json]] = []
351
+ sampler_dicts: list[dict[str, Json]] = []
352
+ image_dicts: list[dict[str, Json]] = []
353
+ accessor_dicts: list[dict[str, Json]] = []
354
+ buffer_view_dicts: list[dict[str, Json]] = []
355
+ extensions_vrm_material_property_dicts: list[Json] = []
356
+ extensions_vrm_dict: dict[str, Json] = {
357
+ "materialProperties": extensions_vrm_material_property_dicts,
358
+ }
359
+ extensions_used: list[str] = []
360
+ material_name_to_material_index: dict[str, int] = {}
361
+ image_name_to_image_index: dict[str, int] = {}
362
+
363
+ self.write_materials(
364
+ progress,
365
+ material_dicts,
366
+ texture_dicts,
367
+ sampler_dicts,
368
+ image_dicts,
369
+ buffer_view_dicts,
370
+ extensions_vrm_material_property_dicts,
371
+ extensions_used,
372
+ buffer0,
373
+ material_name_to_material_index,
374
+ image_name_to_image_index,
375
+ )
376
+
377
+ bone_name_to_node_index: dict[str, int] = {}
378
+ mesh_object_name_to_mesh_index: dict[str, int] = {}
379
+ self.write_scene(
380
+ progress,
381
+ scene_dicts,
382
+ node_dicts,
383
+ mesh_dicts,
384
+ skin_dicts,
385
+ material_dicts,
386
+ accessor_dicts,
387
+ buffer_view_dicts,
388
+ extensions_vrm_material_property_dicts,
389
+ buffer0,
390
+ bone_name_to_node_index,
391
+ mesh_object_name_to_mesh_index,
392
+ material_name_to_material_index,
393
+ )
394
+ self.write_extensions_vrm(
395
+ progress,
396
+ mesh_dicts,
397
+ texture_dicts,
398
+ sampler_dicts,
399
+ image_dicts,
400
+ buffer_view_dicts,
401
+ extensions_vrm_dict,
402
+ extensions_used,
403
+ buffer0,
404
+ image_name_to_image_index,
405
+ bone_name_to_node_index,
406
+ mesh_object_name_to_mesh_index,
407
+ )
408
+
409
+ if scene_dicts:
410
+ json_dict["scenes"] = make_json(scene_dicts)
411
+ json_dict["scene"] = 0
412
+ json_dict["nodes"] = make_json(node_dicts)
413
+ json_dict["meshes"] = make_json(mesh_dicts)
414
+ json_dict["materials"] = make_json(material_dicts)
415
+ json_dict["skins"] = make_json(skin_dicts)
416
+ json_dict["accessors"] = make_json(accessor_dicts)
417
+ json_dict["textures"] = make_json(texture_dicts)
418
+ json_dict["images"] = make_json(image_dicts)
419
+ json_dict["samplers"] = make_json(sampler_dicts)
420
+ json_dict["bufferViews"] = make_json(buffer_view_dicts)
421
+ json_dict["extensionsUsed"] = list(dict.fromkeys(extensions_used).keys())
422
+ json_dict["extensions"] = make_json({"VRM": extensions_vrm_dict})
423
+ json_dict["buffers"] = [
424
+ {
425
+ "byteLength": len(buffer0),
426
+ }
427
+ ]
428
+
429
+ def write_extensions_vrm(
430
+ self,
431
+ progress: Progress,
432
+ mesh_dicts: list[dict[str, Json]],
433
+ texture_dicts: list[dict[str, Json]],
434
+ sampler_dicts: list[dict[str, Json]],
435
+ image_dicts: list[dict[str, Json]],
436
+ buffer_view_dicts: list[dict[str, Json]],
437
+ vrm_dict: dict[str, Json],
438
+ extensions_used: list[str],
439
+ buffer0: bytearray,
440
+ image_name_to_image_index: dict[str, int],
441
+ bone_name_to_node_index: Mapping[str, int],
442
+ mesh_object_name_to_mesh_index: Mapping[str, int],
443
+ ) -> None:
444
+ vrm_dict["specVersion"] = "0.0"
445
+ vrm_dict["exporterVersion"] = self.get_asset_generator()
446
+
447
+ self.write_extensions_vrm_first_person(
448
+ progress, vrm_dict, bone_name_to_node_index, mesh_object_name_to_mesh_index
449
+ )
450
+ self.write_extensions_vrm_meta(
451
+ progress,
452
+ texture_dicts,
453
+ sampler_dicts,
454
+ image_dicts,
455
+ buffer_view_dicts,
456
+ vrm_dict,
457
+ buffer0,
458
+ image_name_to_image_index,
459
+ )
460
+ self.write_extensions_vrm_blend_shape_master(
461
+ progress, mesh_dicts, mesh_object_name_to_mesh_index, vrm_dict
462
+ )
463
+ self.write_extensions_vrm_secondary_animation(
464
+ progress, vrm_dict, bone_name_to_node_index
465
+ )
466
+ self.write_extensions_vrm_humanoid(progress, vrm_dict, bone_name_to_node_index)
467
+ extensions_used.append("VRM")
468
+
469
+ def write_extensions_vrm_humanoid(
470
+ self,
471
+ _progress: Progress,
472
+ vrm_dict: dict[str, Json],
473
+ bone_name_to_node_index: Mapping[str, int],
474
+ ) -> None:
475
+ human_bone_dicts: list[Json] = []
476
+ humanoid_dict: dict[str, Json] = {"humanBones": human_bone_dicts}
477
+ vrm_dict["humanoid"] = humanoid_dict
478
+ humanoid = get_armature_extension(self.armature_data).vrm0.humanoid
479
+ for human_bone_name in HumanBoneSpecifications.all_names:
480
+ for human_bone in humanoid.human_bones:
481
+ if not (
482
+ human_bone.bone == human_bone_name
483
+ and (
484
+ node_index := bone_name_to_node_index.get(
485
+ human_bone.node.bone_name
486
+ )
487
+ )
488
+ is not None
489
+ ):
490
+ continue
491
+ human_bone_dict: dict[str, Json] = {
492
+ "bone": human_bone_name,
493
+ "node": node_index,
494
+ "useDefaultValues": human_bone.use_default_values,
495
+ }
496
+ human_bone_dicts.append(human_bone_dict)
497
+ if not human_bone.use_default_values:
498
+ human_bone_dict.update(
499
+ {
500
+ "min": {
501
+ "x": human_bone.min[0],
502
+ "y": human_bone.min[1],
503
+ "z": human_bone.min[2],
504
+ },
505
+ "max": {
506
+ "x": human_bone.max[0],
507
+ "y": human_bone.max[1],
508
+ "z": human_bone.max[2],
509
+ },
510
+ "center": {
511
+ "x": human_bone.center[0],
512
+ "y": human_bone.center[1],
513
+ "z": human_bone.center[2],
514
+ },
515
+ "axisLength": human_bone.axis_length,
516
+ }
517
+ )
518
+ break
519
+ humanoid_dict["armStretch"] = humanoid.arm_stretch
520
+ humanoid_dict["legStretch"] = humanoid.leg_stretch
521
+ humanoid_dict["upperArmTwist"] = humanoid.upper_arm_twist
522
+ humanoid_dict["lowerArmTwist"] = humanoid.lower_arm_twist
523
+ humanoid_dict["upperLegTwist"] = humanoid.upper_leg_twist
524
+ humanoid_dict["lowerLegTwist"] = humanoid.lower_leg_twist
525
+ humanoid_dict["feetSpacing"] = humanoid.feet_spacing
526
+ humanoid_dict["hasTranslationDoF"] = humanoid.has_translation_dof
527
+
528
+ def write_extensions_vrm_blend_shape_master(
529
+ self,
530
+ _progress: Progress,
531
+ mesh_dicts: list[dict[str, Json]],
532
+ mesh_object_name_to_mesh_index: Mapping[str, int],
533
+ vrm_dict: dict[str, Json],
534
+ ) -> None:
535
+ blend_shape_master = get_armature_extension(
536
+ self.armature_data
537
+ ).vrm0.blend_shape_master
538
+ first_person = get_armature_extension(self.armature_data).vrm0.first_person
539
+ blend_shape_group_dicts: list[Json] = []
540
+ blend_shape_master_dict: dict[str, Json] = {
541
+ "blendShapeGroups": blend_shape_group_dicts
542
+ }
543
+ vrm_dict["blendShapeMaster"] = blend_shape_master_dict
544
+
545
+ remaining_preset_names = [
546
+ preset_name.identifier
547
+ for preset_name in Vrm0BlendShapeGroupPropertyGroup.preset_name_enum
548
+ if preset_name != Vrm0BlendShapeGroupPropertyGroup.PRESET_NAME_UNKNOWN
549
+ ]
550
+
551
+ for blend_shape_group in blend_shape_master.blend_shape_groups:
552
+ blend_shape_group_dict: dict[str, Json] = {}
553
+
554
+ if not blend_shape_group.name:
555
+ continue
556
+ blend_shape_group_dict["name"] = blend_shape_group.name
557
+
558
+ if not blend_shape_group.preset_name:
559
+ continue
560
+
561
+ if blend_shape_group.preset_name != "unknown":
562
+ if blend_shape_group.preset_name not in remaining_preset_names:
563
+ continue
564
+ remaining_preset_names.remove(blend_shape_group.preset_name)
565
+
566
+ blend_shape_group_dict["presetName"] = blend_shape_group.preset_name
567
+
568
+ bind_dicts: list[Json] = []
569
+ blend_shape_group_dict["binds"] = bind_dicts
570
+ for bind in blend_shape_group.binds:
571
+ bind_dict: dict[str, Json] = {}
572
+ mesh_index = mesh_object_name_to_mesh_index.get(
573
+ bind.mesh.mesh_object_name
574
+ )
575
+ if mesh_index is None:
576
+ continue
577
+ bind_dict["mesh"] = mesh_index
578
+
579
+ if not (
580
+ 0 <= mesh_index < len(mesh_dicts)
581
+ and (mesh_dict := mesh_dicts[mesh_index])
582
+ and isinstance(mesh_extras_dict := mesh_dict.get("extras"), dict)
583
+ and isinstance(
584
+ target_names := mesh_extras_dict.get("targetNames"), list
585
+ )
586
+ ):
587
+ continue
588
+
589
+ if bind.index not in target_names:
590
+ continue
591
+
592
+ bind_dict["index"] = target_names.index(bind.index)
593
+ bind_dict["weight"] = min(max(bind.weight * 100, 0), 100)
594
+
595
+ bind_dicts.append(bind_dict)
596
+
597
+ material_value_dicts: list[Json] = []
598
+ blend_shape_group_dict["materialValues"] = material_value_dicts
599
+ for material_value in blend_shape_group.material_values:
600
+ material_value_material = material_value.material
601
+ if not material_value_material or not material_value_material.name:
602
+ continue
603
+
604
+ material_value_dicts.append(
605
+ {
606
+ "materialName": material_value_material.name,
607
+ "propertyName": material_value.property_name,
608
+ "targetValue": make_json(
609
+ material_value.read_target_value_as_float_list()
610
+ ),
611
+ }
612
+ )
613
+
614
+ blend_shape_group_dict["isBinary"] = blend_shape_group.is_binary
615
+ blend_shape_group_dicts.append(blend_shape_group_dict)
616
+
617
+ # VirtualMotionCapture requires some blend shape presets
618
+ # https://twitter.com/sh_akira/status/1674237253231714305
619
+ # Note: the VRM specification does not require them. UniVRM 0.112.0
620
+ # can be configured not to output them.
621
+ for preset_name in remaining_preset_names:
622
+ if first_person.look_at_type_name == "Bone" and preset_name.startswith(
623
+ "look"
624
+ ):
625
+ continue
626
+ name = next(
627
+ (
628
+ enum.name.replace(" ", "")
629
+ for enum in Vrm0BlendShapeGroupPropertyGroup.preset_name_enum
630
+ if enum.identifier == preset_name
631
+ ),
632
+ preset_name.capitalize(),
633
+ )
634
+ blend_shape_group_dicts.append(
635
+ {
636
+ "name": name,
637
+ "presetName": preset_name,
638
+ "binds": [],
639
+ "materialValues": [],
640
+ "isBinary": False,
641
+ }
642
+ )
643
+
644
+ def write_extensions_vrm_secondary_animation(
645
+ self,
646
+ _progress: Progress,
647
+ vrm_dict: dict[str, Json],
648
+ bone_name_to_node_index: Mapping[str, int],
649
+ ) -> None:
650
+ secondary_animation = get_armature_extension(
651
+ self.armature_data
652
+ ).vrm0.secondary_animation
653
+ secondary_animation_dict: dict[str, Json] = {}
654
+ vrm_dict["secondaryAnimation"] = secondary_animation_dict
655
+
656
+ collider_group_dicts: list[Json] = []
657
+ secondary_animation_dict["colliderGroups"] = collider_group_dicts
658
+ secondary_animation = get_armature_extension(
659
+ self.armature_data
660
+ ).vrm0.secondary_animation
661
+
662
+ collider_group_uuid_to_index: dict[str, int] = {}
663
+
664
+ for collider_group in secondary_animation.collider_groups:
665
+ if not collider_group.uuid:
666
+ continue
667
+ node_index = bone_name_to_node_index.get(collider_group.node.bone_name)
668
+ if node_index is None:
669
+ continue
670
+
671
+ collider_group_dict: dict[str, Json] = {}
672
+ collider_dicts: list[Json] = []
673
+ collider_group_dict["colliders"] = collider_dicts
674
+
675
+ collider_group_uuid_to_index[collider_group.uuid] = len(
676
+ collider_group_dicts
677
+ )
678
+ collider_group_dicts.append(collider_group_dict)
679
+
680
+ collider_group_dict["node"] = node_index
681
+
682
+ for collider in collider_group.colliders:
683
+ collider_object = collider.bpy_object
684
+ if not collider_object or not (
685
+ bone := self._armature.pose.bones.get(collider_object.parent_bone)
686
+ ):
687
+ continue
688
+
689
+ collider_dict: dict[str, Json] = {}
690
+ bone_node_world_translation = (
691
+ self._armature.matrix_world @ bone.matrix
692
+ ).to_translation()
693
+ offset = (
694
+ collider_object.matrix_world.to_translation()
695
+ - bone_node_world_translation
696
+ )
697
+
698
+ object_mean_scale = statistics.mean(
699
+ abs(s) for s in collider_object.matrix_world.to_scale()
700
+ )
701
+ collider_dict["radius"] = (
702
+ collider_object.empty_display_size * object_mean_scale
703
+ )
704
+
705
+ collider_dict["offset"] = {
706
+ # Perform a special axis transformation for VRM 0.0.
707
+ # https://github.com/vrm-c/UniVRM/issues/65
708
+ "x": -offset.x,
709
+ "y": offset.z,
710
+ "z": -offset.y,
711
+ }
712
+ collider_dicts.append(collider_dict)
713
+
714
+ bone_group_dicts: list[Json] = []
715
+ secondary_animation_dict["boneGroups"] = bone_group_dicts
716
+ for bone_group in secondary_animation.bone_groups:
717
+ bone_group_dict: dict[str, Json] = {
718
+ "comment": bone_group.comment,
719
+ "stiffiness": bone_group.stiffiness,
720
+ "gravityPower": bone_group.gravity_power,
721
+ "gravityDir": {
722
+ # Perform a special axis transformation for VRM 0.0.
723
+ # https://github.com/vrm-c/vrm-specification/issues/205#issuecomment-753582597
724
+ "x": bone_group.gravity_dir[0],
725
+ "y": bone_group.gravity_dir[2],
726
+ "z": bone_group.gravity_dir[1],
727
+ },
728
+ "dragForce": bone_group.drag_force,
729
+ "center": bone_name_to_node_index.get(bone_group.center.bone_name, -1),
730
+ "hitRadius": bone_group.hit_radius,
731
+ "bones": [
732
+ node_index
733
+ for bone in bone_group.bones
734
+ if (node_index := bone_name_to_node_index.get(bone.bone_name))
735
+ is not None
736
+ ],
737
+ }
738
+ collider_group_indices: list[Json] = []
739
+ for collider_group_reference in bone_group.collider_groups:
740
+ if not collider_group_reference.collider_group_uuid:
741
+ continue
742
+ collider_group_index = collider_group_uuid_to_index.get(
743
+ collider_group_reference.collider_group_uuid
744
+ )
745
+ if collider_group_index is None:
746
+ continue
747
+ if collider_group_index not in collider_group_indices:
748
+ collider_group_indices.append(collider_group_index)
749
+
750
+ bone_group_dict["colliderGroups"] = collider_group_indices
751
+ bone_group_dicts.append(bone_group_dict)
752
+
753
+ def write_extensions_vrm_meta(
754
+ self,
755
+ _progress: Progress,
756
+ texture_dicts: list[dict[str, Json]],
757
+ sampler_dicts: list[dict[str, Json]],
758
+ image_dicts: list[dict[str, Json]],
759
+ buffer_view_dicts: list[dict[str, Json]],
760
+ vrm_dict: dict[str, Json],
761
+ buffer0: bytearray,
762
+ image_name_to_index_dict: dict[str, int],
763
+ ) -> None:
764
+ meta = get_armature_extension(self.armature_data).vrm0.meta
765
+ meta_dict: dict[str, Json] = {
766
+ "title": meta.title,
767
+ "version": meta.version,
768
+ "author": meta.author,
769
+ "contactInformation": meta.contact_information,
770
+ "reference": meta.reference,
771
+ "allowedUserName": meta.allowed_user_name,
772
+ "violentUssageName": meta.violent_ussage_name,
773
+ "sexualUssageName": meta.sexual_ussage_name,
774
+ "commercialUssageName": meta.commercial_ussage_name,
775
+ "otherPermissionUrl": meta.other_permission_url,
776
+ "licenseName": meta.license_name,
777
+ "otherLicenseUrl": meta.other_license_url,
778
+ }
779
+
780
+ if meta.texture:
781
+ image_index = self.find_or_create_image(
782
+ image_dicts,
783
+ buffer_view_dicts,
784
+ buffer0,
785
+ image_name_to_index_dict,
786
+ meta.texture,
787
+ )
788
+ sampler_dict: dict[str, Json] = {
789
+ "magFilter": GL_LINEAR,
790
+ "minFilter": GL_LINEAR,
791
+ "wrapS": GL_REPEAT,
792
+ "wrapT": GL_REPEAT,
793
+ }
794
+ if sampler_dict in sampler_dicts:
795
+ sampler_index = sampler_dicts.index(sampler_dict)
796
+ else:
797
+ sampler_index = len(sampler_dicts)
798
+ sampler_dicts.append(sampler_dict)
799
+ texture_index = len(texture_dicts)
800
+ texture_dicts.append(
801
+ {
802
+ "sampler": sampler_index,
803
+ "source": image_index,
804
+ },
805
+ )
806
+ meta_dict["texture"] = texture_index
807
+
808
+ vrm_dict["meta"] = meta_dict
809
+
810
+ def write_extensions_vrm_first_person(
811
+ self,
812
+ _progress: Progress,
813
+ vrm_dict: dict[str, Json],
814
+ bone_name_to_node_index: Mapping[str, int],
815
+ mesh_object_name_to_mesh_index: Mapping[str, int],
816
+ ) -> None:
817
+ first_person_dict: dict[str, Json] = {}
818
+ vrm_dict["firstPerson"] = first_person_dict
819
+
820
+ first_person = get_armature_extension(self.armature_data).vrm0.first_person
821
+
822
+ if first_person.first_person_bone.bone_name:
823
+ first_person_bone_name: Optional[str] = (
824
+ first_person.first_person_bone.bone_name
825
+ )
826
+ else:
827
+ first_person_bone_name = next(
828
+ human_bone.node.bone_name
829
+ for human_bone in (
830
+ get_armature_extension(self.armature_data).vrm0.humanoid.human_bones
831
+ )
832
+ if human_bone.bone == "head"
833
+ )
834
+
835
+ if first_person_bone_name:
836
+ first_person_bone_index = bone_name_to_node_index.get(
837
+ first_person_bone_name
838
+ )
839
+ if isinstance(first_person_bone_index, int):
840
+ first_person_dict["firstPersonBone"] = first_person_bone_index
841
+
842
+ first_person_dict["firstPersonBoneOffset"] = {
843
+ # Perform a special axis transformation for VRM 0.0.
844
+ # https://github.com/vrm-c/vrm-specification/issues/205#issuecomment-753582597
845
+ "x": first_person.first_person_bone_offset[0],
846
+ "y": first_person.first_person_bone_offset[2],
847
+ "z": first_person.first_person_bone_offset[1],
848
+ }
849
+
850
+ mesh_annotation_dicts: list[Json] = []
851
+ first_person_dict["meshAnnotations"] = mesh_annotation_dicts
852
+ for mesh_annotation in first_person.mesh_annotations:
853
+ mesh_index = mesh_object_name_to_mesh_index.get(
854
+ mesh_annotation.mesh.mesh_object_name
855
+ )
856
+ if mesh_index is None:
857
+ mesh_index = -1
858
+ mesh_annotation_dicts.append(
859
+ {
860
+ "mesh": mesh_index,
861
+ "firstPersonFlag": mesh_annotation.first_person_flag,
862
+ }
863
+ )
864
+ first_person_dict["lookAtTypeName"] = first_person.look_at_type_name
865
+ for look_at, look_at_dict_key in (
866
+ (
867
+ first_person.look_at_horizontal_inner,
868
+ "lookAtHorizontalInner",
869
+ ),
870
+ (
871
+ first_person.look_at_horizontal_outer,
872
+ "lookAtHorizontalOuter",
873
+ ),
874
+ (
875
+ first_person.look_at_vertical_down,
876
+ "lookAtVerticalDown",
877
+ ),
878
+ (
879
+ first_person.look_at_vertical_up,
880
+ "lookAtVerticalUp",
881
+ ),
882
+ ):
883
+ first_person_dict[look_at_dict_key] = {
884
+ "curve": list(look_at.curve),
885
+ "xRange": look_at.x_range,
886
+ "yRange": look_at.y_range,
887
+ }
888
+
889
+ def write_scene(
890
+ self,
891
+ progress: Progress,
892
+ scene_dicts: list[dict[str, Json]],
893
+ node_dicts: list[dict[str, Json]],
894
+ mesh_dicts: list[dict[str, Json]],
895
+ skin_dicts: list[dict[str, Json]],
896
+ material_dicts: list[dict[str, Json]],
897
+ accessor_dicts: list[dict[str, Json]],
898
+ buffer_view_dicts: list[dict[str, Json]],
899
+ extensions_vrm_material_property_dicts: list[Json],
900
+ buffer0: bytearray,
901
+ bone_name_to_node_index: dict[str, int],
902
+ mesh_object_name_to_mesh_index: dict[str, int],
903
+ material_name_to_material_index: Mapping[str, int],
904
+ ) -> None:
905
+ scene0_nodes: list[Json] = []
906
+
907
+ armature_root_node_indices, skin_dict, skin_joints = self.write_armature(
908
+ progress,
909
+ node_dicts,
910
+ accessor_dicts,
911
+ buffer_view_dicts,
912
+ buffer0,
913
+ bone_name_to_node_index,
914
+ )
915
+ scene0_nodes.extend(armature_root_node_indices)
916
+ scene0_nodes.extend(
917
+ self.write_mesh_nodes(
918
+ progress,
919
+ node_dicts,
920
+ mesh_dicts,
921
+ skin_dicts,
922
+ material_dicts,
923
+ accessor_dicts,
924
+ buffer_view_dicts,
925
+ extensions_vrm_material_property_dicts,
926
+ buffer0,
927
+ bone_name_to_node_index,
928
+ mesh_object_name_to_mesh_index,
929
+ material_name_to_material_index,
930
+ skin_dict,
931
+ skin_joints,
932
+ )
933
+ )
934
+
935
+ scene_dicts.append({"nodes": scene0_nodes})
936
+
937
+ def write_mtoon1_downgraded_material(
938
+ self,
939
+ _progress: Progress,
940
+ texture_dicts: list[dict[str, Json]],
941
+ sampler_dicts: list[dict[str, Json]],
942
+ image_dicts: list[dict[str, Json]],
943
+ buffer_view_dicts: list[dict[str, Json]],
944
+ extensions_used: list[str],
945
+ buffer0: bytearray,
946
+ image_name_to_image_index: dict[str, int],
947
+ material: Material,
948
+ material_dict: dict[str, Json],
949
+ vrm_material_property_dict: dict[str, Json],
950
+ ) -> None:
951
+ gltf = get_material_extension(material).mtoon1
952
+ mtoon = gltf.extensions.vrmc_materials_mtoon
953
+
954
+ material_dict |= make_json_dict(
955
+ {
956
+ "alphaMode": gltf.alpha_mode,
957
+ "doubleSided": gltf.double_sided,
958
+ "extensions": {"KHR_materials_unlit": {}},
959
+ }
960
+ )
961
+
962
+ extensions_used.append("KHR_materials_unlit")
963
+ pbr_metallic_roughness_dict: dict[str, Json] = {
964
+ "metallicFactor": 0,
965
+ "roughnessFactor": 0.9,
966
+ }
967
+ keyword_map: dict[str, bool] = {}
968
+ tag_map: dict[str, str] = {}
969
+ float_properties: dict[str, float] = {}
970
+ vector_properties: dict[str, Sequence[float]] = {}
971
+ texture_properties: dict[str, int] = {}
972
+ pbr_metallic_roughness_dict["baseColorFactor"] = list(
973
+ gltf.pbr_metallic_roughness.base_color_factor
974
+ )
975
+ extensions = gltf.pbr_metallic_roughness.base_color_texture.extensions
976
+ khr_texture_transform = extensions.khr_texture_transform
977
+
978
+ vector_properties["_Color"] = convert.linear_to_srgb(
979
+ gltf.pbr_metallic_roughness.base_color_factor
980
+ )
981
+
982
+ if assign_dict(
983
+ pbr_metallic_roughness_dict,
984
+ "baseColorTexture",
985
+ self.create_mtoon1_downgraded_texture_info(
986
+ gltf.pbr_metallic_roughness.base_color_texture,
987
+ texture_properties,
988
+ "_MainTex",
989
+ vector_properties,
990
+ texture_dicts,
991
+ sampler_dicts,
992
+ image_dicts,
993
+ buffer_view_dicts,
994
+ buffer0,
995
+ image_name_to_image_index,
996
+ khr_texture_transform,
997
+ extensions_used,
998
+ ),
999
+ ):
1000
+ vector_properties["_MainTex"] = [
1001
+ khr_texture_transform.offset[0],
1002
+ # https://github.com/vrm-c/UniVRM/issues/930
1003
+ 1 - khr_texture_transform.offset[1] - khr_texture_transform.scale[1],
1004
+ khr_texture_transform.scale[0],
1005
+ khr_texture_transform.scale[1],
1006
+ ]
1007
+
1008
+ vector_properties["_ShadeColor"] = convert.linear_to_srgb(
1009
+ [*mtoon.shade_color_factor, 1]
1010
+ )
1011
+ self.create_mtoon1_downgraded_texture(
1012
+ mtoon.shade_multiply_texture.index,
1013
+ texture_properties,
1014
+ "_ShadeTexture",
1015
+ vector_properties,
1016
+ texture_dicts,
1017
+ sampler_dicts,
1018
+ image_dicts,
1019
+ buffer_view_dicts,
1020
+ buffer0,
1021
+ image_name_to_image_index,
1022
+ )
1023
+
1024
+ float_properties["_BumpScale"] = gltf.normal_texture.scale
1025
+ if assign_dict(
1026
+ material_dict,
1027
+ "normalTexture",
1028
+ self.create_mtoon1_downgraded_texture_info(
1029
+ gltf.normal_texture,
1030
+ texture_properties,
1031
+ "_BumpMap",
1032
+ vector_properties,
1033
+ texture_dicts,
1034
+ sampler_dicts,
1035
+ image_dicts,
1036
+ buffer_view_dicts,
1037
+ buffer0,
1038
+ image_name_to_image_index,
1039
+ khr_texture_transform,
1040
+ extensions_used,
1041
+ ),
1042
+ ):
1043
+ normal_texture_to_index_dict = material_dict.get("normalTexture")
1044
+ if isinstance(normal_texture_to_index_dict, dict):
1045
+ normal_texture_to_index_dict["scale"] = gltf.normal_texture.scale
1046
+ keyword_map["_NORMALMAP"] = True
1047
+
1048
+ self.create_mtoon1_downgraded_texture(
1049
+ gltf.mtoon0_shading_grade_texture,
1050
+ texture_properties,
1051
+ "_ShadingGradeTexture",
1052
+ vector_properties,
1053
+ texture_dicts,
1054
+ sampler_dicts,
1055
+ image_dicts,
1056
+ buffer_view_dicts,
1057
+ buffer0,
1058
+ image_name_to_image_index,
1059
+ )
1060
+ float_properties["_ShadingGradeRate"] = gltf.mtoon0_shading_grade_rate
1061
+
1062
+ float_properties["_ShadeShift"] = convert.mtoon_shading_shift_1_to_0(
1063
+ mtoon.shading_toony_factor, mtoon.shading_shift_factor
1064
+ )
1065
+ float_properties["_ShadeToony"] = convert.mtoon_shading_toony_1_to_0(
1066
+ mtoon.shading_toony_factor, mtoon.shading_shift_factor
1067
+ )
1068
+ float_properties["_IndirectLightIntensity"] = (
1069
+ convert.mtoon_gi_equalization_to_intensity(mtoon.gi_equalization_factor)
1070
+ )
1071
+ float_properties["_RimLightingMix"] = gltf.mtoon0_rim_lighting_mix
1072
+ float_properties["_RimFresnelPower"] = mtoon.parametric_rim_fresnel_power_factor
1073
+ float_properties["_RimLift"] = mtoon.parametric_rim_lift_factor
1074
+
1075
+ emissive_strength = (
1076
+ gltf.extensions.khr_materials_emissive_strength.emissive_strength
1077
+ )
1078
+ emissive_factor = Vector(gltf.emissive_factor)
1079
+ hdr_emissive_factor = (
1080
+ Vector(convert.linear_to_srgb(emissive_factor)) * emissive_strength
1081
+ )
1082
+ vector_properties["_EmissionColor"] = [*hdr_emissive_factor, 1]
1083
+ if emissive_factor.length_squared > 0:
1084
+ material_dict["emissiveFactor"] = list(emissive_factor)
1085
+
1086
+ assign_dict(
1087
+ material_dict,
1088
+ "emissiveTexture",
1089
+ self.create_mtoon1_downgraded_texture_info(
1090
+ gltf.emissive_texture,
1091
+ texture_properties,
1092
+ "_EmissionMap",
1093
+ vector_properties,
1094
+ texture_dicts,
1095
+ sampler_dicts,
1096
+ image_dicts,
1097
+ buffer_view_dicts,
1098
+ buffer0,
1099
+ image_name_to_image_index,
1100
+ khr_texture_transform,
1101
+ extensions_used,
1102
+ ),
1103
+ )
1104
+ if pbr_metallic_roughness_dict:
1105
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
1106
+
1107
+ self.create_mtoon1_downgraded_texture(
1108
+ mtoon.matcap_texture.index,
1109
+ texture_properties,
1110
+ "_SphereAdd",
1111
+ vector_properties,
1112
+ texture_dicts,
1113
+ sampler_dicts,
1114
+ image_dicts,
1115
+ buffer_view_dicts,
1116
+ buffer0,
1117
+ image_name_to_image_index,
1118
+ )
1119
+
1120
+ vector_properties["_RimColor"] = convert.linear_to_srgb(
1121
+ [*mtoon.parametric_rim_color_factor, 1]
1122
+ )
1123
+ self.create_mtoon1_downgraded_texture(
1124
+ mtoon.rim_multiply_texture.index,
1125
+ texture_properties,
1126
+ "_RimTexture",
1127
+ vector_properties,
1128
+ texture_dicts,
1129
+ sampler_dicts,
1130
+ image_dicts,
1131
+ buffer_view_dicts,
1132
+ buffer0,
1133
+ image_name_to_image_index,
1134
+ )
1135
+
1136
+ vector_properties["_OutlineColor"] = convert.linear_to_srgb(
1137
+ [*mtoon.outline_color_factor, 1]
1138
+ )
1139
+ self.create_mtoon1_downgraded_texture(
1140
+ mtoon.outline_width_multiply_texture.index,
1141
+ texture_properties,
1142
+ "_OutlineWidthTexture",
1143
+ vector_properties,
1144
+ texture_dicts,
1145
+ sampler_dicts,
1146
+ image_dicts,
1147
+ buffer_view_dicts,
1148
+ buffer0,
1149
+ image_name_to_image_index,
1150
+ )
1151
+
1152
+ float_properties["_UvAnimScrollX"] = mtoon.uv_animation_scroll_x_speed_factor
1153
+ float_properties["_UvAnimScrollY"] = -mtoon.uv_animation_scroll_y_speed_factor
1154
+ float_properties["_UvAnimRotation"] = mtoon.uv_animation_rotation_speed_factor
1155
+ self.create_mtoon1_downgraded_texture(
1156
+ mtoon.uv_animation_mask_texture.index,
1157
+ texture_properties,
1158
+ "_UvAnimMaskTexture",
1159
+ vector_properties,
1160
+ texture_dicts,
1161
+ sampler_dicts,
1162
+ image_dicts,
1163
+ buffer_view_dicts,
1164
+ buffer0,
1165
+ image_name_to_image_index,
1166
+ )
1167
+
1168
+ float_properties["_OutlineLightingMix"] = mtoon.outline_lighting_mix_factor
1169
+ outline_color_mode = 1 if mtoon.outline_lighting_mix_factor > 0 else 0
1170
+ float_properties["_OutlineColorMode"] = outline_color_mode
1171
+
1172
+ float_properties["_OutlineWidth"] = 0.0
1173
+ outline_width_world = False
1174
+ outline_width_screen = False
1175
+ outline_color_fixed = False
1176
+ outline_color_mixed = False
1177
+ if mtoon.outline_width_mode == mtoon.OUTLINE_WIDTH_MODE_NONE.identifier:
1178
+ float_properties["_OutlineWidthMode"] = 0
1179
+ float_properties["_OutlineLightingMix"] = 0
1180
+ float_properties["_OutlineColorMode"] = 0
1181
+ elif (
1182
+ mtoon.outline_width_mode
1183
+ == mtoon.OUTLINE_WIDTH_MODE_WORLD_COORDINATES.identifier
1184
+ ):
1185
+ float_properties["_OutlineWidth"] = mtoon.outline_width_factor * 100
1186
+ float_properties["_OutlineWidthMode"] = 1
1187
+ outline_width_world = True
1188
+ if outline_color_mode == 0:
1189
+ outline_color_fixed = True
1190
+ else:
1191
+ outline_color_mixed = True
1192
+ elif (
1193
+ mtoon.outline_width_mode
1194
+ == mtoon.OUTLINE_WIDTH_MODE_SCREEN_COORDINATES.identifier
1195
+ ):
1196
+ float_properties["_OutlineWidth"] = mtoon.outline_width_factor * 200
1197
+ float_properties["_OutlineWidthMode"] = 2
1198
+ outline_width_screen = True
1199
+ if outline_color_mode == 0:
1200
+ outline_color_fixed = True
1201
+ else:
1202
+ outline_color_mixed = True
1203
+
1204
+ if outline_width_world:
1205
+ keyword_map["MTOON_OUTLINE_WIDTH_WORLD"] = True
1206
+ elif outline_width_screen:
1207
+ keyword_map["MTOON_OUTLINE_WIDTH_SCREEN"] = True
1208
+
1209
+ if outline_color_fixed:
1210
+ keyword_map["MTOON_OUTLINE_COLOR_FIXED"] = outline_color_fixed
1211
+ elif outline_color_mixed:
1212
+ keyword_map["MTOON_OUTLINE_COLOR_MIXED"] = outline_color_mixed
1213
+
1214
+ float_properties["_Cutoff"] = 0.5
1215
+ if gltf.alpha_mode == gltf.ALPHA_MODE_OPAQUE.identifier:
1216
+ blend_mode = 0
1217
+ src_blend = 1
1218
+ dst_blend = 0
1219
+ z_write = 1
1220
+ alphatest_on = False
1221
+ alphablend_on = False
1222
+ render_queue = -1
1223
+ render_type = "Opaque"
1224
+ elif gltf.alpha_mode == gltf.ALPHA_MODE_MASK.identifier:
1225
+ blend_mode = 1
1226
+ src_blend = 1
1227
+ dst_blend = 0
1228
+ z_write = 1
1229
+ alphatest_on = True
1230
+ alphablend_on = False
1231
+ render_queue = gltf.mtoon0_render_queue
1232
+ render_type = "TransparentCutout"
1233
+ float_properties["_Cutoff"] = gltf.alpha_cutoff
1234
+ material_dict["alphaCutoff"] = gltf.alpha_cutoff
1235
+ elif not mtoon.transparent_with_z_write:
1236
+ blend_mode = 2
1237
+ src_blend = 5
1238
+ dst_blend = 10
1239
+ z_write = 0
1240
+ alphatest_on = False
1241
+ alphablend_on = True
1242
+ render_queue = gltf.mtoon0_render_queue
1243
+ render_type = "Transparent"
1244
+ else:
1245
+ blend_mode = 3
1246
+ src_blend = 5
1247
+ dst_blend = 10
1248
+ z_write = 1
1249
+ alphatest_on = False
1250
+ alphablend_on = True
1251
+ render_queue = gltf.mtoon0_render_queue
1252
+ render_type = "Transparent"
1253
+ float_properties["_Cutoff"] = gltf.alpha_cutoff # for compatibility
1254
+
1255
+ self.create_mtoon1_downgraded_texture(
1256
+ gltf.mtoon0_receive_shadow_texture,
1257
+ texture_properties,
1258
+ "_ReceiveShadowTexture",
1259
+ vector_properties,
1260
+ texture_dicts,
1261
+ sampler_dicts,
1262
+ image_dicts,
1263
+ buffer_view_dicts,
1264
+ buffer0,
1265
+ image_name_to_image_index,
1266
+ )
1267
+ float_properties["_ReceiveShadowRate"] = gltf.mtoon0_receive_shadow_rate
1268
+
1269
+ keyword_map["_ALPHABLEND_ON"] = alphablend_on
1270
+ keyword_map["_ALPHAPREMULTIPLY_ON"] = False
1271
+
1272
+ float_properties["_BlendMode"] = blend_mode
1273
+ float_properties["_SrcBlend"] = src_blend
1274
+ float_properties["_DstBlend"] = dst_blend
1275
+ float_properties["_ZWrite"] = z_write
1276
+ if alphatest_on:
1277
+ keyword_map["_ALPHATEST_ON"] = alphatest_on
1278
+ tag_map["RenderType"] = render_type
1279
+
1280
+ float_properties["_MToonVersion"] = MtoonUnversioned.version
1281
+ if gltf.mtoon0_front_cull_mode:
1282
+ float_properties["_CullMode"] = 1
1283
+ elif material.use_backface_culling:
1284
+ float_properties["_CullMode"] = 2
1285
+ else:
1286
+ float_properties["_CullMode"] = 0
1287
+ float_properties["_OutlineCullMode"] = 1
1288
+ float_properties["_DebugMode"] = 0
1289
+ float_properties["_LightColorAttenuation"] = gltf.mtoon0_light_color_attenuation
1290
+ float_properties["_OutlineScaledMaxDistance"] = (
1291
+ gltf.mtoon0_outline_scaled_max_distance
1292
+ )
1293
+
1294
+ keyword_map["MTOON_DEBUG_NORMAL"] = False
1295
+ keyword_map["MTOON_DEBUG_LITSHADERATE"] = False
1296
+
1297
+ vrm_material_property_dict.update(
1298
+ {
1299
+ "name": material.name,
1300
+ "shader": "VRM/MToon",
1301
+ "keywordMap": make_json(keyword_map),
1302
+ "tagMap": make_json(tag_map),
1303
+ "floatProperties": make_json(float_properties),
1304
+ "vectorProperties": make_json(vector_properties),
1305
+ "textureProperties": make_json(texture_properties),
1306
+ "renderQueue": render_queue,
1307
+ }
1308
+ )
1309
+
1310
+ def find_or_create_image(
1311
+ self,
1312
+ image_dicts: list[dict[str, Json]],
1313
+ buffer_view_dicts: list[dict[str, Json]],
1314
+ buffer0: bytearray,
1315
+ image_name_to_index_dict: dict[str, int],
1316
+ image: Image,
1317
+ ) -> int:
1318
+ image_index = image_name_to_index_dict.get(image.name)
1319
+ if isinstance(image_index, int):
1320
+ return image_index
1321
+
1322
+ image_bytes, mime = image_to_image_bytes(
1323
+ image, self._gltf2_addon_export_settings
1324
+ )
1325
+
1326
+ image_buffer_view_index = len(buffer_view_dicts)
1327
+ buffer_view_dicts.append(
1328
+ {
1329
+ "buffer": 0,
1330
+ "byteOffset": len(buffer0),
1331
+ "byteLength": len(image_bytes),
1332
+ }
1333
+ )
1334
+ buffer0.extend(image_bytes)
1335
+
1336
+ image_index = len(image_dicts)
1337
+ image_dicts.append(
1338
+ {
1339
+ "name": image.name,
1340
+ "mimeType": mime,
1341
+ "bufferView": image_buffer_view_index,
1342
+ }
1343
+ )
1344
+
1345
+ image_name_to_index_dict[image.name] = image_index
1346
+
1347
+ return image_index
1348
+
1349
+ def create_mtoon0_khr_texture_transform(
1350
+ self, node: Node, texture_input_name: str
1351
+ ) -> tuple[Optional[dict[str, Json]], tuple[float, float, float, float]]:
1352
+ default: tuple[Optional[dict[str, Json]], tuple[float, float, float, float]] = (
1353
+ None,
1354
+ (0, 0, 1, 1),
1355
+ )
1356
+
1357
+ texture_input = node.inputs.get(texture_input_name)
1358
+ if not texture_input:
1359
+ return default
1360
+
1361
+ texture_input_links = texture_input.links
1362
+ if not texture_input_links:
1363
+ return default
1364
+
1365
+ x_from_node = texture_input_links[0].from_node
1366
+ if not x_from_node:
1367
+ return default
1368
+
1369
+ x_inputs = x_from_node.inputs
1370
+ if not x_inputs:
1371
+ return default
1372
+
1373
+ x_links = x_inputs[0].links
1374
+ if not x_links:
1375
+ return default
1376
+
1377
+ uv_offset_scaling_node = x_links[0].from_node
1378
+ if not uv_offset_scaling_node or uv_offset_scaling_node.type != "MAPPING":
1379
+ return default
1380
+
1381
+ location_input = uv_offset_scaling_node.inputs.get("Location")
1382
+ if isinstance(location_input, shader.VECTOR_SOCKET_CLASSES):
1383
+ offset_x = location_input.default_value[0]
1384
+ offset_y = location_input.default_value[1]
1385
+ else:
1386
+ offset_x = shader.UV_GROUP_UV_OFFSET_X_DEFAULT
1387
+ offset_y = shader.UV_GROUP_UV_OFFSET_Y_DEFAULT
1388
+
1389
+ scale_input = uv_offset_scaling_node.inputs.get("Scale")
1390
+ if isinstance(scale_input, shader.VECTOR_SOCKET_CLASSES):
1391
+ scale_x = scale_input.default_value[0]
1392
+ scale_y = scale_input.default_value[1]
1393
+ else:
1394
+ scale_x = shader.UV_GROUP_UV_SCALE_X_DEFAULT
1395
+ scale_y = shader.UV_GROUP_UV_SCALE_Y_DEFAULT
1396
+
1397
+ if (
1398
+ abs(offset_x - shader.UV_GROUP_UV_OFFSET_X_DEFAULT) < float_info.epsilon
1399
+ and abs(offset_y - shader.UV_GROUP_UV_OFFSET_Y_DEFAULT) < float_info.epsilon
1400
+ and abs(scale_x - shader.UV_GROUP_UV_SCALE_X_DEFAULT) < float_info.epsilon
1401
+ and abs(scale_y - shader.UV_GROUP_UV_SCALE_Y_DEFAULT) < float_info.epsilon
1402
+ ):
1403
+ return default
1404
+
1405
+ return (
1406
+ {
1407
+ "offset": [offset_x, offset_y],
1408
+ "scale": [scale_x, scale_y],
1409
+ },
1410
+ (offset_x, offset_y, scale_x, scale_y),
1411
+ )
1412
+
1413
+ def create_mtoon0_texture_info_dict(
1414
+ self,
1415
+ context: Context,
1416
+ texture_dicts: list[dict[str, Json]],
1417
+ sampler_dicts: list[dict[str, Json]],
1418
+ image_dicts: list[dict[str, Json]],
1419
+ buffer_view_dicts: list[dict[str, Json]],
1420
+ extensions_used: list[str],
1421
+ buffer0: bytearray,
1422
+ node: Node,
1423
+ texture_input_name: str,
1424
+ image_name_to_index_dict: dict[str, int],
1425
+ *,
1426
+ use_khr_texture_transform: bool = False,
1427
+ ) -> Optional[tuple[dict[str, Json], int, tuple[float, float, float, float]]]:
1428
+ image_name_and_sampler_type = shader.get_image_name_and_sampler_type(
1429
+ node, texture_input_name
1430
+ )
1431
+ if image_name_and_sampler_type is None:
1432
+ return None
1433
+
1434
+ image_name, wrap_type, filter_type = image_name_and_sampler_type
1435
+ image_index = self.find_or_create_image(
1436
+ image_dicts,
1437
+ buffer_view_dicts,
1438
+ buffer0,
1439
+ image_name_to_index_dict,
1440
+ context.blend_data.images[image_name],
1441
+ )
1442
+
1443
+ sampler_dict: dict[str, Json] = {
1444
+ "magFilter": filter_type,
1445
+ "minFilter": filter_type,
1446
+ "wrapS": wrap_type,
1447
+ "wrapT": wrap_type,
1448
+ }
1449
+ if sampler_dict in sampler_dicts:
1450
+ sampler_index = sampler_dicts.index(sampler_dict)
1451
+ else:
1452
+ sampler_index = len(sampler_dicts)
1453
+ sampler_dicts.append(sampler_dict)
1454
+
1455
+ texture_dict: dict[str, Json] = {
1456
+ "sampler": sampler_index,
1457
+ "source": image_index,
1458
+ }
1459
+
1460
+ if texture_dict in texture_dicts:
1461
+ texture_index = texture_dicts.index(texture_dict)
1462
+ else:
1463
+ texture_index = len(texture_dicts)
1464
+ texture_dicts.append(texture_dict)
1465
+
1466
+ khr_texture_transform_dict, vector_property = (
1467
+ self.create_mtoon0_khr_texture_transform(node, texture_input_name)
1468
+ )
1469
+
1470
+ texture_info: dict[str, Json] = {"index": texture_index}
1471
+
1472
+ if use_khr_texture_transform and khr_texture_transform_dict:
1473
+ texture_info["extensions"] = {
1474
+ "KHR_texture_transform": khr_texture_transform_dict
1475
+ }
1476
+ extensions_used.append("KHR_texture_transform")
1477
+
1478
+ return (
1479
+ texture_info,
1480
+ texture_index,
1481
+ vector_property,
1482
+ )
1483
+
1484
+ def write_legacy_mtoon_unversioned_material(
1485
+ self,
1486
+ _progress: Progress,
1487
+ texture_dicts: list[dict[str, Json]],
1488
+ sampler_dicts: list[dict[str, Json]],
1489
+ image_dicts: list[dict[str, Json]],
1490
+ buffer_view_dicts: list[dict[str, Json]],
1491
+ extensions_used: list[str],
1492
+ buffer0: bytearray,
1493
+ image_name_to_image_index: dict[str, int],
1494
+ material: Material,
1495
+ material_dict: dict[str, Json],
1496
+ vrm_material_property_dict: dict[str, Json],
1497
+ node: ShaderNodeGroup,
1498
+ ) -> None:
1499
+ keyword_map: dict[str, bool] = {}
1500
+ tag_map: dict[str, str] = {}
1501
+ texture_properties: dict[str, int] = {}
1502
+ material_dict |= make_json_dict(
1503
+ {
1504
+ "name": material.name,
1505
+ "extensions": {"KHR_materials_unlit": {}},
1506
+ "doubleSided": not material.use_backface_culling,
1507
+ }
1508
+ )
1509
+ extensions_used.append("KHR_materials_unlit")
1510
+ pbr_metallic_roughness_dict: dict[str, Json] = {
1511
+ "metallicFactor": 0,
1512
+ "roughnessFactor": 0.9,
1513
+ }
1514
+
1515
+ color = shader.get_rgba_value_or(
1516
+ node,
1517
+ "DiffuseColor",
1518
+ 0.0,
1519
+ 1.0,
1520
+ (
1521
+ 1.0,
1522
+ 1.0,
1523
+ 1.0,
1524
+ 1.0,
1525
+ ),
1526
+ )
1527
+ emission_color = shader.get_rgba_value(node, "EmissionColor", 0.0, 1.0)
1528
+ if emission_color is None:
1529
+ emission_color = (0.0, 0.0, 0.0, 1.0)
1530
+ else:
1531
+ material_dict["emissiveFactor"] = make_json(emission_color[:3])
1532
+
1533
+ pbr_metallic_roughness_dict["baseColorFactor"] = make_json(color)
1534
+
1535
+ vector_properties: dict[str, Sequence[float]] = {
1536
+ "_Color": color,
1537
+ "_EmissionColor": emission_color,
1538
+ "_ShadeColor": shader.get_rgba_value_or(node, "ShadeColor", 0.0, 1.0),
1539
+ "_OutlineColor": shader.get_rgba_value_or(node, "OutlineColor", 0.0, 1.0),
1540
+ "_RimColor": shader.get_rgba_value_or(node, "RimColor", 0.0, 1.0),
1541
+ }
1542
+
1543
+ float_properties: dict[str, Union[float, int]] = {
1544
+ "_MToonVersion": MtoonUnversioned.version,
1545
+ "_DebugMode": 0,
1546
+ "_ShadeShift": shader.get_float_value_or(node, "ShadeShift"),
1547
+ "_ShadeToony": shader.get_float_value_or(
1548
+ node, "ShadeToony", default_value=0.5
1549
+ ),
1550
+ "_ShadingGradeRate": shader.get_float_value_or(
1551
+ node, "ShadingGradeRate", default_value=0.5
1552
+ ),
1553
+ "_ReceiveShadowRate": shader.get_float_value_or(
1554
+ node, "ReceiveShadowRate", default_value=0.5
1555
+ ),
1556
+ "_LightColorAttenuation": shader.get_float_value_or(
1557
+ node, "LightColorAttenuation", default_value=0.5
1558
+ ),
1559
+ "_IndirectLightIntensity": shader.get_float_value_or(
1560
+ node, "IndirectLightIntensity", default_value=0.5
1561
+ ),
1562
+ "_RimFresnelPower": shader.get_float_value_or(
1563
+ node, "RimFresnelPower", 0.0, float_info.max, default_value=1.0
1564
+ ),
1565
+ "_RimLift": shader.get_float_value_or(node, "RimLift"),
1566
+ "_RimLightingMix": shader.get_float_value_or(node, "RimLightingMix"),
1567
+ "_OutlineLightingMix": shader.get_float_value_or(
1568
+ node, "OutlineLightingMix"
1569
+ ),
1570
+ "_OutlineScaledMaxDistance": shader.get_float_value_or(
1571
+ node, "OutlineScaleMaxDistance"
1572
+ ),
1573
+ "_OutlineWidth": shader.get_float_value_or(node, "OutlineWidth"),
1574
+ "_UvAnimRotation": shader.get_float_value_or(node, "UV_Scroll_Rotation"),
1575
+ "_UvAnimScrollX": shader.get_float_value_or(node, "UV_Scroll_X"),
1576
+ "_UvAnimScrollY": shader.get_float_value_or(node, "UV_Scroll_Y"),
1577
+ }
1578
+
1579
+ alpha_cutoff = shader.get_float_value(node, "CutoffRate", 0, float_info.max)
1580
+ if alpha_cutoff is not None:
1581
+ float_properties["_Cutoff"] = alpha_cutoff
1582
+
1583
+ outline_width_mode = max(
1584
+ 0, min(2, round(shader.get_float_value_or(node, "OutlineWidthMode")))
1585
+ )
1586
+ float_properties["_OutlineWidthMode"] = outline_width_mode
1587
+ if outline_width_mode == 1:
1588
+ keyword_map["MTOON_OUTLINE_WIDTH_WORLD"] = True
1589
+ elif outline_width_mode == 2:
1590
+ keyword_map["MTOON_OUTLINE_WIDTH_SCREEN"] = True
1591
+
1592
+ outline_color_mode = max(
1593
+ 0, min(2, round(shader.get_float_value_or(node, "OutlineColorMode")))
1594
+ )
1595
+ float_properties["_OutlineColorMode"] = outline_color_mode
1596
+ if outline_width_mode > 0:
1597
+ if outline_color_mode == 1:
1598
+ keyword_map["MTOON_OUTLINE_COLOR_MIXED"] = True
1599
+ elif outline_color_mode == 2:
1600
+ keyword_map["MTOON_OUTLINE_COLOR_FIXED"] = True
1601
+
1602
+ if material.blend_method == "OPAQUE":
1603
+ tag_map["RenderType"] = "Opaque"
1604
+ material_dict["alphaMode"] = "OPAQUE"
1605
+ float_properties["_BlendMode"] = 0
1606
+ float_properties["_SrcBlend"] = 1
1607
+ float_properties["_DstBlend"] = 0
1608
+ float_properties["_ZWrite"] = 1
1609
+ float_properties["_OutlineCullMode"] = 1
1610
+ keyword_map["MTOON_DEBUG_LITSHADERATE"] = False
1611
+ keyword_map["MTOON_DEBUG_NORMAL"] = False
1612
+ keyword_map["_ALPHABLEND_ON"] = False
1613
+ keyword_map["_ALPHAPREMULTIPLY_ON"] = False
1614
+ render_queue = -1
1615
+ elif material.blend_method == "CLIP":
1616
+ tag_map["RenderType"] = "TransparentCutout"
1617
+ material_dict["alphaMode"] = "MASK"
1618
+ float_properties["_BlendMode"] = 1
1619
+ float_properties["_SrcBlend"] = 1
1620
+ float_properties["_DstBlend"] = 0
1621
+ float_properties["_ZWrite"] = 1
1622
+ float_properties["_OutlineCullMode"] = 1
1623
+ keyword_map["MTOON_DEBUG_LITSHADERATE"] = False
1624
+ keyword_map["MTOON_DEBUG_NORMAL"] = False
1625
+ keyword_map["_ALPHABLEND_ON"] = False
1626
+ keyword_map["_ALPHAPREMULTIPLY_ON"] = False
1627
+ keyword_map["_ALPHATEST_ON"] = True
1628
+ if alpha_cutoff is not None:
1629
+ material_dict["alphaCutoff"] = alpha_cutoff
1630
+ render_queue = 2450
1631
+ else:
1632
+ tag_map["RenderType"] = "Transparent"
1633
+ material_dict["alphaMode"] = "BLEND"
1634
+ float_properties["_SrcBlend"] = 5
1635
+ float_properties["_DstBlend"] = 10
1636
+ float_properties["_OutlineCullMode"] = 1
1637
+ keyword_map["MTOON_DEBUG_LITSHADERATE"] = False
1638
+ keyword_map["MTOON_DEBUG_NORMAL"] = False
1639
+ keyword_map["_ALPHABLEND_ON"] = True
1640
+ keyword_map["_ALPHAPREMULTIPLY_ON"] = False
1641
+ transparent_with_z_write = shader.get_float_value_or(
1642
+ node, "TransparentWithZWrite", 0
1643
+ )
1644
+ if transparent_with_z_write < float_info.epsilon:
1645
+ float_properties["_BlendMode"] = 2
1646
+ float_properties["_ZWrite"] = 0
1647
+ render_queue = 3000
1648
+ else:
1649
+ float_properties["_BlendMode"] = 3
1650
+ float_properties["_ZWrite"] = 1
1651
+ render_queue = 2501
1652
+
1653
+ float_properties["_CullMode"] = 2 if material.use_backface_culling else 0
1654
+
1655
+ # Control texture addition order for compatibility with old exporter
1656
+
1657
+ main_tex = self.create_mtoon0_texture_info_dict(
1658
+ self._context,
1659
+ texture_dicts,
1660
+ sampler_dicts,
1661
+ image_dicts,
1662
+ buffer_view_dicts,
1663
+ extensions_used,
1664
+ buffer0,
1665
+ node,
1666
+ "MainTexture",
1667
+ image_name_to_image_index,
1668
+ use_khr_texture_transform=True,
1669
+ )
1670
+ if main_tex:
1671
+ (
1672
+ base_color_texture_dict,
1673
+ main_tex_texture_property,
1674
+ main_tex_vector_property,
1675
+ ) = main_tex
1676
+ pbr_metallic_roughness_dict["baseColorTexture"] = base_color_texture_dict
1677
+ texture_properties["_MainTex"] = main_tex_texture_property
1678
+ vector_properties["_MainTex"] = main_tex_vector_property
1679
+
1680
+ # TODO: For compatibility. There might be correct configuration values
1681
+ default_texture_vector_property = [0, 0, 1, 1]
1682
+
1683
+ shade_texture = self.create_mtoon0_texture_info_dict(
1684
+ self._context,
1685
+ texture_dicts,
1686
+ sampler_dicts,
1687
+ image_dicts,
1688
+ buffer_view_dicts,
1689
+ extensions_used,
1690
+ buffer0,
1691
+ node,
1692
+ "ShadeTexture",
1693
+ image_name_to_image_index,
1694
+ )
1695
+ if shade_texture:
1696
+ _, shade_texture_texture_property, _ = shade_texture
1697
+ texture_properties["_ShadeTexture"] = shade_texture_texture_property
1698
+ vector_properties["_ShadeTexture"] = default_texture_vector_property
1699
+
1700
+ bump_scale = shader.get_float_value_or(node, "BumpScale", default_value=0.5)
1701
+ float_properties["_BumpScale"] = bump_scale
1702
+
1703
+ bump_map = self.create_mtoon0_texture_info_dict(
1704
+ self._context,
1705
+ texture_dicts,
1706
+ sampler_dicts,
1707
+ image_dicts,
1708
+ buffer_view_dicts,
1709
+ extensions_used,
1710
+ buffer0,
1711
+ node,
1712
+ "NormalmapTexture",
1713
+ image_name_to_image_index,
1714
+ use_khr_texture_transform=True,
1715
+ )
1716
+ if not bump_map:
1717
+ bump_map = self.create_mtoon0_texture_info_dict(
1718
+ self._context,
1719
+ texture_dicts,
1720
+ sampler_dicts,
1721
+ image_dicts,
1722
+ buffer_view_dicts,
1723
+ extensions_used,
1724
+ buffer0,
1725
+ node,
1726
+ "NomalmapTexture",
1727
+ image_name_to_image_index,
1728
+ use_khr_texture_transform=True,
1729
+ )
1730
+ if bump_map:
1731
+ normal_texture_info, bump_map_texture_property, _ = bump_map
1732
+ material_dict["normalTexture"] = normal_texture_info
1733
+ texture_properties["_BumpMap"] = bump_map_texture_property
1734
+ vector_properties["_BumpMap"] = default_texture_vector_property
1735
+ keyword_map["_NORMALMAP"] = True
1736
+ normal_texture_info["scale"] = bump_scale
1737
+
1738
+ for socket_name, texture_property_key in {
1739
+ "ReceiveShadow_Texture": "_ReceiveShadowTexture",
1740
+ "ShadingGradeTexture": "_ShadingGradeTexture",
1741
+ }.items():
1742
+ texture = self.create_mtoon0_texture_info_dict(
1743
+ self._context,
1744
+ texture_dicts,
1745
+ sampler_dicts,
1746
+ image_dicts,
1747
+ buffer_view_dicts,
1748
+ extensions_used,
1749
+ buffer0,
1750
+ node,
1751
+ socket_name,
1752
+ image_name_to_image_index,
1753
+ )
1754
+ if not texture:
1755
+ continue
1756
+ _, texture_property, _ = texture
1757
+ texture_properties[texture_property_key] = texture_property
1758
+ vector_properties[texture_property_key] = default_texture_vector_property
1759
+
1760
+ emission_map = self.create_mtoon0_texture_info_dict(
1761
+ self._context,
1762
+ texture_dicts,
1763
+ sampler_dicts,
1764
+ image_dicts,
1765
+ buffer_view_dicts,
1766
+ extensions_used,
1767
+ buffer0,
1768
+ node,
1769
+ "Emission_Texture",
1770
+ image_name_to_image_index,
1771
+ use_khr_texture_transform=True,
1772
+ )
1773
+ if emission_map:
1774
+ emissive_texture_info, emission_map_texture_property, _ = emission_map
1775
+ material_dict["emissiveTexture"] = emissive_texture_info
1776
+ texture_properties["_EmissionMap"] = emission_map_texture_property
1777
+ vector_properties["_EmissionMap"] = default_texture_vector_property
1778
+
1779
+ for socket_name, texture_property_key in {
1780
+ "SphereAddTexture": "_SphereAdd",
1781
+ "RimTexture": "_RimTexture",
1782
+ "OutlineWidthTexture": "_OutlineWidthTexture",
1783
+ "UV_Animation_Mask_Texture": "_UvAnimMaskTexture",
1784
+ }.items():
1785
+ texture = self.create_mtoon0_texture_info_dict(
1786
+ self._context,
1787
+ texture_dicts,
1788
+ sampler_dicts,
1789
+ image_dicts,
1790
+ buffer_view_dicts,
1791
+ extensions_used,
1792
+ buffer0,
1793
+ node,
1794
+ socket_name,
1795
+ image_name_to_image_index,
1796
+ )
1797
+ if not texture:
1798
+ continue
1799
+ _, texture_property, _ = texture
1800
+ texture_properties[texture_property_key] = texture_property
1801
+ vector_properties[texture_property_key] = default_texture_vector_property
1802
+
1803
+ if pbr_metallic_roughness_dict:
1804
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
1805
+
1806
+ vrm_material_property_dict.update(
1807
+ {
1808
+ "name": material.name,
1809
+ "shader": "VRM/MToon",
1810
+ "renderQueue": render_queue,
1811
+ "keywordMap": make_json(keyword_map),
1812
+ "tagMap": make_json(tag_map),
1813
+ "floatProperties": make_json(float_properties),
1814
+ "vectorProperties": make_json(vector_properties),
1815
+ "textureProperties": make_json(texture_properties),
1816
+ }
1817
+ )
1818
+
1819
+ def write_legacy_gltf_material(
1820
+ self,
1821
+ _progress: Progress,
1822
+ texture_dicts: list[dict[str, Json]],
1823
+ sampler_dicts: list[dict[str, Json]],
1824
+ image_dicts: list[dict[str, Json]],
1825
+ buffer_view_dicts: list[dict[str, Json]],
1826
+ extensions_used: list[str],
1827
+ buffer0: bytearray,
1828
+ image_name_to_image_index: dict[str, int],
1829
+ material: Material,
1830
+ material_dict: dict[str, Json],
1831
+ vrm_material_property_dict: dict[str, Json],
1832
+ node: ShaderNodeGroup,
1833
+ ) -> None:
1834
+ vrm_material_property_dict |= make_json_dict(
1835
+ {
1836
+ "name": material.name,
1837
+ "shader": "VRM_USE_GLTFSHADER",
1838
+ "keywordMap": {},
1839
+ "tagMap": {},
1840
+ "floatProperties": {},
1841
+ "vectorProperties": {},
1842
+ "textureProperties": {},
1843
+ "extras": {"VRM_Addon_for_Blender_legacy_gltf_material": {}},
1844
+ }
1845
+ )
1846
+
1847
+ pbr_metallic_roughness_dict: dict[str, Json] = {
1848
+ "baseColorFactor": list(
1849
+ shader.get_rgba_value_or(
1850
+ node, "base_Color", 0.0, 1.0, default_value=(1, 1, 1, 1)
1851
+ )
1852
+ ),
1853
+ "metallicFactor": shader.get_float_value_or(node, "metallic", 0.0, 1.0),
1854
+ "roughnessFactor": shader.get_float_value_or(node, "roughness", 0.0, 1.0),
1855
+ }
1856
+
1857
+ material_dict |= make_json_dict(
1858
+ {
1859
+ "name": material.name,
1860
+ "emissiveFactor": list(
1861
+ shader.get_rgb_value_or(node, "emissive_color", 0.0, 1.0)
1862
+ ),
1863
+ "doubleSided": not material.use_backface_culling,
1864
+ }
1865
+ )
1866
+
1867
+ if material.blend_method == "OPAQUE":
1868
+ material_dict["alphaMode"] = "OPAQUE"
1869
+ elif material.blend_method == "CLIP":
1870
+ material_dict["alphaMode"] = "MASK"
1871
+ material_dict["alphaCutoff"] = material.alpha_threshold
1872
+ else:
1873
+ material_dict["alphaMode"] = "BLEND"
1874
+
1875
+ normal_texture = self.create_mtoon0_texture_info_dict(
1876
+ self._context,
1877
+ texture_dicts,
1878
+ sampler_dicts,
1879
+ image_dicts,
1880
+ buffer_view_dicts,
1881
+ extensions_used,
1882
+ buffer0,
1883
+ node,
1884
+ "normal",
1885
+ image_name_to_image_index,
1886
+ )
1887
+ if normal_texture:
1888
+ (normal_texture_dict, _, _) = normal_texture
1889
+ material_dict["normalTexture"] = normal_texture_dict
1890
+
1891
+ emissive_texture = self.create_mtoon0_texture_info_dict(
1892
+ self._context,
1893
+ texture_dicts,
1894
+ sampler_dicts,
1895
+ image_dicts,
1896
+ buffer_view_dicts,
1897
+ extensions_used,
1898
+ buffer0,
1899
+ node,
1900
+ "emissive_texture",
1901
+ image_name_to_image_index,
1902
+ )
1903
+ if emissive_texture:
1904
+ (emissive_texture_dict, _, _) = emissive_texture
1905
+ material_dict["emissiveTexture"] = emissive_texture_dict
1906
+
1907
+ base_color_texture = self.create_mtoon0_texture_info_dict(
1908
+ self._context,
1909
+ texture_dicts,
1910
+ sampler_dicts,
1911
+ image_dicts,
1912
+ buffer_view_dicts,
1913
+ extensions_used,
1914
+ buffer0,
1915
+ node,
1916
+ "color_texture",
1917
+ image_name_to_image_index,
1918
+ )
1919
+ if base_color_texture:
1920
+ (base_color_texture_dict, _, _) = base_color_texture
1921
+ pbr_metallic_roughness_dict["baseColorTexture"] = base_color_texture_dict
1922
+
1923
+ metallic_roughness_texture = self.create_mtoon0_texture_info_dict(
1924
+ self._context,
1925
+ texture_dicts,
1926
+ sampler_dicts,
1927
+ image_dicts,
1928
+ buffer_view_dicts,
1929
+ extensions_used,
1930
+ buffer0,
1931
+ node,
1932
+ "metallic_roughness_texture",
1933
+ image_name_to_image_index,
1934
+ )
1935
+ if metallic_roughness_texture:
1936
+ (metallic_roughness_texture_dict, _, _) = metallic_roughness_texture
1937
+ pbr_metallic_roughness_dict["metallicRoughnessTexture"] = (
1938
+ metallic_roughness_texture_dict
1939
+ )
1940
+
1941
+ occlusion_texture = self.create_mtoon0_texture_info_dict(
1942
+ self._context,
1943
+ texture_dicts,
1944
+ sampler_dicts,
1945
+ image_dicts,
1946
+ buffer_view_dicts,
1947
+ extensions_used,
1948
+ buffer0,
1949
+ node,
1950
+ "occlusion_texture",
1951
+ image_name_to_image_index,
1952
+ )
1953
+ if occlusion_texture:
1954
+ (occlusion_texture_dict, _, _) = occlusion_texture
1955
+ pbr_metallic_roughness_dict["occlusionTexture"] = occlusion_texture_dict
1956
+
1957
+ if pbr_metallic_roughness_dict:
1958
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
1959
+
1960
+ if shader.get_float_value_or(node, "unlit") > 0.5:
1961
+ material_dict["extensions"] = {"KHR_materials_unlit": {}}
1962
+ extensions_used.append("KHR_materials_unlit")
1963
+
1964
+ def write_legacy_transparent_zwrite_material(
1965
+ self,
1966
+ _progress: Progress,
1967
+ texture_dicts: list[dict[str, Json]],
1968
+ sampler_dicts: list[dict[str, Json]],
1969
+ image_dicts: list[dict[str, Json]],
1970
+ buffer_view_dicts: list[dict[str, Json]],
1971
+ extensions_used: list[str],
1972
+ buffer0: bytearray,
1973
+ image_name_to_image_index: dict[str, int],
1974
+ material: Material,
1975
+ material_dict: dict[str, Json],
1976
+ vrm_material_property_dict: dict[str, Json],
1977
+ node: ShaderNodeGroup,
1978
+ ) -> None:
1979
+ vector_properties: dict[str, Sequence[float]] = {}
1980
+ texture_properties: dict[str, int] = {}
1981
+
1982
+ vrm_material_property_dict |= make_json_dict(
1983
+ {
1984
+ "name": material.name,
1985
+ "shader": "VRM/UnlitTransparentZWrite",
1986
+ "renderQueue": 2600,
1987
+ "keywordMap": {},
1988
+ "tagMap": {"RenderType": "Transparent"},
1989
+ "floatProperties": {},
1990
+ }
1991
+ )
1992
+
1993
+ pbr_metallic_roughness_dict: dict[str, Json] = {
1994
+ "baseColorFactor": [1, 1, 1, 1],
1995
+ "metallicFactor": 0,
1996
+ "roughnessFactor": 0.9,
1997
+ }
1998
+
1999
+ material_dict |= make_json_dict(
2000
+ {
2001
+ "name": material.name,
2002
+ "alphaMode": "BLEND",
2003
+ "doubleSided": False,
2004
+ "extensions": {"KHR_materials_unlit": {}},
2005
+ }
2006
+ )
2007
+
2008
+ if pbr_metallic_roughness_dict:
2009
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
2010
+
2011
+ main_tex = self.create_mtoon0_texture_info_dict(
2012
+ self._context,
2013
+ texture_dicts,
2014
+ sampler_dicts,
2015
+ image_dicts,
2016
+ buffer_view_dicts,
2017
+ extensions_used,
2018
+ buffer0,
2019
+ node,
2020
+ "Main_Texture",
2021
+ image_name_to_image_index,
2022
+ )
2023
+ if main_tex:
2024
+ (
2025
+ base_color_texture_dict,
2026
+ main_tex_texture_property,
2027
+ main_tex_vector_property,
2028
+ ) = main_tex
2029
+ pbr_metallic_roughness_dict["baseColorTexture"] = base_color_texture_dict
2030
+ texture_properties["_MainTex"] = main_tex_texture_property
2031
+ vector_properties["_MainTex"] = main_tex_vector_property
2032
+
2033
+ vrm_material_property_dict["vectorProperties"] = make_json(vector_properties)
2034
+ vrm_material_property_dict["textureProperties"] = make_json(texture_properties)
2035
+
2036
+ def write_mmd_material(
2037
+ self,
2038
+ _progress: Progress,
2039
+ texture_dicts: list[dict[str, Json]],
2040
+ sampler_dicts: list[dict[str, Json]],
2041
+ image_dicts: list[dict[str, Json]],
2042
+ buffer_view_dicts: list[dict[str, Json]],
2043
+ _extensions_used: list[str],
2044
+ buffer0: bytearray,
2045
+ image_name_to_image_index: dict[str, int],
2046
+ material: Material,
2047
+ material_dict: dict[str, Json],
2048
+ vrm_material_property_dict: dict[str, Json],
2049
+ mmd_material: MmdMaterial,
2050
+ ) -> None:
2051
+ material_dict |= make_json_dict(
2052
+ {
2053
+ "name": material.name,
2054
+ }
2055
+ )
2056
+ if mmd_material.double_sided:
2057
+ material_dict["doubleSided"] = True
2058
+ if mmd_material.alpha < 1.0:
2059
+ material_dict["alphaMode"] = "BLEND"
2060
+
2061
+ if texture_image := mmd_material.texture:
2062
+ pbr_metallic_roughness_dict: dict[str, Json] = {
2063
+ "baseColorFactor": [0, 0, 0, mmd_material.alpha],
2064
+ }
2065
+ image_index = self.find_or_create_image(
2066
+ image_dicts,
2067
+ buffer_view_dicts,
2068
+ buffer0,
2069
+ image_name_to_image_index,
2070
+ texture_image,
2071
+ )
2072
+
2073
+ sampler_dict: dict[str, Json] = {
2074
+ "magFilter": GL_LINEAR,
2075
+ "minFilter": GL_LINEAR,
2076
+ "wrapS": GL_REPEAT,
2077
+ "wrapT": GL_REPEAT,
2078
+ }
2079
+ if sampler_dict in sampler_dicts:
2080
+ sampler_index = sampler_dicts.index(sampler_dict)
2081
+ else:
2082
+ sampler_index = len(sampler_dicts)
2083
+ sampler_dicts.append(sampler_dict)
2084
+
2085
+ texture_dict: dict[str, Json] = {
2086
+ "sampler": sampler_index,
2087
+ "source": image_index,
2088
+ }
2089
+
2090
+ if texture_dict in texture_dicts:
2091
+ texture_index = texture_dicts.index(texture_dict)
2092
+ else:
2093
+ texture_index = len(texture_dicts)
2094
+ texture_dicts.append(texture_dict)
2095
+
2096
+ texture_info: dict[str, Json] = {"index": texture_index}
2097
+ material_dict["emissiveTexture"] = texture_info
2098
+ material_dict["emissiveFactor"] = [1.0, 1.0, 1.0]
2099
+ else:
2100
+ pbr_metallic_roughness_dict = {
2101
+ "baseColorFactor": [*mmd_material.diffuse_color, mmd_material.alpha]
2102
+ }
2103
+
2104
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
2105
+
2106
+ vrm_material_property_dict |= make_json_dict(
2107
+ {
2108
+ "name": material.name,
2109
+ "shader": "VRM_USE_GLTFSHADER",
2110
+ "keywordMap": {},
2111
+ "tagMap": {},
2112
+ "floatProperties": {},
2113
+ "vectorProperties": {},
2114
+ "textureProperties": {},
2115
+ }
2116
+ )
2117
+
2118
+ def write_gltf_material(
2119
+ self,
2120
+ _progress: Progress,
2121
+ texture_dicts: list[dict[str, Json]],
2122
+ sampler_dicts: list[dict[str, Json]],
2123
+ image_dicts: list[dict[str, Json]],
2124
+ buffer_view_dicts: list[dict[str, Json]],
2125
+ extensions_used: list[str],
2126
+ buffer0: bytearray,
2127
+ gltf2_io_texture_images: list[Gltf2IoTextureImage],
2128
+ material: Material,
2129
+ material_dict: dict[str, Json],
2130
+ vrm_material_property_dict: dict[str, Json],
2131
+ ) -> None:
2132
+ vrm_material_property_dict.update(
2133
+ {
2134
+ "name": material.name,
2135
+ "shader": "VRM_USE_GLTFSHADER",
2136
+ "keywordMap": {},
2137
+ "tagMap": {},
2138
+ "floatProperties": {},
2139
+ "vectorProperties": {},
2140
+ "textureProperties": {},
2141
+ }
2142
+ )
2143
+
2144
+ try:
2145
+ gltf2_io_material = gather_gltf2_io_material(
2146
+ material, self._gltf2_addon_export_settings
2147
+ )
2148
+ if not gltf2_io_material:
2149
+ return
2150
+
2151
+ alpha_cutoff = convert.float_or_none(
2152
+ getattr(gltf2_io_material, "alpha_cutoff", None)
2153
+ )
2154
+ if alpha_cutoff is not None:
2155
+ material_dict["alphaCutoff"] = alpha_cutoff
2156
+
2157
+ alpha_mode = getattr(gltf2_io_material, "alpha_mode", None)
2158
+ if isinstance(alpha_mode, str):
2159
+ material_dict["alphaMode"] = alpha_mode
2160
+
2161
+ double_sided = getattr(gltf2_io_material, "double_sided", None)
2162
+ if isinstance(double_sided, bool):
2163
+ material_dict["doubleSided"] = double_sided
2164
+
2165
+ emissive_factor = convert.sequence_or_none(
2166
+ getattr(gltf2_io_material, "emissive_factor", None)
2167
+ )
2168
+ if emissive_factor is not None:
2169
+ material_dict["emissiveFactor"] = make_json(emissive_factor)
2170
+
2171
+ assign_dict(
2172
+ material_dict,
2173
+ "emissiveTexture",
2174
+ self.create_gltf2_io_texture(
2175
+ getattr(gltf2_io_material, "emissive_texture", None),
2176
+ texture_dicts,
2177
+ sampler_dicts,
2178
+ image_dicts,
2179
+ buffer_view_dicts,
2180
+ buffer0,
2181
+ gltf2_io_texture_images,
2182
+ ),
2183
+ )
2184
+
2185
+ extensions = convert.mapping_or_none(
2186
+ getattr(gltf2_io_material, "extensions", None)
2187
+ )
2188
+ if extensions is not None:
2189
+ extensions_dict: dict[str, Json] = {}
2190
+
2191
+ # https://github.com/KhronosGroup/glTF/tree/19a1d820040239bca1327fc26220ae8cae9f948c/extensions/2.0/Khronos/KHR_materials_unlit
2192
+ if extensions.get("KHR_materials_unlit") is not None:
2193
+ extensions_dict["KHR_materials_unlit"] = {}
2194
+ extensions_used.append("KHR_materials_unlit")
2195
+
2196
+ # https://github.com/KhronosGroup/glTF/blob/9c4a3567384b4d9f2706cdd9623bbb5ca7b341ad/extensions/2.0/Khronos/KHR_materials_emissive_strength
2197
+ khr_materials_emissive_strength = convert.mapping_or_none(
2198
+ getattr(
2199
+ extensions.get("KHR_materials_emissive_strength"),
2200
+ "extension",
2201
+ None,
2202
+ )
2203
+ )
2204
+ if isinstance(khr_materials_emissive_strength, dict):
2205
+ emissive_strength = convert.float_or_none(
2206
+ khr_materials_emissive_strength.get("emissiveStrength")
2207
+ )
2208
+ if (
2209
+ emissive_strength is not None
2210
+ and emissive_strength >= 0
2211
+ and emissive_strength != 1.0
2212
+ ):
2213
+ extensions_dict["KHR_materials_emissive_strength"] = {
2214
+ "emissiveStrength": emissive_strength,
2215
+ }
2216
+ extensions_used.append("KHR_materials_emissive_strength")
2217
+
2218
+ if extensions_dict:
2219
+ material_dict["extensions"] = extensions_dict
2220
+
2221
+ assign_dict(
2222
+ material_dict,
2223
+ "normalTexture",
2224
+ self.create_gltf2_io_texture(
2225
+ getattr(gltf2_io_material, "normal_texture", None),
2226
+ texture_dicts,
2227
+ sampler_dicts,
2228
+ image_dicts,
2229
+ buffer_view_dicts,
2230
+ buffer0,
2231
+ gltf2_io_texture_images,
2232
+ ),
2233
+ )
2234
+
2235
+ assign_dict(
2236
+ material_dict,
2237
+ "occlusionTexture",
2238
+ self.create_gltf2_io_texture(
2239
+ getattr(gltf2_io_material, "occlusion_texture", None),
2240
+ texture_dicts,
2241
+ sampler_dicts,
2242
+ image_dicts,
2243
+ buffer_view_dicts,
2244
+ buffer0,
2245
+ gltf2_io_texture_images,
2246
+ ),
2247
+ )
2248
+
2249
+ pbr_metallic_roughness = getattr(
2250
+ gltf2_io_material, "pbr_metallic_roughness", None
2251
+ )
2252
+ if pbr_metallic_roughness is not None:
2253
+ pbr_metallic_roughness_dict: dict[str, Json] = {}
2254
+
2255
+ base_color_factor = convert.sequence_or_none(
2256
+ getattr(pbr_metallic_roughness, "base_color_factor", None)
2257
+ )
2258
+ if base_color_factor is not None:
2259
+ pbr_metallic_roughness_dict["baseColorFactor"] = make_json(
2260
+ base_color_factor
2261
+ )
2262
+
2263
+ assign_dict(
2264
+ pbr_metallic_roughness_dict,
2265
+ "baseColorTexture",
2266
+ self.create_gltf2_io_texture(
2267
+ getattr(pbr_metallic_roughness, "base_color_texture", None),
2268
+ texture_dicts,
2269
+ sampler_dicts,
2270
+ image_dicts,
2271
+ buffer_view_dicts,
2272
+ buffer0,
2273
+ gltf2_io_texture_images,
2274
+ ),
2275
+ )
2276
+
2277
+ metallic_factor = convert.float_or_none(
2278
+ getattr(pbr_metallic_roughness, "metallic_factor", None)
2279
+ )
2280
+ if metallic_factor is not None:
2281
+ pbr_metallic_roughness_dict["metallicFactor"] = metallic_factor
2282
+
2283
+ assign_dict(
2284
+ pbr_metallic_roughness_dict,
2285
+ "metallicRoughnessTexture",
2286
+ self.create_gltf2_io_texture(
2287
+ getattr(
2288
+ pbr_metallic_roughness, "metallic_roughness_texture", None
2289
+ ),
2290
+ texture_dicts,
2291
+ sampler_dicts,
2292
+ image_dicts,
2293
+ buffer_view_dicts,
2294
+ buffer0,
2295
+ gltf2_io_texture_images,
2296
+ ),
2297
+ )
2298
+
2299
+ roughness_factor = convert.float_or_none(
2300
+ getattr(pbr_metallic_roughness, "roughness_factor", None)
2301
+ )
2302
+ if roughness_factor is not None:
2303
+ pbr_metallic_roughness_dict["roughnessFactor"] = roughness_factor
2304
+
2305
+ if pbr_metallic_roughness_dict:
2306
+ material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
2307
+ except Exception:
2308
+ _logger.exception("Failed to generate glTF Material using glTF 2.0 add-on")
2309
+
2310
+ def write_material(
2311
+ self,
2312
+ progress: Progress,
2313
+ material_dicts: list[dict[str, Json]],
2314
+ texture_dicts: list[dict[str, Json]],
2315
+ sampler_dicts: list[dict[str, Json]],
2316
+ image_dicts: list[dict[str, Json]],
2317
+ buffer_view_dicts: list[dict[str, Json]],
2318
+ extensions_vrm_material_property_dicts: list[Json],
2319
+ extensions_used: list[str],
2320
+ buffer0: bytearray,
2321
+ material_name_to_material_index: dict[str, int],
2322
+ image_name_to_image_index: dict[str, int],
2323
+ gltf2_io_texture_images: list[Gltf2IoTextureImage],
2324
+ material: Material,
2325
+ ) -> None:
2326
+ material_dict: dict[str, Json] = {
2327
+ "name": material.name,
2328
+ }
2329
+ vrm_material_property_dict: dict[str, Json] = {
2330
+ "name": material.name,
2331
+ "shader": "VRM_USE_GLTFSHADER",
2332
+ }
2333
+ material_index = len(material_dicts)
2334
+ material_name_to_material_index[material.name] = material_index
2335
+ material_dicts.append(material_dict)
2336
+ extensions_vrm_material_property_dicts.append(vrm_material_property_dict)
2337
+
2338
+ mtoon1 = get_material_extension(material).mtoon1
2339
+ if mtoon1.enabled:
2340
+ self.write_mtoon1_downgraded_material(
2341
+ progress,
2342
+ texture_dicts,
2343
+ sampler_dicts,
2344
+ image_dicts,
2345
+ buffer_view_dicts,
2346
+ extensions_used,
2347
+ buffer0,
2348
+ image_name_to_image_index,
2349
+ material,
2350
+ material_dict,
2351
+ vrm_material_property_dict,
2352
+ )
2353
+ return
2354
+
2355
+ if not (legacy_addon_material := LegacyAddonMaterial.try_parse(material)):
2356
+ pass
2357
+ elif legacy_addon_material.shader_name == "MToon_unversioned":
2358
+ self.write_legacy_mtoon_unversioned_material(
2359
+ progress,
2360
+ texture_dicts,
2361
+ sampler_dicts,
2362
+ image_dicts,
2363
+ buffer_view_dicts,
2364
+ extensions_used,
2365
+ buffer0,
2366
+ image_name_to_image_index,
2367
+ material,
2368
+ material_dict,
2369
+ vrm_material_property_dict,
2370
+ legacy_addon_material.shader_node_group,
2371
+ )
2372
+ return
2373
+ elif legacy_addon_material.shader_name == "GLTF":
2374
+ self.write_legacy_gltf_material(
2375
+ progress,
2376
+ texture_dicts,
2377
+ sampler_dicts,
2378
+ image_dicts,
2379
+ buffer_view_dicts,
2380
+ extensions_used,
2381
+ buffer0,
2382
+ image_name_to_image_index,
2383
+ material,
2384
+ material_dict,
2385
+ vrm_material_property_dict,
2386
+ legacy_addon_material.shader_node_group,
2387
+ )
2388
+ return
2389
+ elif legacy_addon_material.shader_name == "TRANSPARENT_ZWRITE":
2390
+ self.write_legacy_transparent_zwrite_material(
2391
+ progress,
2392
+ texture_dicts,
2393
+ sampler_dicts,
2394
+ image_dicts,
2395
+ buffer_view_dicts,
2396
+ extensions_used,
2397
+ buffer0,
2398
+ image_name_to_image_index,
2399
+ material,
2400
+ material_dict,
2401
+ vrm_material_property_dict,
2402
+ legacy_addon_material.shader_node_group,
2403
+ )
2404
+ return
2405
+
2406
+ mmd_material = MmdMaterial.try_parse(material)
2407
+ if mmd_material:
2408
+ self.write_mmd_material(
2409
+ progress,
2410
+ texture_dicts,
2411
+ sampler_dicts,
2412
+ image_dicts,
2413
+ buffer_view_dicts,
2414
+ extensions_used,
2415
+ buffer0,
2416
+ image_name_to_image_index,
2417
+ material,
2418
+ material_dict,
2419
+ vrm_material_property_dict,
2420
+ mmd_material,
2421
+ )
2422
+ return
2423
+
2424
+ self.write_gltf_material(
2425
+ progress,
2426
+ texture_dicts,
2427
+ sampler_dicts,
2428
+ image_dicts,
2429
+ buffer_view_dicts,
2430
+ extensions_used,
2431
+ buffer0,
2432
+ gltf2_io_texture_images,
2433
+ material,
2434
+ material_dict,
2435
+ vrm_material_property_dict,
2436
+ )
2437
+
2438
+ def write_materials(
2439
+ self,
2440
+ _progress: Progress,
2441
+ material_dicts: list[dict[str, Json]],
2442
+ texture_dicts: list[dict[str, Json]],
2443
+ sampler_dicts: list[dict[str, Json]],
2444
+ image_dicts: list[dict[str, Json]],
2445
+ buffer_view_dicts: list[dict[str, Json]],
2446
+ extensions_vrm_material_property_dicts: list[Json],
2447
+ extensions_used: list[str],
2448
+ buffer0: bytearray,
2449
+ material_name_to_material_index: dict[str, int],
2450
+ image_name_to_image_index: dict[str, int],
2451
+ ) -> None:
2452
+ gltf2_io_texture_images: list[Vrm0Exporter.Gltf2IoTextureImage] = []
2453
+ for material in search.export_materials(self._context, self._export_objects):
2454
+ self.write_material(
2455
+ _progress,
2456
+ material_dicts,
2457
+ texture_dicts,
2458
+ sampler_dicts,
2459
+ image_dicts,
2460
+ buffer_view_dicts,
2461
+ extensions_vrm_material_property_dicts,
2462
+ extensions_used,
2463
+ buffer0,
2464
+ material_name_to_material_index,
2465
+ image_name_to_image_index,
2466
+ gltf2_io_texture_images,
2467
+ material,
2468
+ )
2469
+
2470
+ def write_armature(
2471
+ self,
2472
+ progress: Progress,
2473
+ node_dicts: list[dict[str, Json]],
2474
+ accessor_dicts: list[dict[str, Json]],
2475
+ buffer_view_dicts: list[dict[str, Json]],
2476
+ buffer0: bytearray,
2477
+ bone_name_to_node_index: dict[str, int],
2478
+ ) -> tuple[Sequence[int], Mapping[str, Json], Sequence[int]]:
2479
+ bones = [bone for bone in self._armature.pose.bones if not bone.parent]
2480
+ root_node_index = len(node_dicts)
2481
+ if not bones:
2482
+ _logger.error("No bones")
2483
+ node_dicts.append(
2484
+ {
2485
+ "name": self._armature.name,
2486
+ }
2487
+ )
2488
+ return [root_node_index], {}, []
2489
+
2490
+ bone_name_to_inverse_bind_matrix: dict[str, Matrix] = {}
2491
+ humanoid_root_bone_index: Optional[int] = None
2492
+ humanoid_root_bone: Optional[PoseBone] = None
2493
+
2494
+ if len(bones) == 1:
2495
+ humanoid_root_bone = bones[0]
2496
+ humanoid_root_bone_index = self.write_armature_bone_nodes(
2497
+ progress,
2498
+ node_dicts,
2499
+ buffer0,
2500
+ bones[0],
2501
+ bone_name_to_node_index,
2502
+ bone_name_to_inverse_bind_matrix,
2503
+ )
2504
+ scene_node_indices: list[int] = [humanoid_root_bone_index]
2505
+ else:
2506
+ # If there are multiple root bones, expand each to the scene
2507
+ # This follows the old exporter specification, but I'm not
2508
+ # confident it's correct
2509
+ # It might be better to create a parent bone and attach skin to it
2510
+ scene_node_indices = []
2511
+ for bone in bones:
2512
+ root_node_index = self.write_armature_bone_nodes(
2513
+ progress,
2514
+ node_dicts,
2515
+ buffer0,
2516
+ bone,
2517
+ bone_name_to_node_index,
2518
+ bone_name_to_inverse_bind_matrix,
2519
+ )
2520
+ scene_node_indices.append(root_node_index)
2521
+
2522
+ # Check if there are bones belonging to Humanoid,
2523
+ # and set them as humanoid_root_bone_index
2524
+ humanoid = get_armature_extension(self.armature_data).vrm0.humanoid
2525
+ traversing_bones = [bone]
2526
+ while traversing_bones:
2527
+ traversing_bone = traversing_bones.pop()
2528
+ if any(
2529
+ human_bone.node.bone_name == traversing_bone.name
2530
+ for human_bone in humanoid.human_bones
2531
+ ):
2532
+ humanoid_root_bone = bone
2533
+ humanoid_root_bone_index = root_node_index
2534
+ break
2535
+ traversing_bones.extend(traversing_bone.children)
2536
+
2537
+ if humanoid_root_bone is None or humanoid_root_bone_index is None:
2538
+ _logger.error("No human bone")
2539
+ return [root_node_index], {}, []
2540
+
2541
+ while len(buffer0) % 4:
2542
+ buffer0.append(0)
2543
+ inverse_bind_matrices_offset = len(buffer0)
2544
+
2545
+ skin_joint_node_indices: list[int] = []
2546
+ inverse_bind_matrix_struct = struct.Struct("<16f")
2547
+ for bone_name, node_index in bone_name_to_node_index.items():
2548
+ skin_joint_node_indices.append(node_index)
2549
+ inverse_bind_matrix = bone_name_to_inverse_bind_matrix.get(bone_name)
2550
+ if inverse_bind_matrix is None:
2551
+ message = f"No inverse bind matrix for {bone_name}"
2552
+ raise AssertionError(message)
2553
+ buffer0.extend(
2554
+ inverse_bind_matrix_struct.pack(*itertools.chain(*inverse_bind_matrix))
2555
+ )
2556
+
2557
+ buffer_view_index = len(buffer_view_dicts)
2558
+ buffer_view_dicts.append(
2559
+ {
2560
+ "buffer": 0,
2561
+ "byteOffset": inverse_bind_matrices_offset,
2562
+ "byteLength": len(buffer0) - inverse_bind_matrices_offset,
2563
+ }
2564
+ )
2565
+
2566
+ accessor_index = len(accessor_dicts)
2567
+ accessor_dicts.append(
2568
+ {
2569
+ "bufferView": buffer_view_index,
2570
+ "byteOffset": 0,
2571
+ "type": "MAT4",
2572
+ "componentType": GL_FLOAT,
2573
+ "count": len(skin_joint_node_indices),
2574
+ }
2575
+ )
2576
+
2577
+ # Hitogata 0.6.0.1 seems to error when sharing skin,
2578
+ # so give each mesh its own skin with the same content
2579
+ skin_dict: dict[str, Json] = {
2580
+ "joints": make_json(skin_joint_node_indices),
2581
+ "inverseBindMatrices": accessor_index,
2582
+ "skeleton": humanoid_root_bone_index,
2583
+ }
2584
+
2585
+ return scene_node_indices, skin_dict, skin_joint_node_indices
2586
+
2587
+ def write_armature_bone_nodes(
2588
+ self,
2589
+ progress: Progress,
2590
+ node_dicts: list[dict[str, Json]],
2591
+ buffer0: bytearray,
2592
+ bone: PoseBone,
2593
+ bone_name_to_node_index: dict[str, int],
2594
+ bone_name_to_inverse_bind_matrices: dict[str, Matrix],
2595
+ ) -> int:
2596
+ node_index = len(node_dicts)
2597
+ bone_name_to_node_index[bone.name] = node_index
2598
+
2599
+ parent_bone = bone.parent
2600
+ if parent_bone is None:
2601
+ parent_world_translation = Vector((0, 0, 0))
2602
+ else:
2603
+ parent_world_translation = self._armature.matrix_world @ parent_bone.head
2604
+
2605
+ world_translation = self._armature.matrix_world @ bone.head
2606
+
2607
+ bone_name_to_inverse_bind_matrices[bone.name] = Matrix(
2608
+ (
2609
+ (1, 0, 0, 0),
2610
+ (0, 1, 0, 0),
2611
+ (0, 0, 1, 0),
2612
+ (world_translation.x, -world_translation.z, -world_translation.y, 1),
2613
+ )
2614
+ )
2615
+
2616
+ node_dict: dict[str, Json] = {
2617
+ "name": bone.name,
2618
+ "translation": make_json(
2619
+ convert.axis_blender_to_gltf(
2620
+ world_translation - parent_world_translation
2621
+ )
2622
+ ),
2623
+ }
2624
+ node_dicts.append(node_dict)
2625
+ if bone.children:
2626
+ node_dict["children"] = [
2627
+ self.write_armature_bone_nodes(
2628
+ progress,
2629
+ node_dicts,
2630
+ buffer0,
2631
+ child,
2632
+ bone_name_to_node_index,
2633
+ bone_name_to_inverse_bind_matrices,
2634
+ )
2635
+ for child in bone.children
2636
+ ]
2637
+ return node_index
2638
+
2639
+ def get_or_write_cluster_empty_material(
2640
+ self,
2641
+ material_dicts: list[dict[str, Json]],
2642
+ extensions_vrm_material_property_dicts: list[Json],
2643
+ ) -> int:
2644
+ # Cluster does not allow primitives without materials,
2645
+ # so assign empty material.
2646
+ missing_material_name = "glTF_2_0_default_material"
2647
+ for i, material_dict in enumerate(material_dicts):
2648
+ if material_dict.get("name") == missing_material_name:
2649
+ return i
2650
+
2651
+ new_missing_material_index = len(material_dicts)
2652
+ material_dicts.append({"name": missing_material_name})
2653
+ extensions_vrm_material_property_dicts.append(
2654
+ {
2655
+ "name": missing_material_name,
2656
+ "shader": "VRM_USE_GLTFSHADER",
2657
+ "keywordMap": {},
2658
+ "tagMap": {},
2659
+ "floatProperties": {},
2660
+ "vectorProperties": {},
2661
+ "textureProperties": {},
2662
+ }
2663
+ )
2664
+ return new_missing_material_index
2665
+
2666
+ def collect_vertex(
2667
+ self,
2668
+ obj: Object,
2669
+ main_mesh_data: Mesh,
2670
+ vertex_index: int,
2671
+ texcoord: Optional[tuple[float, float]],
2672
+ normal: tuple[float, float, float],
2673
+ vertex_group_index_to_joint: Mapping[int, int],
2674
+ bone_name_to_node_index: Mapping[str, int],
2675
+ skin_joints: Sequence[int],
2676
+ vertex_attributes_collector: VertexAttributesCollector,
2677
+ vertex_index_to_weights_and_joints: dict[
2678
+ int,
2679
+ tuple[tuple[float, float, float, float], tuple[int, int, int, int]],
2680
+ ],
2681
+ *,
2682
+ have_skin: bool,
2683
+ ) -> int:
2684
+ already_added_vertex_index = (
2685
+ vertex_attributes_collector.find_added_vertex_index(
2686
+ blender_vertex_index=vertex_index,
2687
+ normal=normal,
2688
+ texcoord=texcoord,
2689
+ )
2690
+ )
2691
+ if isinstance(already_added_vertex_index, int):
2692
+ return already_added_vertex_index
2693
+
2694
+ vertex = main_mesh_data.vertices[vertex_index]
2695
+ position_x, position_y, position_z = vertex.co
2696
+
2697
+ joints: Optional[tuple[int, int, int, int]] = None
2698
+ weights: Optional[tuple[float, float, float, float]] = None
2699
+ if have_skin:
2700
+ weights_and_joints = vertex_index_to_weights_and_joints.get(vertex_index)
2701
+ if weights_and_joints is not None:
2702
+ weights, joints = weights_and_joints
2703
+ else:
2704
+ weight_and_joint_list: list[tuple[float, int]] = [
2705
+ (weight, joint)
2706
+ for vertex_group_element in vertex.groups
2707
+ if (
2708
+ (
2709
+ joint := vertex_group_index_to_joint.get(
2710
+ vertex_group_element.group
2711
+ )
2712
+ )
2713
+ is not None
2714
+ )
2715
+ # Set joint to zero when weight is zero
2716
+ # https://github.com/KhronosGroup/glTF/tree/f33f90ad9439a228bf90cde8319d851a52a3f470/specification/2.0#skinned-mesh-attributes
2717
+ and not (
2718
+ (weight := vertex_group_element.weight) < float_info.epsilon
2719
+ )
2720
+ ]
2721
+ weight_and_joint_list.sort(reverse=True)
2722
+ while len(weight_and_joint_list) < 4:
2723
+ weight_and_joint_list.append((0.0, 0))
2724
+
2725
+ weights = (
2726
+ weight_and_joint_list[0][0],
2727
+ weight_and_joint_list[1][0],
2728
+ weight_and_joint_list[2][0],
2729
+ weight_and_joint_list[3][0],
2730
+ )
2731
+ joints = (
2732
+ weight_and_joint_list[0][1],
2733
+ weight_and_joint_list[1][1],
2734
+ weight_and_joint_list[2][1],
2735
+ weight_and_joint_list[3][1],
2736
+ )
2737
+
2738
+ total_weight = sum(weights)
2739
+ if total_weight < float_info.epsilon:
2740
+ _logger.debug(
2741
+ "No weight on vertex index=%d mesh=%s",
2742
+ vertex_index,
2743
+ main_mesh_data.name,
2744
+ )
2745
+
2746
+ # Attach near bone
2747
+ joint = None
2748
+ mesh_parent: Optional[Object] = obj
2749
+ while mesh_parent:
2750
+ if mesh_parent.parent_type == "BONE":
2751
+ if (
2752
+ mesh_parent.parent == self._armature
2753
+ and (
2754
+ bone_index := bone_name_to_node_index.get(
2755
+ mesh_parent.parent_bone
2756
+ )
2757
+ )
2758
+ is not None
2759
+ and bone_index in skin_joints
2760
+ ):
2761
+ joint = skin_joints.index(bone_index)
2762
+ break
2763
+ if mesh_parent.parent_type == "OBJECT":
2764
+ mesh_parent = mesh_parent.parent
2765
+ else:
2766
+ break
2767
+
2768
+ if joint is None:
2769
+ # TODO: Probably better to use root bone traced from hips
2770
+ # rather than hips itself
2771
+ ext = get_armature_extension(self.armature_data)
2772
+ for human_bone in ext.vrm0.humanoid.human_bones:
2773
+ if human_bone.bone != "hips":
2774
+ continue
2775
+ if (
2776
+ bone_index := bone_name_to_node_index.get(
2777
+ human_bone.node.bone_name
2778
+ )
2779
+ ) is not None and bone_index in skin_joints:
2780
+ joint = skin_joints.index(bone_index)
2781
+
2782
+ if joint is None:
2783
+ message = "No fallback bone index found"
2784
+ if not skin_joints:
2785
+ raise ValueError(message)
2786
+ _logger.error(message)
2787
+ joint = skin_joints[0]
2788
+
2789
+ weights = (1.0, 0, 0, 0)
2790
+ joints = (joint, 0, 0, 0)
2791
+ else:
2792
+ weights = (
2793
+ weights[0] / total_weight,
2794
+ weights[1] / total_weight,
2795
+ weights[2] / total_weight,
2796
+ weights[3] / total_weight,
2797
+ )
2798
+
2799
+ vertex_index_to_weights_and_joints[vertex_index] = (weights, joints)
2800
+
2801
+ return vertex_attributes_collector.add_vertex(
2802
+ blender_vertex_index=vertex_index,
2803
+ position=convert.axis_blender_to_gltf(
2804
+ (
2805
+ position_x,
2806
+ position_y,
2807
+ position_z,
2808
+ )
2809
+ ),
2810
+ normal=normal,
2811
+ texcoord=texcoord,
2812
+ joints=joints,
2813
+ weights=weights,
2814
+ )
2815
+
2816
+ def collect_morph_target(
2817
+ self,
2818
+ main_mesh_data: Mesh,
2819
+ shape_key_mesh_data: Mesh,
2820
+ gltf_vertex_index: int,
2821
+ vertex_index: int,
2822
+ vertex_morph_target_collector: VertexMorphTargetCollector,
2823
+ vertex_index_to_morph_normal_diffs: Sequence[tuple[float, float, float]],
2824
+ ) -> None:
2825
+ if gltf_vertex_index < 0:
2826
+ return
2827
+
2828
+ main_mesh_data_vertices = main_mesh_data.vertices
2829
+ shape_key_mesh_data_vertices = shape_key_mesh_data.vertices
2830
+ if vertex_index < min(
2831
+ len(main_mesh_data_vertices), len(shape_key_mesh_data_vertices)
2832
+ ):
2833
+ (
2834
+ position_x,
2835
+ position_y,
2836
+ position_z,
2837
+ ) = main_mesh_data_vertices[vertex_index].co
2838
+ (
2839
+ shape_key_position_x,
2840
+ shape_key_position_y,
2841
+ shape_key_position_z,
2842
+ ) = shape_key_mesh_data_vertices[vertex_index].co
2843
+ target_position = convert.axis_blender_to_gltf(
2844
+ (
2845
+ shape_key_position_x - position_x,
2846
+ shape_key_position_y - position_y,
2847
+ shape_key_position_z - position_z,
2848
+ )
2849
+ )
2850
+ else:
2851
+ target_position = (0, 0, 0)
2852
+
2853
+ if vertex_index < len(vertex_index_to_morph_normal_diffs):
2854
+ morph_normal_diff = vertex_index_to_morph_normal_diffs[vertex_index]
2855
+ target_normal = convert.axis_blender_to_gltf(morph_normal_diff)
2856
+ else:
2857
+ target_normal = (0, 0, 0)
2858
+
2859
+ vertex_morph_target_collector.add_vertex(
2860
+ vertex_index=gltf_vertex_index,
2861
+ target_position=target_position,
2862
+ target_normal=target_normal,
2863
+ )
2864
+
2865
+ def write_mesh_node(
2866
+ self,
2867
+ _progress: Progress,
2868
+ node_dicts: list[dict[str, Json]],
2869
+ mesh_dicts: list[dict[str, Json]],
2870
+ skin_dicts: list[dict[str, Json]],
2871
+ material_dicts: list[dict[str, Json]],
2872
+ accessor_dicts: list[dict[str, Json]],
2873
+ buffer_view_dicts: list[dict[str, Json]],
2874
+ extensions_vrm_material_property_dicts: list[Json],
2875
+ buffer0: bytearray,
2876
+ object_name_to_node_index: dict[str, int],
2877
+ bone_name_to_node_index: Mapping[str, int],
2878
+ obj: Object,
2879
+ mesh_convertible_objects: Sequence[Object],
2880
+ mesh_object_name_to_mesh_index: dict[str, int],
2881
+ material_name_to_material_index: Mapping[str, int],
2882
+ skin_dict: Mapping[str, Json],
2883
+ skin_joints: Sequence[int],
2884
+ ) -> Optional[int]:
2885
+ if obj.type not in MESH_CONVERTIBLE_OBJECT_TYPES:
2886
+ return None
2887
+
2888
+ have_skin = bool(skin_joints) and self.have_skin(obj)
2889
+
2890
+ node_index = len(node_dicts)
2891
+ node_dict: dict[str, Json] = {"name": obj.name}
2892
+
2893
+ parent_node_index = None
2894
+ parent_translation = None
2895
+ if have_skin:
2896
+ # Becomes scene root node when there's skin
2897
+ pass
2898
+ else:
2899
+ if (
2900
+ obj.parent_type in {"ARMATURE", "OBJECT"}
2901
+ and (
2902
+ parent := self.get_export_parent_object(
2903
+ obj, mesh_convertible_objects
2904
+ )
2905
+ )
2906
+ and parent in self._export_objects
2907
+ ):
2908
+ # TODO: Don't restore nested meshes for compatibility, but will
2909
+ # restore in the future
2910
+ # parent_translation = parent.matrix_world.to_translation()
2911
+ # parent_node_index = object_name_to_node_index.get(parent.name)
2912
+ pass
2913
+
2914
+ if obj.parent_type == "BONE" and (
2915
+ bone := self._armature.pose.bones.get(obj.parent_bone)
2916
+ ):
2917
+ parent_translation = (
2918
+ self._armature.matrix_world @ bone.matrix
2919
+ ).to_translation()
2920
+ parent_node_index = bone_name_to_node_index.get(obj.parent_bone)
2921
+
2922
+ if parent_node_index is not None and 0 <= parent_node_index < len(node_dicts):
2923
+ parent_node_dict = node_dicts[parent_node_index]
2924
+ parent_children = parent_node_dict.get("children")
2925
+ if parent_children is None or not isinstance(
2926
+ parent_children, MutableSequence
2927
+ ):
2928
+ parent_children = []
2929
+ parent_node_dict["children"] = parent_children
2930
+ parent_children.append(node_index)
2931
+
2932
+ if not parent_translation:
2933
+ parent_translation = Vector((0, 0, 0))
2934
+
2935
+ if not have_skin:
2936
+ node_dict["translation"] = make_json(
2937
+ convert.axis_blender_to_gltf(
2938
+ obj.matrix_world.to_translation() - parent_translation
2939
+ )
2940
+ )
2941
+
2942
+ node_dicts.append(node_dict)
2943
+ object_name_to_node_index[obj.name] = node_index
2944
+
2945
+ scene_node_index = None
2946
+ if parent_node_index is None:
2947
+ scene_node_index = node_index
2948
+
2949
+ original_mesh_convertible = obj.data
2950
+ if not isinstance(original_mesh_convertible, (Curve, Mesh)):
2951
+ _logger.error(
2952
+ "Unexpected mesh convertible object type: %s",
2953
+ type(original_mesh_convertible),
2954
+ )
2955
+ return scene_node_index
2956
+
2957
+ mesh_index = len(mesh_dicts)
2958
+
2959
+ vertex_morph_target_collectors: dict[
2960
+ str, Vrm0Exporter.VertexMorphTargetCollector
2961
+ ] = {}
2962
+ key_block_name_to_value: dict[str, float] = {}
2963
+ original_shape_keys: Optional[Key] = None
2964
+ if isinstance(original_mesh_convertible, Mesh):
2965
+ original_mesh_convertible.calc_loop_triangles()
2966
+ original_shape_keys = original_mesh_convertible.shape_keys
2967
+ if original_shape_keys:
2968
+ for key_block in original_shape_keys.key_blocks:
2969
+ key_block_name_to_value[key_block.name] = key_block.value
2970
+ key_block.value = 0
2971
+ self._context.view_layer.update()
2972
+
2973
+ with save_workspace(self._context):
2974
+ # TODO: Executing move for compatibility with old addon,
2975
+ # but seems unnecessary
2976
+ mesh_data_transform = Matrix.Identity(4)
2977
+ if not have_skin:
2978
+ mesh_data_transform @= Matrix.Translation(
2979
+ -obj.matrix_world.to_translation()
2980
+ )
2981
+ mesh_data_transform @= obj.matrix_world
2982
+
2983
+ main_mesh_data = generate_evaluated_mesh(
2984
+ self._context,
2985
+ obj,
2986
+ )
2987
+ if main_mesh_data is None:
2988
+ return scene_node_index
2989
+ main_mesh_data.transform(mesh_data_transform)
2990
+
2991
+ if bpy.app.version < (4, 1):
2992
+ main_mesh_data.calc_normals_split()
2993
+
2994
+ material_slot_index_to_material_name: Mapping[int, str] = {
2995
+ material_index: material_ref.name
2996
+ for material_index, material_slot in enumerate(obj.material_slots)
2997
+ if (material_ref := material_slot.material) and material_ref.name
2998
+ }
2999
+
3000
+ material_index_to_vertex_indices: dict[int, bytearray] = {}
3001
+ vertex_indices_struct = struct.Struct("<I")
3002
+
3003
+ vertex_group_index_to_joint: Mapping[int, int] = {
3004
+ vertex_group_index: skin_joints.index(vertex_group_node_index)
3005
+ for vertex_group_index, vertex_group in enumerate(obj.vertex_groups)
3006
+ if (
3007
+ vertex_group_node_index := bone_name_to_node_index.get(
3008
+ vertex_group.name
3009
+ )
3010
+ )
3011
+ is not None
3012
+ and vertex_group_node_index in skin_joints
3013
+ }
3014
+
3015
+ vertex_attributes_collector = Vrm0Exporter.VertexAttributesCollector()
3016
+ uv_layers = main_mesh_data.uv_layers
3017
+ uv_layer = next(
3018
+ (
3019
+ uv_layer
3020
+ for uv_layer in uv_layers
3021
+ if uv_layer and uv_layer.active_render
3022
+ ),
3023
+ None,
3024
+ )
3025
+ main_mesh_data.calc_loop_triangles()
3026
+
3027
+ loop_index_to_texcoord: list[Optional[tuple[float, float]]] = []
3028
+ if uv_layer:
3029
+ loop_index_to_texcoord.extend(
3030
+ (uv_data.uv[0], 1 - uv_data.uv[1]) for uv_data in uv_layer.data
3031
+ )
3032
+ else:
3033
+ loop_index_to_texcoord.extend(None for _ in main_mesh_data.loops)
3034
+ loop_index_to_gltf_vertex_index: list[Optional[int]] = [None] * len(
3035
+ main_mesh_data.loops
3036
+ )
3037
+ vertex_index_to_weights_and_joints: dict[
3038
+ int,
3039
+ tuple[tuple[float, float, float, float], tuple[int, int, int, int]],
3040
+ ] = {}
3041
+ for loop_triangle in main_mesh_data.loop_triangles:
3042
+ material_slot_index = loop_triangle.material_index
3043
+ material_name = material_slot_index_to_material_name.get(
3044
+ material_slot_index
3045
+ )
3046
+ material_index = None
3047
+ if material_name:
3048
+ material_index = material_name_to_material_index.get(material_name)
3049
+ if material_index is None:
3050
+ material_index = self.get_or_write_cluster_empty_material(
3051
+ material_dicts, extensions_vrm_material_property_dicts
3052
+ )
3053
+
3054
+ vertex_indices = material_index_to_vertex_indices.get(material_index)
3055
+ if vertex_indices is None:
3056
+ vertex_indices = bytearray()
3057
+ material_index_to_vertex_indices[material_index] = vertex_indices
3058
+
3059
+ for loop_index in loop_triangle.loops:
3060
+ loop = main_mesh_data.loops[loop_index]
3061
+ original_vertex_index = loop.vertex_index
3062
+ texcoord = loop_index_to_texcoord[loop_index]
3063
+
3064
+ # Use loop normals instead of vertex normals. This may lose
3065
+ # something, but it's judged safer to keep it the same as the glTF
3066
+ # 2.0 addon.
3067
+ # https://github.com/KhronosGroup/glTF-Blender-IO/pull/1127
3068
+ normal = convert.axis_blender_to_gltf(
3069
+ Vrm0Exporter.safe_normalized_vector(loop.normal)
3070
+ )
3071
+
3072
+ vertex_index = self.collect_vertex(
3073
+ obj,
3074
+ main_mesh_data,
3075
+ original_vertex_index,
3076
+ texcoord,
3077
+ normal,
3078
+ vertex_group_index_to_joint,
3079
+ bone_name_to_node_index,
3080
+ skin_joints,
3081
+ vertex_attributes_collector,
3082
+ vertex_index_to_weights_and_joints,
3083
+ have_skin=have_skin,
3084
+ )
3085
+ loop_index_to_gltf_vertex_index[loop_index] = vertex_index
3086
+ vertex_indices.extend(vertex_indices_struct.pack(vertex_index))
3087
+
3088
+ if original_shape_keys:
3089
+ no_morph_normal_export_vertex_indices: set[int] = (
3090
+ self.create_no_morph_normal_export_vertex_indices(
3091
+ self._context,
3092
+ main_mesh_data,
3093
+ )
3094
+ )
3095
+
3096
+ reference_vertex_normal_vectors = (
3097
+ Vrm0Exporter.create_export_vertex_normal_vectors(
3098
+ no_morph_normal_export_vertex_indices,
3099
+ main_mesh_data,
3100
+ )
3101
+ )
3102
+
3103
+ # Create mesh with modifiers applied for each shape key.
3104
+ # This is because for VRM 0.x, glTF Node rotation and scale
3105
+ # are normalized, but when rotation or scale is applied in
3106
+ # pose mode, the weight calculation for that normalization is
3107
+ # not applied to shape keys, so we need to recalculate the
3108
+ # change amount for each shape key ourselves.
3109
+ # There might be bad patterns where vertex indices change,
3110
+ # so there's room for improvement.
3111
+ for shape_key in original_shape_keys.key_blocks:
3112
+ if original_shape_keys.reference_key.name == shape_key.name:
3113
+ continue
3114
+
3115
+ shape_key.value = 1.0
3116
+ self._context.view_layer.update()
3117
+ shape_mesh_data = generate_evaluated_mesh(
3118
+ self._context,
3119
+ obj,
3120
+ )
3121
+ shape_key.value = 0.0
3122
+ self._context.view_layer.update()
3123
+
3124
+ if not shape_mesh_data:
3125
+ continue
3126
+
3127
+ shape_mesh_data.transform(mesh_data_transform)
3128
+
3129
+ if bpy.app.version < (4, 1):
3130
+ shape_mesh_data.calc_normals_split()
3131
+
3132
+ vertex_morph_target_collector = vertex_morph_target_collectors.get(
3133
+ shape_key.name
3134
+ )
3135
+ if vertex_morph_target_collector is None:
3136
+ vertex_morph_target_collector = (
3137
+ Vrm0Exporter.VertexMorphTargetCollector(
3138
+ vertex_attributes_collector.count
3139
+ )
3140
+ )
3141
+ vertex_morph_target_collectors[shape_key.name] = (
3142
+ vertex_morph_target_collector
3143
+ )
3144
+
3145
+ shape_mesh_data.calc_loop_triangles()
3146
+ vertex_index_to_morph_normal_diffs = (
3147
+ Vrm0Exporter.create_vertex_index_to_morph_normal_diffs(
3148
+ no_morph_normal_export_vertex_indices,
3149
+ shape_mesh_data,
3150
+ reference_vertex_normal_vectors,
3151
+ )
3152
+ )
3153
+
3154
+ for loop_triangle in main_mesh_data.loop_triangles:
3155
+ for loop_index in loop_triangle.loops:
3156
+ loop = main_mesh_data.loops[loop_index]
3157
+ original_vertex_index = loop.vertex_index
3158
+ gltf_vertex_index = loop_index_to_gltf_vertex_index[
3159
+ loop_index
3160
+ ]
3161
+ if gltf_vertex_index is None:
3162
+ continue
3163
+ self.collect_morph_target(
3164
+ main_mesh_data,
3165
+ shape_mesh_data,
3166
+ gltf_vertex_index,
3167
+ original_vertex_index,
3168
+ vertex_morph_target_collector,
3169
+ vertex_index_to_morph_normal_diffs,
3170
+ )
3171
+
3172
+ if shape_mesh_data.users:
3173
+ _logger.warning(
3174
+ 'Failed to remove "%s" with %d users while exporting'
3175
+ " shape key meshes",
3176
+ shape_mesh_data.name,
3177
+ shape_mesh_data.users,
3178
+ )
3179
+ else:
3180
+ self._context.blend_data.meshes.remove(shape_mesh_data)
3181
+
3182
+ if main_mesh_data.users:
3183
+ _logger.warning(
3184
+ 'Failed to remove "%s" with %d users while exporting meshes',
3185
+ main_mesh_data.name,
3186
+ main_mesh_data.users,
3187
+ )
3188
+ else:
3189
+ self._context.blend_data.meshes.remove(main_mesh_data)
3190
+ main_mesh_data = None
3191
+
3192
+ if original_shape_keys:
3193
+ for key_block_name, value in key_block_name_to_value.items():
3194
+ key_block = original_shape_keys.key_blocks.get(key_block_name)
3195
+ if key_block:
3196
+ key_block.value = value
3197
+ self._context.view_layer.update()
3198
+
3199
+ if not material_index_to_vertex_indices:
3200
+ return scene_node_index
3201
+
3202
+ node_dict["mesh"] = mesh_index
3203
+
3204
+ primitive_dicts: list[dict[str, Json]] = []
3205
+
3206
+ while len(buffer0) % 4:
3207
+ buffer0.append(0)
3208
+
3209
+ # TODO: Make buffer writing class independent
3210
+
3211
+ # Write indices
3212
+ for (
3213
+ primitive_material_index,
3214
+ vertex_indices,
3215
+ ) in material_index_to_vertex_indices.items():
3216
+ indices_buffer_offset = len(buffer0)
3217
+ buffer0.extend(vertex_indices)
3218
+ indices_buffer_view_index = len(buffer_view_dicts)
3219
+ buffer_view_dicts.append(
3220
+ {
3221
+ "buffer": 0,
3222
+ "byteOffset": indices_buffer_offset,
3223
+ "byteLength": len(vertex_indices),
3224
+ }
3225
+ )
3226
+ indices_accessor_index = len(accessor_dicts)
3227
+ if vertex_indices_struct.size == 0:
3228
+ message = "vertex_indices_struct.size is 0"
3229
+ raise AssertionError(message)
3230
+ if len(vertex_indices) % vertex_indices_struct.size != 0:
3231
+ message = (
3232
+ "vertex_indices length is not a multiple of "
3233
+ "vertex_indices_struct.size"
3234
+ )
3235
+ raise AssertionError(message)
3236
+
3237
+ accessor_dicts.append(
3238
+ {
3239
+ "bufferView": indices_buffer_view_index,
3240
+ "byteOffset": 0,
3241
+ "type": "SCALAR",
3242
+ "componentType": GL_UNSIGNED_INT,
3243
+ "count": len(vertex_indices) // vertex_indices_struct.size,
3244
+ }
3245
+ )
3246
+ primitive_dicts.append(
3247
+ {
3248
+ "material": primitive_material_index,
3249
+ "indices": indices_accessor_index,
3250
+ }
3251
+ )
3252
+
3253
+ # Made attributes shared by design
3254
+ primitive_attribute_dict: dict[str, Json] = {}
3255
+ for primitive_dict in primitive_dicts:
3256
+ primitive_dict["attributes"] = primitive_attribute_dict
3257
+
3258
+ position_buffer_offset = len(buffer0)
3259
+ buffer0.extend(vertex_attributes_collector.position)
3260
+ position_buffer_view_index = len(buffer_view_dicts)
3261
+ buffer_view_dicts.append(
3262
+ {
3263
+ "buffer": 0,
3264
+ "byteOffset": position_buffer_offset,
3265
+ "byteLength": len(vertex_attributes_collector.position),
3266
+ }
3267
+ )
3268
+ position_accessor_index = len(accessor_dicts)
3269
+
3270
+ accessor_dicts.append(
3271
+ {
3272
+ "bufferView": position_buffer_view_index,
3273
+ "byteOffset": 0,
3274
+ "type": "VEC3",
3275
+ "componentType": GL_FLOAT,
3276
+ "count": vertex_attributes_collector.count,
3277
+ "min": [
3278
+ vertex_attributes_collector.position_min_x,
3279
+ vertex_attributes_collector.position_min_y,
3280
+ vertex_attributes_collector.position_min_z,
3281
+ ],
3282
+ "max": [
3283
+ vertex_attributes_collector.position_max_x,
3284
+ vertex_attributes_collector.position_max_y,
3285
+ vertex_attributes_collector.position_max_z,
3286
+ ],
3287
+ }
3288
+ )
3289
+ primitive_attribute_dict["POSITION"] = position_accessor_index
3290
+
3291
+ normal_buffer_offset = len(buffer0)
3292
+ buffer0.extend(vertex_attributes_collector.normal)
3293
+ normal_buffer_view_index = len(buffer_view_dicts)
3294
+ buffer_view_dicts.append(
3295
+ {
3296
+ "buffer": 0,
3297
+ "byteOffset": normal_buffer_offset,
3298
+ "byteLength": len(vertex_attributes_collector.normal),
3299
+ }
3300
+ )
3301
+ normal_accessor_index = len(accessor_dicts)
3302
+ accessor_dicts.append(
3303
+ {
3304
+ "bufferView": normal_buffer_view_index,
3305
+ "byteOffset": 0,
3306
+ "type": "VEC3",
3307
+ "componentType": GL_FLOAT,
3308
+ "count": vertex_attributes_collector.count,
3309
+ }
3310
+ )
3311
+ primitive_attribute_dict["NORMAL"] = normal_accessor_index
3312
+
3313
+ primitive_texcoord = vertex_attributes_collector.texcoord
3314
+ if primitive_texcoord:
3315
+ texcoord_buffer_offset = len(buffer0)
3316
+ buffer0.extend(primitive_texcoord)
3317
+ texcoord_buffer_view_index = len(buffer_view_dicts)
3318
+ buffer_view_dicts.append(
3319
+ {
3320
+ "buffer": 0,
3321
+ "byteOffset": texcoord_buffer_offset,
3322
+ "byteLength": len(primitive_texcoord),
3323
+ }
3324
+ )
3325
+ texcoord_accessor_index = len(accessor_dicts)
3326
+ accessor_dicts.append(
3327
+ {
3328
+ "bufferView": texcoord_buffer_view_index,
3329
+ "byteOffset": 0,
3330
+ "type": "VEC2",
3331
+ "componentType": GL_FLOAT,
3332
+ "count": vertex_attributes_collector.count,
3333
+ }
3334
+ )
3335
+ primitive_attribute_dict["TEXCOORD_0"] = texcoord_accessor_index
3336
+
3337
+ primitive_joints = vertex_attributes_collector.joints
3338
+ if primitive_joints:
3339
+ joints_buffer_offset = len(buffer0)
3340
+ buffer0.extend(primitive_joints)
3341
+ joints_buffer_view_index = len(buffer_view_dicts)
3342
+ buffer_view_dicts.append(
3343
+ {
3344
+ "buffer": 0,
3345
+ "byteOffset": joints_buffer_offset,
3346
+ "byteLength": len(primitive_joints),
3347
+ }
3348
+ )
3349
+ joints_accessor_index = len(accessor_dicts)
3350
+ accessor_dicts.append(
3351
+ {
3352
+ "bufferView": joints_buffer_view_index,
3353
+ "byteOffset": 0,
3354
+ "type": "VEC4",
3355
+ "componentType": GL_UNSIGNED_SHORT,
3356
+ "count": vertex_attributes_collector.count,
3357
+ }
3358
+ )
3359
+ primitive_attribute_dict["JOINTS_0"] = joints_accessor_index
3360
+
3361
+ primitive_weights = vertex_attributes_collector.weights
3362
+ if primitive_weights:
3363
+ while len(buffer0) % 4:
3364
+ buffer0.append(0)
3365
+
3366
+ weights_buffer_offset = len(buffer0)
3367
+ buffer0.extend(primitive_weights)
3368
+ weights_buffer_view_index = len(buffer_view_dicts)
3369
+ buffer_view_dicts.append(
3370
+ {
3371
+ "buffer": 0,
3372
+ "byteOffset": weights_buffer_offset,
3373
+ "byteLength": len(primitive_weights),
3374
+ }
3375
+ )
3376
+ weights_accessor_index = len(accessor_dicts)
3377
+ accessor_dicts.append(
3378
+ {
3379
+ "bufferView": weights_buffer_view_index,
3380
+ "byteOffset": 0,
3381
+ "type": "VEC4",
3382
+ "componentType": GL_FLOAT,
3383
+ "count": vertex_attributes_collector.count,
3384
+ }
3385
+ )
3386
+ primitive_attribute_dict["WEIGHTS_0"] = weights_accessor_index
3387
+
3388
+ # Made targets shared by design
3389
+ morph_target_names = make_json(vertex_morph_target_collectors.keys())
3390
+ if vertex_morph_target_collectors:
3391
+ while len(buffer0) % 4:
3392
+ buffer0.append(0)
3393
+
3394
+ primitive_target_dicts: list[dict[str, Json]] = []
3395
+ for target in vertex_morph_target_collectors.values():
3396
+ target_position_buffer_offset = len(buffer0)
3397
+ buffer0.extend(target.position)
3398
+ target_position_buffer_view_index = len(buffer_view_dicts)
3399
+ buffer_view_dicts.append(
3400
+ {
3401
+ "buffer": 0,
3402
+ "byteOffset": target_position_buffer_offset,
3403
+ "byteLength": len(target.position),
3404
+ }
3405
+ )
3406
+ target_position_accessor_index = len(accessor_dicts)
3407
+
3408
+ accessor_dicts.append(
3409
+ {
3410
+ "bufferView": target_position_buffer_view_index,
3411
+ "byteOffset": 0,
3412
+ "type": "VEC3",
3413
+ "componentType": GL_FLOAT,
3414
+ "count": vertex_attributes_collector.count,
3415
+ "min": [
3416
+ target.position_min_x,
3417
+ target.position_min_y,
3418
+ target.position_min_z,
3419
+ ],
3420
+ "max": [
3421
+ target.position_max_x,
3422
+ target.position_max_y,
3423
+ target.position_max_z,
3424
+ ],
3425
+ }
3426
+ )
3427
+
3428
+ target_normal_buffer_offset = len(buffer0)
3429
+ buffer0.extend(target.normal)
3430
+ target_normal_buffer_view_index = len(buffer_view_dicts)
3431
+ buffer_view_dicts.append(
3432
+ {
3433
+ "buffer": 0,
3434
+ "byteOffset": target_normal_buffer_offset,
3435
+ "byteLength": len(target.normal),
3436
+ }
3437
+ )
3438
+ target_normal_accessor_index = len(accessor_dicts)
3439
+ accessor_dicts.append(
3440
+ {
3441
+ "bufferView": target_normal_buffer_view_index,
3442
+ "byteOffset": 0,
3443
+ "type": "VEC3",
3444
+ "componentType": GL_FLOAT,
3445
+ "count": vertex_attributes_collector.count,
3446
+ }
3447
+ )
3448
+
3449
+ primitive_target_dicts.append(
3450
+ {
3451
+ "POSITION": target_position_accessor_index,
3452
+ "NORMAL": target_normal_accessor_index,
3453
+ }
3454
+ )
3455
+
3456
+ for primitive_dict in primitive_dicts:
3457
+ material = next(
3458
+ (
3459
+ self._context.blend_data.materials.get(material_name)
3460
+ for material_name, material_index in (
3461
+ material_name_to_material_index.items()
3462
+ )
3463
+ if material_index == primitive_dict.get("material")
3464
+ ),
3465
+ None,
3466
+ )
3467
+ if not material:
3468
+ export_shape_key_normals = True
3469
+ else:
3470
+ mtoon1 = get_material_extension(material).mtoon1
3471
+ if mtoon1.export_shape_key_normals:
3472
+ export_shape_key_normals = True
3473
+ elif mtoon1.enabled or (
3474
+ (
3475
+ legacy_addon_material := LegacyAddonMaterial.try_parse(
3476
+ material
3477
+ )
3478
+ )
3479
+ and legacy_addon_material.shader_name == "MToon_unversioned"
3480
+ ):
3481
+ export_shape_key_normals = False
3482
+ else:
3483
+ export_shape_key_normals = True
3484
+
3485
+ primitive_dict["targets"] = make_json(
3486
+ primitive_target_dict
3487
+ if export_shape_key_normals
3488
+ else {
3489
+ k: v for k, v in primitive_target_dict.items() if k != "NORMAL"
3490
+ }
3491
+ for primitive_target_dict in primitive_target_dicts
3492
+ )
3493
+ primitive_dict["extras"] = {
3494
+ # for compatibility with legacy practices
3495
+ # https://github.com/vrm-c/UniVRM/blob/38ccb92300c9ab41c72eb3d5b8dc8ce664a659d5/Assets/UniGLTF/Runtime/UniGLTF/Format/ExtensionsAndExtras/gltf_mesh_extras_targetNames.cs#L7-L12
3496
+ "targetNames": morph_target_names,
3497
+ }
3498
+
3499
+ mesh_dict = {
3500
+ "name": original_mesh_convertible.name,
3501
+ "primitives": make_json(primitive_dicts),
3502
+ }
3503
+ if morph_target_names:
3504
+ mesh_dict["extras"] = {
3505
+ # targetNames is not included in glTF specification, but is used
3506
+ # in many implementations
3507
+ # https://github.com/KhronosGroup/glTF/blob/0251c5c0cce8daec69bd54f29f891e3d0cdb52c8/specification/2.0/Specification.adoc?plain=1#L1500-L1504
3508
+ "targetNames": morph_target_names,
3509
+ }
3510
+ morph_target_weights = [
3511
+ min(key_block_name_to_value.get(morph_target_name, 0), 1)
3512
+ for morph_target_name in vertex_morph_target_collectors
3513
+ ]
3514
+ if any(weight > 0 for weight in morph_target_weights):
3515
+ mesh_dict["weights"] = make_json(morph_target_weights)
3516
+
3517
+ mesh_dicts.append(mesh_dict)
3518
+ mesh_object_name_to_mesh_index[obj.name] = mesh_index
3519
+ if skin_dict and have_skin:
3520
+ # TODO: Create separate skin for each mesh
3521
+ node_dict["skin"] = len(skin_dicts)
3522
+ skin_dicts.append(dict(skin_dict))
3523
+
3524
+ return scene_node_index
3525
+
3526
+ def get_export_parent_object(
3527
+ self, obj: Object, mesh_convertible_objects: Sequence[Object]
3528
+ ) -> Optional[Object]:
3529
+ if obj.parent_type != "OBJECT":
3530
+ return None
3531
+
3532
+ parent = obj.parent
3533
+ while parent:
3534
+ if parent in mesh_convertible_objects:
3535
+ return parent
3536
+ parent = parent.parent
3537
+
3538
+ return None
3539
+
3540
+ def write_mesh_nodes(
3541
+ self,
3542
+ progress: Progress,
3543
+ node_dicts: list[dict[str, Json]],
3544
+ mesh_dicts: list[dict[str, Json]],
3545
+ skin_dicts: list[dict[str, Json]],
3546
+ material_dicts: list[dict[str, Json]],
3547
+ accessor_dicts: list[dict[str, Json]],
3548
+ buffer_view_dicts: list[dict[str, Json]],
3549
+ extensions_vrm_material_property_dicts: list[Json],
3550
+ buffer0: bytearray,
3551
+ bone_name_to_node_index: Mapping[str, int],
3552
+ mesh_object_name_to_mesh_index: dict[str, int],
3553
+ material_name_to_material_index: Mapping[str, int],
3554
+ skin_dict: Mapping[str, Json],
3555
+ skin_joints: Sequence[int],
3556
+ ) -> list[int]:
3557
+ object_name_to_node_index: dict[str, int] = {}
3558
+ node_indices: list[int] = []
3559
+
3560
+ mesh_convertible_objects = [
3561
+ mesh_object
3562
+ for mesh_object in self._export_objects
3563
+ if mesh_object.type in search.MESH_CONVERTIBLE_OBJECT_TYPES
3564
+ ]
3565
+
3566
+ # Sort meshes according to parent-child relationships
3567
+ while True:
3568
+ swapped = False
3569
+ for mesh_object in list(mesh_convertible_objects):
3570
+ parent_mesh_object = self.get_export_parent_object(
3571
+ mesh_object, mesh_convertible_objects
3572
+ )
3573
+ if not parent_mesh_object:
3574
+ continue
3575
+
3576
+ if mesh_convertible_objects.index(
3577
+ mesh_object
3578
+ ) < mesh_convertible_objects.index(parent_mesh_object):
3579
+ mesh_convertible_objects.remove(mesh_object)
3580
+ mesh_convertible_objects.append(mesh_object)
3581
+ swapped = True
3582
+
3583
+ if not swapped:
3584
+ break
3585
+
3586
+ for obj in self._export_objects:
3587
+ node_index = None
3588
+ with save_workspace(self._context, obj):
3589
+ node_index = self.write_mesh_node(
3590
+ progress,
3591
+ node_dicts,
3592
+ mesh_dicts,
3593
+ skin_dicts,
3594
+ material_dicts,
3595
+ accessor_dicts,
3596
+ buffer_view_dicts,
3597
+ extensions_vrm_material_property_dicts,
3598
+ buffer0,
3599
+ object_name_to_node_index,
3600
+ bone_name_to_node_index,
3601
+ obj,
3602
+ mesh_convertible_objects,
3603
+ mesh_object_name_to_mesh_index,
3604
+ material_name_to_material_index,
3605
+ skin_dict,
3606
+ skin_joints,
3607
+ )
3608
+ if node_index is None:
3609
+ continue
3610
+ node_indices.append(node_index)
3611
+
3612
+ return node_indices
3613
+
3614
+ @property
3615
+ def armature_data(self) -> Armature:
3616
+ if not self._armature:
3617
+ message = "armature is not set"
3618
+ raise AssertionError(message)
3619
+ armature_data = self._armature.data
3620
+ if not isinstance(armature_data, Armature):
3621
+ message = f"{type(armature_data)} is not an Armature"
3622
+ raise TypeError(message)
3623
+ return armature_data
3624
+
3625
+ def create_gltf2_io_texture(
3626
+ self,
3627
+ gltf2_io_texture_info: object,
3628
+ texture_dicts: list[dict[str, Json]],
3629
+ sampler_dicts: list[dict[str, Json]],
3630
+ image_dicts: list[dict[str, Json]],
3631
+ buffer_view_dicts: list[dict[str, Json]],
3632
+ buffer0: bytearray,
3633
+ gltf2_io_texture_images: list[Gltf2IoTextureImage],
3634
+ ) -> Json:
3635
+ source = getattr(getattr(gltf2_io_texture_info, "index", None), "source", None)
3636
+ if not source:
3637
+ return None
3638
+
3639
+ source_name = getattr(source, "name", None)
3640
+ if not isinstance(source_name, str):
3641
+ source_name = ""
3642
+
3643
+ source_mime_type = getattr(source, "mime_type", None)
3644
+ if not isinstance(source_mime_type, str):
3645
+ source_mime_type = "image/png"
3646
+
3647
+ source_buffer_view_data = getattr(
3648
+ getattr(source, "buffer_view", None), "data", None
3649
+ )
3650
+ if not isinstance(source_buffer_view_data, bytes):
3651
+ return None
3652
+
3653
+ export_image_index = None
3654
+ for gltf2_io_texture_image in gltf2_io_texture_images:
3655
+ if gltf2_io_texture_image.name != source_name:
3656
+ continue
3657
+ if gltf2_io_texture_image.mime_type != source_mime_type:
3658
+ continue
3659
+ if gltf2_io_texture_image.image_bytes != source_buffer_view_data:
3660
+ continue
3661
+ export_image_index = gltf2_io_texture_image.export_image_index
3662
+ break
3663
+
3664
+ if export_image_index is None:
3665
+ image_base_name = re.sub(
3666
+ r"^BlenderVrmAddonImport[0-9]+Image[0-9]+_", "", source_name
3667
+ )
3668
+
3669
+ # Avoid duplicate naming for compatibility with old exporter
3670
+ image_name = image_base_name
3671
+ for count in range(100000):
3672
+ if count:
3673
+ image_name = image_base_name + "." + str(count)
3674
+ if not any(
3675
+ image_name == image_dict.get("name") for image_dict in image_dicts
3676
+ ):
3677
+ break
3678
+
3679
+ image_buffer_view_index = len(buffer_view_dicts)
3680
+ buffer_view_dicts.append(
3681
+ {
3682
+ "buffer": 0,
3683
+ "byteOffset": len(buffer0),
3684
+ "byteLength": len(source_buffer_view_data),
3685
+ }
3686
+ )
3687
+ buffer0.extend(source_buffer_view_data)
3688
+
3689
+ export_image_index = len(image_dicts)
3690
+ image_dict: dict[str, Json] = {
3691
+ "name": image_name,
3692
+ "mimeType": source_mime_type,
3693
+ "bufferView": image_buffer_view_index,
3694
+ }
3695
+ image_dicts.append(image_dict)
3696
+
3697
+ gltf2_io_texture_images.append(
3698
+ Vrm0Exporter.Gltf2IoTextureImage(
3699
+ name=source_name,
3700
+ mime_type=source_mime_type,
3701
+ image_bytes=source_buffer_view_data,
3702
+ export_image_index=export_image_index,
3703
+ )
3704
+ )
3705
+
3706
+ sampler = getattr(
3707
+ getattr(gltf2_io_texture_info, "index", None), "sampler", None
3708
+ )
3709
+ wrap_s = getattr(sampler, "wrap_s", None)
3710
+ wrap_t = getattr(sampler, "wrap_t", None)
3711
+ mag_filter = getattr(sampler, "mag_filter", None)
3712
+ min_filter = getattr(sampler, "min_filter", None)
3713
+
3714
+ if not isinstance(wrap_s, int):
3715
+ wrap_s = GL_REPEAT
3716
+ if not isinstance(wrap_t, int):
3717
+ wrap_t = GL_REPEAT
3718
+ if not isinstance(mag_filter, int):
3719
+ mag_filter = GL_LINEAR
3720
+ if not isinstance(min_filter, int):
3721
+ min_filter = GL_LINEAR
3722
+
3723
+ sampler_dict: dict[str, Json] = {
3724
+ "magFilter": mag_filter,
3725
+ "minFilter": min_filter,
3726
+ "wrapS": wrap_s,
3727
+ "wrapT": wrap_t,
3728
+ }
3729
+ if sampler_dict in sampler_dicts:
3730
+ sampler_index = sampler_dicts.index(sampler_dict)
3731
+ else:
3732
+ sampler_index = len(sampler_dicts)
3733
+ sampler_dicts.append(sampler_dict)
3734
+
3735
+ texture_dict: dict[str, Json] = {
3736
+ "sampler": sampler_index,
3737
+ "source": export_image_index,
3738
+ }
3739
+
3740
+ if texture_dict in texture_dicts:
3741
+ texture_index = texture_dicts.index(texture_dict)
3742
+ else:
3743
+ texture_index = len(texture_dicts)
3744
+ texture_dicts.append(texture_dict)
3745
+
3746
+ texture_info: dict[str, Json] = {"index": texture_index}
3747
+
3748
+ texture_info_scale = convert.float_or_none(
3749
+ getattr(gltf2_io_texture_info, "scale", None)
3750
+ )
3751
+ if texture_info_scale is not None:
3752
+ texture_info["scale"] = texture_info_scale
3753
+
3754
+ texture_info_strength = convert.float_or_none(
3755
+ getattr(gltf2_io_texture_info, "strength", None)
3756
+ )
3757
+ if texture_info_strength is not None:
3758
+ texture_info["strength"] = texture_info_strength
3759
+
3760
+ return make_json(texture_info)
3761
+
3762
+ def create_mtoon1_downgraded_texture(
3763
+ self,
3764
+ texture: Union[Mtoon0TexturePropertyGroup, Mtoon1TexturePropertyGroup],
3765
+ texture_properties: dict[str, int],
3766
+ texture_properties_key: str,
3767
+ vector_properties: dict[str, Sequence[float]],
3768
+ texture_dicts: list[dict[str, Json]],
3769
+ sampler_dicts: list[dict[str, Json]],
3770
+ image_dicts: list[dict[str, Json]],
3771
+ buffer_view_dicts: list[dict[str, Json]],
3772
+ buffer0: bytearray,
3773
+ image_name_to_image_index: dict[str, int],
3774
+ ) -> Optional[dict[str, Json]]:
3775
+ source = texture.get_connected_node_image()
3776
+ if not source:
3777
+ return None
3778
+
3779
+ wrap_s = Mtoon1SamplerPropertyGroup.wrap_enum.identifier_to_value(
3780
+ texture.sampler.wrap_s,
3781
+ Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.value,
3782
+ )
3783
+
3784
+ wrap_t = Mtoon1SamplerPropertyGroup.wrap_enum.identifier_to_value(
3785
+ texture.sampler.wrap_t,
3786
+ Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.value,
3787
+ )
3788
+
3789
+ mag_filter = Mtoon1SamplerPropertyGroup.mag_filter_enum.identifier_to_value(
3790
+ texture.sampler.mag_filter,
3791
+ Mtoon1SamplerPropertyGroup.MAG_FILTER_DEFAULT.value,
3792
+ )
3793
+
3794
+ min_filter = Mtoon1SamplerPropertyGroup.min_filter_enum.identifier_to_value(
3795
+ texture.sampler.min_filter,
3796
+ Mtoon1SamplerPropertyGroup.MIN_FILTER_DEFAULT.value,
3797
+ )
3798
+
3799
+ sampler_dict: dict[str, Json] = {
3800
+ "magFilter": mag_filter,
3801
+ "minFilter": min_filter,
3802
+ "wrapS": wrap_s,
3803
+ "wrapT": wrap_t,
3804
+ }
3805
+ if sampler_dict in sampler_dicts:
3806
+ sampler_index = sampler_dicts.index(sampler_dict)
3807
+ else:
3808
+ sampler_index = len(sampler_dicts)
3809
+ sampler_dicts.append(sampler_dict)
3810
+
3811
+ image_index = self.find_or_create_image(
3812
+ image_dicts,
3813
+ buffer_view_dicts,
3814
+ buffer0,
3815
+ image_name_to_image_index,
3816
+ source,
3817
+ )
3818
+
3819
+ texture_dict: dict[str, Json] = {
3820
+ "sampler": sampler_index,
3821
+ "source": image_index,
3822
+ }
3823
+ if texture_dict in texture_dicts:
3824
+ texture_index = texture_dicts.index(texture_dict)
3825
+ else:
3826
+ texture_index = len(texture_dicts)
3827
+ texture_dicts.append(texture_dict)
3828
+
3829
+ texture_properties[texture_properties_key] = texture_index
3830
+ vector_properties[texture_properties_key] = [0, 0, 1, 1]
3831
+
3832
+ texture_info: dict[str, Json] = {"index": texture_index}
3833
+ return texture_info
3834
+
3835
+ def create_mtoon1_downgraded_texture_info(
3836
+ self,
3837
+ texture_info: Mtoon1TextureInfoPropertyGroup,
3838
+ texture_properties: dict[str, int],
3839
+ texture_properties_key: str,
3840
+ vector_properties: dict[str, Sequence[float]],
3841
+ texture_dicts: list[dict[str, Json]],
3842
+ sampler_dicts: list[dict[str, Json]],
3843
+ image_dicts: list[dict[str, Json]],
3844
+ buffer_view_dicts: list[dict[str, Json]],
3845
+ buffer0: bytearray,
3846
+ image_name_to_image_index: dict[str, int],
3847
+ khr_texture_transform: Mtoon1KhrTextureTransformPropertyGroup,
3848
+ extensions_used: list[str],
3849
+ ) -> Optional[dict[str, Json]]:
3850
+ texture_info_dict = self.create_mtoon1_downgraded_texture(
3851
+ texture_info.index,
3852
+ texture_properties,
3853
+ texture_properties_key,
3854
+ vector_properties,
3855
+ texture_dicts,
3856
+ sampler_dicts,
3857
+ image_dicts,
3858
+ buffer_view_dicts,
3859
+ buffer0,
3860
+ image_name_to_image_index,
3861
+ )
3862
+ if texture_info_dict is None:
3863
+ return None
3864
+
3865
+ if not khr_texture_transform.is_default():
3866
+ texture_info_dict["extensions"] = {
3867
+ "KHR_texture_transform": {
3868
+ "offset": list(khr_texture_transform.offset),
3869
+ "scale": list(khr_texture_transform.scale),
3870
+ }
3871
+ }
3872
+ if "KHR_texture_transform" not in extensions_used:
3873
+ extensions_used.append("KHR_texture_transform")
3874
+ return texture_info_dict
3875
+
3876
+ @staticmethod
3877
+ def create_no_morph_normal_export_vertex_indices(
3878
+ context: Context,
3879
+ mesh_data: Mesh,
3880
+ ) -> set[int]:
3881
+ # Collect vertex indices where normal difference is forced to zero
3882
+ # setting is enabled
3883
+ exclusion_vertex_indices: set[int] = set()
3884
+ material_refs = mesh_data.materials
3885
+ for polygon in mesh_data.polygons:
3886
+ material_ref_index = polygon.material_index
3887
+ if not (0 <= material_ref_index < len(material_refs)):
3888
+ continue
3889
+ material_ref = material_refs[material_ref_index]
3890
+ if material_ref is None:
3891
+ continue
3892
+ # Use non-evaluated material
3893
+ material = context.blend_data.materials.get(material_ref.name)
3894
+ if material is None:
3895
+ continue
3896
+ material_extension = get_material_extension(material)
3897
+ if material_extension.mtoon1.export_shape_key_normals:
3898
+ continue
3899
+ if material_extension.mtoon1.enabled or (
3900
+ (legacy_addon_material := LegacyAddonMaterial.try_parse(material))
3901
+ and legacy_addon_material.shader_name == "MToon_unversioned"
3902
+ ):
3903
+ exclusion_vertex_indices.update(polygon.vertices)
3904
+
3905
+ return exclusion_vertex_indices
3906
+
3907
+ @staticmethod
3908
+ def create_export_vertex_normal_vectors(
3909
+ no_morph_normal_export_vertex_indices: set[int],
3910
+ mesh_data: Mesh,
3911
+ ) -> list[Vector]:
3912
+ # Collect normal values for each shape key
3913
+ # Use split (loop) normals instead of vertex normals
3914
+ # https://github.com/KhronosGroup/glTF-Blender-IO/pull/1129
3915
+ vertex_normal_sum_vectors = [Vector([0.0, 0.0, 0.0])] * len(mesh_data.vertices)
3916
+ for loop_triangle in mesh_data.loop_triangles:
3917
+ for vertex_index, normal in zip(
3918
+ loop_triangle.vertices, loop_triangle.split_normals
3919
+ ):
3920
+ if vertex_index in no_morph_normal_export_vertex_indices:
3921
+ continue
3922
+ if not (0 <= vertex_index < len(vertex_normal_sum_vectors)):
3923
+ continue
3924
+ vertex_normal_sum_vectors[vertex_index] = (
3925
+ # Normally we would use the += operator, but for some
3926
+ # reason the result changes so we don't use it
3927
+ vertex_normal_sum_vectors[vertex_index] + Vector(normal)
3928
+ )
3929
+ return [
3930
+ Vrm0Exporter.safe_normalized_vector(vector)
3931
+ for vector in vertex_normal_sum_vectors
3932
+ ]
3933
+
3934
+ @staticmethod
3935
+ def create_vertex_index_to_morph_normal_diffs(
3936
+ no_morph_normal_export_vertex_indices: set[int],
3937
+ shape_key_mesh_data: Mesh,
3938
+ reference_vertex_normal_vectors: list[Vector],
3939
+ ) -> Sequence[tuple[float, float, float]]:
3940
+ vertex_normal_vectors = Vrm0Exporter.create_export_vertex_normal_vectors(
3941
+ no_morph_normal_export_vertex_indices,
3942
+ shape_key_mesh_data,
3943
+ )
3944
+ # Collect normal differences from reference key for each shape key
3945
+ vertex_index_to_morph_normal_diffs = [
3946
+ (
3947
+ vertex_normal_vector.x - reference_vertex_normal_vector.x,
3948
+ vertex_normal_vector.y - reference_vertex_normal_vector.y,
3949
+ vertex_normal_vector.z - reference_vertex_normal_vector.z,
3950
+ )
3951
+ for vertex_normal_vector, reference_vertex_normal_vector in zip(
3952
+ vertex_normal_vectors,
3953
+ reference_vertex_normal_vectors,
3954
+ )
3955
+ ]
3956
+ return vertex_index_to_morph_normal_diffs
3957
+
3958
+ @staticmethod
3959
+ def safe_normalized_vector(vector: Vector) -> Vector:
3960
+ x, y, z = vector
3961
+
3962
+ x_inf = math.isinf(x)
3963
+ y_inf = math.isinf(y)
3964
+ z_inf = math.isinf(z)
3965
+ if x_inf or y_inf or z_inf:
3966
+ x = math.copysign(1.0 if x_inf else 0, x)
3967
+ y = math.copysign(1.0 if y_inf else 0, y)
3968
+ z = math.copysign(1.0 if z_inf else 0, z)
3969
+ return Vector((x, y, z)).normalized()
3970
+
3971
+ x_nan = math.isnan(x)
3972
+ y_nan = math.isnan(y)
3973
+ z_nan = math.isnan(z)
3974
+ if x_nan or y_nan or z_nan:
3975
+ if x_nan:
3976
+ x = 0.0
3977
+ if y_nan:
3978
+ y = 0.0
3979
+ if z_nan:
3980
+ z = 0.0
3981
+ vector = Vector((x, y, z))
3982
+
3983
+ if vector.length_squared < float_info.epsilon:
3984
+ return Vector((0.0, 0.0, -1.0))
3985
+
3986
+ return vector.normalized()
3987
+
3988
+ def have_skin(self, mesh: Object) -> bool:
3989
+ # TODO: This method has false positives but is kept as-is for compatibility.
3990
+ # In the future, this will be replaced with correct implementation
3991
+ while mesh:
3992
+ if any(
3993
+ modifier.show_viewport and modifier.type == "ARMATURE"
3994
+ for modifier in mesh.modifiers
3995
+ ):
3996
+ return True
3997
+ parent_mesh = mesh.parent
3998
+ if not parent_mesh:
3999
+ return True
4000
+ if (
4001
+ mesh.parent_type == "BONE"
4002
+ and parent_mesh.type == "ARMATURE"
4003
+ and mesh.parent_bone
4004
+ ):
4005
+ return False
4006
+ if (
4007
+ mesh.parent_type != "OBJECT"
4008
+ or parent_mesh.type not in search.MESH_CONVERTIBLE_OBJECT_TYPES
4009
+ ):
4010
+ return True
4011
+ mesh = parent_mesh
4012
+ return True
4013
+
4014
+ def get_asset_generator(self) -> str:
4015
+ addon_version = get_addon_version()
4016
+ if environ.get("BLENDER_VRM_USE_TEST_EXPORTER_VERSION") == "true":
4017
+ addon_version = (999, 999, 999)
4018
+ return "saturday06_blender_vrm_exporter_experimental_" + ".".join(
4019
+ map(str, addon_version)
4020
+ )