glTF export avoid crash when collision name in attributes
[blender-addons.git] / io_scene_gltf2 / blender / exp / gltf2_blender_gather_primitives_extract.py
bloba41986d36a36e438103b2c234f323f9784be8312
1 # SPDX-License-Identifier: Apache-2.0
2 # Copyright 2018-2021 The glTF-Blender-IO authors.
4 import numpy as np
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
33 @classmethod
34 def apply_mat_to_all(cls, matrix, vectors):
35 """Given matrix m and vectors [v1,v2,...], computes [m@v1,m@v2,...]"""
36 # Linear part
37 m = matrix.to_3x3() if len(matrix) == 4 else matrix
38 res = np.matmul(vectors, np.array(m.transposed()))
39 # Translation part
40 if len(matrix) == 4:
41 res += np.array(matrix.translation)
42 return res
44 @classmethod
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)
49 @classmethod
50 def zup2yup(cls, array):
51 # x,y,z -> x,z,-y
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']
61 if self.use_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:
67 try:
68 self.blender_mesh.calc_tangents()
69 self.use_tangents = True
70 except Exception:
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
86 self.armature = None
87 self.skin = None
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
98 is_child_of_arma = (
99 self.armature and
100 self.blender_object and
101 self.blender_object.parent_type == "BONE" and
102 self.blender_object.parent.name == self.armature.name
104 if is_child_of_arma:
105 self.armature = None
107 if self.armature:
108 self.skin = gltf2_blender_gather_skins.gather_skin(self.export_settings['vtree'].nodes[self.uuid_for_skined_data].armature, self.export_settings)
109 if not self.skin:
110 self.armature = None
112 self.key_blocks = []
113 if self.blender_mesh.shape_keys and self.export_settings['gltf_morph']:
114 self.key_blocks = [
115 key_block
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)
122 self.locs = None
123 self.morph_locs = None
124 self.__get_positions()
126 if self.skin:
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):
139 class KeepAttribute:
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):
147 attr = {}
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":
156 continue
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:
162 continue
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:
168 continue
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")
176 continue
178 else:
179 # Custom attributes
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:
183 continue
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:
190 continue
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")
199 continue
201 self.blender_attributes.append(attr)
203 # Manage POSITION
204 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)
212 # Manage NORMALS
213 if self.use_normals:
214 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):
224 attr = {}
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)
231 # Manage TANGENT
232 if self.use_tangents:
233 attr = {}
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):
242 attr = {}
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:
253 attr = {}
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
263 # - NORMAL
264 # - MORPH_NORMAL
265 # - TANGENT
266 # So, the following needs to be AFTER the 3 others.
267 if self.use_morph_tangents:
268 attr = {}
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:
300 continue
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':
304 continue
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
313 # verts.
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))
323 # Find loose edges
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
332 # Find loose points
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
349 del vidxs
351 for attr in self.blender_attributes:
352 if 'skip_getting_to_dots' in attr:
353 continue
354 if 'get' not in attr:
355 continue
356 attr['get'](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
371 else:
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):
384 primitives = []
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:
393 continue
395 # Now just move all the data for prim_dots into attribute arrays
397 self.attributes = {}
399 self.blender_idxs = self.prim_dots['vertex_index']
401 for attr in self.blender_attributes:
402 if 'set' in attr:
403 attr['set'](attr)
404 else: # Regular case
405 self.__set_regular_attribute(attr)
407 if self.skin:
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):
414 if j < len(bones):
415 joint, weight = bones[j]
416 else:
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
425 primitives.append({
426 'attributes': self.attributes,
427 'indices': indices,
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)
439 self.attributes = {}
441 for attr in self.blender_attributes:
442 if attr['blender_domain'] != 'POINT':
443 continue
444 if 'set' in attr:
445 attr['set'](attr)
446 else:
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'])
456 if self.skin:
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):
463 if j < len(bones):
464 joint, weight = bones[j]
465 else:
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
474 primitives.append({
475 'attributes': self.attributes,
476 'indices': indices,
477 'mode': 1, # LINES
478 'material': 0
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
486 self.attributes = {}
488 for attr in self.blender_attributes:
489 if attr['blender_domain'] != 'POINT':
490 continue
491 if 'set' in attr:
492 attr['set'](attr)
493 else:
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'])
503 if self.skin:
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):
510 if j < len(bones):
511 joint, weight = bones[j]
512 else:
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
521 primitives.append({
522 'attributes': self.attributes,
523 'mode': 0, # POINTS
524 'material': 0
527 print_console('INFO', 'Primitives created: %d' % len(primitives))
529 return 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)
539 self.morph_locs = []
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:
558 vs -= self.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)
600 data_dots = colors
602 del colors
603 # colors are already linear, no need to switch color space
604 for i in range(4):
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'])
620 else:
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
645 pass
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'])
652 else:
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']:
659 if attr['len'] > 1:
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
674 pass
675 elif attr['blender_domain'] in ['FACE']:
676 if attr['len'] > 1:
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]
688 else:
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
698 # u,v -> u,1-v
699 uvs[:, 1] *= -1
700 uvs[:, 1] += 1
702 self.dots[attr['gltf_attribute_name'] + '0'] = uvs[:, 0]
703 self.dots[attr['gltf_attribute_name'] + '1'] = uvs[:, 1]
704 del uvs
706 def __get_normals(self):
707 """Get normal for each loop."""
708 key_blocks = self.key_blocks if self.use_morph_normals else []
709 if key_blocks:
710 self.normals = key_blocks[0].relative_key.normals_split_get()
711 self.normals = np.array(self.normals, dtype=np.float32)
712 else:
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)
746 ns[is_zero, 2] = 1
748 # glTF stores deltas in morph targets
749 for ns in self.morph_normals:
750 ns -= self.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):
758 self.__get_normals()
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]
768 del self.normals
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]
776 del self.tangents
777 self.__get_bitangent_signs()
778 self.dots[attr['gltf_attribute_name'] + "3"] = self.signs
779 del 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
805 # TODO: confirm
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
809 if flipped:
810 self.signs *= -1
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
824 self.vert_bones = []
825 max_num_influences = 0
827 for vertex in self.blender_mesh.vertices:
828 bones = []
829 if vertex.groups:
830 for group_element in vertex.groups:
831 weight = group_element.weight
832 if weight <= min_influence:
833 continue
834 try:
835 joint = group_to_joint[group_element.group]
836 except Exception:
837 continue
838 if joint is None:
839 continue
840 bones.append((joint, weight))
841 bones.sort(key=lambda x: x[1], reverse=True)
842 if not bones:
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)
898 t_morph = Vector(t)
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
917 else:
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'])