diff --git a/leenkx/blender/lnx/exporter_opt.py b/leenkx/blender/lnx/exporter_opt.py index ed12025..eff669f 100644 --- a/leenkx/blender/lnx/exporter_opt.py +++ b/leenkx/blender/lnx/exporter_opt.py @@ -1,445 +1,446 @@ -""" -Exports smaller geometry but is slower. -To be replaced with https://github.com/zeux/meshoptimizer -""" -from typing import Optional - -import bpy -from mathutils import Vector -import numpy as np - -import lnx.utils -from lnx import log - -if lnx.is_reload(__name__): - log = lnx.reload_module(log) - lnx.utils = lnx.reload_module(lnx.utils) -else: - lnx.enable_reload(__name__) - - -class Vertex: - __slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "bone_weights", "bone_indices", "bone_count", "vertex_index") - - def __init__(self, mesh: bpy.types.Mesh, loop: bpy.types.MeshLoop, vcol0: Optional[bpy.types.Attribute]): - self.vertex_index = loop.vertex_index - loop_idx = loop.index - self.co = mesh.vertices[self.vertex_index].co[:] - self.normal = loop.normal[:] - self.uvs = tuple(layer.data[loop_idx].uv[:] for layer in mesh.uv_layers) - self.col = [0.0, 0.0, 0.0] if vcol0 is None else vcol0.data[loop_idx].color[:] - self.loop_indices = [loop_idx] - self.index = 0 - - def __hash__(self): - return hash((self.co, self.normal, self.uvs)) - - def __eq__(self, other): - eq = ( - (self.co == other.co) and - (self.normal == other.normal) and - (self.uvs == other.uvs) and - (self.col == other.col) - ) - if eq: - indices = self.loop_indices + other.loop_indices - self.loop_indices = indices - other.loop_indices = indices - return eq - - -def calc_tangents(posa, nora, uva, ias, scale_pos): - num_verts = int(len(posa) / 4) - tangents = np.empty(num_verts * 3, dtype=' 0 - if self.has_baked_material(bobject, export_mesh.materials): - has_tex = True - has_tex1 = has_tex and num_uv_layers > 1 - num_colors = self.get_num_vertex_colors(export_mesh) - has_col = self.get_export_vcols(export_mesh) and num_colors > 0 - has_tang = self.has_tangents(export_mesh) - - pdata = np.empty(num_verts * 4, dtype=' maxdim: - maxdim = abs(v.uv[0]) - if abs(v.uv[1]) > maxdim: - maxdim = abs(v.uv[1]) - if has_tex1: - lay1 = uv_layers[t1map] - for v in lay1.data: - if abs(v.uv[0]) > maxdim: - maxdim = abs(v.uv[0]) - maxdim_uvlayer = lay1 - if abs(v.uv[1]) > maxdim: - maxdim = abs(v.uv[1]) - maxdim_uvlayer = lay1 - if has_morph_target: - morph_data = np.empty(num_verts * 2, dtype=' maxdim: - maxdim = abs(v.uv[0]) - maxdim_uvlayer = lay2 - if abs(v.uv[1]) > maxdim: - maxdim = abs(v.uv[1]) - maxdim_uvlayer = lay2 - if maxdim > 1: - o['scale_tex'] = maxdim - invscale_tex = (1 / o['scale_tex']) * 32767 - else: - invscale_tex = 1 * 32767 - self.check_uv_precision(export_mesh, maxdim, maxdim_uvlayer, invscale_tex) - - if has_col: - cdata = np.empty(num_verts * 3, dtype=' 2: - o['scale_pos'] = maxdim / 2 - else: - o['scale_pos'] = 1.0 - if has_armature: # Allow up to 2x bigger bounds for skinned mesh - o['scale_pos'] *= 2.0 - - scale_pos = o['scale_pos'] - invscale_pos = (1 / scale_pos) * 32767 - - # Make arrays - for i, v in enumerate(vert_list): - v.index = i - co = v.co - normal = v.normal - i4 = i * 4 - i2 = i * 2 - pdata[i4 ] = co[0] - pdata[i4 + 1] = co[1] - pdata[i4 + 2] = co[2] - pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale - ndata[i2 ] = normal[0] - ndata[i2 + 1] = normal[1] - if has_tex: - uv = v.uvs[t0map] - t0data[i2 ] = uv[0] - t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y - if has_tex1: - uv = v.uvs[t1map] - t1data[i2 ] = uv[0] - t1data[i2 + 1] = 1.0 - uv[1] - if has_morph_target: - uv = v.uvs[morph_uv_index] - morph_data[i2 ] = uv[0] - morph_data[i2 + 1] = 1.0 - uv[1] - if has_col: - i3 = i * 3 - cdata[i3 ] = v.col[0] - cdata[i3 + 1] = v.col[1] - cdata[i3 + 2] = v.col[2] - - # Indices - # Create dict for every material slot - prims = {ma.name if ma else '': [] for ma in export_mesh.materials} - v_maps = {ma.name if ma else '': [] for ma in export_mesh.materials} - if not prims: - # No materials - prims = {'': []} - v_maps = {'': []} - - # Create dict of {loop_indices : vertex} with each loop_index in each vertex in Vertex_list - vert_dict = {i : v for v in vert_list for i in v.loop_indices} - # For each polygon in a mesh - for poly in export_mesh.polygons: - # Index of the first loop of this polygon - first = poly.loop_start - # No materials assigned - if len(export_mesh.materials) == 0: - # Get prim - prim = prims[''] - v_map = v_maps[''] - else: - # First material - mat = export_mesh.materials[min(poly.material_index, len(export_mesh.materials) - 1)] - # Get prim for this material - prim = prims[mat.name if mat else ''] - v_map = v_maps[mat.name if mat else ''] - # List of indices for each loop_index belonging to this polygon - indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)] - v_indices = [vert_dict[i].vertex_index for i in range(first, first+poly.loop_total)] - - # If 3 loops per polygon (Triangle?) - if poly.loop_total == 3: - prim += indices - v_map += v_indices - # If > 3 loops per polygon (Non-Triangular?) - elif poly.loop_total > 3: - for i in range(poly.loop_total-2): - prim += (indices[-1], indices[i], indices[i + 1]) - v_map += (v_indices[-1], v_indices[i], v_indices[i + 1]) - - # Write indices - o['index_arrays'] = [] - for mat, prim in prims.items(): - idata = [0] * len(prim) - v_map_data = [0] * len(prim) - v_map_sub = v_maps[mat] - for i, v in enumerate(prim): - idata[i] = v - v_map_data[i] = v_map_sub[i] - if len(idata) == 0: # No face assigned - continue - ia = {'values': idata, 'material': 0, 'vertex_map': v_map_data} - # Find material index for multi-mat mesh - if len(export_mesh.materials) > 1: - for i in range(0, len(export_mesh.materials)): - if (export_mesh.materials[i] is not None and mat == export_mesh.materials[i].name) or \ - (export_mesh.materials[i] is None and mat == ''): # Default material for empty slots - ia['material'] = i - break - o['index_arrays'].append(ia) - - if has_tang: - tangdata = calc_tangents(pdata, ndata, t0data, o['index_arrays'], scale_pos) - - pdata *= invscale_pos - ndata *= 32767 - pdata = np.array(pdata, dtype=' max_bones: - log.warn(bobject.name + ' - ' + str(bone_count) + ' bones found, exceeds maximum of ' + str(max_bones) + ' bones defined - raise the value in Camera Data - Leenkx Render Props - Max Bones') - - for i in range(bone_count): - boneRef = self.find_bone(bone_array[i].name) - if boneRef: - oskin['bone_ref_array'].append(boneRef[1]["structName"]) - oskin['bone_len_array'].append(bone_array[i].length) - else: - oskin['bone_ref_array'].append("") - oskin['bone_len_array'].append(0.0) - - # Write the bind pose transform array - oskin['transformsI'] = [] - for i in range(bone_count): - skeletonI = (armature.matrix_world @ bone_array[i].matrix_local).inverted_safe() - skeletonI = (skeletonI @ bobject.matrix_world) - oskin['transformsI'].append(self.write_matrix(skeletonI)) - - # Export the per-vertex bone influence data - group_remap = [] - for group in bobject.vertex_groups: - for i in range(bone_count): - if bone_array[i].name == group.name: - group_remap.append(i) - break - else: - group_remap.append(-1) - - bone_count_array = np.empty(len(vert_list), dtype='= 0: #and bone_weight != 0.0: - bone_values.append((bone_weight, bone_index)) - total_weight += bone_weight - bone_count += 1 - - if bone_count > 4: - bone_count = 4 - bone_values.sort(reverse=True) - bone_values = bone_values[:4] - - bone_count_array[index] = bone_count - for bv in bone_values: - bone_weight_array[count] = bv[0] * 32767 - bone_index_array[count] = bv[1] - count += 1 - - if total_weight not in (0.0, 1.0): - normalizer = 1.0 / total_weight - for i in range(bone_count): - bone_weight_array[count - i - 1] *= normalizer - - oskin['bone_count_array'] = bone_count_array - oskin['bone_index_array'] = bone_index_array[:count] - oskin['bone_weight_array'] = bone_weight_array[:count] - - # Bone constraints - for bone in armature.pose.bones: - if len(bone.constraints) > 0: - if 'constraints' not in oskin: - oskin['constraints'] = [] - self.add_constraints(bone, oskin, bone=True) +""" +Exports smaller geometry but is slower. +To be replaced with https://github.com/zeux/meshoptimizer +""" +from typing import Optional + +import bpy +from mathutils import Vector +import numpy as np + +import lnx.utils +from lnx import log + +if lnx.is_reload(__name__): + log = lnx.reload_module(log) + lnx.utils = lnx.reload_module(lnx.utils) +else: + lnx.enable_reload(__name__) + + +class Vertex: + __slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "bone_weights", "bone_indices", "bone_count", "vertex_index") + + def __init__(self, mesh: bpy.types.Mesh, loop: bpy.types.MeshLoop, vcol0: Optional[bpy.types.Attribute]): + self.vertex_index = loop.vertex_index + loop_idx = loop.index + self.co = mesh.vertices[self.vertex_index].co[:] + self.normal = loop.normal[:] + self.uvs = tuple(layer.data[loop_idx].uv[:] for layer in mesh.uv_layers) + self.col = [0.0, 0.0, 0.0] if vcol0 is None else vcol0.data[loop_idx].color[:] + self.loop_indices = [loop_idx] + self.index = 0 + + def __hash__(self): + return hash((self.co, self.normal, self.uvs)) + + def __eq__(self, other): + eq = ( + (self.co == other.co) and + (self.normal == other.normal) and + (self.uvs == other.uvs) and + (self.col == other.col) + ) + if eq: + indices = self.loop_indices + other.loop_indices + self.loop_indices = indices + other.loop_indices = indices + return eq + + +def calc_tangents(posa, nora, uva, ias, scale_pos): + num_verts = int(len(posa) / 4) + tangents = np.empty(num_verts * 3, dtype=' 0 # TODO FIXME: this should use an `and` instead of `or`. Workaround to completely ignore if the mesh has the `export_uvs` flag. Only checking the `uv_layers` to bypass issues with materials in linked objects. + if self.has_baked_material(bobject, export_mesh.materials): + has_tex = True + has_tex1 = has_tex and num_uv_layers > 1 + num_colors = self.get_num_vertex_colors(export_mesh) + has_col = self.get_export_vcols(export_mesh) and num_colors > 0 + has_tang = self.has_tangents(export_mesh) + + pdata = np.empty(num_verts * 4, dtype=' maxdim: + maxdim = abs(v.uv[0]) + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + if has_tex1: + lay1 = uv_layers[t1map] + for v in lay1.data: + if abs(v.uv[0]) > maxdim: + maxdim = abs(v.uv[0]) + maxdim_uvlayer = lay1 + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + maxdim_uvlayer = lay1 + if has_morph_target: + morph_data = np.empty(num_verts * 2, dtype=' maxdim: + maxdim = abs(v.uv[0]) + maxdim_uvlayer = lay2 + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + maxdim_uvlayer = lay2 + if maxdim > 1: + o['scale_tex'] = maxdim + invscale_tex = (1 / o['scale_tex']) * 32767 + else: + invscale_tex = 1 * 32767 + self.check_uv_precision(export_mesh, maxdim, maxdim_uvlayer, invscale_tex) + + if has_col: + cdata = np.empty(num_verts * 3, dtype=' 2: + o['scale_pos'] = maxdim / 2 + else: + o['scale_pos'] = 1.0 + if has_armature: # Allow up to 2x bigger bounds for skinned mesh + o['scale_pos'] *= 2.0 + + scale_pos = o['scale_pos'] + invscale_pos = (1 / scale_pos) * 32767 + + # Make arrays + for i, v in enumerate(vert_list): + v.index = i + co = v.co + normal = v.normal + i4 = i * 4 + i2 = i * 2 + pdata[i4 ] = co[0] + pdata[i4 + 1] = co[1] + pdata[i4 + 2] = co[2] + pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale + ndata[i2 ] = normal[0] + ndata[i2 + 1] = normal[1] + if has_tex: + uv = v.uvs[t0map] + t0data[i2 ] = uv[0] + t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y + if has_tex1: + uv = v.uvs[t1map] + t1data[i2 ] = uv[0] + t1data[i2 + 1] = 1.0 - uv[1] + if has_morph_target: + uv = v.uvs[morph_uv_index] + morph_data[i2 ] = uv[0] + morph_data[i2 + 1] = 1.0 - uv[1] + if has_col: + i3 = i * 3 + cdata[i3 ] = v.col[0] + cdata[i3 + 1] = v.col[1] + cdata[i3 + 2] = v.col[2] + + # Indices + # Create dict for every material slot + prims = {ma.name if ma else '': [] for ma in export_mesh.materials} + v_maps = {ma.name if ma else '': [] for ma in export_mesh.materials} + if not prims: + # No materials + prims = {'': []} + v_maps = {'': []} + + # Create dict of {loop_indices : vertex} with each loop_index in each vertex in Vertex_list + vert_dict = {i : v for v in vert_list for i in v.loop_indices} + # For each polygon in a mesh + for poly in export_mesh.polygons: + # Index of the first loop of this polygon + first = poly.loop_start + # No materials assigned + if len(export_mesh.materials) == 0: + # Get prim + prim = prims[''] + v_map = v_maps[''] + else: + # First material + mat = export_mesh.materials[min(poly.material_index, len(export_mesh.materials) - 1)] + # Get prim for this material + prim = prims[mat.name if mat else ''] + v_map = v_maps[mat.name if mat else ''] + # List of indices for each loop_index belonging to this polygon + indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)] + v_indices = [vert_dict[i].vertex_index for i in range(first, first+poly.loop_total)] + + # If 3 loops per polygon (Triangle?) + if poly.loop_total == 3: + prim += indices + v_map += v_indices + # If > 3 loops per polygon (Non-Triangular?) + elif poly.loop_total > 3: + for i in range(poly.loop_total-2): + prim += (indices[-1], indices[i], indices[i + 1]) + v_map += (v_indices[-1], v_indices[i], v_indices[i + 1]) + + # Write indices + o['index_arrays'] = [] + for mat, prim in prims.items(): + idata = [0] * len(prim) + v_map_data = [0] * len(prim) + v_map_sub = v_maps[mat] + for i, v in enumerate(prim): + idata[i] = v + v_map_data[i] = v_map_sub[i] + if len(idata) == 0: # No face assigned + continue + ia = {'values': idata, 'material': 0, 'vertex_map': v_map_data} + # Find material index for multi-mat mesh + if len(export_mesh.materials) > 1: + for i in range(0, len(export_mesh.materials)): + if (export_mesh.materials[i] is not None and mat == export_mesh.materials[i].name) or \ + (export_mesh.materials[i] is None and mat == ''): # Default material for empty slots + ia['material'] = i + break + o['index_arrays'].append(ia) + + if has_tang: + tangdata = calc_tangents(pdata, ndata, t0data, o['index_arrays'], scale_pos) + + pdata *= invscale_pos + ndata *= 32767 + pdata = np.array(pdata, dtype=' max_bones: + log.warn(bobject.name + ' - ' + str(bone_count) + ' bones found, exceeds maximum of ' + str(max_bones) + ' bones defined - raise the value in Camera Data - Leenkx Render Props - Max Bones') + + for i in range(bone_count): + boneRef = self.find_bone(bone_array[i].name) + if boneRef: + oskin['bone_ref_array'].append(boneRef[1]["structName"]) + oskin['bone_len_array'].append(bone_array[i].length) + else: + oskin['bone_ref_array'].append("") + oskin['bone_len_array'].append(0.0) + + # Write the bind pose transform array + oskin['transformsI'] = [] + for i in range(bone_count): + skeletonI = (armature.matrix_world @ bone_array[i].matrix_local).inverted_safe() + skeletonI = (skeletonI @ bobject.matrix_world) + oskin['transformsI'].append(self.write_matrix(skeletonI)) + + # Export the per-vertex bone influence data + group_remap = [] + for group in bobject.vertex_groups: + for i in range(bone_count): + if bone_array[i].name == group.name: + group_remap.append(i) + break + else: + group_remap.append(-1) + + bone_count_array = np.empty(len(vert_list), dtype='= 0: #and bone_weight != 0.0: + bone_values.append((bone_weight, bone_index)) + total_weight += bone_weight + bone_count += 1 + + if bone_count > 4: + bone_count = 4 + bone_values.sort(reverse=True) + bone_values = bone_values[:4] + + bone_count_array[index] = bone_count + for bv in bone_values: + bone_weight_array[count] = bv[0] * 32767 + bone_index_array[count] = bv[1] + count += 1 + + if total_weight not in (0.0, 1.0): + normalizer = 1.0 / total_weight + for i in range(bone_count): + bone_weight_array[count - i - 1] *= normalizer + + oskin['bone_count_array'] = bone_count_array + oskin['bone_index_array'] = bone_index_array[:count] + oskin['bone_weight_array'] = bone_weight_array[:count] + + # Bone constraints + for bone in armature.pose.bones: + if len(bone.constraints) > 0: + if 'constraints' not in oskin: + oskin['constraints'] = [] + self.add_constraints(bone, oskin, bone=True)