Rewrite mesh exporter to support armature & poses

This commit is contained in:
Nikita Lisitsa 2021-07-13 11:55:57 +03:00
parent fc47633c42
commit de1950f1a6
3 changed files with 229 additions and 53 deletions

View file

@ -5,6 +5,7 @@
#include <psemek/gfx/array.hpp>
#include <psemek/gfx/buffer.hpp>
#include <psemek/gfx/attribs.hpp>
#include <psemek/gfx/armature.hpp>
#include <psemek/geom/vector.hpp>
#include <psemek/geom/point.hpp>
@ -341,29 +342,17 @@ namespace psemek::gfx
load_instance(instances.data(), instances.size(), usage);
}
using pose_library = std::unordered_map<std::string_view, util::span<bone_transform<float> const>>;
struct imported_mesh
{
attribs_description attribs;
util::span<char const> vertices;
util::span<std::uint32_t const> indices;
struct bone
{
std::uint32_t parent;
util::span<bone const> bones;
static constexpr std::uint32_t null = std::uint32_t(-1);
};
util::span<bone> bones;
struct bone_pose
{
geom::vector<float, 3> translation;
float scale;
geom::quaternion<float> rotation;
};
std::unordered_map<std::string_view, util::span<bone_pose>> poses;
pose_library poses;
};
imported_mesh load_mesh(std::string_view data);

View file

@ -212,16 +212,25 @@ 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<std::uint32_t>();
auto parse_section_mesh = [&]
{
vertex_format = s.read<std::uint32_t>();
if (vertex_format & POSITION_MASK)
result.attribs += make_attribs_description<geom::point<float, 3>>();
@ -235,14 +244,79 @@ namespace psemek::gfx
if (vertex_format & TEXCOORDS_MASK)
result.attribs += make_attribs_description<geom::vector<float, 2>>();
if (vertex_format & WEIGHTS_MASK)
result.attribs += make_attribs_description<gfx::integer<geom::vector<std::uint8_t, 2>>, gfx::normalized<geom::vector<std::uint8_t, 2>>>();
auto vertex_count = s.read<std::uint32_t>();
auto vertex_ptr = s.read_raw(vertex_count * result.attribs.vertex_size);
auto index_count = s.read<std::uint32_t>();
auto index_ptr = reinterpret_cast<std::uint32_t const *>(s.read_raw(index_count * sizeof(std::uint32_t)));
auto index_ptr = s.read_ptr<std::uint32_t>(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<std::uint32_t>();
auto bone_ptr = s.read_ptr<bone>(bone_count);
result.bones = {bone_ptr, bone_ptr + bone_count};
};
auto parse_section_pose = [&]
{
auto name_length = s.read<std::uint32_t>();
auto name_ptr = s.read_raw(name_length);
auto pose_count = s.read<std::uint32_t>();
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<bone_transform<float>>(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<std::uint32_t>();
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;
}

View file

@ -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('<B', obj.value)
if type(obj) == int:
if obj < 0:
return struct.pack('<i', obj)
@ -138,12 +242,21 @@ def to_bytes(obj):
for x in obj:
res += to_bytes(x)
return res
if type(obj) == str:
b = obj.encode('utf-8')
return struct.pack('<I', len(b)) + b
if skeleton is not None:
# TODO: support skeletons
assert False
else:
data = to_bytes((vertex_format, vertices, indices))
SECTION_MESH = 1
SECTION_BONES = 2
SECTION_POSE = 3
data = to_bytes((SECTION_MESH, vertex_format, vertices, indices))
if armature:
data += to_bytes((SECTION_BONES, bones))
for name, pdata in poses:
data += to_bytes((SECTION_POSE, name, pdata))
filename = sys.argv[sys.argv.index('--') + 1]