diff --git a/addons/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/addons/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index f7318c2b9..bacf9cafb 100644 --- a/addons/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/addons/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -85,11 +85,14 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert use_materials = export_settings[gltf2_blender_export_keys.MATERIALS] - # Fetch vert positions and bone data (joint,weights) + # Fetch vert data + use_facemaps = bool(blender_mesh.face_maps) + # Fetch vert positions and bone data (joint,weights) locs, morph_locs = __get_positions(blender_mesh, key_blocks, armature, blender_object, export_settings) if skin: vert_bones, num_joint_sets = __get_bone_data(blender_mesh, skin, blender_vertex_groups) + extra_vgroup_weights = __get_extra_vgroups(blender_mesh, modifiers, blender_vertex_groups) # In Blender there is both per-vert data, like position, and also per-loop # (loop=corner-of-poly) data, like normals or UVs. glTF only has per-vert @@ -124,6 +127,8 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert ('morph%dny' % morph_i, np.float32), ('morph%dnz' % morph_i, np.float32), ] + if use_facemaps: + dot_fields += [('facemaps', np.float32)] dots = np.empty(len(blender_mesh.loops), dtype=np.dtype(dot_fields)) @@ -171,6 +176,9 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert dots['color%da' % col_i] = colors[:, 3] del colors + if use_facemaps: + dots['facemaps'] = __get_facemaps(blender_mesh) + # Calculate triangles and sort them into primitives. blender_mesh.calc_loop_triangles() @@ -277,6 +285,12 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert attributes['JOINTS_%d' % i] = js attributes['WEIGHTS_%d' % i] = ws + for vgroup_name, weights in extra_vgroup_weights.items(): + attributes['_VG_' + vgroup_name] = weights[blender_idxs] + + if use_facemaps: + attributes['_FACEMAPS'] = prim_dots['facemaps'] + primitives.append({ 'attributes': attributes, 'indices': indices, @@ -541,6 +555,37 @@ def __get_colors(blender_mesh, color_i): return colors +def __get_extra_vgroups(blender_mesh, modifiers, blender_vertex_groups): + """Get vertex weights for vgroup that aren't used for skinning.""" + if not blender_vertex_groups: + return {} + + # Find vgroups used for skinning + skinning_vgroup_names = set() + for m in (modifiers or []): + if m.type == 'ARMATURE' and m.use_vertex_groups: + if m.object and m.object.type == 'ARMATURE': + for bone in m.object.data.bones: + skinning_vgroup_names.add(bone.name) + + if len(skinning_vgroup_names) == len(blender_vertex_groups): + return {} + + vgroup_weights = {} + for i, vgroup in enumerate(blender_vertex_groups): + if vgroup.name in skinning_vgroup_names: continue + weights = [] + for vertex in blender_mesh.vertices: + weight = 0.0 + for vge in vertex.groups: + if vge.group == i: + weight = vge.weight + break + weights.append(weight) + vgroup_weights[vgroup.name] = np.array(weights, dtype=np.float32) + return vgroup_weights + + def __get_bone_data(blender_mesh, skin, blender_vertex_groups): joint_name_to_index = {joint.name: index for index, joint in enumerate(skin.joints)} group_to_joint = [joint_name_to_index.get(g.name) for g in blender_vertex_groups] @@ -575,6 +620,20 @@ def __get_bone_data(blender_mesh, skin, blender_vertex_groups): return vert_bones, num_joint_sets +def __get_facemaps(blender_mesh): + """Gets a facemap index for each loop.""" + poly_facemap = np.empty(len(blender_mesh.polygons), dtype=np.float32) + blender_mesh.face_maps[0].data.foreach_get('value', poly_facemap) + + # Get polygon_index for each loop in the mesh + loop_polyidx = np.zeros(len(blender_mesh.loops), dtype=np.uint32) + for polyi, poly in enumerate(blender_mesh.polygons): + for i in range(poly.loop_start, poly.loop_start + poly.loop_total): + loop_polyidx[i] = polyi + + return poly_facemap[loop_polyidx] + + def __zup2yup(array): # x,y,z -> x,z,-y array[:, [1,2]] = array[:, [2,1]] # x,z,y diff --git a/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py index 13c347dc6..a574199af 100644 --- a/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py +++ b/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py @@ -286,9 +286,12 @@ def __gather_extensions(blender_object, export_settings): def __gather_extras(blender_object, export_settings): + extras = {} if export_settings['gltf_extras']: - return generate_extras(blender_object) - return None + extras = generate_extras(blender_object) or {} + if blender_object.face_maps: + extras['facemaps'] = [face_map.name for face_map in blender_object.face_maps] + return extras or None def __gather_matrix(blender_object, export_settings): @@ -339,12 +342,12 @@ def __gather_mesh(blender_object, library, export_settings): skip_filter = False # If no skin are exported, no need to have vertex group, this will create a cache miss if not export_settings[gltf2_blender_export_keys.SKINS]: - vertex_groups = None + #vertex_groups = None modifiers = None else: # Check if there is an armature modidier if len([mod for mod in blender_object.modifiers if mod.type == "ARMATURE"]) == 0: - vertex_groups = None # Not needed if no armature, avoid a cache miss + #vertex_groups = None # Not needed if no armature, avoid a cache miss modifiers = None materials = tuple(ms.material for ms in blender_object.material_slots) diff --git a/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py b/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py index 126037ab5..8c6712d43 100644 --- a/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py +++ b/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py @@ -34,6 +34,7 @@ def gather_primitive_attributes(blender_primitive, export_settings): attributes.update(__gather_texcoord(blender_primitive, export_settings)) attributes.update(__gather_colors(blender_primitive, export_settings)) attributes.update(__gather_skins(blender_primitive, export_settings)) + attributes.update(__gather_custom_data(blender_primitive, export_settings)) return attributes @@ -45,6 +46,9 @@ def array_to_accessor(array, component_type, data_type, include_max_and_min=Fals array = np.array(array, dtype=dtype) array = array.reshape(len(array) // num_elems, num_elems) + if num_elems == 1 and len(array.shape) == 1: + array = array.reshape(len(array), 1) + assert array.dtype == dtype assert array.shape[1] == num_elems @@ -213,3 +217,110 @@ def __gather_skins(blender_primitive, export_settings): joint_id = 'JOINTS_' + str(bone_set_index) weight_id = 'WEIGHTS_' + str(bone_set_index) return attributes + + +def custom_data_array_to_accessor(array): + if type(array) is not np.ndarray: + array = np.array(array, dtype=np.float32) + + assert array.dtype == np.float32 + assert len(array.shape) == 1 + + # Calculate how big a sparse array would be and switch to it if smaller + + indices_nonzero = np.nonzero(array)[0] + num_nonzero = len(indices_nonzero) + + if num_nonzero == 0: + return gltf2_io.Accessor( + count=len(array), + component_type=gltf2_io_constants.ComponentType.Float, + type=gltf2_io_constants.DataType.Scalar, + buffer_view=None, + byte_offset=None, + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + ) + + index_size = ( + 1 if indices_nonzero[-1] < 256 else + 2 if indices_nonzero[-1] < 65536 else + 4 + ) + value_size = 4 # float32 + + dense_bin_size = len(array) * value_size + sparse_bin_size = num_nonzero * (index_size + value_size) + bin_size_increase = sparse_bin_size - dense_bin_size + json_size_increase = 160 # approximate + net_size_increase = bin_size_increase + json_size_increase + + if net_size_increase >= 0: + # Dense is better + return array_to_accessor( + array, + component_type=gltf2_io_constants.ComponentType.Float, + data_type=gltf2_io_constants.DataType.Scalar, + ) + + index_type = ( + gltf2_io_constants.ComponentType.UnsignedByte if index_size == 1 else + gltf2_io_constants.ComponentType.UnsignedShort if index_size == 2 else + gltf2_io_constants.ComponentType.UnsignedInt + ) + index_dtype = gltf2_io_constants.ComponentType.to_numpy_dtype(index_type) + indices_nonzero = indices_nonzero.astype(index_dtype) + values_nonzero = array[indices_nonzero] + + return gltf2_io.Accessor( + buffer_view=None, + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(array), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + type=gltf2_io_constants.DataType.Scalar, + sparse=gltf2_io.AccessorSparse( + count=num_nonzero, + indices=gltf2_io.AccessorSparseIndices( + buffer_view=gltf2_io_binary_data.BinaryData(indices_nonzero.tobytes()), + component_type=index_type, + byte_offset=None, + extensions=None, + extras=None, + ), + values=gltf2_io.AccessorSparseValues( + buffer_view=gltf2_io_binary_data.BinaryData(values_nonzero.tobytes()), + byte_offset=None, + extensions=None, + extras=None, + ), + extensions=None, + extras=None, + ), + ) + + +def __gather_custom_data(blender_primitive, export_settings): + attributes = {} + for key in blender_primitive["attributes"]: + if key == '_FACEMAPS': + attributes[key] = array_to_accessor( + blender_primitive["attributes"][key], + component_type=gltf2_io_constants.ComponentType.Float, + data_type=gltf2_io_constants.DataType.Scalar, + ) + elif key.startswith('_VG_'): + attributes[key] = custom_data_array_to_accessor( + blender_primitive["attributes"][key], + ) + return attributes