From de1950f1a69c152ba4054c22d4816ad802def7a1 Mon Sep 17 00:00:00 2001 From: lisyarus Date: Tue, 13 Jul 2021 11:55:57 +0300 Subject: [PATCH] Rewrite mesh exporter to support armature & poses --- libs/gfx/include/psemek/gfx/mesh.hpp | 21 +--- libs/gfx/source/mesh.cpp | 112 +++++++++++++++---- tools/convert-mesh/bin/convert-mesh.py | 149 ++++++++++++++++++++++--- 3 files changed, 229 insertions(+), 53 deletions(-) diff --git a/libs/gfx/include/psemek/gfx/mesh.hpp b/libs/gfx/include/psemek/gfx/mesh.hpp index b8d1468e..2a26e7cb 100644 --- a/libs/gfx/include/psemek/gfx/mesh.hpp +++ b/libs/gfx/include/psemek/gfx/mesh.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -341,29 +342,17 @@ namespace psemek::gfx load_instance(instances.data(), instances.size(), usage); } + using pose_library = std::unordered_map const>>; + struct imported_mesh { attribs_description attribs; util::span vertices; util::span indices; - struct bone - { - std::uint32_t parent; + util::span bones; - static constexpr std::uint32_t null = std::uint32_t(-1); - }; - - util::span bones; - - struct bone_pose - { - geom::vector translation; - float scale; - geom::quaternion rotation; - }; - - std::unordered_map> poses; + pose_library poses; }; imported_mesh load_mesh(std::string_view data); diff --git a/libs/gfx/source/mesh.cpp b/libs/gfx/source/mesh.cpp index 4b5a8336..40132f18 100644 --- a/libs/gfx/source/mesh.cpp +++ b/libs/gfx/source/mesh.cpp @@ -212,37 +212,111 @@ namespace psemek::gfx imported_mesh load_mesh(std::string_view data) { // These should be in sync with convert-mesh.py - static std::uint32_t const POSITION_MASK = 1; - static std::uint32_t const NORMALS_MASK = 2; - static std::uint32_t const COLORS_MASK = 4; - static std::uint32_t const TEXCOORDS_MASK = 8; + static std::uint32_t const SECTION_MESH = 1; + static std::uint32_t const SECTION_BONES = 2; + static std::uint32_t const SECTION_POSE = 3; + + static std::uint32_t const POSITION_MASK = 1 << 0; + static std::uint32_t const NORMALS_MASK = 1 << 1; + static std::uint32_t const COLORS_MASK = 1 << 2; + static std::uint32_t const TEXCOORDS_MASK = 1 << 3; + static std::uint32_t const WEIGHTS_MASK = 1 << 4; + + std::uint32_t vertex_format = 0; util::binary_istream s{data}; imported_mesh result; - auto vertex_format = s.read(); + auto parse_section_mesh = [&] + { + vertex_format = s.read(); - if (vertex_format & POSITION_MASK) - result.attribs += make_attribs_description>(); + if (vertex_format & POSITION_MASK) + result.attribs += make_attribs_description>(); - if (vertex_format & NORMALS_MASK) - result.attribs += make_attribs_description>(); + if (vertex_format & NORMALS_MASK) + result.attribs += make_attribs_description>(); - if (vertex_format & COLORS_MASK) - result.attribs += make_attribs_description>>(); + if (vertex_format & COLORS_MASK) + result.attribs += make_attribs_description>>(); - if (vertex_format & TEXCOORDS_MASK) - result.attribs += make_attribs_description>(); + if (vertex_format & TEXCOORDS_MASK) + result.attribs += make_attribs_description>(); - auto vertex_count = s.read(); - auto vertex_ptr = s.read_raw(vertex_count * result.attribs.vertex_size); + if (vertex_format & WEIGHTS_MASK) + result.attribs += make_attribs_description>, gfx::normalized>>(); - auto index_count = s.read(); - auto index_ptr = reinterpret_cast(s.read_raw(index_count * sizeof(std::uint32_t))); + auto vertex_count = s.read(); + auto vertex_ptr = s.read_raw(vertex_count * result.attribs.vertex_size); - result.vertices = {vertex_ptr, vertex_ptr + vertex_count * result.attribs.vertex_size}; - result.indices = {index_ptr, index_ptr + index_count}; + auto index_count = s.read(); + auto index_ptr = s.read_ptr(index_count); + + result.vertices = {vertex_ptr, vertex_ptr + vertex_count * result.attribs.vertex_size}; + result.indices = {index_ptr, index_ptr + index_count}; + }; + + auto parse_section_bones = [&] + { + auto bone_count = s.read(); + auto bone_ptr = s.read_ptr(bone_count); + + result.bones = {bone_ptr, bone_ptr + bone_count}; + }; + + auto parse_section_pose = [&] + { + auto name_length = s.read(); + auto name_ptr = s.read_raw(name_length); + + auto pose_count = s.read(); + if (pose_count != result.bones.size()) + throw std::runtime_error("Number of transforms in a pose must be equal to the number of bones"); + auto pose_ptr = s.read_ptr>(pose_count); + + std::string_view name(name_ptr, name_ptr + name_length); + + result.poses[name] = {pose_ptr, pose_ptr + pose_count}; + }; + + bool had_section_mesh = false; + bool had_section_bones = false; + + while (!s.eof()) + { + auto section_type = s.read(); + + switch (section_type) + { + case SECTION_MESH: + if (had_section_mesh) + throw std::runtime_error("Section 'mesh' must not repeat"); + parse_section_mesh(); + had_section_mesh = true; + break; + case SECTION_BONES: + if (had_section_bones) + throw std::runtime_error("Section 'bones' must not repeat"); + if (!had_section_mesh) + throw std::runtime_error("Section 'bones' must come after section 'mesh'"); + if ((vertex_format & WEIGHTS_MASK) == 0) + throw std::runtime_error("Section 'bones' requires weights in vertex format"); + parse_section_bones(); + had_section_bones = true; + break; + case SECTION_POSE: + if (!had_section_bones) + throw std::runtime_error("Section 'pose' must come after section 'bones'"); + parse_section_pose(); + break; + default: + throw std::runtime_error("Unknown section code " + util::to_string(section_type)); + } + } + + if (!had_section_mesh) + throw std::runtime_error("Section 'mesh' must be present"); return result; } diff --git a/tools/convert-mesh/bin/convert-mesh.py b/tools/convert-mesh/bin/convert-mesh.py index 2c2667c9..de4a2111 100644 --- a/tools/convert-mesh/bin/convert-mesh.py +++ b/tools/convert-mesh/bin/convert-mesh.py @@ -5,16 +5,17 @@ import struct bpy.ops.object.mode_set(mode = 'OBJECT') +obj = None mesh = None flat = False if 'mesh' in bpy.data.objects: - mesh = bpy.data.objects['mesh'].data + obj = bpy.data.objects['mesh'] print('Using smooth normals') if 'mesh_flat' in bpy.data.objects: - mesh = bpy.data.objects['mesh_flat'].data + obj = bpy.data.objects['mesh_flat'] print('Using flat normals') flat = True -assert mesh +mesh = obj.data colors = None if len(mesh.vertex_colors) > 0: @@ -26,27 +27,33 @@ if len(mesh.uv_layers) > 0: texcoords = mesh.uv_layers.active.data print('Found texture coordinates') -skeleton = None -if 'skeleton' in bpy.data.objects: - skeleton = bpy.data.objects['skeleton'].data - print('Found skeleton') +armature = None +bone_names = None +if 'armature' in bpy.data.objects: + armature = bpy.data.objects['armature'] + print('Found armature with {} bones'.format(len(armature.data.bones))) + bone_names = [b.name for b in armature.data.bones] POSITION_MASK = 1 NORMAL_MASK = 2 COLOR_MASK = 4 TEXCOORD_MASK = 8 +WEIGHTS_MASK = 16 vertex_format = POSITION_MASK | NORMAL_MASK if colors: vertex_format |= COLOR_MASK if texcoords: vertex_format |= TEXCOORD_MASK -print("Using vertex format", format(vertex_format, '04b')) +if armature: + vertex_format |= WEIGHTS_MASK +print("Using vertex format", format(vertex_format, '05b')) vertex_coords = [] vertex_normals = [] vertex_colors = [] vertex_texcoords = [] +vertex_weights = [] indices = [] @@ -63,6 +70,23 @@ if flat: vertex_colors.append(tuple(colors[li].color)) if texcoords: vertex_texcoords.append(tuple(texcoords[li].uv)) + if armature: + weights = [] + for g in v.groups: + name = obj.vertex_groups[g.group].name + if name in bone_names: + weights.append((bone_names.index(name), g.weight)) + for g, w in weights: + if g >= len(armature.data.bones): + print("Invalid vertex group index {} for armature weights".format(g), file=sys.stderr) + sys.exit(1) + weights = list(sorted(weights, key=lambda p: -p[1]))[:2] + while len(weights) < 2: + weights.append((0,0.0)) + weights = list(sorted(weights, key=lambda p: -p[1]))[:2] + W = sum(p[1] for p in weights) + weights = [(i,w/W) for i,w in weights] + vertex_weights[vi] = weights indices.append(i) else: @@ -74,6 +98,8 @@ else: vertex_colors = [None] * len(vertex_coords) if texcoords: vertex_texcoords = [None] * len(vertex_coords) + if armature: + vertex_weights = [None] * len(vertex_coords) for p in mesh.loop_triangles: for li in p.loops: @@ -84,6 +110,23 @@ else: vertex_colors[vi] = tuple(colors[li].color) if texcoords: vertex_texcoords[vi] = tuple(texcoords[li].uv) + if armature: + weights = [] + for g in v.groups: + name = obj.vertex_groups[g.group].name + if name in bone_names: + weights.append((bone_names.index(name), g.weight)) + for g, w in weights: + if g >= len(armature.data.bones): + print("Invalid vertex group index {} for armature weights".format(g), file=sys.stderr) + sys.exit(1) + weights = list(sorted(weights, key=lambda p: -p[1]))[:2] + while len(weights) < 2: + weights.append((0,0.0)) + W = sum(p[1] for p in weights) + weights = [(i,w/W) for i,w in weights] + vertex_weights[vi] = weights + assert (len(indices) % 3) == 0 assert len(vertex_coords) == len(vertex_normals) @@ -91,6 +134,14 @@ if colors: assert len(vertex_coords) == len(vertex_colors) if texcoords: assert len(vertex_coords) == len(vertex_texcoords) +if armature: + assert len(vertex_coords) == len(vertex_weights) + + +class Uint8: + def __init__(self, value): + self.value = int(value) + if colors: for i in range(len(vertex_colors)): @@ -99,6 +150,21 @@ if colors: c = (c << 8) | int(max(0, min(255, vertex_colors[i][k] * 255))) vertex_colors[i] = c +if armature: + for i in range(len(vertex_weights)): + gs = [] + for g, w in vertex_weights[i]: + gs.append(Uint8(g)) + + ws = [] + for g, w in vertex_weights[i]: + ws.append(Uint8(w * 255.0)) + ws[0].value += 255 - sum(w.value for w in ws) + + assert len(gs) == 2 + assert len(ws) == 2 + vertex_weights[i] = tuple(gs + ws) + vertices = [] for i in range(len(vertex_coords)): attribs = [vertex_coords[i], vertex_normals[i]] @@ -106,21 +172,59 @@ for i in range(len(vertex_coords)): attribs.append(vertex_colors[i]) if texcoords: attribs.append(vertex_texcoords[i]) + if armature: + attribs.append(vertex_weights[i]) vertices.append(tuple(attribs)) print(len(vertices), 'vertices') print(len(indices), 'indices') -if skeleton is not None: +if armature is not None: bones = [] - for b in skeleton.data.bones: + for b in armature.data.bones: + p = None if b.parent is None: - bones.append(-1) + p = -1 else: - pi = list(skeleton.data.bones).index(b.parent) - bones.append(pi) + p = list(armature.data.bones).index(b.parent) + bones.append((p, tuple(b.head_local), tuple(map(tuple, b.matrix_local.to_3x3())))) + +poses = [] +for index, pose in enumerate(bpy.data.objects['armature'].pose_library.pose_markers): + print("Found pose", pose.name) + + # [rotation, scale, translation] + data = [] + for i in range(len(bone_names)): + data.append([[None, None, None, None], [None, None, None], [None, None, None]]) + + marker = armature.pose_library.pose_markers[index] + frame = marker.frame + action = bpy.data.actions[armature.pose_library.name] + for g in action.groups: + if not g.name in bone_names: + continue + bone_index = bone_names.index(g.name) + for ch in g.channels: + value = ch.evaluate(float(frame)) + if ch.data_path.find('rotation_quaternion') != -1: + data[bone_index][0][ch.array_index] = value + elif ch.data_path.find('scale') != -1: + data[bone_index][1][ch.array_index] = value + elif ch.data_path.find('location') != -1: + data[bone_index][2][ch.array_index] = value + + new_data = [] + for d in data: + rotation = tuple([d[0][1], d[0][2], d[0][3], d[0][0]]) + scale = d[1][0] + translation = tuple(d[2]) + new_data.append((rotation, scale, translation)) + poses.append((pose.name, new_data)) def to_bytes(obj): + if type(obj) == Uint8: + return struct.pack('