1 # SPDX-License-Identifier: Apache-2.0
2 # Copyright 2018-2021 The glTF-Blender-IO authors.
5 from mathutils
import Vector
6 from ...io
.com
.gltf2_io_debug
import print_console
7 from ...io
.com
.gltf2_io_constants
import NORMALS_ROUNDING_DIGIT
8 from ...io
.exp
.gltf2_io_user_extensions
import export_user_extensions
9 from ...io
.com
import gltf2_io_constants
10 from ..com
import gltf2_blender_conversion
11 from . import gltf2_blender_gather_skins
13 def extract_primitives(blender_mesh
, uuid_for_skined_data
, blender_vertex_groups
, modifiers
, export_settings
):
14 """Extract primitives from a mesh."""
15 print_console('INFO', 'Extracting primitive: ' + blender_mesh
.name
)
17 primitive_creator
= PrimitiveCreator(blender_mesh
, uuid_for_skined_data
, blender_vertex_groups
, modifiers
, export_settings
)
18 primitive_creator
.prepare_data()
19 primitive_creator
.define_attributes()
20 primitive_creator
.create_dots_data_structure()
21 primitive_creator
.populate_dots_data()
22 primitive_creator
.primitive_split()
23 return primitive_creator
.primitive_creation()
25 class PrimitiveCreator
:
26 def __init__(self
, blender_mesh
, uuid_for_skined_data
, blender_vertex_groups
, modifiers
, export_settings
):
27 self
.blender_mesh
= blender_mesh
28 self
.uuid_for_skined_data
= uuid_for_skined_data
29 self
.blender_vertex_groups
= blender_vertex_groups
30 self
.modifiers
= modifiers
31 self
.export_settings
= export_settings
34 def apply_mat_to_all(cls
, matrix
, vectors
):
35 """Given matrix m and vectors [v1,v2,...], computes [m@v1,m@v2,...]"""
37 m
= matrix
.to_3x3() if len(matrix
) == 4 else matrix
38 res
= np
.matmul(vectors
, np
.array(m
.transposed()))
41 res
+= np
.array(matrix
.translation
)
45 def normalize_vecs(cls
, vectors
):
46 norms
= np
.linalg
.norm(vectors
, axis
=1, keepdims
=True)
47 np
.divide(vectors
, norms
, out
=vectors
, where
=norms
!= 0)
50 def zup2yup(cls
, array
):
52 array
[:, [1,2]] = array
[:, [2,1]] # x,z,y
53 array
[:, 2] *= -1 # x,z,-y
55 def prepare_data(self
):
56 self
.blender_object
= None
57 if self
.uuid_for_skined_data
:
58 self
.blender_object
= self
.export_settings
['vtree'].nodes
[self
.uuid_for_skined_data
].blender_object
60 self
.use_normals
= self
.export_settings
['gltf_normals']
62 self
.blender_mesh
.calc_normals_split()
64 self
.use_tangents
= False
65 if self
.use_normals
and self
.export_settings
['gltf_tangents']:
66 if self
.blender_mesh
.uv_layers
.active
and len(self
.blender_mesh
.uv_layers
) > 0:
68 self
.blender_mesh
.calc_tangents()
69 self
.use_tangents
= True
71 print_console('WARNING', 'Could not calculate tangents. Please try to triangulate the mesh first.')
73 self
.tex_coord_max
= 0
74 if self
.export_settings
['gltf_texcoords']:
75 if self
.blender_mesh
.uv_layers
.active
:
76 self
.tex_coord_max
= len(self
.blender_mesh
.uv_layers
)
78 self
.use_morph_normals
= self
.use_normals
and self
.export_settings
['gltf_morph_normal']
79 self
.use_morph_tangents
= self
.use_morph_normals
and self
.use_tangents
and self
.export_settings
['gltf_morph_tangent']
81 self
.use_materials
= self
.export_settings
['gltf_materials']
83 self
.blender_attributes
= []
85 # Check if we have to export skin
88 if self
.export_settings
['gltf_skins']:
89 if self
.modifiers
is not None:
90 modifiers_dict
= {m
.type: m
for m
in self
.modifiers
}
91 if "ARMATURE" in modifiers_dict
:
92 modifier
= modifiers_dict
["ARMATURE"]
93 self
.armature
= modifier
.object
95 # Skin must be ignored if the object is parented to a bone of the armature
96 # (This creates an infinite recursive error)
97 # So ignoring skin in that case
100 self
.blender_object
and
101 self
.blender_object
.parent_type
== "BONE" and
102 self
.blender_object
.parent
.name
== self
.armature
.name
108 self
.skin
= gltf2_blender_gather_skins
.gather_skin(self
.export_settings
['vtree'].nodes
[self
.uuid_for_skined_data
].armature
, self
.export_settings
)
113 if self
.blender_mesh
.shape_keys
and self
.export_settings
['gltf_morph']:
116 for key_block
in self
.blender_mesh
.shape_keys
.key_blocks
117 if not (key_block
== key_block
.relative_key
or key_block
.mute
)
120 # Fetch vert positions and bone data (joint,weights)
123 self
.morph_locs
= None
124 self
.__get
_positions
()
127 self
.__get
_bone
_data
()
128 if self
.need_neutral_bone
is True:
129 # Need to create a fake joint at root of armature
130 # In order to assign not assigned vertices to it
131 # But for now, this is not yet possible, we need to wait the armature node is created
132 # Just store this, to be used later
133 armature_uuid
= self
.export_settings
['vtree'].nodes
[self
.uuid_for_skined_data
].armature
134 self
.export_settings
['vtree'].nodes
[armature_uuid
].need_neutral_bone
= True
136 def define_attributes(self
):
140 def __init__(self
, attr_name
):
141 self
.attr_name
= attr_name
142 self
.keep
= attr_name
.startswith("_")
144 # Manage attributes + COLOR_0
145 for blender_attribute_index
, blender_attribute
in enumerate(self
.blender_mesh
.attributes
):
148 attr
['blender_attribute_index'] = blender_attribute_index
149 attr
['blender_name'] = blender_attribute
.name
150 attr
['blender_domain'] = blender_attribute
.domain
151 attr
['blender_data_type'] = blender_attribute
.data_type
153 # For now, we don't export edge data, because I need to find how to
154 # get from edge data to dots data
155 if attr
['blender_domain'] == "EDGE":
158 # Some type are not exportable (example : String)
159 if gltf2_blender_conversion
.get_component_type(blender_attribute
.data_type
) is None or \
160 gltf2_blender_conversion
.get_data_type(blender_attribute
.data_type
) is None:
164 if self
.blender_mesh
.color_attributes
.find(blender_attribute
.name
) == self
.blender_mesh
.color_attributes
.render_color_index \
165 and self
.blender_mesh
.color_attributes
.render_color_index
!= -1:
167 if self
.export_settings
['gltf_colors'] is False:
169 attr
['gltf_attribute_name'] = 'COLOR_0'
170 attr
['get'] = self
.get_function()
172 # Seems we sometime can have name collision about attributes
173 # Avoid crash and ignoring one of duplicated attribute name
174 if 'COLOR_0' in [a
['gltf_attribute_name'] for a
in self
.blender_attributes
]:
175 print_console('WARNING', 'Attribute (vertex color) collision name: ' + blender_attribute
.name
+ ", ignoring one of them")
180 # Keep only attributes that starts with _
181 # As Blender create lots of attributes that are internal / not needed are as duplicated of standard glTF accessors (position, uv, material_index...)
182 if self
.export_settings
['gltf_attributes'] is False:
184 # Check if there is an extension that want to keep this attribute, or change the exported name
185 keep_attribute
= KeepAttribute(blender_attribute
.name
)
187 export_user_extensions('gather_attribute_keep', self
.export_settings
, keep_attribute
)
189 if keep_attribute
.keep
is False:
192 attr
['gltf_attribute_name'] = keep_attribute
.attr_name
.upper()
193 attr
['get'] = self
.get_function()
195 # Seems we sometime can have name collision about attributes
196 # Avoid crash and ignoring one of duplicated attribute name
197 if attr
['gltf_attribute_name'] in [a
['gltf_attribute_name'] for a
in self
.blender_attributes
]:
198 print_console('WARNING', 'Attribute collision name: ' + blender_attribute
.name
+ ", ignoring one of them")
201 self
.blender_attributes
.append(attr
)
205 attr
['blender_data_type'] = 'FLOAT_VECTOR'
206 attr
['blender_domain'] = 'POINT'
207 attr
['gltf_attribute_name'] = 'POSITION'
208 attr
['set'] = self
.set_function()
209 attr
['skip_getting_to_dots'] = True
210 self
.blender_attributes
.append(attr
)
215 attr
['blender_data_type'] = 'FLOAT_VECTOR'
216 attr
['blender_domain'] = 'CORNER'
217 attr
['gltf_attribute_name'] = 'NORMAL'
218 attr
['gltf_attribute_name_morph'] = 'MORPH_NORMAL_'
219 attr
['get'] = self
.get_function()
220 self
.blender_attributes
.append(attr
)
222 # Manage uvs TEX_COORD_x
223 for tex_coord_i
in range(self
.tex_coord_max
):
225 attr
['blender_data_type'] = 'FLOAT2'
226 attr
['blender_domain'] = 'CORNER'
227 attr
['gltf_attribute_name'] = 'TEXCOORD_' + str(tex_coord_i
)
228 attr
['get'] = self
.get_function()
229 self
.blender_attributes
.append(attr
)
232 if self
.use_tangents
:
234 attr
['blender_data_type'] = 'FLOAT_VECTOR_4'
235 attr
['blender_domain'] = 'CORNER'
236 attr
['gltf_attribute_name'] = 'TANGENT'
237 attr
['get'] = self
.get_function()
238 self
.blender_attributes
.append(attr
)
240 # Manage MORPH_POSITION_x
241 for morph_i
, vs
in enumerate(self
.morph_locs
):
243 attr
['blender_attribute_index'] = morph_i
244 attr
['blender_data_type'] = 'FLOAT_VECTOR'
245 attr
['blender_domain'] = 'POINT'
246 attr
['gltf_attribute_name'] = 'MORPH_POSITION_' + str(morph_i
)
247 attr
['skip_getting_to_dots'] = True
248 attr
['set'] = self
.set_function()
249 self
.blender_attributes
.append(attr
)
251 # Manage MORPH_NORMAL_x
252 if self
.use_morph_normals
:
254 attr
['blender_attribute_index'] = morph_i
255 attr
['blender_data_type'] = 'FLOAT_VECTOR'
256 attr
['blender_domain'] = 'CORNER'
257 attr
['gltf_attribute_name'] = 'MORPH_NORMAL_' + str(morph_i
)
258 # No get function is set here, because data are set from NORMALS
259 self
.blender_attributes
.append(attr
)
261 # Manage MORPH_TANGENT_x
262 # This is a particular case, where we need to have the following data already calculated
266 # So, the following needs to be AFTER the 3 others.
267 if self
.use_morph_tangents
:
269 attr
['blender_attribute_index'] = morph_i
270 attr
['blender_data_type'] = 'FLOAT_VECTOR'
271 attr
['blender_domain'] = 'CORNER'
272 attr
['gltf_attribute_name'] = 'MORPH_TANGENT_' + str(morph_i
)
273 attr
['gltf_attribute_name_normal'] = "NORMAL"
274 attr
['gltf_attribute_name_morph_normal'] = "MORPH_NORMAL_" + str(morph_i
)
275 attr
['gltf_attribute_name_tangent'] = "TANGENT"
276 attr
['skip_getting_to_dots'] = True
277 attr
['set'] = self
.set_function()
278 self
.blender_attributes
.append(attr
)
280 for attr
in self
.blender_attributes
:
281 attr
['len'] = gltf2_blender_conversion
.get_data_length(attr
['blender_data_type'])
282 attr
['type'] = gltf2_blender_conversion
.get_numpy_type(attr
['blender_data_type'])
285 # Now we have all attribtues, we can change order if we want
286 # Note that the glTF specification doesn't say anything about order
287 # Attributes are defined only by name
288 # But if user want it in a particular order, he can use this hook to perform it
289 export_user_extensions('gather_attributes_change', self
.export_settings
, self
.blender_attributes
)
291 def create_dots_data_structure(self
):
292 # Now that we get all attributes that are going to be exported, create numpy array that will store them
293 dot_fields
= [('vertex_index', np
.uint32
)]
294 if self
.export_settings
['gltf_loose_edges']:
295 dot_fields_edges
= [('vertex_index', np
.uint32
)]
296 if self
.export_settings
['gltf_loose_points']:
297 dot_fields_points
= [('vertex_index', np
.uint32
)]
298 for attr
in self
.blender_attributes
:
299 if 'skip_getting_to_dots' in attr
:
301 for i
in range(attr
['len']):
302 dot_fields
.append((attr
['gltf_attribute_name'] + str(i
), attr
['type']))
303 if attr
['blender_domain'] != 'POINT':
305 if self
.export_settings
['gltf_loose_edges']:
306 dot_fields_edges
.append((attr
['gltf_attribute_name'] + str(i
), attr
['type']))
307 if self
.export_settings
['gltf_loose_points']:
308 dot_fields_points
.append((attr
['gltf_attribute_name'] + str(i
), attr
['type']))
310 # In Blender there is both per-vert data, like position, and also per-loop
311 # (loop=corner-of-poly) data, like normals or UVs. glTF only has per-vert
312 # data, so we need to split Blender verts up into potentially-multiple glTF
315 # First, we'll collect a "dot" for every loop: a struct that stores all the
316 # attributes at that loop, namely the vertex index (which determines all
317 # per-vert data), and all the per-loop data like UVs, etc.
319 # Each unique dot will become one unique glTF vert.
321 self
.dots
= np
.empty(len(self
.blender_mesh
.loops
), dtype
=np
.dtype(dot_fields
))
324 if self
.export_settings
['gltf_loose_edges']:
325 loose_edges
= [e
for e
in self
.blender_mesh
.edges
if e
.is_loose
]
326 self
.blender_idxs_edges
= [vi
for e
in loose_edges
for vi
in e
.vertices
]
327 self
.blender_idxs_edges
= np
.array(self
.blender_idxs_edges
, dtype
=np
.uint32
)
329 self
.dots_edges
= np
.empty(len(self
.blender_idxs_edges
), dtype
=np
.dtype(dot_fields_edges
))
330 self
.dots_edges
['vertex_index'] = self
.blender_idxs_edges
333 if self
.export_settings
['gltf_loose_points']:
334 verts_in_edge
= set(vi
for e
in self
.blender_mesh
.edges
for vi
in e
.vertices
)
335 self
.blender_idxs_points
= [
336 vi
for vi
, _
in enumerate(self
.blender_mesh
.vertices
)
337 if vi
not in verts_in_edge
339 self
.blender_idxs_points
= np
.array(self
.blender_idxs_points
, dtype
=np
.uint32
)
341 self
.dots_points
= np
.empty(len(self
.blender_idxs_points
), dtype
=np
.dtype(dot_fields_points
))
342 self
.dots_points
['vertex_index'] = self
.blender_idxs_points
345 def populate_dots_data(self
):
346 vidxs
= np
.empty(len(self
.blender_mesh
.loops
))
347 self
.blender_mesh
.loops
.foreach_get('vertex_index', vidxs
)
348 self
.dots
['vertex_index'] = vidxs
351 for attr
in self
.blender_attributes
:
352 if 'skip_getting_to_dots' in attr
:
354 if 'get' not in attr
:
358 def primitive_split(self
):
359 # Calculate triangles and sort them into primitives.
361 self
.blender_mesh
.calc_loop_triangles()
362 loop_indices
= np
.empty(len(self
.blender_mesh
.loop_triangles
) * 3, dtype
=np
.uint32
)
363 self
.blender_mesh
.loop_triangles
.foreach_get('loops', loop_indices
)
365 self
.prim_indices
= {} # maps material index to TRIANGLES-style indices into dots
367 if self
.use_materials
== "NONE": # Only for None. For placeholder and export, keep primitives
368 # Put all vertices into one primitive
369 self
.prim_indices
[-1] = loop_indices
372 # Bucket by material index.
374 tri_material_idxs
= np
.empty(len(self
.blender_mesh
.loop_triangles
), dtype
=np
.uint32
)
375 self
.blender_mesh
.loop_triangles
.foreach_get('material_index', tri_material_idxs
)
376 loop_material_idxs
= np
.repeat(tri_material_idxs
, 3) # material index for every loop
377 unique_material_idxs
= np
.unique(tri_material_idxs
)
378 del tri_material_idxs
380 for material_idx
in unique_material_idxs
:
381 self
.prim_indices
[material_idx
] = loop_indices
[loop_material_idxs
== material_idx
]
383 def primitive_creation(self
):
386 for material_idx
, dot_indices
in self
.prim_indices
.items():
387 # Extract just dots used by this primitive, deduplicate them, and
388 # calculate indices into this deduplicated list.
389 self
.prim_dots
= self
.dots
[dot_indices
]
390 self
.prim_dots
, indices
= np
.unique(self
.prim_dots
, return_inverse
=True)
392 if len(self
.prim_dots
) == 0:
395 # Now just move all the data for prim_dots into attribute arrays
399 self
.blender_idxs
= self
.prim_dots
['vertex_index']
401 for attr
in self
.blender_attributes
:
405 self
.__set
_regular
_attribute
(attr
)
408 joints
= [[] for _
in range(self
.num_joint_sets
)]
409 weights
= [[] for _
in range(self
.num_joint_sets
)]
411 for vi
in self
.blender_idxs
:
412 bones
= self
.vert_bones
[vi
]
413 for j
in range(0, 4 * self
.num_joint_sets
):
415 joint
, weight
= bones
[j
]
417 joint
, weight
= 0, 0.0
418 joints
[j
//4].append(joint
)
419 weights
[j
//4].append(weight
)
421 for i
, (js
, ws
) in enumerate(zip(joints
, weights
)):
422 self
.attributes
['JOINTS_%d' % i
] = js
423 self
.attributes
['WEIGHTS_%d' % i
] = ws
426 'attributes': self
.attributes
,
428 'material': material_idx
431 if self
.export_settings
['gltf_loose_edges']:
433 if self
.blender_idxs_edges
.shape
[0] > 0:
434 # Export one glTF vert per unique Blender vert in a loose edge
435 self
.blender_idxs
= self
.blender_idxs_edges
436 dots_edges
, indices
= np
.unique(self
.dots_edges
, return_inverse
=True)
437 self
.blender_idxs
= np
.unique(self
.blender_idxs_edges
)
441 for attr
in self
.blender_attributes
:
442 if attr
['blender_domain'] != 'POINT':
447 res
= np
.empty((len(dots_edges
), attr
['len']), dtype
=attr
['type'])
448 for i
in range(attr
['len']):
449 res
[:, i
] = dots_edges
[attr
['gltf_attribute_name'] + str(i
)]
450 self
.attributes
[attr
['gltf_attribute_name']] = {}
451 self
.attributes
[attr
['gltf_attribute_name']]["data"] = res
452 self
.attributes
[attr
['gltf_attribute_name']]["component_type"] = gltf2_blender_conversion
.get_component_type(attr
['blender_data_type'])
453 self
.attributes
[attr
['gltf_attribute_name']]["data_type"] = gltf2_blender_conversion
.get_data_type(attr
['blender_data_type'])
457 joints
= [[] for _
in range(self
.num_joint_sets
)]
458 weights
= [[] for _
in range(self
.num_joint_sets
)]
460 for vi
in self
.blender_idxs
:
461 bones
= self
.vert_bones
[vi
]
462 for j
in range(0, 4 * self
.num_joint_sets
):
464 joint
, weight
= bones
[j
]
466 joint
, weight
= 0, 0.0
467 joints
[j
//4].append(joint
)
468 weights
[j
//4].append(weight
)
470 for i
, (js
, ws
) in enumerate(zip(joints
, weights
)):
471 self
.attributes
['JOINTS_%d' % i
] = js
472 self
.attributes
['WEIGHTS_%d' % i
] = ws
475 'attributes': self
.attributes
,
481 if self
.export_settings
['gltf_loose_points']:
483 if self
.blender_idxs_points
.shape
[0] > 0:
484 self
.blender_idxs
= self
.blender_idxs_points
488 for attr
in self
.blender_attributes
:
489 if attr
['blender_domain'] != 'POINT':
494 res
= np
.empty((len(self
.blender_idxs
), attr
['len']), dtype
=attr
['type'])
495 for i
in range(attr
['len']):
496 res
[:, i
] = self
.dots_points
[attr
['gltf_attribute_name'] + str(i
)]
497 self
.attributes
[attr
['gltf_attribute_name']] = {}
498 self
.attributes
[attr
['gltf_attribute_name']]["data"] = res
499 self
.attributes
[attr
['gltf_attribute_name']]["component_type"] = gltf2_blender_conversion
.get_component_type(attr
['blender_data_type'])
500 self
.attributes
[attr
['gltf_attribute_name']]["data_type"] = gltf2_blender_conversion
.get_data_type(attr
['blender_data_type'])
504 joints
= [[] for _
in range(self
.num_joint_sets
)]
505 weights
= [[] for _
in range(self
.num_joint_sets
)]
507 for vi
in self
.blender_idxs
:
508 bones
= self
.vert_bones
[vi
]
509 for j
in range(0, 4 * self
.num_joint_sets
):
511 joint
, weight
= bones
[j
]
513 joint
, weight
= 0, 0.0
514 joints
[j
//4].append(joint
)
515 weights
[j
//4].append(weight
)
517 for i
, (js
, ws
) in enumerate(zip(joints
, weights
)):
518 self
.attributes
['JOINTS_%d' % i
] = js
519 self
.attributes
['WEIGHTS_%d' % i
] = ws
522 'attributes': self
.attributes
,
527 print_console('INFO', 'Primitives created: %d' % len(primitives
))
531 ################################## Get ##################################################
533 def __get_positions(self
):
534 self
.locs
= np
.empty(len(self
.blender_mesh
.vertices
) * 3, dtype
=np
.float32
)
535 source
= self
.key_blocks
[0].relative_key
.data
if self
.key_blocks
else self
.blender_mesh
.vertices
536 source
.foreach_get('co', self
.locs
)
537 self
.locs
= self
.locs
.reshape(len(self
.blender_mesh
.vertices
), 3)
540 for key_block
in self
.key_blocks
:
541 vs
= np
.empty(len(self
.blender_mesh
.vertices
) * 3, dtype
=np
.float32
)
542 key_block
.data
.foreach_get('co', vs
)
543 vs
= vs
.reshape(len(self
.blender_mesh
.vertices
), 3)
544 self
.morph_locs
.append(vs
)
546 # Transform for skinning
547 if self
.armature
and self
.blender_object
:
548 # apply_matrix = armature.matrix_world.inverted_safe() @ blender_object.matrix_world
549 # loc_transform = armature.matrix_world @ apply_matrix
551 loc_transform
= self
.blender_object
.matrix_world
552 self
.locs
[:] = PrimitiveCreator
.apply_mat_to_all(loc_transform
, self
.locs
)
553 for vs
in self
.morph_locs
:
554 vs
[:] = PrimitiveCreator
.apply_mat_to_all(loc_transform
, vs
)
556 # glTF stores deltas in morph targets
557 for vs
in self
.morph_locs
:
560 if self
.export_settings
['gltf_yup']:
561 PrimitiveCreator
.zup2yup(self
.locs
)
562 for vs
in self
.morph_locs
:
563 PrimitiveCreator
.zup2yup(vs
)
565 def get_function(self
):
567 def getting_function(attr
):
568 if attr
['gltf_attribute_name'] == "COLOR_0":
569 self
.__get
_color
_attribute
(attr
)
570 elif attr
['gltf_attribute_name'].startswith("_"):
571 self
.__get
_layer
_attribute
(attr
)
572 elif attr
['gltf_attribute_name'].startswith("TEXCOORD_"):
573 self
.__get
_uvs
_attribute
(int(attr
['gltf_attribute_name'].split("_")[-1]), attr
)
574 elif attr
['gltf_attribute_name'] == "NORMAL":
575 self
.__get
_normal
_attribute
(attr
)
576 elif attr
['gltf_attribute_name'] == "TANGENT":
577 self
.__get
_tangent
_attribute
(attr
)
579 return getting_function
582 def __get_color_attribute(self
, attr
):
583 blender_color_idx
= self
.blender_mesh
.color_attributes
.render_color_index
585 if attr
['blender_domain'] == "POINT":
586 colors
= np
.empty(len(self
.blender_mesh
.vertices
) * 4, dtype
=np
.float32
)
587 elif attr
['blender_domain'] == "CORNER":
588 colors
= np
.empty(len(self
.blender_mesh
.loops
) * 4, dtype
=np
.float32
)
589 self
.blender_mesh
.color_attributes
[blender_color_idx
].data
.foreach_get('color', colors
)
590 if attr
['blender_domain'] == "POINT":
591 colors
= colors
.reshape(-1, 4)
592 data_dots
= colors
[self
.dots
['vertex_index']]
593 if self
.export_settings
['gltf_loose_edges']:
594 data_dots_edges
= colors
[self
.dots_edges
['vertex_index']]
595 if self
.export_settings
['gltf_loose_points']:
596 data_dots_points
= colors
[self
.dots_points
['vertex_index']]
598 elif attr
['blender_domain'] == "CORNER":
599 colors
= colors
.reshape(-1, 4)
603 # colors are already linear, no need to switch color space
605 self
.dots
[attr
['gltf_attribute_name'] + str(i
)] = data_dots
[:, i
]
606 if self
.export_settings
['gltf_loose_edges'] and attr
['blender_domain'] == "POINT":
607 self
.dots_edges
[attr
['gltf_attribute_name'] + str(i
)] = data_dots_edges
[:, i
]
608 if self
.export_settings
['gltf_loose_points'] and attr
['blender_domain'] == "POINT":
609 self
.dots_points
[attr
['gltf_attribute_name'] + str(i
)] = data_dots_points
[:, i
]
611 def __get_layer_attribute(self
, attr
):
612 if attr
['blender_domain'] in ['CORNER']:
613 data
= np
.empty(len(self
.blender_mesh
.loops
) * attr
['len'], dtype
=attr
['type'])
614 elif attr
['blender_domain'] in ['POINT']:
615 data
= np
.empty(len(self
.blender_mesh
.vertices
) * attr
['len'], dtype
=attr
['type'])
616 elif attr
['blender_domain'] in ['EDGE']:
617 data
= np
.empty(len(self
.blender_mesh
.edges
) * attr
['len'], dtype
=attr
['type'])
618 elif attr
['blender_domain'] in ['FACE']:
619 data
= np
.empty(len(self
.blender_mesh
.polygons
) * attr
['len'], dtype
=attr
['type'])
621 print_console("ERROR", "domain not known")
623 if attr
['blender_data_type'] == "BYTE_COLOR":
624 self
.blender_mesh
.attributes
[attr
['blender_attribute_index']].data
.foreach_get('color', data
)
625 data
= data
.reshape(-1, attr
['len'])
626 elif attr
['blender_data_type'] == "INT8":
627 self
.blender_mesh
.attributes
[attr
['blender_attribute_index']].data
.foreach_get('value', data
)
628 data
= data
.reshape(-1, attr
['len'])
629 elif attr
['blender_data_type'] == "FLOAT2":
630 self
.blender_mesh
.attributes
[attr
['blender_attribute_index']].data
.foreach_get('vector', data
)
631 data
= data
.reshape(-1, attr
['len'])
632 elif attr
['blender_data_type'] == "BOOLEAN":
633 self
.blender_mesh
.attributes
[attr
['blender_attribute_index']].data
.foreach_get('value', data
)
634 data
= data
.reshape(-1, attr
['len'])
635 elif attr
['blender_data_type'] == "STRING":
636 self
.blender_mesh
.attributes
[attr
['blender_attribute_index']].data
.foreach_get('value', data
)
637 data
= data
.reshape(-1, attr
['len'])
638 elif attr
['blender_data_type'] == "FLOAT_COLOR":
639 self
.blender_mesh
.attributes
[attr
['blender_attribute_index']].data
.foreach_get('color', data
)
640 data
= data
.reshape(-1, attr
['len'])
641 elif attr
['blender_data_type'] == "FLOAT_VECTOR":
642 self
.blender_mesh
.attributes
[attr
['blender_attribute_index']].data
.foreach_get('vector', data
)
643 data
= data
.reshape(-1, attr
['len'])
644 elif attr
['blender_data_type'] == "FLOAT_VECTOR_4": # Specific case for tangent
646 elif attr
['blender_data_type'] == "INT":
647 self
.blender_mesh
.attributes
[attr
['blender_attribute_index']].data
.foreach_get('value', data
)
648 data
= data
.reshape(-1, attr
['len'])
649 elif attr
['blender_data_type'] == "FLOAT":
650 self
.blender_mesh
.attributes
[attr
['blender_attribute_index']].data
.foreach_get('value', data
)
651 data
= data
.reshape(-1, attr
['len'])
653 print_console('ERROR',"blender type not found " + attr
['blender_data_type'])
655 if attr
['blender_domain'] in ['CORNER']:
656 for i
in range(attr
['len']):
657 self
.dots
[attr
['gltf_attribute_name'] + str(i
)] = data
[:, i
]
658 elif attr
['blender_domain'] in ['POINT']:
660 data
= data
.reshape(-1, attr
['len'])
661 data_dots
= data
[self
.dots
['vertex_index']]
662 if self
.export_settings
['gltf_loose_edges']:
663 data_dots_edges
= data
[self
.dots_edges
['vertex_index']]
664 if self
.export_settings
['gltf_loose_points']:
665 data_dots_points
= data
[self
.dots_points
['vertex_index']]
666 for i
in range(attr
['len']):
667 self
.dots
[attr
['gltf_attribute_name'] + str(i
)] = data_dots
[:, i
]
668 if self
.export_settings
['gltf_loose_edges']:
669 self
.dots_edges
[attr
['gltf_attribute_name'] + str(i
)] = data_dots_edges
[:, i
]
670 if self
.export_settings
['gltf_loose_points']:
671 self
.dots_points
[attr
['gltf_attribute_name'] + str(i
)] = data_dots_points
[:, i
]
672 elif attr
['blender_domain'] in ['EDGE']:
673 # No edge attribute exports
675 elif attr
['blender_domain'] in ['FACE']:
677 data
= data
.reshape(-1, attr
['len'])
678 # data contains face attribute, and is len(faces) long
679 # We need to dispatch these len(faces) attribute in each dots lines
680 data_attr
= np
.empty(self
.dots
.shape
[0] * attr
['len'], dtype
=attr
['type'])
681 data_attr
= data_attr
.reshape(-1, attr
['len'])
682 for idx
, poly
in enumerate(self
.blender_mesh
.polygons
):
683 data_attr
[list(poly
.loop_indices
)] = data
[idx
]
684 data_attr
= data_attr
.reshape(-1, attr
['len'])
685 for i
in range(attr
['len']):
686 self
.dots
[attr
['gltf_attribute_name'] + str(i
)] = data_attr
[:, i
]
689 print_console("ERROR", "domain not known")
691 def __get_uvs_attribute(self
, blender_uv_idx
, attr
):
692 layer
= self
.blender_mesh
.uv_layers
[blender_uv_idx
]
693 uvs
= np
.empty(len(self
.blender_mesh
.loops
) * 2, dtype
=np
.float32
)
694 layer
.data
.foreach_get('uv', uvs
)
695 uvs
= uvs
.reshape(len(self
.blender_mesh
.loops
), 2)
697 # Blender UV space -> glTF UV space
702 self
.dots
[attr
['gltf_attribute_name'] + '0'] = uvs
[:, 0]
703 self
.dots
[attr
['gltf_attribute_name'] + '1'] = uvs
[:, 1]
706 def __get_normals(self
):
707 """Get normal for each loop."""
708 key_blocks
= self
.key_blocks
if self
.use_morph_normals
else []
710 self
.normals
= key_blocks
[0].relative_key
.normals_split_get()
711 self
.normals
= np
.array(self
.normals
, dtype
=np
.float32
)
713 self
.normals
= np
.empty(len(self
.blender_mesh
.loops
) * 3, dtype
=np
.float32
)
714 self
.blender_mesh
.calc_normals_split()
715 self
.blender_mesh
.loops
.foreach_get('normal', self
.normals
)
717 self
.normals
= self
.normals
.reshape(len(self
.blender_mesh
.loops
), 3)
719 self
.normals
= np
.round(self
.normals
, NORMALS_ROUNDING_DIGIT
)
720 # Force normalization of normals in case some normals are not (why ?)
721 PrimitiveCreator
.normalize_vecs(self
.normals
)
723 self
.morph_normals
= []
724 for key_block
in key_blocks
:
725 ns
= np
.array(key_block
.normals_split_get(), dtype
=np
.float32
)
726 ns
= ns
.reshape(len(self
.blender_mesh
.loops
), 3)
727 ns
= np
.round(ns
, NORMALS_ROUNDING_DIGIT
)
728 self
.morph_normals
.append(ns
)
730 # Transform for skinning
731 if self
.armature
and self
.blender_object
:
732 apply_matrix
= (self
.armature
.matrix_world
.inverted_safe() @ self
.blender_object
.matrix_world
)
733 apply_matrix
= apply_matrix
.to_3x3().inverted_safe().transposed()
734 normal_transform
= self
.armature
.matrix_world
.to_3x3() @ apply_matrix
736 self
.normals
[:] = PrimitiveCreator
.apply_mat_to_all(normal_transform
, self
.normals
)
737 PrimitiveCreator
.normalize_vecs(self
.normals
)
738 for ns
in self
.morph_normals
:
739 ns
[:] = PrimitiveCreator
.apply_mat_to_all(normal_transform
, ns
)
740 PrimitiveCreator
.normalize_vecs(ns
)
742 for ns
in [self
.normals
, *self
.morph_normals
]:
743 # Replace zero normals with the unit UP vector.
744 # Seems to happen sometimes with degenerate tris?
745 is_zero
= ~ns
.any(axis
=1)
748 # glTF stores deltas in morph targets
749 for ns
in self
.morph_normals
:
752 if self
.export_settings
['gltf_yup']:
753 PrimitiveCreator
.zup2yup(self
.normals
)
754 for ns
in self
.morph_normals
:
755 PrimitiveCreator
.zup2yup(ns
)
757 def __get_normal_attribute(self
, attr
):
759 self
.dots
[attr
['gltf_attribute_name'] + "0"] = self
.normals
[:, 0]
760 self
.dots
[attr
['gltf_attribute_name'] + "1"] = self
.normals
[:, 1]
761 self
.dots
[attr
['gltf_attribute_name'] + "2"] = self
.normals
[:, 2]
763 if self
.use_morph_normals
:
764 for morph_i
, ns
in enumerate(self
.morph_normals
):
765 self
.dots
[attr
['gltf_attribute_name_morph'] + str(morph_i
) + "0"] = ns
[:, 0]
766 self
.dots
[attr
['gltf_attribute_name_morph'] + str(morph_i
) + "1"] = ns
[:, 1]
767 self
.dots
[attr
['gltf_attribute_name_morph'] + str(morph_i
) + "2"] = ns
[:, 2]
769 del self
.morph_normals
771 def __get_tangent_attribute(self
, attr
):
772 self
.__get
_tangents
()
773 self
.dots
[attr
['gltf_attribute_name'] + "0"] = self
.tangents
[:, 0]
774 self
.dots
[attr
['gltf_attribute_name'] + "1"] = self
.tangents
[:, 1]
775 self
.dots
[attr
['gltf_attribute_name'] + "2"] = self
.tangents
[:, 2]
777 self
.__get
_bitangent
_signs
()
778 self
.dots
[attr
['gltf_attribute_name'] + "3"] = self
.signs
781 def __get_tangents(self
):
782 """Get an array of the tangent for each loop."""
783 self
.tangents
= np
.empty(len(self
.blender_mesh
.loops
) * 3, dtype
=np
.float32
)
784 self
.blender_mesh
.loops
.foreach_get('tangent', self
.tangents
)
785 self
.tangents
= self
.tangents
.reshape(len(self
.blender_mesh
.loops
), 3)
787 # Transform for skinning
788 if self
.armature
and self
.blender_object
:
789 apply_matrix
= self
.armature
.matrix_world
.inverted_safe() @ self
.blender_object
.matrix_world
790 tangent_transform
= apply_matrix
.to_quaternion().to_matrix()
791 self
.tangents
= PrimitiveCreator
.apply_mat_to_all(tangent_transform
, self
.tangents
)
792 PrimitiveCreator
.normalize_vecs(self
.tangents
)
794 if self
.export_settings
['gltf_yup']:
795 PrimitiveCreator
.zup2yup(self
.tangents
)
798 def __get_bitangent_signs(self
):
799 self
.signs
= np
.empty(len(self
.blender_mesh
.loops
), dtype
=np
.float32
)
800 self
.blender_mesh
.loops
.foreach_get('bitangent_sign', self
.signs
)
802 # Transform for skinning
803 if self
.armature
and self
.blender_object
:
804 # Bitangent signs should flip when handedness changes
806 apply_matrix
= self
.armature
.matrix_world
.inverted_safe() @ self
.blender_object
.matrix_world
807 tangent_transform
= apply_matrix
.to_quaternion().to_matrix()
808 flipped
= tangent_transform
.determinant() < 0
812 # No change for Zup -> Yup
815 def __get_bone_data(self
):
817 self
.need_neutral_bone
= False
818 min_influence
= 0.0001
820 joint_name_to_index
= {joint
.name
: index
for index
, joint
in enumerate(self
.skin
.joints
)}
821 group_to_joint
= [joint_name_to_index
.get(g
.name
) for g
in self
.blender_vertex_groups
]
823 # List of (joint, weight) pairs for each vert
825 max_num_influences
= 0
827 for vertex
in self
.blender_mesh
.vertices
:
830 for group_element
in vertex
.groups
:
831 weight
= group_element
.weight
832 if weight
<= min_influence
:
835 joint
= group_to_joint
[group_element
.group
]
840 bones
.append((joint
, weight
))
841 bones
.sort(key
=lambda x
: x
[1], reverse
=True)
843 # Is not assign to any bone
844 bones
= ((len(self
.skin
.joints
), 1.0),) # Assign to a joint that will be created later
845 self
.need_neutral_bone
= True
846 self
.vert_bones
.append(bones
)
847 if len(bones
) > max_num_influences
:
848 max_num_influences
= len(bones
)
850 # How many joint sets do we need? 1 set = 4 influences
851 self
.num_joint_sets
= (max_num_influences
+ 3) // 4
853 ##################################### Set ###################################
854 def set_function(self
):
856 def setting_function(attr
):
857 if attr
['gltf_attribute_name'] == "POSITION":
858 self
.__set
_positions
_attribute
(attr
)
859 elif attr
['gltf_attribute_name'].startswith("MORPH_POSITION_"):
860 self
.__set
_morph
_locs
_attribute
(attr
)
861 elif attr
['gltf_attribute_name'].startswith("MORPH_TANGENT_"):
862 self
.__set
_morph
_tangent
_attribute
(attr
)
864 return setting_function
866 def __set_positions_attribute(self
, attr
):
867 self
.attributes
[attr
['gltf_attribute_name']] = {}
868 self
.attributes
[attr
['gltf_attribute_name']]["data"] = self
.locs
[self
.blender_idxs
]
869 self
.attributes
[attr
['gltf_attribute_name']]["data_type"] = gltf2_io_constants
.DataType
.Vec3
870 self
.attributes
[attr
['gltf_attribute_name']]["component_type"] = gltf2_io_constants
.ComponentType
.Float
873 def __set_morph_locs_attribute(self
, attr
):
874 self
.attributes
[attr
['gltf_attribute_name']] = {}
875 self
.attributes
[attr
['gltf_attribute_name']]["data"] = self
.morph_locs
[attr
['blender_attribute_index']][self
.blender_idxs
]
877 def __set_morph_tangent_attribute(self
, attr
):
878 # Morph tangent are after these 3 others, so, they are already calculated
879 self
.normals
= self
.attributes
[attr
['gltf_attribute_name_normal']]["data"]
880 self
.morph_normals
= self
.attributes
[attr
['gltf_attribute_name_morph_normal']]["data"]
881 self
.tangents
= self
.attributes
[attr
['gltf_attribute_name_tangent']]["data"]
883 self
.__calc
_morph
_tangents
()
884 self
.attributes
[attr
['gltf_attribute_name']] = {}
885 self
.attributes
[attr
['gltf_attribute_name']]["data"] = self
.morph_tangents
887 def __calc_morph_tangents(self
):
888 # TODO: check if this works
889 self
.morph_tangents
= np
.empty((len(self
.normals
), 3), dtype
=np
.float32
)
891 for i
in range(len(self
.normals
)):
892 n
= Vector(self
.normals
[i
])
893 morph_n
= n
+ Vector(self
.morph_normals
[i
]) # convert back to non-delta
894 t
= Vector(self
.tangents
[i
, :3])
896 rotation
= morph_n
.rotation_difference(n
)
899 t_morph
.rotate(rotation
)
900 self
.morph_tangents
[i
] = t_morph
- t
# back to delta
902 def __set_regular_attribute(self
, attr
):
903 res
= np
.empty((len(self
.prim_dots
), attr
['len']), dtype
=attr
['type'])
904 for i
in range(attr
['len']):
905 res
[:, i
] = self
.prim_dots
[attr
['gltf_attribute_name'] + str(i
)]
906 self
.attributes
[attr
['gltf_attribute_name']] = {}
907 self
.attributes
[attr
['gltf_attribute_name']]["data"] = res
908 if 'gltf_attribute_name' == "NORMAL":
909 self
.attributes
[attr
['gltf_attribute_name']]["component_type"] = gltf2_io_constants
.ComponentType
.Float
910 self
.attributes
[attr
['gltf_attribute_name']]["data_type"] = gltf2_io_constants
.DataType
.Vec3
911 elif 'gltf_attribute_name' == "TANGENT":
912 self
.attributes
[attr
['gltf_attribute_name']]["component_type"] = gltf2_io_constants
.ComponentType
.Float
913 self
.attributes
[attr
['gltf_attribute_name']]["data_type"] = gltf2_io_constants
.DataType
.Vec4
914 elif attr
['gltf_attribute_name'].startswith('TEXCOORD_'):
915 self
.attributes
[attr
['gltf_attribute_name']]["component_type"] = gltf2_io_constants
.ComponentType
.Float
916 self
.attributes
[attr
['gltf_attribute_name']]["data_type"] = gltf2_io_constants
.DataType
.Vec2
918 self
.attributes
[attr
['gltf_attribute_name']]["component_type"] = gltf2_blender_conversion
.get_component_type(attr
['blender_data_type'])
919 self
.attributes
[attr
['gltf_attribute_name']]["data_type"] = gltf2_blender_conversion
.get_data_type(attr
['blender_data_type'])