glTF exporter: Big gltf primitive extraction refactoring + Blender attributes export
This commit is contained in:
parent
e77b55e45a
commit
51e15a9db4
|
@ -4,7 +4,7 @@
|
|||
bl_info = {
|
||||
'name': 'glTF 2.0 format',
|
||||
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
|
||||
"version": (3, 4, 22),
|
||||
"version": (3, 4, 23),
|
||||
'blender': (3, 3, 0),
|
||||
'location': 'File > Import-Export',
|
||||
'description': 'Import-Export as glTF 2.0',
|
||||
|
@ -273,6 +273,12 @@ class ExportGLTF2_Base:
|
|||
default=True
|
||||
)
|
||||
|
||||
export_attributes: BoolProperty(
|
||||
name='Attributes',
|
||||
description='Export Attributes',
|
||||
default=False
|
||||
)
|
||||
|
||||
use_mesh_edges: BoolProperty(
|
||||
name='Loose Edges',
|
||||
description=(
|
||||
|
@ -579,6 +585,7 @@ class ExportGLTF2_Base:
|
|||
|
||||
export_settings['gltf_materials'] = self.export_materials
|
||||
export_settings['gltf_colors'] = self.export_colors
|
||||
export_settings['gltf_attributes'] = self.export_attributes
|
||||
export_settings['gltf_cameras'] = self.export_cameras
|
||||
|
||||
export_settings['gltf_original_specular'] = self.export_original_specular
|
||||
|
@ -808,6 +815,7 @@ class GLTF_PT_export_geometry_mesh(bpy.types.Panel):
|
|||
col.active = operator.export_normals
|
||||
col.prop(operator, 'export_tangents')
|
||||
layout.prop(operator, 'export_colors')
|
||||
layout.prop(operator, 'export_attributes')
|
||||
|
||||
col = layout.column()
|
||||
col.prop(operator, 'use_mesh_edges')
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
# Copyright 2018-2021 The glTF-Blender-IO authors.
|
||||
|
||||
from math import sin, cos
|
||||
import numpy as np
|
||||
from io_scene_gltf2.io.com import gltf2_io_constants
|
||||
|
||||
def texture_transform_blender_to_gltf(mapping_transform):
|
||||
"""
|
||||
|
@ -48,3 +50,55 @@ def get_target(property):
|
|||
"scale": "scale",
|
||||
"value": "weights"
|
||||
}.get(property)
|
||||
|
||||
def get_component_type(attribute_component_type):
|
||||
return {
|
||||
"INT8": gltf2_io_constants.ComponentType.Float,
|
||||
"BYTE_COLOR": gltf2_io_constants.ComponentType.UnsignedShort,
|
||||
"FLOAT2": gltf2_io_constants.ComponentType.Float,
|
||||
"FLOAT_COLOR": gltf2_io_constants.ComponentType.Float,
|
||||
"FLOAT_VECTOR": gltf2_io_constants.ComponentType.Float,
|
||||
"FLOAT_VECTOR_4": gltf2_io_constants.ComponentType.Float,
|
||||
"INT": gltf2_io_constants.ComponentType.Float, # No signed Int in glTF accessor
|
||||
"FLOAT": gltf2_io_constants.ComponentType.Float,
|
||||
"BOOLEAN": gltf2_io_constants.ComponentType.Float
|
||||
}.get(attribute_component_type)
|
||||
|
||||
def get_data_type(attribute_component_type):
|
||||
return {
|
||||
"INT8": gltf2_io_constants.DataType.Scalar,
|
||||
"BYTE_COLOR": gltf2_io_constants.DataType.Vec4,
|
||||
"FLOAT2": gltf2_io_constants.DataType.Vec2,
|
||||
"FLOAT_COLOR": gltf2_io_constants.DataType.Vec4,
|
||||
"FLOAT_VECTOR": gltf2_io_constants.DataType.Vec3,
|
||||
"FLOAT_VECTOR_4": gltf2_io_constants.DataType.Vec4,
|
||||
"INT": gltf2_io_constants.DataType.Scalar,
|
||||
"FLOAT": gltf2_io_constants.DataType.Scalar,
|
||||
"BOOLEAN": gltf2_io_constants.DataType.Scalar,
|
||||
}.get(attribute_component_type)
|
||||
|
||||
def get_data_length(attribute_component_type):
|
||||
return {
|
||||
"INT8": 1,
|
||||
"BYTE_COLOR": 4,
|
||||
"FLOAT2": 2,
|
||||
"FLOAT_COLOR": 4,
|
||||
"FLOAT_VECTOR": 3,
|
||||
"FLOAT_VECTOR_4": 4,
|
||||
"INT": 1,
|
||||
"FLOAT": 1,
|
||||
"BOOLEAN": 1
|
||||
}.get(attribute_component_type)
|
||||
|
||||
def get_numpy_type(attribute_component_type):
|
||||
return {
|
||||
"INT8": np.float32,
|
||||
"BYTE_COLOR": np.float32,
|
||||
"FLOAT2": np.float32,
|
||||
"FLOAT_COLOR": np.float32,
|
||||
"FLOAT_VECTOR": np.float32,
|
||||
"FLOAT_VECTOR_4": np.float32,
|
||||
"INT": np.float32, #signed integer are not supported by glTF
|
||||
"FLOAT": np.float32,
|
||||
"BOOLEAN": np.float32
|
||||
}.get(attribute_component_type)
|
||||
|
|
|
@ -1,619 +0,0 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2018-2021 The glTF-Blender-IO authors.
|
||||
|
||||
import numpy as np
|
||||
from mathutils import Vector
|
||||
|
||||
from . import gltf2_blender_export_keys
|
||||
from ...io.com.gltf2_io_debug import print_console
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes
|
||||
|
||||
|
||||
def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings):
|
||||
"""Extract primitives from a mesh."""
|
||||
print_console('INFO', 'Extracting primitive: ' + blender_mesh.name)
|
||||
|
||||
blender_object = None
|
||||
if uuid_for_skined_data:
|
||||
blender_object = export_settings['vtree'].nodes[uuid_for_skined_data].blender_object
|
||||
|
||||
use_normals = export_settings[gltf2_blender_export_keys.NORMALS]
|
||||
if use_normals:
|
||||
blender_mesh.calc_normals_split()
|
||||
|
||||
use_tangents = False
|
||||
if use_normals and export_settings[gltf2_blender_export_keys.TANGENTS]:
|
||||
if blender_mesh.uv_layers.active and len(blender_mesh.uv_layers) > 0:
|
||||
try:
|
||||
blender_mesh.calc_tangents()
|
||||
use_tangents = True
|
||||
except Exception:
|
||||
print_console('WARNING', 'Could not calculate tangents. Please try to triangulate the mesh first.')
|
||||
|
||||
tex_coord_max = 0
|
||||
if export_settings[gltf2_blender_export_keys.TEX_COORDS]:
|
||||
if blender_mesh.uv_layers.active:
|
||||
tex_coord_max = len(blender_mesh.uv_layers)
|
||||
|
||||
color_max = 0
|
||||
if export_settings[gltf2_blender_export_keys.COLORS]:
|
||||
color_max = len(blender_mesh.vertex_colors)
|
||||
|
||||
colors_attributes = []
|
||||
rendered_color_idx = blender_mesh.attributes.render_color_index
|
||||
|
||||
if color_max > 0:
|
||||
colors_attributes.append(rendered_color_idx)
|
||||
# Then find other ones
|
||||
colors_attributes.extend([
|
||||
i for i in range(len(blender_mesh.color_attributes)) if i != rendered_color_idx \
|
||||
and blender_mesh.vertex_colors.find(blender_mesh.color_attributes[i].name) != -1
|
||||
])
|
||||
|
||||
|
||||
armature = None
|
||||
skin = None
|
||||
if blender_vertex_groups and export_settings[gltf2_blender_export_keys.SKINS]:
|
||||
if modifiers is not None:
|
||||
modifiers_dict = {m.type: m for m in modifiers}
|
||||
if "ARMATURE" in modifiers_dict:
|
||||
modifier = modifiers_dict["ARMATURE"]
|
||||
armature = modifier.object
|
||||
|
||||
# Skin must be ignored if the object is parented to a bone of the armature
|
||||
# (This creates an infinite recursive error)
|
||||
# So ignoring skin in that case
|
||||
is_child_of_arma = (
|
||||
armature and
|
||||
blender_object and
|
||||
blender_object.parent_type == "BONE" and
|
||||
blender_object.parent.name == armature.name
|
||||
)
|
||||
if is_child_of_arma:
|
||||
armature = None
|
||||
|
||||
if armature:
|
||||
skin = gltf2_blender_gather_nodes.gather_skin(uuid_for_skined_data, export_settings)
|
||||
if not skin:
|
||||
armature = None
|
||||
|
||||
use_morph_normals = use_normals and export_settings[gltf2_blender_export_keys.MORPH_NORMAL]
|
||||
use_morph_tangents = use_morph_normals and use_tangents and export_settings[gltf2_blender_export_keys.MORPH_TANGENT]
|
||||
|
||||
key_blocks = []
|
||||
# Shape Keys can't be retrieve when using Apply Modifiers (Blender/bpy limitation)
|
||||
if export_settings[gltf2_blender_export_keys.APPLY] is False and blender_mesh.shape_keys and export_settings[gltf2_blender_export_keys.MORPH]:
|
||||
key_blocks = [
|
||||
key_block
|
||||
for key_block in blender_mesh.shape_keys.key_blocks
|
||||
if not (key_block == key_block.relative_key or key_block.mute)
|
||||
]
|
||||
|
||||
use_materials = export_settings[gltf2_blender_export_keys.MATERIALS]
|
||||
|
||||
# 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, need_neutral_bone = __get_bone_data(blender_mesh, skin, blender_vertex_groups)
|
||||
if need_neutral_bone is True:
|
||||
# Need to create a fake joint at root of armature
|
||||
# In order to assign not assigned vertices to it
|
||||
# But for now, this is not yet possible, we need to wait the armature node is created
|
||||
# Just store this, to be used later
|
||||
armature_uuid = export_settings['vtree'].nodes[uuid_for_skined_data].armature
|
||||
export_settings['vtree'].nodes[armature_uuid].need_neutral_bone = True
|
||||
|
||||
# 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
|
||||
# data, so we need to split Blender verts up into potentially-multiple glTF
|
||||
# verts.
|
||||
#
|
||||
# First, we'll collect a "dot" for every loop: a struct that stores all the
|
||||
# attributes at that loop, namely the vertex index (which determines all
|
||||
# per-vert data), and all the per-loop data like UVs, etc.
|
||||
#
|
||||
# Each unique dot will become one unique glTF vert.
|
||||
|
||||
# List all fields the dot struct needs.
|
||||
dot_fields = [('vertex_index', np.uint32)]
|
||||
if use_normals:
|
||||
dot_fields += [('nx', np.float32), ('ny', np.float32), ('nz', np.float32)]
|
||||
if use_tangents:
|
||||
dot_fields += [('tx', np.float32), ('ty', np.float32), ('tz', np.float32), ('tw', np.float32)]
|
||||
for uv_i in range(tex_coord_max):
|
||||
dot_fields += [('uv%dx' % uv_i, np.float32), ('uv%dy' % uv_i, np.float32)]
|
||||
for col_i, _ in enumerate(colors_attributes):
|
||||
dot_fields += [
|
||||
('color%dr' % col_i, np.float32),
|
||||
('color%dg' % col_i, np.float32),
|
||||
('color%db' % col_i, np.float32),
|
||||
('color%da' % col_i, np.float32),
|
||||
]
|
||||
if use_morph_normals:
|
||||
for morph_i, _ in enumerate(key_blocks):
|
||||
dot_fields += [
|
||||
('morph%dnx' % morph_i, np.float32),
|
||||
('morph%dny' % morph_i, np.float32),
|
||||
('morph%dnz' % morph_i, np.float32),
|
||||
]
|
||||
|
||||
dots = np.empty(len(blender_mesh.loops), dtype=np.dtype(dot_fields))
|
||||
|
||||
vidxs = np.empty(len(blender_mesh.loops))
|
||||
blender_mesh.loops.foreach_get('vertex_index', vidxs)
|
||||
dots['vertex_index'] = vidxs
|
||||
del vidxs
|
||||
|
||||
if use_normals:
|
||||
kbs = key_blocks if use_morph_normals else []
|
||||
normals, morph_normals = __get_normals(
|
||||
blender_mesh, kbs, armature, blender_object, export_settings
|
||||
)
|
||||
dots['nx'] = normals[:, 0]
|
||||
dots['ny'] = normals[:, 1]
|
||||
dots['nz'] = normals[:, 2]
|
||||
del normals
|
||||
for morph_i, ns in enumerate(morph_normals):
|
||||
dots['morph%dnx' % morph_i] = ns[:, 0]
|
||||
dots['morph%dny' % morph_i] = ns[:, 1]
|
||||
dots['morph%dnz' % morph_i] = ns[:, 2]
|
||||
del morph_normals
|
||||
|
||||
if use_tangents:
|
||||
tangents = __get_tangents(blender_mesh, armature, blender_object, export_settings)
|
||||
dots['tx'] = tangents[:, 0]
|
||||
dots['ty'] = tangents[:, 1]
|
||||
dots['tz'] = tangents[:, 2]
|
||||
del tangents
|
||||
signs = __get_bitangent_signs(blender_mesh, armature, blender_object, export_settings)
|
||||
dots['tw'] = signs
|
||||
del signs
|
||||
|
||||
for uv_i in range(tex_coord_max):
|
||||
uvs = __get_uvs(blender_mesh, uv_i)
|
||||
dots['uv%dx' % uv_i] = uvs[:, 0]
|
||||
dots['uv%dy' % uv_i] = uvs[:, 1]
|
||||
del uvs
|
||||
|
||||
colors_types = []
|
||||
for col_i, blender_col_i in enumerate(colors_attributes):
|
||||
colors, colors_type, domain = __get_colors(blender_mesh, col_i, blender_col_i)
|
||||
if domain == "POINT":
|
||||
colors = colors[dots['vertex_index']]
|
||||
colors_types.append(colors_type)
|
||||
dots['color%dr' % col_i] = colors[:, 0]
|
||||
dots['color%dg' % col_i] = colors[:, 1]
|
||||
dots['color%db' % col_i] = colors[:, 2]
|
||||
dots['color%da' % col_i] = colors[:, 3]
|
||||
del colors
|
||||
|
||||
# Calculate triangles and sort them into primitives.
|
||||
|
||||
blender_mesh.calc_loop_triangles()
|
||||
loop_indices = np.empty(len(blender_mesh.loop_triangles) * 3, dtype=np.uint32)
|
||||
blender_mesh.loop_triangles.foreach_get('loops', loop_indices)
|
||||
|
||||
prim_indices = {} # maps material index to TRIANGLES-style indices into dots
|
||||
|
||||
if use_materials == "NONE": # Only for None. For placeholder and export, keep primitives
|
||||
# Put all vertices into one primitive
|
||||
prim_indices[-1] = loop_indices
|
||||
|
||||
else:
|
||||
# Bucket by material index.
|
||||
|
||||
tri_material_idxs = np.empty(len(blender_mesh.loop_triangles), dtype=np.uint32)
|
||||
blender_mesh.loop_triangles.foreach_get('material_index', tri_material_idxs)
|
||||
loop_material_idxs = np.repeat(tri_material_idxs, 3) # material index for every loop
|
||||
unique_material_idxs = np.unique(tri_material_idxs)
|
||||
del tri_material_idxs
|
||||
|
||||
for material_idx in unique_material_idxs:
|
||||
prim_indices[material_idx] = loop_indices[loop_material_idxs == material_idx]
|
||||
|
||||
# Create all the primitives.
|
||||
|
||||
primitives = []
|
||||
|
||||
for material_idx, dot_indices in prim_indices.items():
|
||||
# Extract just dots used by this primitive, deduplicate them, and
|
||||
# calculate indices into this deduplicated list.
|
||||
prim_dots = dots[dot_indices]
|
||||
prim_dots, indices = np.unique(prim_dots, return_inverse=True)
|
||||
|
||||
if len(prim_dots) == 0:
|
||||
continue
|
||||
|
||||
# Now just move all the data for prim_dots into attribute arrays
|
||||
|
||||
attributes = {}
|
||||
|
||||
blender_idxs = prim_dots['vertex_index']
|
||||
|
||||
attributes['POSITION'] = locs[blender_idxs]
|
||||
|
||||
for morph_i, vs in enumerate(morph_locs):
|
||||
attributes['MORPH_POSITION_%d' % morph_i] = vs[blender_idxs]
|
||||
|
||||
if use_normals:
|
||||
normals = np.empty((len(prim_dots), 3), dtype=np.float32)
|
||||
normals[:, 0] = prim_dots['nx']
|
||||
normals[:, 1] = prim_dots['ny']
|
||||
normals[:, 2] = prim_dots['nz']
|
||||
attributes['NORMAL'] = normals
|
||||
|
||||
if use_tangents:
|
||||
tangents = np.empty((len(prim_dots), 4), dtype=np.float32)
|
||||
tangents[:, 0] = prim_dots['tx']
|
||||
tangents[:, 1] = prim_dots['ty']
|
||||
tangents[:, 2] = prim_dots['tz']
|
||||
tangents[:, 3] = prim_dots['tw']
|
||||
attributes['TANGENT'] = tangents
|
||||
|
||||
if use_morph_normals:
|
||||
for morph_i, _ in enumerate(key_blocks):
|
||||
ns = np.empty((len(prim_dots), 3), dtype=np.float32)
|
||||
ns[:, 0] = prim_dots['morph%dnx' % morph_i]
|
||||
ns[:, 1] = prim_dots['morph%dny' % morph_i]
|
||||
ns[:, 2] = prim_dots['morph%dnz' % morph_i]
|
||||
attributes['MORPH_NORMAL_%d' % morph_i] = ns
|
||||
|
||||
if use_morph_tangents:
|
||||
attributes['MORPH_TANGENT_%d' % morph_i] = __calc_morph_tangents(normals, ns, tangents)
|
||||
|
||||
for tex_coord_i in range(tex_coord_max):
|
||||
uvs = np.empty((len(prim_dots), 2), dtype=np.float32)
|
||||
uvs[:, 0] = prim_dots['uv%dx' % tex_coord_i]
|
||||
uvs[:, 1] = prim_dots['uv%dy' % tex_coord_i]
|
||||
attributes['TEXCOORD_%d' % tex_coord_i] = uvs
|
||||
|
||||
for color_i, _ in enumerate(colors_attributes):
|
||||
colors = np.empty((len(prim_dots), 4), dtype=np.float32)
|
||||
colors[:, 0] = prim_dots['color%dr' % color_i]
|
||||
colors[:, 1] = prim_dots['color%dg' % color_i]
|
||||
colors[:, 2] = prim_dots['color%db' % color_i]
|
||||
colors[:, 3] = prim_dots['color%da' % color_i]
|
||||
attributes['COLOR_%d' % color_i] = {}
|
||||
attributes['COLOR_%d' % color_i]["data"] = colors
|
||||
|
||||
attributes['COLOR_%d' % color_i]["norm"] = colors_types[color_i] == "BYTE_COLOR"
|
||||
|
||||
if skin:
|
||||
joints = [[] for _ in range(num_joint_sets)]
|
||||
weights = [[] for _ in range(num_joint_sets)]
|
||||
|
||||
for vi in blender_idxs:
|
||||
bones = vert_bones[vi]
|
||||
for j in range(0, 4 * num_joint_sets):
|
||||
if j < len(bones):
|
||||
joint, weight = bones[j]
|
||||
else:
|
||||
joint, weight = 0, 0.0
|
||||
joints[j//4].append(joint)
|
||||
weights[j//4].append(weight)
|
||||
|
||||
for i, (js, ws) in enumerate(zip(joints, weights)):
|
||||
attributes['JOINTS_%d' % i] = js
|
||||
attributes['WEIGHTS_%d' % i] = ws
|
||||
|
||||
primitives.append({
|
||||
'attributes': attributes,
|
||||
'indices': indices,
|
||||
'material': material_idx,
|
||||
})
|
||||
|
||||
if export_settings['gltf_loose_edges']:
|
||||
# Find loose edges
|
||||
loose_edges = [e for e in blender_mesh.edges if e.is_loose]
|
||||
blender_idxs = [vi for e in loose_edges for vi in e.vertices]
|
||||
|
||||
if blender_idxs:
|
||||
# Export one glTF vert per unique Blender vert in a loose edge
|
||||
blender_idxs = np.array(blender_idxs, dtype=np.uint32)
|
||||
blender_idxs, indices = np.unique(blender_idxs, return_inverse=True)
|
||||
|
||||
attributes = {}
|
||||
|
||||
attributes['POSITION'] = locs[blender_idxs]
|
||||
|
||||
for morph_i, vs in enumerate(morph_locs):
|
||||
attributes['MORPH_POSITION_%d' % morph_i] = vs[blender_idxs]
|
||||
|
||||
if skin:
|
||||
joints = [[] for _ in range(num_joint_sets)]
|
||||
weights = [[] for _ in range(num_joint_sets)]
|
||||
|
||||
for vi in blender_idxs:
|
||||
bones = vert_bones[vi]
|
||||
for j in range(0, 4 * num_joint_sets):
|
||||
if j < len(bones):
|
||||
joint, weight = bones[j]
|
||||
else:
|
||||
joint, weight = 0, 0.0
|
||||
joints[j//4].append(joint)
|
||||
weights[j//4].append(weight)
|
||||
|
||||
for i, (js, ws) in enumerate(zip(joints, weights)):
|
||||
attributes['JOINTS_%d' % i] = js
|
||||
attributes['WEIGHTS_%d' % i] = ws
|
||||
|
||||
primitives.append({
|
||||
'attributes': attributes,
|
||||
'indices': indices,
|
||||
'mode': 1, # LINES
|
||||
'material': 0,
|
||||
})
|
||||
|
||||
if export_settings['gltf_loose_points']:
|
||||
# Find loose points
|
||||
verts_in_edge = set(vi for e in blender_mesh.edges for vi in e.vertices)
|
||||
blender_idxs = [
|
||||
vi for vi, _ in enumerate(blender_mesh.vertices)
|
||||
if vi not in verts_in_edge
|
||||
]
|
||||
|
||||
if blender_idxs:
|
||||
blender_idxs = np.array(blender_idxs, dtype=np.uint32)
|
||||
|
||||
attributes = {}
|
||||
|
||||
attributes['POSITION'] = locs[blender_idxs]
|
||||
|
||||
for morph_i, vs in enumerate(morph_locs):
|
||||
attributes['MORPH_POSITION_%d' % morph_i] = vs[blender_idxs]
|
||||
|
||||
if skin:
|
||||
joints = [[] for _ in range(num_joint_sets)]
|
||||
weights = [[] for _ in range(num_joint_sets)]
|
||||
|
||||
for vi in blender_idxs:
|
||||
bones = vert_bones[vi]
|
||||
for j in range(0, 4 * num_joint_sets):
|
||||
if j < len(bones):
|
||||
joint, weight = bones[j]
|
||||
else:
|
||||
joint, weight = 0, 0.0
|
||||
joints[j//4].append(joint)
|
||||
weights[j//4].append(weight)
|
||||
|
||||
for i, (js, ws) in enumerate(zip(joints, weights)):
|
||||
attributes['JOINTS_%d' % i] = js
|
||||
attributes['WEIGHTS_%d' % i] = ws
|
||||
|
||||
primitives.append({
|
||||
'attributes': attributes,
|
||||
'mode': 0, # POINTS
|
||||
'material': 0,
|
||||
})
|
||||
|
||||
print_console('INFO', 'Primitives created: %d' % len(primitives))
|
||||
|
||||
return primitives
|
||||
|
||||
|
||||
def __get_positions(blender_mesh, key_blocks, armature, blender_object, export_settings):
|
||||
locs = np.empty(len(blender_mesh.vertices) * 3, dtype=np.float32)
|
||||
source = key_blocks[0].relative_key.data if key_blocks else blender_mesh.vertices
|
||||
source.foreach_get('co', locs)
|
||||
locs = locs.reshape(len(blender_mesh.vertices), 3)
|
||||
|
||||
morph_locs = []
|
||||
for key_block in key_blocks:
|
||||
vs = np.empty(len(blender_mesh.vertices) * 3, dtype=np.float32)
|
||||
key_block.data.foreach_get('co', vs)
|
||||
vs = vs.reshape(len(blender_mesh.vertices), 3)
|
||||
morph_locs.append(vs)
|
||||
|
||||
# Transform for skinning
|
||||
if armature and blender_object:
|
||||
# apply_matrix = armature.matrix_world.inverted_safe() @ blender_object.matrix_world
|
||||
# loc_transform = armature.matrix_world @ apply_matrix
|
||||
|
||||
loc_transform = blender_object.matrix_world
|
||||
locs[:] = __apply_mat_to_all(loc_transform, locs)
|
||||
for vs in morph_locs:
|
||||
vs[:] = __apply_mat_to_all(loc_transform, vs)
|
||||
|
||||
# glTF stores deltas in morph targets
|
||||
for vs in morph_locs:
|
||||
vs -= locs
|
||||
|
||||
if export_settings[gltf2_blender_export_keys.YUP]:
|
||||
__zup2yup(locs)
|
||||
for vs in morph_locs:
|
||||
__zup2yup(vs)
|
||||
|
||||
return locs, morph_locs
|
||||
|
||||
|
||||
def __get_normals(blender_mesh, key_blocks, armature, blender_object, export_settings):
|
||||
"""Get normal for each loop."""
|
||||
if key_blocks:
|
||||
normals = key_blocks[0].relative_key.normals_split_get()
|
||||
normals = np.array(normals, dtype=np.float32)
|
||||
else:
|
||||
normals = np.empty(len(blender_mesh.loops) * 3, dtype=np.float32)
|
||||
blender_mesh.calc_normals_split()
|
||||
blender_mesh.loops.foreach_get('normal', normals)
|
||||
|
||||
normals = normals.reshape(len(blender_mesh.loops), 3)
|
||||
|
||||
morph_normals = []
|
||||
for key_block in key_blocks:
|
||||
ns = np.array(key_block.normals_split_get(), dtype=np.float32)
|
||||
ns = ns.reshape(len(blender_mesh.loops), 3)
|
||||
morph_normals.append(ns)
|
||||
|
||||
# Transform for skinning
|
||||
if armature and blender_object:
|
||||
apply_matrix = (armature.matrix_world.inverted_safe() @ blender_object.matrix_world)
|
||||
apply_matrix = apply_matrix.to_3x3().inverted_safe().transposed()
|
||||
normal_transform = armature.matrix_world.to_3x3() @ apply_matrix
|
||||
|
||||
normals[:] = __apply_mat_to_all(normal_transform, normals)
|
||||
__normalize_vecs(normals)
|
||||
for ns in morph_normals:
|
||||
ns[:] = __apply_mat_to_all(normal_transform, ns)
|
||||
__normalize_vecs(ns)
|
||||
|
||||
for ns in [normals, *morph_normals]:
|
||||
# Replace zero normals with the unit UP vector.
|
||||
# Seems to happen sometimes with degenerate tris?
|
||||
is_zero = ~ns.any(axis=1)
|
||||
ns[is_zero, 2] = 1
|
||||
|
||||
# glTF stores deltas in morph targets
|
||||
for ns in morph_normals:
|
||||
ns -= normals
|
||||
|
||||
if export_settings[gltf2_blender_export_keys.YUP]:
|
||||
__zup2yup(normals)
|
||||
for ns in morph_normals:
|
||||
__zup2yup(ns)
|
||||
|
||||
return normals, morph_normals
|
||||
|
||||
|
||||
def __get_tangents(blender_mesh, armature, blender_object, export_settings):
|
||||
"""Get an array of the tangent for each loop."""
|
||||
tangents = np.empty(len(blender_mesh.loops) * 3, dtype=np.float32)
|
||||
blender_mesh.loops.foreach_get('tangent', tangents)
|
||||
tangents = tangents.reshape(len(blender_mesh.loops), 3)
|
||||
|
||||
# Transform for skinning
|
||||
if armature and blender_object:
|
||||
apply_matrix = armature.matrix_world.inverted_safe() @ blender_object.matrix_world
|
||||
tangent_transform = apply_matrix.to_quaternion().to_matrix()
|
||||
tangents = __apply_mat_to_all(tangent_transform, tangents)
|
||||
__normalize_vecs(tangents)
|
||||
|
||||
if export_settings[gltf2_blender_export_keys.YUP]:
|
||||
__zup2yup(tangents)
|
||||
|
||||
return tangents
|
||||
|
||||
|
||||
def __get_bitangent_signs(blender_mesh, armature, blender_object, export_settings):
|
||||
signs = np.empty(len(blender_mesh.loops), dtype=np.float32)
|
||||
blender_mesh.loops.foreach_get('bitangent_sign', signs)
|
||||
|
||||
# Transform for skinning
|
||||
if armature and blender_object:
|
||||
# Bitangent signs should flip when handedness changes
|
||||
# TODO: confirm
|
||||
apply_matrix = armature.matrix_world.inverted_safe() @ blender_object.matrix_world
|
||||
tangent_transform = apply_matrix.to_quaternion().to_matrix()
|
||||
flipped = tangent_transform.determinant() < 0
|
||||
if flipped:
|
||||
signs *= -1
|
||||
|
||||
# No change for Zup -> Yup
|
||||
|
||||
return signs
|
||||
|
||||
|
||||
def __calc_morph_tangents(normals, morph_normal_deltas, tangents):
|
||||
# TODO: check if this works
|
||||
morph_tangent_deltas = np.empty((len(normals), 3), dtype=np.float32)
|
||||
|
||||
for i in range(len(normals)):
|
||||
n = Vector(normals[i])
|
||||
morph_n = n + Vector(morph_normal_deltas[i]) # convert back to non-delta
|
||||
t = Vector(tangents[i, :3])
|
||||
|
||||
rotation = morph_n.rotation_difference(n)
|
||||
|
||||
t_morph = Vector(t)
|
||||
t_morph.rotate(rotation)
|
||||
morph_tangent_deltas[i] = t_morph - t # back to delta
|
||||
|
||||
return morph_tangent_deltas
|
||||
|
||||
|
||||
def __get_uvs(blender_mesh, uv_i):
|
||||
layer = blender_mesh.uv_layers[uv_i]
|
||||
uvs = np.empty(len(blender_mesh.loops) * 2, dtype=np.float32)
|
||||
layer.data.foreach_get('uv', uvs)
|
||||
uvs = uvs.reshape(len(blender_mesh.loops), 2)
|
||||
|
||||
# Blender UV space -> glTF UV space
|
||||
# u,v -> u,1-v
|
||||
uvs[:, 1] *= -1
|
||||
uvs[:, 1] += 1
|
||||
|
||||
return uvs
|
||||
|
||||
|
||||
def __get_colors(blender_mesh, color_i, blender_color_i):
|
||||
if blender_mesh.color_attributes[blender_color_i].domain == "POINT":
|
||||
colors = np.empty(len(blender_mesh.vertices) * 4, dtype=np.float32) #POINT
|
||||
else:
|
||||
colors = np.empty(len(blender_mesh.loops) * 4, dtype=np.float32) #CORNER
|
||||
blender_mesh.color_attributes[blender_color_i].data.foreach_get('color', colors)
|
||||
colors = colors.reshape(-1, 4)
|
||||
# colors are already linear, no need to switch color space
|
||||
return colors, blender_mesh.color_attributes[blender_color_i].data_type, blender_mesh.color_attributes[blender_color_i].domain
|
||||
|
||||
|
||||
def __get_bone_data(blender_mesh, skin, blender_vertex_groups):
|
||||
|
||||
need_neutral_bone = False
|
||||
min_influence = 0.0001
|
||||
|
||||
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]
|
||||
|
||||
# List of (joint, weight) pairs for each vert
|
||||
vert_bones = []
|
||||
max_num_influences = 0
|
||||
|
||||
for vertex in blender_mesh.vertices:
|
||||
bones = []
|
||||
if vertex.groups:
|
||||
for group_element in vertex.groups:
|
||||
weight = group_element.weight
|
||||
if weight <= min_influence:
|
||||
continue
|
||||
try:
|
||||
joint = group_to_joint[group_element.group]
|
||||
except Exception:
|
||||
continue
|
||||
if joint is None:
|
||||
continue
|
||||
bones.append((joint, weight))
|
||||
bones.sort(key=lambda x: x[1], reverse=True)
|
||||
if not bones:
|
||||
# Is not assign to any bone
|
||||
bones = ((len(skin.joints), 1.0),) # Assign to a joint that will be created later
|
||||
need_neutral_bone = True
|
||||
vert_bones.append(bones)
|
||||
if len(bones) > max_num_influences:
|
||||
max_num_influences = len(bones)
|
||||
|
||||
# How many joint sets do we need? 1 set = 4 influences
|
||||
num_joint_sets = (max_num_influences + 3) // 4
|
||||
|
||||
return vert_bones, num_joint_sets, need_neutral_bone
|
||||
|
||||
|
||||
def __zup2yup(array):
|
||||
# x,y,z -> x,z,-y
|
||||
array[:, [1,2]] = array[:, [2,1]] # x,z,y
|
||||
array[:, 2] *= -1 # x,z,-y
|
||||
|
||||
|
||||
def __apply_mat_to_all(matrix, vectors):
|
||||
"""Given matrix m and vectors [v1,v2,...], computes [m@v1,m@v2,...]"""
|
||||
# Linear part
|
||||
m = matrix.to_3x3() if len(matrix) == 4 else matrix
|
||||
res = np.matmul(vectors, np.array(m.transposed()))
|
||||
# Translation part
|
||||
if len(matrix) == 4:
|
||||
res += np.array(matrix.translation)
|
||||
return res
|
||||
|
||||
|
||||
def __normalize_vecs(vectors):
|
||||
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
|
||||
np.divide(vectors, norms, out=vectors, where=norms != 0)
|
|
@ -10,32 +10,34 @@ from io_scene_gltf2.io.com import gltf2_io_debug
|
|||
from io_scene_gltf2.io.exp import gltf2_io_binary_data
|
||||
|
||||
|
||||
|
||||
def gather_primitive_attributes(blender_primitive, export_settings):
|
||||
"""
|
||||
Gathers the attributes, such as POSITION, NORMAL, TANGENT from a blender primitive.
|
||||
Gathers the attributes, such as POSITION, NORMAL, TANGENT, and all custom attributes from a blender primitive
|
||||
|
||||
:return: a dictionary of attributes
|
||||
"""
|
||||
attributes = {}
|
||||
attributes.update(__gather_position(blender_primitive, export_settings))
|
||||
attributes.update(__gather_normal(blender_primitive, export_settings))
|
||||
attributes.update(__gather_tangent(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))
|
||||
|
||||
# loop on each attribute extracted
|
||||
# for skinning, all linked attributes (WEIGHTS_ and JOINTS_) need to be calculated
|
||||
# in one shot (because of normalization), so we need to check that it is called only once.
|
||||
|
||||
skin_done = False
|
||||
|
||||
for attribute in blender_primitive["attributes"]:
|
||||
if (attribute.startswith("JOINTS_") or attribute.startswith("WEIGHTS_")) and skin_done is True:
|
||||
continue
|
||||
if attribute.startswith("MORPH_"):
|
||||
continue # Target for morphs will be managed later
|
||||
attributes.update(__gather_attribute(blender_primitive, attribute, export_settings))
|
||||
if (attribute.startswith("JOINTS_") or attribute.startswith("WEIGHTS_")):
|
||||
skin_done = True
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def array_to_accessor(array, component_type, data_type, include_max_and_min=False):
|
||||
dtype = gltf2_io_constants.ComponentType.to_numpy_dtype(component_type)
|
||||
num_elems = gltf2_io_constants.DataType.num_elements(data_type)
|
||||
|
||||
if type(array) is not np.ndarray:
|
||||
array = np.array(array, dtype=dtype)
|
||||
array = array.reshape(len(array) // num_elems, num_elems)
|
||||
|
||||
assert array.dtype == dtype
|
||||
assert array.shape[1] == num_elems
|
||||
|
||||
amax = None
|
||||
amin = None
|
||||
|
@ -58,109 +60,6 @@ def array_to_accessor(array, component_type, data_type, include_max_and_min=Fals
|
|||
type=data_type,
|
||||
)
|
||||
|
||||
|
||||
def __gather_position(blender_primitive, export_settings):
|
||||
position = blender_primitive["attributes"]["POSITION"]
|
||||
return {
|
||||
"POSITION": array_to_accessor(
|
||||
position,
|
||||
component_type=gltf2_io_constants.ComponentType.Float,
|
||||
data_type=gltf2_io_constants.DataType.Vec3,
|
||||
include_max_and_min=True
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def __gather_normal(blender_primitive, export_settings):
|
||||
if not export_settings[gltf2_blender_export_keys.NORMALS]:
|
||||
return {}
|
||||
if 'NORMAL' not in blender_primitive["attributes"]:
|
||||
return {}
|
||||
normal = blender_primitive["attributes"]['NORMAL']
|
||||
return {
|
||||
"NORMAL": array_to_accessor(
|
||||
normal,
|
||||
component_type=gltf2_io_constants.ComponentType.Float,
|
||||
data_type=gltf2_io_constants.DataType.Vec3,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def __gather_tangent(blender_primitive, export_settings):
|
||||
if not export_settings[gltf2_blender_export_keys.TANGENTS]:
|
||||
return {}
|
||||
if 'TANGENT' not in blender_primitive["attributes"]:
|
||||
return {}
|
||||
tangent = blender_primitive["attributes"]['TANGENT']
|
||||
return {
|
||||
"TANGENT": array_to_accessor(
|
||||
tangent,
|
||||
component_type=gltf2_io_constants.ComponentType.Float,
|
||||
data_type=gltf2_io_constants.DataType.Vec4,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def __gather_texcoord(blender_primitive, export_settings):
|
||||
attributes = {}
|
||||
if export_settings[gltf2_blender_export_keys.TEX_COORDS]:
|
||||
tex_coord_index = 0
|
||||
tex_coord_id = 'TEXCOORD_' + str(tex_coord_index)
|
||||
while blender_primitive["attributes"].get(tex_coord_id) is not None:
|
||||
tex_coord = blender_primitive["attributes"][tex_coord_id]
|
||||
attributes[tex_coord_id] = array_to_accessor(
|
||||
tex_coord,
|
||||
component_type=gltf2_io_constants.ComponentType.Float,
|
||||
data_type=gltf2_io_constants.DataType.Vec2,
|
||||
)
|
||||
tex_coord_index += 1
|
||||
tex_coord_id = 'TEXCOORD_' + str(tex_coord_index)
|
||||
return attributes
|
||||
|
||||
|
||||
def __gather_colors(blender_primitive, export_settings):
|
||||
attributes = {}
|
||||
if export_settings[gltf2_blender_export_keys.COLORS]:
|
||||
color_index = 0
|
||||
color_id = 'COLOR_' + str(color_index)
|
||||
while blender_primitive["attributes"].get(color_id) is not None:
|
||||
colors = blender_primitive["attributes"][color_id]["data"]
|
||||
|
||||
if type(colors) is not np.ndarray:
|
||||
colors = np.array(colors, dtype=np.float32)
|
||||
colors = colors.reshape(len(colors) // 4, 4)
|
||||
|
||||
if blender_primitive["attributes"][color_id]["norm"] is True:
|
||||
comp_type = gltf2_io_constants.ComponentType.UnsignedShort
|
||||
|
||||
# Convert to normalized ushorts
|
||||
colors *= 65535
|
||||
colors += 0.5 # bias for rounding
|
||||
colors = colors.astype(np.uint16)
|
||||
|
||||
else:
|
||||
comp_type = gltf2_io_constants.ComponentType.Float
|
||||
|
||||
attributes[color_id] = gltf2_io.Accessor(
|
||||
buffer_view=gltf2_io_binary_data.BinaryData(colors.tobytes(), gltf2_io_constants.BufferViewTarget.ARRAY_BUFFER),
|
||||
byte_offset=None,
|
||||
component_type=comp_type,
|
||||
count=len(colors),
|
||||
extensions=None,
|
||||
extras=None,
|
||||
max=None,
|
||||
min=None,
|
||||
name=None,
|
||||
normalized=blender_primitive["attributes"][color_id]["norm"],
|
||||
sparse=None,
|
||||
type=gltf2_io_constants.DataType.Vec4,
|
||||
)
|
||||
|
||||
color_index += 1
|
||||
color_id = 'COLOR_' + str(color_index)
|
||||
return attributes
|
||||
|
||||
|
||||
def __gather_skins(blender_primitive, export_settings):
|
||||
attributes = {}
|
||||
|
||||
|
@ -208,8 +107,10 @@ def __gather_skins(blender_primitive, export_settings):
|
|||
component_type = gltf2_io_constants.ComponentType.UnsignedShort
|
||||
if max(internal_joint) < 256:
|
||||
component_type = gltf2_io_constants.ComponentType.UnsignedByte
|
||||
joints = np.array(internal_joint, dtype= gltf2_io_constants.ComponentType.to_numpy_dtype(component_type))
|
||||
joints = joints.reshape(-1, 4)
|
||||
joint = array_to_accessor(
|
||||
internal_joint,
|
||||
joints,
|
||||
component_type,
|
||||
data_type=gltf2_io_constants.DataType.Vec4,
|
||||
)
|
||||
|
@ -236,3 +137,48 @@ def __gather_skins(blender_primitive, export_settings):
|
|||
attributes[weight_id] = weight
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def __gather_attribute(blender_primitive, attribute, export_settings):
|
||||
data = blender_primitive["attributes"][attribute]
|
||||
|
||||
|
||||
include_max_and_mins = {
|
||||
"POSITION": True
|
||||
}
|
||||
|
||||
if (attribute.startswith("_COLOR") or attribute.startswith("COLOR_")) and blender_primitive["attributes"][attribute]['component_type'] == gltf2_io_constants.ComponentType.UnsignedShort:
|
||||
# Byte Color vertex color, need to normalize
|
||||
|
||||
data['data'] *= 65535
|
||||
data['data'] += 0.5 # bias for rounding
|
||||
data['data'] = data['data'].astype(np.uint16)
|
||||
|
||||
return { attribute : gltf2_io.Accessor(
|
||||
buffer_view=gltf2_io_binary_data.BinaryData(data['data'].tobytes(), gltf2_io_constants.BufferViewTarget.ARRAY_BUFFER),
|
||||
byte_offset=None,
|
||||
component_type=data['component_type'],
|
||||
count=len(data['data']),
|
||||
extensions=None,
|
||||
extras=None,
|
||||
max=None,
|
||||
min=None,
|
||||
name=None,
|
||||
normalized=True,
|
||||
sparse=None,
|
||||
type=data['data_type'],
|
||||
)
|
||||
}
|
||||
|
||||
elif attribute.startswith("JOINTS_") or attribute.startswith("WEIGHTS_"):
|
||||
return __gather_skins(blender_primitive, export_settings)
|
||||
|
||||
else:
|
||||
return {
|
||||
attribute: array_to_accessor(
|
||||
data['data'],
|
||||
component_type=data['component_type'],
|
||||
data_type=data['data_type'],
|
||||
include_max_and_min=include_max_and_mins.get(attribute, False)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import numpy as np
|
|||
from .gltf2_blender_export_keys import NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH
|
||||
|
||||
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_extract
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitives_extract
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials
|
||||
|
@ -112,7 +112,7 @@ def __gather_cache_primitives(
|
|||
"""
|
||||
primitives = []
|
||||
|
||||
blender_primitives = gltf2_blender_extract.extract_primitives(
|
||||
blender_primitives = gltf2_blender_gather_primitives_extract.extract_primitives(
|
||||
blender_mesh, uuid_for_skined_data, vertex_groups, modifiers, export_settings)
|
||||
|
||||
for internal_primitive in blender_primitives:
|
||||
|
@ -184,7 +184,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings
|
|||
|
||||
if blender_primitive["attributes"].get(target_position_id) is not None:
|
||||
target = {}
|
||||
internal_target_position = blender_primitive["attributes"][target_position_id]
|
||||
internal_target_position = blender_primitive["attributes"][target_position_id]["data"]
|
||||
target["POSITION"] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
|
||||
internal_target_position,
|
||||
component_type=gltf2_io_constants.ComponentType.Float,
|
||||
|
@ -196,7 +196,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings
|
|||
and export_settings[MORPH_NORMAL] \
|
||||
and blender_primitive["attributes"].get(target_normal_id) is not None:
|
||||
|
||||
internal_target_normal = blender_primitive["attributes"][target_normal_id]
|
||||
internal_target_normal = blender_primitive["attributes"][target_normal_id]["data"]
|
||||
target['NORMAL'] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
|
||||
internal_target_normal,
|
||||
component_type=gltf2_io_constants.ComponentType.Float,
|
||||
|
@ -206,7 +206,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings
|
|||
if export_settings[TANGENTS] \
|
||||
and export_settings[MORPH_TANGENT] \
|
||||
and blender_primitive["attributes"].get(target_tangent_id) is not None:
|
||||
internal_target_tangent = blender_primitive["attributes"][target_tangent_id]
|
||||
internal_target_tangent = blender_primitive["attributes"][target_tangent_id]["data"]
|
||||
target['TANGENT'] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
|
||||
internal_target_tangent,
|
||||
component_type=gltf2_io_constants.ComponentType.Float,
|
||||
|
|
|
@ -0,0 +1,863 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2018-2021 The glTF-Blender-IO authors.
|
||||
|
||||
import numpy as np
|
||||
from mathutils import Vector
|
||||
|
||||
from . import gltf2_blender_export_keys
|
||||
from ...io.com.gltf2_io_debug import print_console
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
|
||||
from io_scene_gltf2.io.com import gltf2_io_constants
|
||||
from io_scene_gltf2.blender.com import gltf2_blender_conversion
|
||||
|
||||
|
||||
def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings):
|
||||
"""Extract primitives from a mesh."""
|
||||
print_console('INFO', 'Extracting primitive: ' + blender_mesh.name)
|
||||
|
||||
primitive_creator = PrimitiveCreator(blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings)
|
||||
primitive_creator.prepare_data()
|
||||
primitive_creator.define_attributes()
|
||||
primitive_creator.create_dots_data_structure()
|
||||
primitive_creator.populate_dots_data()
|
||||
primitive_creator.primitive_split()
|
||||
return primitive_creator.primitive_creation()
|
||||
|
||||
class PrimitiveCreator:
|
||||
def __init__(self, blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings):
|
||||
self.blender_mesh = blender_mesh
|
||||
self.uuid_for_skined_data = uuid_for_skined_data
|
||||
self.blender_vertex_groups = blender_vertex_groups
|
||||
self.modifiers = modifiers
|
||||
self.export_settings = export_settings
|
||||
|
||||
@classmethod
|
||||
def apply_mat_to_all(cls, matrix, vectors):
|
||||
"""Given matrix m and vectors [v1,v2,...], computes [m@v1,m@v2,...]"""
|
||||
# Linear part
|
||||
m = matrix.to_3x3() if len(matrix) == 4 else matrix
|
||||
res = np.matmul(vectors, np.array(m.transposed()))
|
||||
# Translation part
|
||||
if len(matrix) == 4:
|
||||
res += np.array(matrix.translation)
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def normalize_vecs(cls, vectors):
|
||||
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
|
||||
np.divide(vectors, norms, out=vectors, where=norms != 0)
|
||||
|
||||
@classmethod
|
||||
def zup2yup(cls, array):
|
||||
# x,y,z -> x,z,-y
|
||||
array[:, [1,2]] = array[:, [2,1]] # x,z,y
|
||||
array[:, 2] *= -1 # x,z,-y
|
||||
|
||||
def prepare_data(self):
|
||||
self.blender_object = None
|
||||
if self.uuid_for_skined_data:
|
||||
self.blender_object = self.export_settings['vtree'].nodes[self.uuid_for_skined_data].blender_object
|
||||
|
||||
self.use_normals = self.export_settings[gltf2_blender_export_keys.NORMALS]
|
||||
if self.use_normals:
|
||||
self.blender_mesh.calc_normals_split()
|
||||
|
||||
self.use_tangents = False
|
||||
if self.use_normals and self.export_settings[gltf2_blender_export_keys.TANGENTS]:
|
||||
if self.blender_mesh.uv_layers.active and len(self.blender_mesh.uv_layers) > 0:
|
||||
try:
|
||||
self.blender_mesh.calc_tangents()
|
||||
self.use_tangents = True
|
||||
except Exception:
|
||||
print_console('WARNING', 'Could not calculate tangents. Please try to triangulate the mesh first.')
|
||||
|
||||
self.tex_coord_max = 0
|
||||
if self.export_settings[gltf2_blender_export_keys.TEX_COORDS]:
|
||||
if self.blender_mesh.uv_layers.active:
|
||||
self.tex_coord_max = len(self.blender_mesh.uv_layers)
|
||||
|
||||
self.use_morph_normals = self.use_normals and self.export_settings[gltf2_blender_export_keys.MORPH_NORMAL]
|
||||
self.use_morph_tangents = self.use_morph_normals and self.use_tangents and self.export_settings[gltf2_blender_export_keys.MORPH_TANGENT]
|
||||
|
||||
self.use_materials = self.export_settings[gltf2_blender_export_keys.MATERIALS]
|
||||
|
||||
self.blender_attributes = []
|
||||
|
||||
# Check if we have to export skin
|
||||
self.armature = None
|
||||
self.skin = None
|
||||
if self.blender_vertex_groups and self.export_settings[gltf2_blender_export_keys.SKINS]:
|
||||
if self.modifiers is not None:
|
||||
modifiers_dict = {m.type: m for m in self.modifiers}
|
||||
if "ARMATURE" in modifiers_dict:
|
||||
modifier = modifiers_dict["ARMATURE"]
|
||||
self.armature = modifier.object
|
||||
|
||||
# Skin must be ignored if the object is parented to a bone of the armature
|
||||
# (This creates an infinite recursive error)
|
||||
# So ignoring skin in that case
|
||||
is_child_of_arma = (
|
||||
self.armature and
|
||||
self.blender_object and
|
||||
self.blender_object.parent_type == "BONE" and
|
||||
self.blender_object.parent.name == self.armature.name
|
||||
)
|
||||
if is_child_of_arma:
|
||||
self.armature = None
|
||||
|
||||
if self.armature:
|
||||
self.skin = gltf2_blender_gather_skins.gather_skin(self.export_settings['vtree'].nodes[self.uuid_for_skined_data].armature, self.export_settings)
|
||||
if not self.skin:
|
||||
self.armature = None
|
||||
|
||||
self.key_blocks = []
|
||||
if self.export_settings[gltf2_blender_export_keys.APPLY] is False and self.blender_mesh.shape_keys and self.export_settings[gltf2_blender_export_keys.MORPH]:
|
||||
self.key_blocks = [
|
||||
key_block
|
||||
for key_block in self.blender_mesh.shape_keys.key_blocks
|
||||
if not (key_block == key_block.relative_key or key_block.mute)
|
||||
]
|
||||
|
||||
# Fetch vert positions and bone data (joint,weights)
|
||||
|
||||
self.locs = None
|
||||
self.morph_locs = None
|
||||
self.__get_positions()
|
||||
|
||||
if self.skin:
|
||||
self.__get_bone_data()
|
||||
if self.need_neutral_bone is True:
|
||||
# Need to create a fake joint at root of armature
|
||||
# In order to assign not assigned vertices to it
|
||||
# But for now, this is not yet possible, we need to wait the armature node is created
|
||||
# Just store this, to be used later
|
||||
armature_uuid = self.export_settings['vtree'].nodes[self.uuid_for_skined_data].armature
|
||||
self.export_settings['vtree'].nodes[armature_uuid].need_neutral_bone = True
|
||||
|
||||
def define_attributes(self):
|
||||
# Manage attributes + COLOR_0
|
||||
for blender_attribute_index, blender_attribute in enumerate(self.blender_mesh.attributes):
|
||||
attr = {}
|
||||
attr['blender_attribute_index'] = blender_attribute_index
|
||||
attr['blender_name'] = blender_attribute.name
|
||||
attr['blender_domain'] = blender_attribute.domain
|
||||
attr['blender_data_type'] = blender_attribute.data_type
|
||||
|
||||
# For now, we don't export edge data, because I need to find how to
|
||||
# get from edge data to dots data
|
||||
if attr['blender_domain'] == "EDGE":
|
||||
continue
|
||||
|
||||
# Some type are not exportable (example : String)
|
||||
if gltf2_blender_conversion.get_component_type(blender_attribute.data_type) is None or \
|
||||
gltf2_blender_conversion.get_data_type(blender_attribute.data_type) is None:
|
||||
|
||||
continue
|
||||
|
||||
if self.blender_mesh.color_attributes.find(blender_attribute.name) == self.blender_mesh.color_attributes.render_color_index \
|
||||
and self.blender_mesh.color_attributes.render_color_index != -1:
|
||||
|
||||
if self.export_settings[gltf2_blender_export_keys.COLORS] is False:
|
||||
continue
|
||||
attr['gltf_attribute_name'] = 'COLOR_0'
|
||||
attr['get'] = self.get_function()
|
||||
|
||||
else:
|
||||
attr['gltf_attribute_name'] = '_' + blender_attribute.name.upper()
|
||||
attr['get'] = self.get_function()
|
||||
if self.export_settings['gltf_attributes'] is False:
|
||||
continue
|
||||
|
||||
self.blender_attributes.append(attr)
|
||||
|
||||
# Manage POSITION
|
||||
attr = {}
|
||||
attr['blender_data_type'] = 'FLOAT_VECTOR'
|
||||
attr['blender_domain'] = 'POINT'
|
||||
attr['gltf_attribute_name'] = 'POSITION'
|
||||
attr['set'] = self.set_function()
|
||||
attr['skip_getting_to_dots'] = True
|
||||
self.blender_attributes.append(attr)
|
||||
|
||||
# Manage uvs TEX_COORD_x
|
||||
for tex_coord_i in range(self.tex_coord_max):
|
||||
attr = {}
|
||||
attr['blender_data_type'] = 'FLOAT2'
|
||||
attr['blender_domain'] = 'CORNER'
|
||||
attr['gltf_attribute_name'] = 'TEXCOORD_' + str(tex_coord_i)
|
||||
attr['get'] = self.get_function()
|
||||
self.blender_attributes.append(attr)
|
||||
|
||||
# Manage NORMALS
|
||||
if self.use_normals:
|
||||
attr = {}
|
||||
attr['blender_data_type'] = 'FLOAT_VECTOR'
|
||||
attr['blender_domain'] = 'CORNER'
|
||||
attr['gltf_attribute_name'] = 'NORMAL'
|
||||
attr['gltf_attribute_name_morph'] = 'MORPH_NORMAL_'
|
||||
attr['get'] = self.get_function()
|
||||
self.blender_attributes.append(attr)
|
||||
|
||||
# Manage TANGENT
|
||||
if self.use_tangents:
|
||||
attr = {}
|
||||
attr['blender_data_type'] = 'FLOAT_VECTOR_4'
|
||||
attr['blender_domain'] = 'CORNER'
|
||||
attr['gltf_attribute_name'] = 'TANGENT'
|
||||
attr['get'] = self.get_function()
|
||||
self.blender_attributes.append(attr)
|
||||
|
||||
# Manage MORPH_POSITION_x
|
||||
for morph_i, vs in enumerate(self.morph_locs):
|
||||
attr = {}
|
||||
attr['blender_attribute_index'] = morph_i
|
||||
attr['blender_data_type'] = 'FLOAT_VECTOR'
|
||||
attr['blender_domain'] = 'POINT'
|
||||
attr['gltf_attribute_name'] = 'MORPH_POSITION_' + str(morph_i)
|
||||
attr['skip_getting_to_dots'] = True
|
||||
attr['set'] = self.set_function()
|
||||
self.blender_attributes.append(attr)
|
||||
|
||||
# Manage MORPH_NORMAL_x
|
||||
if self.use_morph_normals:
|
||||
attr = {}
|
||||
attr['blender_attribute_index'] = morph_i
|
||||
attr['blender_data_type'] = 'FLOAT_VECTOR'
|
||||
attr['blender_domain'] = 'CORNER'
|
||||
attr['gltf_attribute_name'] = 'MORPH_NORMAL_' + str(morph_i)
|
||||
# No get function is set here, because data are set from NORMALS
|
||||
self.blender_attributes.append(attr)
|
||||
|
||||
# Manage MORPH_TANGENT_x
|
||||
# This is a particular case, where we need to have the following data already calculated
|
||||
# - NORMAL
|
||||
# - MORPH_NORMAL
|
||||
# - TANGENT
|
||||
# So, the following needs to be AFTER the 3 others.
|
||||
if self.use_morph_tangents:
|
||||
attr = {}
|
||||
attr['blender_attribute_index'] = morph_i
|
||||
attr['blender_data_type'] = 'FLOAT_VECTOR'
|
||||
attr['blender_domain'] = 'CORNER'
|
||||
attr['gltf_attribute_name'] = 'MORPH_TANGENT_' + str(morph_i)
|
||||
attr['gltf_attribute_name_normal'] = "NORMAL"
|
||||
attr['gltf_attribute_name_morph_normal'] = "MORPH_NORMAL_" + str(morph_i)
|
||||
attr['gltf_attribute_name_tangent'] = "TANGENT"
|
||||
attr['skip_getting_to_dots'] = True
|
||||
attr['set'] = self.set_function()
|
||||
self.blender_attributes.append(attr)
|
||||
|
||||
for attr in self.blender_attributes:
|
||||
attr['len'] = gltf2_blender_conversion.get_data_length(attr['blender_data_type'])
|
||||
attr['type'] = gltf2_blender_conversion.get_numpy_type(attr['blender_data_type'])
|
||||
|
||||
def create_dots_data_structure(self):
|
||||
# Now that we get all attributes that are going to be exported, create numpy array that will store them
|
||||
dot_fields = [('vertex_index', np.uint32)]
|
||||
if self.export_settings['gltf_loose_edges']:
|
||||
dot_fields_edges = [('vertex_index', np.uint32)]
|
||||
if self.export_settings['gltf_loose_points']:
|
||||
dot_fields_points = [('vertex_index', np.uint32)]
|
||||
for attr in self.blender_attributes:
|
||||
if 'skip_getting_to_dots' in attr:
|
||||
continue
|
||||
for i in range(attr['len']):
|
||||
dot_fields.append((attr['gltf_attribute_name'] + str(i), attr['type']))
|
||||
if attr['blender_domain'] != 'POINT':
|
||||
continue
|
||||
if self.export_settings['gltf_loose_edges']:
|
||||
dot_fields_edges.append((attr['gltf_attribute_name'] + str(i), attr['type']))
|
||||
if self.export_settings['gltf_loose_points']:
|
||||
dot_fields_points.append((attr['gltf_attribute_name'] + str(i), attr['type']))
|
||||
|
||||
# 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
|
||||
# data, so we need to split Blender verts up into potentially-multiple glTF
|
||||
# verts.
|
||||
#
|
||||
# First, we'll collect a "dot" for every loop: a struct that stores all the
|
||||
# attributes at that loop, namely the vertex index (which determines all
|
||||
# per-vert data), and all the per-loop data like UVs, etc.
|
||||
#
|
||||
# Each unique dot will become one unique glTF vert.
|
||||
|
||||
self.dots = np.empty(len(self.blender_mesh.loops), dtype=np.dtype(dot_fields))
|
||||
|
||||
# Find loose edges
|
||||
if self.export_settings['gltf_loose_edges']:
|
||||
loose_edges = [e for e in self.blender_mesh.edges if e.is_loose]
|
||||
self.blender_idxs_edges = [vi for e in loose_edges for vi in e.vertices]
|
||||
self.blender_idxs_edges = np.array(self.blender_idxs_edges, dtype=np.uint32)
|
||||
|
||||
self.dots_edges = np.empty(len(self.blender_idxs_edges), dtype=np.dtype(dot_fields_edges))
|
||||
self.dots_edges['vertex_index'] = self.blender_idxs_edges
|
||||
|
||||
# Find loose points
|
||||
if self.export_settings['gltf_loose_points']:
|
||||
verts_in_edge = set(vi for e in self.blender_mesh.edges for vi in e.vertices)
|
||||
self.blender_idxs_points = [
|
||||
vi for vi, _ in enumerate(self.blender_mesh.vertices)
|
||||
if vi not in verts_in_edge
|
||||
]
|
||||
self.blender_idxs_points = np.array(self.blender_idxs_points, dtype=np.uint32)
|
||||
|
||||
self.dots_points = np.empty(len(self.blender_idxs_points), dtype=np.dtype(dot_fields_points))
|
||||
self.dots_points['vertex_index'] = self.blender_idxs_points
|
||||
|
||||
|
||||
def populate_dots_data(self):
|
||||
vidxs = np.empty(len(self.blender_mesh.loops))
|
||||
self.blender_mesh.loops.foreach_get('vertex_index', vidxs)
|
||||
self.dots['vertex_index'] = vidxs
|
||||
del vidxs
|
||||
|
||||
for attr in self.blender_attributes:
|
||||
if 'skip_getting_to_dots' in attr:
|
||||
continue
|
||||
if 'get' not in attr:
|
||||
continue
|
||||
attr['get'](attr)
|
||||
|
||||
def primitive_split(self):
|
||||
# Calculate triangles and sort them into primitives.
|
||||
|
||||
self.blender_mesh.calc_loop_triangles()
|
||||
loop_indices = np.empty(len(self.blender_mesh.loop_triangles) * 3, dtype=np.uint32)
|
||||
self.blender_mesh.loop_triangles.foreach_get('loops', loop_indices)
|
||||
|
||||
self.prim_indices = {} # maps material index to TRIANGLES-style indices into dots
|
||||
|
||||
if self.use_materials == "NONE": # Only for None. For placeholder and export, keep primitives
|
||||
# Put all vertices into one primitive
|
||||
self.prim_indices[-1] = loop_indices
|
||||
|
||||
else:
|
||||
# Bucket by material index.
|
||||
|
||||
tri_material_idxs = np.empty(len(self.blender_mesh.loop_triangles), dtype=np.uint32)
|
||||
self.blender_mesh.loop_triangles.foreach_get('material_index', tri_material_idxs)
|
||||
loop_material_idxs = np.repeat(tri_material_idxs, 3) # material index for every loop
|
||||
unique_material_idxs = np.unique(tri_material_idxs)
|
||||
del tri_material_idxs
|
||||
|
||||
for material_idx in unique_material_idxs:
|
||||
self.prim_indices[material_idx] = loop_indices[loop_material_idxs == material_idx]
|
||||
|
||||
def primitive_creation(self):
|
||||
primitives = []
|
||||
|
||||
for material_idx, dot_indices in self.prim_indices.items():
|
||||
# Extract just dots used by this primitive, deduplicate them, and
|
||||
# calculate indices into this deduplicated list.
|
||||
self.prim_dots = self.dots[dot_indices]
|
||||
self.prim_dots, indices = np.unique(self.prim_dots, return_inverse=True)
|
||||
|
||||
if len(self.prim_dots) == 0:
|
||||
continue
|
||||
|
||||
# Now just move all the data for prim_dots into attribute arrays
|
||||
|
||||
self.attributes = {}
|
||||
|
||||
self.blender_idxs = self.prim_dots['vertex_index']
|
||||
|
||||
for attr in self.blender_attributes:
|
||||
if 'set' in attr:
|
||||
attr['set'](attr)
|
||||
else: # Regular case
|
||||
self.__set_regular_attribute(attr)
|
||||
|
||||
if self.skin:
|
||||
joints = [[] for _ in range(self.num_joint_sets)]
|
||||
weights = [[] for _ in range(self.num_joint_sets)]
|
||||
|
||||
for vi in self.blender_idxs:
|
||||
bones = self.vert_bones[vi]
|
||||
for j in range(0, 4 * self.num_joint_sets):
|
||||
if j < len(bones):
|
||||
joint, weight = bones[j]
|
||||
else:
|
||||
joint, weight = 0, 0.0
|
||||
joints[j//4].append(joint)
|
||||
weights[j//4].append(weight)
|
||||
|
||||
for i, (js, ws) in enumerate(zip(joints, weights)):
|
||||
self.attributes['JOINTS_%d' % i] = js
|
||||
self.attributes['WEIGHTS_%d' % i] = ws
|
||||
|
||||
primitives.append({
|
||||
'attributes': self.attributes,
|
||||
'indices': indices,
|
||||
'material': material_idx
|
||||
})
|
||||
|
||||
if self.export_settings['gltf_loose_edges']:
|
||||
|
||||
if self.blender_idxs_edges.shape[0] > 0:
|
||||
# Export one glTF vert per unique Blender vert in a loose edge
|
||||
self.blender_idxs = self.blender_idxs_edges
|
||||
dots_edges, indices = np.unique(self.dots_edges, return_inverse=True)
|
||||
self.blender_idxs = np.unique(self.blender_idxs_edges)
|
||||
|
||||
self.attributes = {}
|
||||
|
||||
for attr in self.blender_attributes:
|
||||
if attr['blender_domain'] != 'POINT':
|
||||
continue
|
||||
if 'set' in attr:
|
||||
attr['set'](attr)
|
||||
else:
|
||||
res = np.empty((len(dots_edges), attr['len']), dtype=attr['type'])
|
||||
for i in range(attr['len']):
|
||||
res[:, i] = dots_edges[attr['gltf_attribute_name'] + str(i)]
|
||||
self.attributes[attr['gltf_attribute_name']] = {}
|
||||
self.attributes[attr['gltf_attribute_name']]["data"] = res
|
||||
self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_blender_conversion.get_component_type(attr['blender_data_type'])
|
||||
self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_blender_conversion.get_data_type(attr['blender_data_type'])
|
||||
|
||||
|
||||
if self.skin:
|
||||
joints = [[] for _ in range(self.num_joint_sets)]
|
||||
weights = [[] for _ in range(self.num_joint_sets)]
|
||||
|
||||
for vi in self.blender_idxs:
|
||||
bones = self.vert_bones[vi]
|
||||
for j in range(0, 4 * self.num_joint_sets):
|
||||
if j < len(bones):
|
||||
joint, weight = bones[j]
|
||||
else:
|
||||
joint, weight = 0, 0.0
|
||||
joints[j//4].append(joint)
|
||||
weights[j//4].append(weight)
|
||||
|
||||
for i, (js, ws) in enumerate(zip(joints, weights)):
|
||||
self.attributes['JOINTS_%d' % i] = js
|
||||
self.attributes['WEIGHTS_%d' % i] = ws
|
||||
|
||||
primitives.append({
|
||||
'attributes': self.attributes,
|
||||
'indices': indices,
|
||||
'mode': 1, # LINES
|
||||
'material': 0
|
||||
})
|
||||
|
||||
if self.export_settings['gltf_loose_points']:
|
||||
|
||||
if self.blender_idxs_points.shape[0] > 0:
|
||||
self.blender_idxs = self.blender_idxs_points
|
||||
|
||||
self.attributes = {}
|
||||
|
||||
for attr in self.blender_attributes:
|
||||
if attr['blender_domain'] != 'POINT':
|
||||
continue
|
||||
if 'set' in attr:
|
||||
attr['set'](attr)
|
||||
else:
|
||||
res = np.empty((len(self.blender_idxs), attr['len']), dtype=attr['type'])
|
||||
for i in range(attr['len']):
|
||||
res[:, i] = self.dots_points[attr['gltf_attribute_name'] + str(i)]
|
||||
self.attributes[attr['gltf_attribute_name']] = {}
|
||||
self.attributes[attr['gltf_attribute_name']]["data"] = res
|
||||
self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_blender_conversion.get_component_type(attr['blender_data_type'])
|
||||
self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_blender_conversion.get_data_type(attr['blender_data_type'])
|
||||
|
||||
|
||||
if self.skin:
|
||||
joints = [[] for _ in range(self.num_joint_sets)]
|
||||
weights = [[] for _ in range(self.num_joint_sets)]
|
||||
|
||||
for vi in self.blender_idxs:
|
||||
bones = self.vert_bones[vi]
|
||||
for j in range(0, 4 * self.num_joint_sets):
|
||||
if j < len(bones):
|
||||
joint, weight = bones[j]
|
||||
else:
|
||||
joint, weight = 0, 0.0
|
||||
joints[j//4].append(joint)
|
||||
weights[j//4].append(weight)
|
||||
|
||||
for i, (js, ws) in enumerate(zip(joints, weights)):
|
||||
self.attributes['JOINTS_%d' % i] = js
|
||||
self.attributes['WEIGHTS_%d' % i] = ws
|
||||
|
||||
primitives.append({
|
||||
'attributes': self.attributes,
|
||||
'mode': 0, # POINTS
|
||||
'material': 0
|
||||
})
|
||||
|
||||
print_console('INFO', 'Primitives created: %d' % len(primitives))
|
||||
|
||||
return primitives
|
||||
|
||||
################################## Get ##################################################
|
||||
|
||||
def __get_positions(self):
|
||||
self.locs = np.empty(len(self.blender_mesh.vertices) * 3, dtype=np.float32)
|
||||
source = self.key_blocks[0].relative_key.data if self.key_blocks else self.blender_mesh.vertices
|
||||
source.foreach_get('co', self.locs)
|
||||
self.locs = self.locs.reshape(len(self.blender_mesh.vertices), 3)
|
||||
|
||||
self.morph_locs = []
|
||||
for key_block in self.key_blocks:
|
||||
vs = np.empty(len(self.blender_mesh.vertices) * 3, dtype=np.float32)
|
||||
key_block.data.foreach_get('co', vs)
|
||||
vs = vs.reshape(len(self.blender_mesh.vertices), 3)
|
||||
self.morph_locs.append(vs)
|
||||
|
||||
# Transform for skinning
|
||||
if self.armature and self.blender_object:
|
||||
# apply_matrix = armature.matrix_world.inverted_safe() @ blender_object.matrix_world
|
||||
# loc_transform = armature.matrix_world @ apply_matrix
|
||||
|
||||
loc_transform = self.blender_object.matrix_world
|
||||
self.locs[:] = PrimitiveCreator.apply_mat_to_all(loc_transform, self.locs)
|
||||
for vs in self.morph_locs:
|
||||
vs[:] = PrimitiveCreator.apply_mat_to_all(loc_transform, vs)
|
||||
|
||||
# glTF stores deltas in morph targets
|
||||
for vs in self.morph_locs:
|
||||
vs -= self.locs
|
||||
|
||||
if self.export_settings[gltf2_blender_export_keys.YUP]:
|
||||
PrimitiveCreator.zup2yup(self.locs)
|
||||
for vs in self.morph_locs:
|
||||
PrimitiveCreator.zup2yup(vs)
|
||||
|
||||
def get_function(self):
|
||||
|
||||
def getting_function(attr):
|
||||
if attr['gltf_attribute_name'] == "COLOR_0":
|
||||
self.__get_color_attribute(attr)
|
||||
elif attr['gltf_attribute_name'].startswith("_"):
|
||||
self.__get_layer_attribute(attr)
|
||||
elif attr['gltf_attribute_name'].startswith("TEXCOORD_"):
|
||||
self.__get_uvs_attribute(int(attr['gltf_attribute_name'].split("_")[-1]), attr)
|
||||
elif attr['gltf_attribute_name'] == "NORMAL":
|
||||
self.__get_normal_attribute(attr)
|
||||
elif attr['gltf_attribute_name'] == "TANGENT":
|
||||
self.__get_tangent_attribute(attr)
|
||||
|
||||
return getting_function
|
||||
|
||||
|
||||
def __get_color_attribute(self, attr):
|
||||
blender_color_idx = self.blender_mesh.color_attributes.render_color_index
|
||||
|
||||
if attr['blender_domain'] == "POINT":
|
||||
colors = np.empty(len(self.blender_mesh.vertices) * 4, dtype=np.float32)
|
||||
elif attr['blender_domain'] == "CORNER":
|
||||
colors = np.empty(len(self.blender_mesh.loops) * 4, dtype=np.float32)
|
||||
self.blender_mesh.color_attributes[blender_color_idx].data.foreach_get('color', colors)
|
||||
if attr['blender_domain'] == "POINT":
|
||||
colors = colors.reshape(-1, 4)
|
||||
colors = colors[self.dots['vertex_index']]
|
||||
elif attr['blender_domain'] == "CORNER":
|
||||
colors = colors.reshape(-1, 4)
|
||||
# colors are already linear, no need to switch color space
|
||||
self.dots[attr['gltf_attribute_name'] + '0'] = colors[:, 0]
|
||||
self.dots[attr['gltf_attribute_name'] + '1'] = colors[:, 1]
|
||||
self.dots[attr['gltf_attribute_name'] + '2'] = colors[:, 2]
|
||||
self.dots[attr['gltf_attribute_name'] + '3'] = colors[:, 3]
|
||||
del colors
|
||||
|
||||
|
||||
def __get_layer_attribute(self, attr):
|
||||
if attr['blender_domain'] in ['CORNER']:
|
||||
data = np.empty(len(self.blender_mesh.loops) * attr['len'], dtype=attr['type'])
|
||||
elif attr['blender_domain'] in ['POINT']:
|
||||
data = np.empty(len(self.blender_mesh.vertices) * attr['len'], dtype=attr['type'])
|
||||
elif attr['blender_domain'] in ['EDGE']:
|
||||
data = np.empty(len(self.blender_mesh.edges) * attr['len'], dtype=attr['type'])
|
||||
elif attr['blender_domain'] in ['FACE']:
|
||||
data = np.empty(len(self.blender_mesh.polygons) * attr['len'], dtype=attr['type'])
|
||||
else:
|
||||
print_console("ERROR", "domain not known")
|
||||
|
||||
if attr['blender_data_type'] == "BYTE_COLOR":
|
||||
self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('color', data)
|
||||
data = data.reshape(-1, attr['len'])
|
||||
elif attr['blender_data_type'] == "INT8":
|
||||
self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('value', data)
|
||||
data = data.reshape(-1, attr['len'])
|
||||
elif attr['blender_data_type'] == "FLOAT2":
|
||||
self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('vector', data)
|
||||
data = data.reshape(-1, attr['len'])
|
||||
elif attr['blender_data_type'] == "BOOLEAN":
|
||||
self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('value', data)
|
||||
data = data.reshape(-1, attr['len'])
|
||||
elif attr['blender_data_type'] == "STRING":
|
||||
self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('value', data)
|
||||
data = data.reshape(-1, attr['len'])
|
||||
elif attr['blender_data_type'] == "FLOAT_COLOR":
|
||||
self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('color', data)
|
||||
data = data.reshape(-1, attr['len'])
|
||||
elif attr['blender_data_type'] == "FLOAT_VECTOR":
|
||||
self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('vector', data)
|
||||
data = data.reshape(-1, attr['len'])
|
||||
elif attr['blender_data_type'] == "FLOAT_VECTOR_4": # Specific case for tangent
|
||||
pass
|
||||
elif attr['blender_data_type'] == "INT":
|
||||
self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('value', data)
|
||||
data = data.reshape(-1, attr['len'])
|
||||
elif attr['blender_data_type'] == "FLOAT":
|
||||
self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('value', data)
|
||||
data = data.reshape(-1, attr['len'])
|
||||
else:
|
||||
print_console('ERROR',"blender type not found " + attr['blender_data_type'])
|
||||
|
||||
if attr['blender_domain'] in ['CORNER']:
|
||||
for i in range(attr['len']):
|
||||
self.dots[attr['gltf_attribute_name'] + str(i)] = data[:, i]
|
||||
elif attr['blender_domain'] in ['POINT']:
|
||||
if attr['len'] > 1:
|
||||
data = data.reshape(-1, attr['len'])
|
||||
data_dots = data[self.dots['vertex_index']]
|
||||
if self.export_settings['gltf_loose_edges']:
|
||||
data_dots_edges = data[self.dots_edges['vertex_index']]
|
||||
if self.export_settings['gltf_loose_points']:
|
||||
data_dots_points = data[self.dots_points['vertex_index']]
|
||||
for i in range(attr['len']):
|
||||
self.dots[attr['gltf_attribute_name'] + str(i)] = data_dots[:, i]
|
||||
if self.export_settings['gltf_loose_edges']:
|
||||
self.dots_edges[attr['gltf_attribute_name'] + str(i)] = data_dots_edges[:, i]
|
||||
if self.export_settings['gltf_loose_points']:
|
||||
self.dots_points[attr['gltf_attribute_name'] + str(i)] = data_dots_points[:, i]
|
||||
elif attr['blender_domain'] in ['EDGE']:
|
||||
# No edge attribute exports
|
||||
pass
|
||||
elif attr['blender_domain'] in ['FACE']:
|
||||
if attr['len'] > 1:
|
||||
data = data.reshape(-1, attr['len'])
|
||||
data = data.repeat(4, axis=0)
|
||||
for i in range(attr['len']):
|
||||
self.dots[attr['gltf_attribute_name'] + str(i)] = data[:, i]
|
||||
|
||||
else:
|
||||
print_console("ERROR", "domain not known")
|
||||
|
||||
def __get_uvs_attribute(self, blender_uv_idx, attr):
|
||||
layer = self.blender_mesh.uv_layers[blender_uv_idx]
|
||||
uvs = np.empty(len(self.blender_mesh.loops) * 2, dtype=np.float32)
|
||||
layer.data.foreach_get('uv', uvs)
|
||||
uvs = uvs.reshape(len(self.blender_mesh.loops), 2)
|
||||
|
||||
# Blender UV space -> glTF UV space
|
||||
# u,v -> u,1-v
|
||||
uvs[:, 1] *= -1
|
||||
uvs[:, 1] += 1
|
||||
|
||||
self.dots[attr['gltf_attribute_name'] + '0'] = uvs[:, 0]
|
||||
self.dots[attr['gltf_attribute_name'] + '1'] = uvs[:, 1]
|
||||
del uvs
|
||||
|
||||
def __get_normals(self):
|
||||
"""Get normal for each loop."""
|
||||
key_blocks = self.key_blocks if self.use_morph_normals else []
|
||||
if key_blocks:
|
||||
self.normals = key_blocks[0].relative_key.normals_split_get()
|
||||
self.normals = np.array(self.normals, dtype=np.float32)
|
||||
else:
|
||||
self.normals = np.empty(len(self.blender_mesh.loops) * 3, dtype=np.float32)
|
||||
self.blender_mesh.calc_normals_split()
|
||||
self.blender_mesh.loops.foreach_get('normal', self.normals)
|
||||
|
||||
self.normals = self.normals.reshape(len(self.blender_mesh.loops), 3)
|
||||
|
||||
self.morph_normals = []
|
||||
for key_block in key_blocks:
|
||||
ns = np.array(key_block.normals_split_get(), dtype=np.float32)
|
||||
ns = ns.reshape(len(self.blender_mesh.loops), 3)
|
||||
self.morph_normals.append(ns)
|
||||
|
||||
# Transform for skinning
|
||||
if self.armature and self.blender_object:
|
||||
apply_matrix = (self.armature.matrix_world.inverted_safe() @ self.blender_object.matrix_world)
|
||||
apply_matrix = apply_matrix.to_3x3().inverted_safe().transposed()
|
||||
normal_transform = self.armature.matrix_world.to_3x3() @ apply_matrix
|
||||
|
||||
self.normals[:] = PrimitiveCreator.apply_mat_to_all(normal_transform, self.normals)
|
||||
PrimitiveCreator.normalize_vecs(self.normals)
|
||||
for ns in self.morph_normals:
|
||||
ns[:] = PrimitiveCreator.apply_mat_to_all(normal_transform, ns)
|
||||
PrimitiveCreator.normalize_vecs(ns)
|
||||
|
||||
for ns in [self.normals, *self.morph_normals]:
|
||||
# Replace zero normals with the unit UP vector.
|
||||
# Seems to happen sometimes with degenerate tris?
|
||||
is_zero = ~ns.any(axis=1)
|
||||
ns[is_zero, 2] = 1
|
||||
|
||||
# glTF stores deltas in morph targets
|
||||
for ns in self.morph_normals:
|
||||
ns -= self.normals
|
||||
|
||||
if self.export_settings[gltf2_blender_export_keys.YUP]:
|
||||
PrimitiveCreator.zup2yup(self.normals)
|
||||
for ns in self.morph_normals:
|
||||
PrimitiveCreator.zup2yup(ns)
|
||||
|
||||
def __get_normal_attribute(self, attr):
|
||||
self.__get_normals()
|
||||
self.dots[attr['gltf_attribute_name'] + "0"] = self.normals[:, 0]
|
||||
self.dots[attr['gltf_attribute_name'] + "1"] = self.normals[:, 1]
|
||||
self.dots[attr['gltf_attribute_name'] + "2"] = self.normals[:, 2]
|
||||
|
||||
if self.use_morph_normals:
|
||||
for morph_i, ns in enumerate(self.morph_normals):
|
||||
self.dots[attr['gltf_attribute_name_morph'] + str(morph_i) + "0"] = ns[:, 0]
|
||||
self.dots[attr['gltf_attribute_name_morph'] + str(morph_i) + "1"] = ns[:, 1]
|
||||
self.dots[attr['gltf_attribute_name_morph'] + str(morph_i) + "2"] = ns[:, 2]
|
||||
del self.normals
|
||||
del self.morph_normals
|
||||
|
||||
def __get_tangent_attribute(self, attr):
|
||||
self.__get_tangents()
|
||||
self.dots[attr['gltf_attribute_name'] + "0"] = self.tangents[:, 0]
|
||||
self.dots[attr['gltf_attribute_name'] + "1"] = self.tangents[:, 1]
|
||||
self.dots[attr['gltf_attribute_name'] + "2"] = self.tangents[:, 2]
|
||||
del self.tangents
|
||||
self.__get_bitangent_signs()
|
||||
self.dots[attr['gltf_attribute_name'] + "3"] = self.signs
|
||||
del self.signs
|
||||
|
||||
def __get_tangents(self):
|
||||
"""Get an array of the tangent for each loop."""
|
||||
self.tangents = np.empty(len(self.blender_mesh.loops) * 3, dtype=np.float32)
|
||||
self.blender_mesh.loops.foreach_get('tangent', self.tangents)
|
||||
self.tangents = self.tangents.reshape(len(self.blender_mesh.loops), 3)
|
||||
|
||||
# Transform for skinning
|
||||
if self.armature and self.blender_object:
|
||||
apply_matrix = self.armature.matrix_world.inverted_safe() @ self.blender_object.matrix_world
|
||||
tangent_transform = apply_matrix.to_quaternion().to_matrix()
|
||||
self.tangents = PrimitiveCreator.apply_mat_to_all(tangent_transform, self.tangents)
|
||||
PrimitiveCreator.normalize_vecs(self.tangents)
|
||||
|
||||
if self.export_settings[gltf2_blender_export_keys.YUP]:
|
||||
PrimitiveCreator.zup2yup(self.tangents)
|
||||
|
||||
|
||||
def __get_bitangent_signs(self):
|
||||
self.signs = np.empty(len(self.blender_mesh.loops), dtype=np.float32)
|
||||
self.blender_mesh.loops.foreach_get('bitangent_sign', signs)
|
||||
|
||||
# Transform for skinning
|
||||
if self.armature and self.blender_object:
|
||||
# Bitangent signs should flip when handedness changes
|
||||
# TODO: confirm
|
||||
apply_matrix = self.armature.matrix_world.inverted_safe() @ self.blender_object.matrix_world
|
||||
tangent_transform = apply_matrix.to_quaternion().to_matrix()
|
||||
flipped = tangent_transform.determinant() < 0
|
||||
if flipped:
|
||||
signs *= -1
|
||||
|
||||
# No change for Zup -> Yup
|
||||
|
||||
|
||||
def __get_bone_data(self):
|
||||
|
||||
self.need_neutral_bone = False
|
||||
min_influence = 0.0001
|
||||
|
||||
joint_name_to_index = {joint.name: index for index, joint in enumerate(self.skin.joints)}
|
||||
group_to_joint = [joint_name_to_index.get(g.name) for g in self.blender_vertex_groups]
|
||||
|
||||
# List of (joint, weight) pairs for each vert
|
||||
self.vert_bones = []
|
||||
max_num_influences = 0
|
||||
|
||||
for vertex in self.blender_mesh.vertices:
|
||||
bones = []
|
||||
if vertex.groups:
|
||||
for group_element in vertex.groups:
|
||||
weight = group_element.weight
|
||||
if weight <= min_influence:
|
||||
continue
|
||||
try:
|
||||
joint = group_to_joint[group_element.group]
|
||||
except Exception:
|
||||
continue
|
||||
if joint is None:
|
||||
continue
|
||||
bones.append((joint, weight))
|
||||
bones.sort(key=lambda x: x[1], reverse=True)
|
||||
if not bones:
|
||||
# Is not assign to any bone
|
||||
bones = ((len(self.skin.joints), 1.0),) # Assign to a joint that will be created later
|
||||
self.need_neutral_bone = True
|
||||
self.vert_bones.append(bones)
|
||||
if len(bones) > max_num_influences:
|
||||
max_num_influences = len(bones)
|
||||
|
||||
# How many joint sets do we need? 1 set = 4 influences
|
||||
self.num_joint_sets = (max_num_influences + 3) // 4
|
||||
|
||||
##################################### Set ###################################
|
||||
def set_function(self):
|
||||
|
||||
def setting_function(attr):
|
||||
if attr['gltf_attribute_name'] == "POSITION":
|
||||
self.__set_positions_attribute(attr)
|
||||
elif attr['gltf_attribute_name'].startswith("MORPH_POSITION_"):
|
||||
self.__set_morph_locs_attribute(attr)
|
||||
elif attr['gltf_attribute_name'].startswith("MORPH_TANGENT_"):
|
||||
self.__set_morph_tangent_attribute(attr)
|
||||
|
||||
return setting_function
|
||||
|
||||
def __set_positions_attribute(self, attr):
|
||||
self.attributes[attr['gltf_attribute_name']] = {}
|
||||
self.attributes[attr['gltf_attribute_name']]["data"] = self.locs[self.blender_idxs]
|
||||
self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_io_constants.DataType.Vec3
|
||||
self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_io_constants.ComponentType.Float
|
||||
|
||||
|
||||
def __set_morph_locs_attribute(self, attr):
|
||||
self.attributes[attr['gltf_attribute_name']] = {}
|
||||
self.attributes[attr['gltf_attribute_name']]["data"] = self.morph_locs[attr['blender_attribute_index']][self.blender_idxs]
|
||||
|
||||
def __set_morph_tangent_attribute(self, attr):
|
||||
# Morph tangent are after these 3 others, so, they are already calculated
|
||||
self.normals = self.attributes[attr['gltf_attribute_name_normal']]["data"]
|
||||
self.morph_normals = self.attributes[attr['gltf_attribute_name_morph_normal']]["data"]
|
||||
self.tangent = self.attributes[attr['gltf_attribute_name_tangent']]["data"]
|
||||
|
||||
self.__calc_morph_tangents()
|
||||
self.attributes[attr['gltf_attribute_name']] = {}
|
||||
self.attributes[attr['gltf_attribute_name']]["data"] = self.morph_tangents
|
||||
|
||||
def __calc_morph_tangents(self):
|
||||
# TODO: check if this works
|
||||
self.morph_tangent_deltas = np.empty((len(self.normals), 3), dtype=np.float32)
|
||||
|
||||
for i in range(len(self.normals)):
|
||||
n = Vector(self.normals[i])
|
||||
morph_n = n + Vector(self.morph_normal_deltas[i]) # convert back to non-delta
|
||||
t = Vector(self.tangents[i, :3])
|
||||
|
||||
rotation = morph_n.rotation_difference(n)
|
||||
|
||||
t_morph = Vector(t)
|
||||
t_morph.rotate(rotation)
|
||||
self.morph_tangent_deltas[i] = t_morph - t # back to delta
|
||||
|
||||
def __set_regular_attribute(self, attr):
|
||||
res = np.empty((len(self.prim_dots), attr['len']), dtype=attr['type'])
|
||||
for i in range(attr['len']):
|
||||
res[:, i] = self.prim_dots[attr['gltf_attribute_name'] + str(i)]
|
||||
self.attributes[attr['gltf_attribute_name']] = {}
|
||||
self.attributes[attr['gltf_attribute_name']]["data"] = res
|
||||
if 'gltf_attribute_name' == "NORMAL":
|
||||
self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_io_constants.ComponentType.Float
|
||||
self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_io_constants.DataType.Vec3
|
||||
elif 'gltf_attribute_name' == "TANGENT":
|
||||
self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_io_constants.ComponentType.Float
|
||||
self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_io_constants.DataType.Vec4
|
||||
elif attr['gltf_attribute_name'].startswith('TEXCOORD_'):
|
||||
self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_io_constants.ComponentType.Float
|
||||
self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_io_constants.DataType.Vec2
|
||||
else:
|
||||
self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_blender_conversion.get_component_type(attr['blender_data_type'])
|
||||
self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_blender_conversion.get_data_type(attr['blender_data_type'])
|
|
@ -11,7 +11,6 @@ from mathutils import Quaternion, Matrix
|
|||
from io_scene_gltf2.io.com import gltf2_io
|
||||
from io_scene_gltf2.io.imp.gltf2_io_binary import BinaryData
|
||||
from io_scene_gltf2.io.com import gltf2_io_constants
|
||||
from .gltf2_blender_gather_primitive_attributes import array_to_accessor
|
||||
from io_scene_gltf2.io.exp import gltf2_io_binary_data
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors
|
||||
|
||||
|
|
Loading…
Reference in New Issue