Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export: write custom attribute arrays for non-skinning vgroups #1515

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion addons/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Comment on lines +345 to +350
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can lead to miss cache

modifiers = None

materials = tuple(ms.material for ms in blender_object.material_slots)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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

Expand Down Expand Up @@ -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