[build_actor] Add skeleton and animation support (#3638)

This adds a feature to `build_actor` to support importing skeletons and
animations from .glb files.

Multiple animations are handled and will use the name in the GLB. The
default `viewer` process will end up playing back the first animation.

There are a few limitations:
- You can only have around 100 bones. It is technically possibly to have
slightly more, but certain animations may fail to compress when there
are more than ~100 bones.
- Currently, all animations have 60 keyframes per second. This is a
higher quality than what is normally used. If animation size becomes
problematic, we could make this customizable somehow.
- There is no support for the `align` bone.

---------

Co-authored-by: water111 <awaterford1111445@gmail.com>
This commit is contained in:
water111
2024-08-16 11:25:53 -04:00
committed by GitHub
parent b0cd59e6ba
commit f2e7606f1b
13 changed files with 1042 additions and 113 deletions
+11
View File
@@ -369,6 +369,17 @@ struct Matrix {
return result;
}
Matrix<T, Rows, Cols> transposed() const {
static_assert(Rows == Cols);
Matrix<T, Rows, Cols> ret;
for (int i = 0; i < Rows; i++) {
for (int j = 0; j < Cols; j++) {
ret(i, j) = operator()(j, i);
}
}
return ret;
}
template <int OtherCols>
Matrix<T, Rows, OtherCols> operator*(const Matrix<T, Cols, OtherCols>& y) const {
Matrix<T, Rows, OtherCols> result;
+166 -8
View File
@@ -44,6 +44,20 @@ std::vector<math::Vector<u8, 4>> extract_color_from_vec4_float(const u8* data,
return result;
}
std::vector<math::Vector<u8, 4>> extract_color_from_vec3_float(const u8* data,
u32 count,
u32 stride) {
std::vector<math::Vector<u8, 4>> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
math::Vector<float, 3> temp;
memcpy(&temp, data, sizeof(math::Vector<float, 3>));
data += stride;
result.emplace_back(temp.x() * 255, temp.y() * 255, temp.z() * 255, 255);
}
return result;
}
/*!
* Convert a GLTF color buffer (u16 format) to u8 colors.
*/
@@ -61,6 +75,16 @@ std::vector<math::Vector<u8, 4>> extract_color_from_vec4_u16(const u8* data,
return result;
}
std::vector<math::Vector<u8, 4>> extract_color_from_vec4_u8(const u8* data, u32 count, u32 stride) {
std::vector<math::Vector<u8, 4>> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
result.push_back(math::Vector<u8, 4>(data[0], data[1], data[2], data[3]));
data += stride;
}
return result;
}
/*!
* Convert a GLTF index buffer
*/
@@ -92,6 +116,74 @@ std::vector<u32> gltf_index_buffer(const tinygltf::Model& model,
}
}
std::vector<math::Matrix4f> extract_mat4(const tinygltf::Model& model, int accessor_idx) {
const auto& accessor = model.accessors[accessor_idx];
const auto& buffer_view = model.bufferViews[accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const u8* data_ptr = buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset;
const auto stride = accessor.ByteStride(buffer_view);
const auto count = accessor.count;
// ASSERT(buffer_view.target == TINYGLTF_TARGET_ARRAY_BUFFER); // ??
ASSERT(accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT);
ASSERT(accessor.type == TINYGLTF_TYPE_MAT4);
std::vector<math::Matrix4f> result(accessor.count);
for (size_t x = 0; x < count; x++) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
memcpy(&result[x](j, i), data_ptr + sizeof(float) * (i * 4 + j), sizeof(float));
}
}
data_ptr += stride;
}
return result;
}
JointsAndWeights convert_per_vertex_data(const math::Vector4f& weights,
const math::Vector<u8, 4>& joints) {
int discard_idx = -1;
float discard_weight = 100;
for (int i = 0; i < 4; i++) {
if (weights[i] < discard_weight) {
discard_idx = i;
discard_weight = weights[i];
}
}
JointsAndWeights ret;
int dst = 0;
float sum = 0;
for (int src = 0; src < 4; src++) {
if (src == discard_idx) {
continue;
}
// this +1 is to account for align not existing in the gltf.
ret.joints[dst] = joints[src] + 2;
ret.weights[dst] = weights[src];
sum += ret.weights[dst];
dst++;
}
ret.weights /= sum;
return ret;
}
std::vector<JointsAndWeights> extract_and_flatten_joints_and_weights(
const tinygltf::Model& model,
const tinygltf::Primitive& prim) {
auto weights =
extract_vec<float, 4>(model, prim.attributes.at("WEIGHTS_0"), TINYGLTF_COMPONENT_TYPE_FLOAT);
auto joints = extract_vec<u8, 4>(model, prim.attributes.at("JOINTS_0"),
TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE);
std::vector<JointsAndWeights> ret;
ASSERT(weights.size() == joints.size());
for (size_t i = 0; i < weights.size(); i++) {
ret.push_back(convert_per_vertex_data(weights[i], joints[i]));
}
return ret;
}
/*!
* Extract positions, colors, and normals from a mesh.
*/
@@ -148,19 +240,37 @@ ExtractedVertices gltf_vertices(const tinygltf::Model& model,
buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset;
const auto byte_stride = attrib_accessor.ByteStride(buffer_view);
const auto count = attrib_accessor.count;
ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC4, "COLOR_0 wasn't vec4");
std::vector<math::Vector<u8, 4>> colors;
switch (attrib_accessor.componentType) {
case TINYGLTF_COMPONENT_TYPE_FLOAT:
colors = extract_color_from_vec4_float(data_ptr, count, byte_stride);
switch (attrib_accessor.type) {
case TINYGLTF_TYPE_VEC4:
switch (attrib_accessor.componentType) {
case TINYGLTF_COMPONENT_TYPE_FLOAT:
colors = extract_color_from_vec4_float(data_ptr, count, byte_stride);
break;
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
colors = extract_color_from_vec4_u16(data_ptr, count, byte_stride);
break;
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
colors = extract_color_from_vec4_u8(data_ptr, count, byte_stride);
break;
default:
lg::die("Unknown type for COLOR_0: {}", attrib_accessor.componentType);
}
break;
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
colors = extract_color_from_vec4_u16(data_ptr, count, byte_stride);
case TINYGLTF_TYPE_VEC3:
switch (attrib_accessor.componentType) {
case TINYGLTF_COMPONENT_TYPE_FLOAT:
colors = extract_color_from_vec3_float(data_ptr, count, byte_stride);
break;
default:
lg::die("unkonwn component type for vec3 color {}", attrib_accessor.componentType);
}
break;
default:
lg::die("Unknown component type for COLOR_0: {}", attrib_accessor.componentType);
lg::die("unknown attribute type for color {}", attrib_accessor.type);
}
vtx_colors.insert(vtx_colors.end(), colors.begin(), colors.end());
}
}
@@ -348,6 +458,15 @@ math::Vector4f vector4f_from_gltf(const std::vector<double>& in) {
return math::Vector4f{in[0], in[1], in[2], in[3]};
}
math::Matrix4f matrix_from_trs(const math::Vector3f& trans,
const math::Vector4f& quat,
const math::Vector3f& scale) {
math::Matrix4f t = affine_translation(trans);
math::Matrix4f r = affine_rot_qxyzw(quat);
math::Matrix4f s = affine_scale(scale);
return t * r * s;
}
math::Matrix4f matrix_from_node(const tinygltf::Node& node) {
if (!node.matrix.empty()) {
math::Matrix4f result;
@@ -464,4 +583,43 @@ DrawMode draw_mode_from_sampler(const tinygltf::Sampler& sampler) {
return mode;
}
std::optional<int> find_single_skin(const tinygltf::Model& model,
const std::vector<NodeWithTransform>& all_nodes) {
std::optional<int> skin_index;
for (const auto& n : all_nodes) {
const auto& node = model.nodes.at(n.node_idx);
if (node.skin >= 0) {
if (skin_index && *skin_index != node.skin) {
lg::die("GLTF contains multiple skins, but only one skin per actor is supported.");
}
skin_index = node.skin;
}
}
return skin_index;
}
std::vector<float> extract_floats(const tinygltf::Model& model, int accessor_idx) {
const auto& accessor = model.accessors[accessor_idx];
const auto& buffer_view = model.bufferViews[accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const u8* data_ptr = buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset;
const auto stride = accessor.ByteStride(buffer_view);
const auto count = accessor.count;
// ASSERT(buffer_view.target == TINYGLTF_TARGET_ARRAY_BUFFER); // ??
if (accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) {
lg::die("mismatched format, wanted {} but got {}", TINYGLTF_COMPONENT_TYPE_FLOAT,
accessor.componentType);
}
ASSERT(accessor.type == TINYGLTF_TYPE_SCALAR);
std::vector<float> result(accessor.count);
for (size_t x = 0; x < count; x++) {
memcpy(&result[x], data_ptr, sizeof(float));
data_ptr += stride;
}
return result;
}
} // namespace gltf_util
+59
View File
@@ -1,11 +1,13 @@
#pragma once
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "common/common_types.h"
#include "common/custom_data/Tfrag3Data.h"
#include "common/log/log.h"
#include "common/math/Vector.h"
#include "third-party/tiny_gltf/tiny_gltf.h"
@@ -28,10 +30,19 @@ std::vector<u32> index_list_to_u32(const u8* data, u32 num_verts, u32 offset, u3
return result;
}
struct JointsAndWeights {
math::Vector<u8, 3> joints = math::Vector<u8, 3>::zero();
math::Vector<float, 3> weights = math::Vector<float, 3>::zero();
};
std::vector<math::Vector3f> extract_vec3f(const u8* data, u32 count, u32 stride);
std::vector<math::Vector2f> extract_vec2f(const u8* data, u32 count, u32 stride);
std::vector<math::Vector<u8, 4>> extract_color_from_vec4_u16(const u8* data, u32 count, u32 stride);
std::vector<u32> gltf_index_buffer(const tinygltf::Model& model, int indices_idx, u32 index_offset);
std::vector<math::Matrix4f> extract_mat4(const tinygltf::Model& model, int accessor_idx);
std::vector<JointsAndWeights> extract_and_flatten_joints_and_weights(
const tinygltf::Model& model,
const tinygltf::Primitive& prim);
struct ExtractedVertices {
std::vector<tfrag3::PreloadedVertex> vtx;
@@ -68,4 +79,52 @@ std::vector<NodeWithTransform> flatten_nodes_from_all_scenes(const tinygltf::Mod
DrawMode draw_mode_from_sampler(const tinygltf::Sampler& sampler);
/*!
* Find the index of the skin for this model. Returns nullopt if there is no skin, the index of the
* skin if there is a single skin used, or fatal error if there are multiple skins.
*/
std::optional<int> find_single_skin(const tinygltf::Model& model,
const std::vector<NodeWithTransform>& all_nodes);
template <typename T, int n>
std::vector<math::Vector<T, n>> extract_vec(const tinygltf::Model& model,
int accessor_idx,
int format) {
const auto& accessor = model.accessors[accessor_idx];
const auto& buffer_view = model.bufferViews[accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const u8* data_ptr = buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset;
const auto stride = accessor.ByteStride(buffer_view);
const auto count = accessor.count;
// ASSERT(buffer_view.target == TINYGLTF_TARGET_ARRAY_BUFFER); // ??
if (accessor.componentType != format) {
lg::die("mismatched format, wanted {} but got {}", format, accessor.componentType);
}
ASSERT(accessor.componentType == format);
switch (n) {
case 3:
ASSERT(accessor.type == TINYGLTF_TYPE_VEC3);
break;
case 4:
ASSERT(accessor.type == TINYGLTF_TYPE_VEC4);
break;
default:
ASSERT_NOT_REACHED();
}
std::vector<math::Vector<T, n>> result(accessor.count);
for (size_t x = 0; x < count; x++) {
for (int i = 0; i < n; i++) {
memcpy(&result[x][i], data_ptr + sizeof(T) * i, sizeof(T));
}
data_ptr += stride;
}
return result;
}
std::vector<float> extract_floats(const tinygltf::Model& model, int accessor_idx);
math::Matrix4f matrix_from_trs(const math::Vector3f& trans,
const math::Vector4f& quat,
const math::Vector3f& scale);
} // namespace gltf_util
@@ -99,9 +99,9 @@ const tfrag3::MercVertex& find_closest(const std::vector<tfrag3::MercVertex>& ol
float y,
float z) {
float best_dist = 1e10;
int best_idx = 0;
size_t best_idx = 0;
for (int i = 0; i < old.size(); i++) {
for (size_t i = 0; i < old.size(); i++) {
auto& v = old[i];
float dx = v.pos[0] - x;
float dy = v.pos[1] - y;
+1
View File
@@ -6,6 +6,7 @@ add_library(compiler
emitter/Register.cpp
debugger/disassemble.cpp
build_level/common/build_level.cpp
build_actor/common/animation_processing.cpp
build_actor/common/MercExtract.cpp
build_level/jak1/build_level.cpp
build_level/jak2/build_level.cpp
+22 -19
View File
@@ -55,26 +55,29 @@ void extract(const std::string& name,
out.normals.insert(out.normals.end(), verts.normals.begin(), verts.normals.end());
ASSERT(out.new_colors.size() == out.new_vertices.size());
// TODO: just putting it all in one material
if (prim.attributes.count("JOINTS_0") && prim.attributes.count("WEIGHTS_0")) {
auto joints_and_weights = gltf_util::extract_and_flatten_joints_and_weights(model, prim);
ASSERT(joints_and_weights.size() == verts.vtx.size());
out.joints_and_weights.insert(out.joints_and_weights.end(), joints_and_weights.begin(),
joints_and_weights.end());
} else {
// add fake data for vertices without this data
gltf_util::JointsAndWeights dummy;
dummy.joints[0] = 3;
dummy.weights[0] = 1.f;
for (size_t i = 0; i < out.new_vertices.size(); i++) {
out.joints_and_weights.push_back(dummy);
}
}
// real draw details will be filled out in the next loop.
auto& draw = draw_by_material[prim.material];
draw.mode = gltf_util::make_default_draw_mode(); // todo rm
draw.tree_tex_id = 0; // todo rm
draw.num_triangles += prim_indices.size() / 3;
draw.no_strip = true;
// if (draw.vis_groups.empty()) {
// auto& grp = draw.vis_groups.emplace_back();
// grp.num_inds += prim_indices.size();
// grp.num_tris += draw.num_triangles;
// grp.vis_idx_in_pc_bvh = UINT32_MAX;
// } else {
// auto& grp = draw.vis_groups.back();
// grp.num_inds += prim_indices.size();
// grp.num_tris += draw.num_triangles;
// grp.vis_idx_in_pc_bvh = UINT32_MAX;
// }
draw.index_count = prim_indices.size();
draw.first_index = index_offset + out.new_indices.size();
out.new_indices.insert(out.new_indices.end(), prim_indices.begin(), prim_indices.end());
}
}
@@ -136,18 +139,18 @@ void merc_convert(MercSwapData& out, const MercExtractData& in) {
x.normal[0] = in.normals.at(i).x();
x.normal[1] = in.normals.at(i).y();
x.normal[2] = in.normals.at(i).z();
x.weights[0] = 1.0f;
x.weights[1] = 0.0f;
x.weights[2] = 0.0f;
x.weights[0] = in.joints_and_weights.at(i).weights[0];
x.weights[1] = in.joints_and_weights.at(i).weights[1];
x.weights[2] = in.joints_and_weights.at(i).weights[2];
x.st[0] = y.s;
x.st[1] = y.t;
x.rgba[0] = in.new_colors[i][0];
x.rgba[1] = in.new_colors[i][1];
x.rgba[2] = in.new_colors[i][2];
x.rgba[3] = in.new_colors[i][3];
x.mats[0] = 3;
x.mats[1] = 0;
x.mats[2] = 0;
x.mats[0] = in.joints_and_weights.at(i).joints[0];
x.mats[1] = in.joints_and_weights.at(i).joints[1];
x.mats[2] = in.joints_and_weights.at(i).joints[2];
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ struct MercExtractData {
std::vector<tfrag3::PreloadedVertex> new_vertices;
std::vector<math::Vector<u8, 4>> new_colors;
std::vector<math::Vector3f> normals;
std::vector<gltf_util::JointsAndWeights> joints_and_weights;
tfrag3::MercModel new_model;
};
@@ -0,0 +1,281 @@
#include "animation_processing.h"
#include "common/log/log.h"
#include "common/util/gltf_util.h"
namespace anim {
namespace {
int find_max_joint(const tinygltf::Animation& anim, const std::map<int, int>& node_to_joint) {
int max_joint = 0;
for (const auto& channel : anim.channels) {
if (node_to_joint.find(channel.target_node) != node_to_joint.end()) {
max_joint = std::max(node_to_joint.at(channel.target_node), max_joint);
} else {
lg::error("animated node not in our skeleton!!");
}
}
return max_joint;
}
template <typename T>
std::vector<T> compute_keyframes(const std::vector<float>& times,
const std::vector<T>& values,
float framerate) {
std::vector<T> ret;
ASSERT(!times.empty());
ASSERT(times.size() == values.size());
size_t i = 0;
float t = 0;
while (t < times.back()) {
// advance input keyframe
while ((i + 1 < times.size()) // can advance
&& times.at(i + 1) < t // next keyframe is before sample point
) {
i++;
}
const float fraction = (t - times.at(i)) / (times.at(i + 1) - times.at(i));
ret.push_back(values.at(i) * (1.f - fraction) + values.at(i + 1) * fraction);
// lg::info("{} + {:.3f}, {}", i, fraction, ret.back().to_string_aligned());
t += 1.f / framerate;
}
return ret;
}
template <int n>
std::vector<math::Vector<float, n>> extract_keyframed_gltf_vecn(
const tinygltf::Model& model,
const tinygltf::AnimationSampler& sampler,
float framerate) {
std::vector<float> times = gltf_util::extract_floats(model, sampler.input);
std::vector<math::Vector<float, n>> values =
gltf_util::extract_vec<float, n>(model, sampler.output, TINYGLTF_COMPONENT_TYPE_FLOAT);
ASSERT(times.size() == values.size());
return compute_keyframes(times, values, framerate);
}
} // namespace
UncompressedJointAnim extract_anim_from_gltf(const tinygltf::Model& model,
const tinygltf::Animation& anim,
const std::map<int, int>& node_to_joint,
float framerate) {
UncompressedJointAnim out;
out.name = anim.name;
lg::info("Processing animation {}", anim.name);
const int max_joint = find_max_joint(anim, node_to_joint);
lg::info("Max joint is {}", max_joint);
out.joints.resize(max_joint + 1);
for (const auto& channel : anim.channels) {
const int channel_node_idx = channel.target_node;
// const auto& channel_node = model.nodes.at(channel_node_idx);
const int channel_joint = node_to_joint.at(channel_node_idx);
// lg::info("channel for {} {} / {}", channel_joint, channel_node.name, channel.target_path);
const auto& sampler = anim.samplers.at(channel.sampler);
if (channel.target_path == "translation") {
out.joints.at(channel_joint).trans_frames =
extract_keyframed_gltf_vecn<3>(model, sampler, framerate);
} else if (channel.target_path == "rotation") {
out.joints.at(channel_joint).quat_frames =
extract_keyframed_gltf_vecn<4>(model, sampler, framerate);
} else if (channel.target_path == "scale") {
out.joints.at(channel_joint).scale_frames =
extract_keyframed_gltf_vecn<3>(model, sampler, framerate);
} else {
lg::die("unknown target_path {}", channel.target_path);
}
}
size_t max_frames = 0;
for (auto& joint : out.joints) {
max_frames = std::max(max_frames, joint.quat_frames.size());
max_frames = std::max(max_frames, joint.trans_frames.size());
max_frames = std::max(max_frames, joint.scale_frames.size());
}
lg::info("max frames is {}", max_frames);
// make up data for missing joints (like align, for example)
for (auto& joint : out.joints) {
if (joint.quat_frames.size() < max_frames) {
lg::warn("joint with {} / {} quat frames!", joint.quat_frames.size(), max_frames);
if (joint.quat_frames.empty()) {
joint.quat_frames.emplace_back(0, 0, 0, 1);
}
while (joint.quat_frames.size() < max_frames) {
joint.quat_frames.push_back(joint.quat_frames.back());
}
}
if (joint.trans_frames.size() < max_frames) {
lg::warn("joint with {} / {} trans frames!", joint.trans_frames.size(), max_frames);
if (joint.trans_frames.empty()) {
joint.trans_frames.emplace_back(0, 0, 0);
}
while (joint.trans_frames.size() < max_frames) {
joint.trans_frames.push_back(joint.trans_frames.back());
}
}
if (joint.scale_frames.size() < max_frames) {
lg::warn("joint with {} / {} scale frames!", joint.scale_frames.size(), max_frames);
if (joint.scale_frames.empty()) {
joint.scale_frames.emplace_back(1, 1, 1);
}
while (joint.scale_frames.size() < max_frames) {
joint.scale_frames.push_back(joint.scale_frames.back());
}
}
}
out.framerate = framerate;
out.frames = max_frames;
return out;
}
namespace {
template <int n>
bool is_constant(const std::vector<math::Vector<float, n>>& in) {
if (in.empty()) {
return true;
}
auto first = in.at(0);
for (auto& x : in) {
if (x != first) {
return false;
}
}
return true;
}
bool can_use_small_trans(const std::vector<math::Vector3f>& trans_frames) {
constexpr float kMaxTrans = 32767.f * (4.f / 4096.f);
for (auto& trans : trans_frames) {
for (int i = 0; i < 3; i++) {
if (trans[i] > kMaxTrans || trans[i] < -kMaxTrans) {
return false;
}
}
}
return true;
}
bool is_matrix_constant(const UncompressedSingleJointAnim& anim) {
return is_constant(anim.quat_frames) && is_constant(anim.scale_frames) &&
is_constant(anim.trans_frames);
}
void compress_frame_to_matrix(CompressedFrame* frame,
const math::Vector3f& trans,
const math::Vector4f& quat,
const math::Vector3f& scale) {
auto mat = gltf_util::matrix_from_trs(trans * 4096, quat, scale);
constexpr int n = 4 * 4 * sizeof(float) / sizeof(u64);
u64 data[n];
memcpy(data, mat.data(), 4 * 4 * sizeof(float));
frame->data64.insert(frame->data64.end(), data, data + n);
}
void compress_trans(CompressedFrame* frame, const math::Vector3f& trans, bool big) {
if (big) {
// 64, 64, 32
u64 data[1];
memcpy(data, trans.data(), 2 * sizeof(float));
frame->data64.push_back(data[0]);
u32 data_32[1];
memcpy(data_32, &trans.z(), sizeof(float));
frame->data32.push_back(data_32[0]);
} else {
constexpr float kTransScale = 4.f / 4096.f;
s16 data1[3];
for (int i = 0; i < 3; i++) {
data1[i] = s16(trans[i] / kTransScale);
}
u32 data2[1];
memcpy(data2, data1, 4);
frame->data32.push_back(data2[0]);
frame->data16.push_back(data1[2]);
}
}
void compress_quat(CompressedFrame* frame, const math::Vector4f& quat) {
constexpr float kQuatScale = 0.000030517578125;
s16 data1[4];
for (int i = 0; i < 4; i++) {
data1[i] = s16(quat[i] / kQuatScale);
}
u64 data2[1];
memcpy(data2, data1, 8);
frame->data64.push_back(data2[0]);
}
void compress_scale(CompressedFrame* frame, const math::Vector3f& scale) {
constexpr float kScaleScale = 0.000244140625;
s16 data1[3];
for (int i = 0; i < 3; i++) {
data1[i] = s16(scale[i] / kScaleScale);
}
u32 data2[1];
memcpy(data2, data1, 4);
frame->data32.push_back(data2[0]);
frame->data16.push_back(data1[2]);
}
} // namespace
CompressedAnim compress_animation(const UncompressedJointAnim& in) {
ASSERT(in.joints.size() >= 2); // need two matrix joints.
CompressedAnim out;
out.name = in.name;
out.framerate = in.framerate;
out.frames.resize(in.frames);
for (int matrix = 0; matrix < 2; matrix++) {
const auto& joint_data = in.joints.at(matrix);
if (is_matrix_constant(joint_data)) {
out.matrix_animated[matrix] = false;
compress_frame_to_matrix(&out.fixed, joint_data.trans_frames[0], joint_data.quat_frames[0],
joint_data.scale_frames[0]);
} else {
out.matrix_animated[matrix] = true;
for (int i = 0; i < in.frames; i++) {
compress_frame_to_matrix(&out.frames[i], joint_data.trans_frames[i],
joint_data.quat_frames[i], joint_data.scale_frames[i]);
}
}
}
for (size_t joint = 2; joint < in.joints.size(); joint++) {
const auto& joint_data = in.joints.at(joint);
auto& metadata = out.joint_metadata.emplace_back();
metadata.animated_trans = !is_constant(joint_data.trans_frames);
metadata.animated_quat = !is_constant(joint_data.quat_frames);
metadata.animated_scale = !is_constant(joint_data.scale_frames);
metadata.big_trans_mode = !can_use_small_trans(joint_data.trans_frames);
if (metadata.animated_trans) {
for (int i = 0; i < in.frames; i++) {
compress_trans(&out.frames[i], joint_data.trans_frames[i], metadata.big_trans_mode);
}
} else {
compress_trans(&out.fixed, joint_data.trans_frames[0], metadata.big_trans_mode);
}
if (metadata.animated_quat) {
for (int i = 0; i < in.frames; i++) {
compress_quat(&out.frames[i], joint_data.quat_frames[i]);
}
} else {
compress_quat(&out.fixed, joint_data.quat_frames[0]);
}
if (metadata.animated_scale) {
for (int i = 0; i < in.frames; i++) {
compress_scale(&out.frames[i], joint_data.scale_frames[i]);
}
} else {
compress_scale(&out.fixed, joint_data.scale_frames[0]);
}
}
return out;
}
} // namespace anim
@@ -0,0 +1,69 @@
#pragma once
#include <string>
#include <vector>
#include "common/common_types.h"
#include "common/math/Vector.h"
#include "third-party/tiny_gltf/tiny_gltf.h"
namespace anim {
struct UncompressedSingleJointAnim {
std::vector<math::Vector3f> trans_frames;
std::vector<math::Vector3f> scale_frames;
std::vector<math::Vector4f> quat_frames;
};
struct UncompressedJointAnim {
std::string name;
std::vector<UncompressedSingleJointAnim> joints;
float framerate = 60;
int frames = 0;
};
struct CompressedMatrixMetadata {
bool is_animated[2];
};
struct CompressedFrame {
std::vector<u16> data16;
std::vector<u32> data32;
std::vector<u64> data64;
int size_bytes() const { return data16.size() * 2 + data32.size() * 4 + data64.size() * 8; }
};
struct CompressedJointMetadata {
bool animated_trans = false;
bool animated_quat = false;
bool animated_scale = false;
bool big_trans_mode = false;
};
struct CompressedAnim {
std::string name;
CompressedFrame fixed;
std::vector<CompressedFrame> frames;
bool matrix_animated[2] = {false, false};
std::vector<CompressedJointMetadata> joint_metadata;
float framerate = 60;
};
/*!
* Load animation data from GLTF file.
* @param model The GLTF model containing the animation
* @param anim The animation to convert
* @param node_to_joint Mapping from GLTF node index to the joint index
* @param framerate Number of key-frames per second. (this doesn't have to match frame rate, the
* game will interpolate between keyframes as needed.)
*/
UncompressedJointAnim extract_anim_from_gltf(const tinygltf::Model& model,
const tinygltf::Animation& anim,
const std::map<int, int>& node_to_joint,
float framerate);
CompressedAnim compress_animation(const UncompressedJointAnim& in);
} // namespace anim
+392 -53
View File
@@ -1,14 +1,143 @@
#include "build_actor.h"
#include "common/log/log.h"
#include "common/math/geometry.h"
#include "goalc/build_actor/common/MercExtract.h"
#include "goalc/build_actor/common/animation_processing.h"
#include "third-party/tiny_gltf/tiny_gltf.h"
using namespace gltf_util;
namespace jak1 {
JointAnimCompressedHDR::JointAnimCompressedHDR(const anim::CompressedAnim& anim) {
matrix_bits = 0;
if (anim.matrix_animated[0]) {
matrix_bits |= 1;
}
if (anim.matrix_animated[1]) {
matrix_bits |= 2;
}
for (auto& word : control_bits) {
word = 0;
}
for (size_t i = 0; i < anim.joint_metadata.size(); i++) {
const int word_idx = i / 8;
if (word_idx >= 14) {
lg::error("too many joints!!");
break;
}
u32 val = 0;
const auto& metadata = anim.joint_metadata[i];
if (metadata.animated_trans) {
val |= (0b1);
}
if (metadata.animated_quat) {
val |= (0b10);
}
if (metadata.animated_scale) {
val |= (0b100);
}
if (metadata.big_trans_mode) {
val |= (0b1000);
}
val = (val << (4 * (i % 8)));
control_bits[word_idx] |= val;
}
num_joints = 2 + anim.joint_metadata.size();
}
JointAnimCompressedFixed::JointAnimCompressedFixed(const anim::CompressedAnim& anim) : hdr(anim) {
u8* dest = (u8*)data;
const u8* u64_src = (const u8*)anim.fixed.data64.data();
const u8* u32_src = (const u8*)anim.fixed.data32.data();
const u8* u16_src = (const u8*)anim.fixed.data16.data();
const int u64_size = anim.fixed.data64.size() * sizeof(u64);
const int u32_size = anim.fixed.data32.size() * sizeof(u32);
const int u16_size = anim.fixed.data16.size() * sizeof(u16);
if (u64_size + u32_size + u16_size > 133 * 4 * 4) {
lg::die("fixed sizes are {} and {}", 133 * 4 * 4, u64_size + u32_size + u16_size);
}
ASSERT(u64_size + u32_size + u16_size <= 133 * 4 * 4);
offset_64 = 0;
memcpy(dest, u64_src, u64_size);
dest += u64_size;
offset_32 = offset_64 + u64_size;
memcpy(dest, u32_src, u32_size);
dest += u32_size;
offset_16 = offset_32 + u32_size;
memcpy(dest, u16_src, u16_size);
reserved = 0;
num_data_qw_used = (15 + u64_size + u32_size + u16_size) / 16;
ASSERT(num_data_qw_used <= 133);
}
JointAnimCompressedFrame::JointAnimCompressedFrame(const anim::CompressedFrame& frame) {
reserved = 0;
u8* dest = (u8*)data;
const u8* u64_src = (const u8*)frame.data64.data();
const u8* u32_src = (const u8*)frame.data32.data();
const u8* u16_src = (const u8*)frame.data16.data();
const int u64_size = frame.data64.size() * sizeof(u64);
const int u32_size = frame.data32.size() * sizeof(u32);
const int u16_size = frame.data16.size() * sizeof(u16);
if (u64_size + u32_size + u16_size > 133 * 4 * 4) {
lg::die("frame sizes are {} and {}", 133 * 4 * 4, u64_size + u32_size + u16_size);
}
offset_64 = 0;
memcpy(dest, u64_src, u64_size);
dest += u64_size;
offset_32 = offset_64 + u64_size;
memcpy(dest, u32_src, u32_size);
dest += u32_size;
offset_16 = offset_32 + u32_size;
memcpy(dest, u16_src, u16_size);
reserved = 0;
num_data_qw_used = (15 + u64_size + u32_size + u16_size) / 16;
ASSERT(num_data_qw_used <= 133);
}
JointAnimCompressedControl::JointAnimCompressedControl(const anim::CompressedAnim& anim)
: fixed(anim) {
num_frames = anim.frames.size();
for (auto& in_frame : anim.frames) {
frame.emplace_back(in_frame);
}
fixed_qwc = fixed.num_data_qw_used;
frame_qwc = frame.at(0).num_data_qw_used;
}
ArtJointAnim::ArtJointAnim(const anim::CompressedAnim& anim, const std::vector<Joint>& joints)
: frames(anim) {
this->name = anim.name;
length = joints.size();
speed = 1.0f;
artist_base = 0.0f;
artist_step = 1.0f;
master_art_group_name = name;
master_art_group_index = 2;
for (auto& joint : joints) {
data.emplace_back(joint, anim.frames.size());
}
}
std::map<int, size_t> g_joint_map;
size_t Joint::generate(DataObjectGenerator& gen) const {
@@ -36,6 +165,7 @@ size_t JointAnimCompressed::generate(DataObjectGenerator& gen) const {
size_t result = gen.current_offset_bytes();
gen.add_ref_to_string_in_pool(name);
gen.add_word((length << 16) + number);
// data is always nullptr.
// for (auto& word : data) {
// gen.add_word(word);
// }
@@ -43,12 +173,19 @@ size_t JointAnimCompressed::generate(DataObjectGenerator& gen) const {
}
size_t JointAnimCompressedFrame::generate(DataObjectGenerator& gen) const {
gen.align(16); // might have been 8, but 16 doesn't hurt.
size_t result = gen.current_offset_bytes();
gen.add_word(offset_64); // 0
gen.add_word(offset_32); // 4
gen.add_word(offset_16); // 8
gen.add_word(reserved); // 12
gen.align(4);
for (u32 i = 0; i < num_data_qw_used; i++) {
for (int j = 0; j < 4; j++) {
gen.add_word_float(data[i][j]);
}
}
return result;
}
@@ -59,11 +196,11 @@ size_t JointAnimCompressedHDR::generate(DataObjectGenerator& gen) const {
}
gen.add_word(num_joints);
gen.add_word(matrix_bits);
gen.align(4);
return result;
}
size_t JointAnimCompressedFixed::generate(DataObjectGenerator& gen) const {
gen.align(16); // might have been 8, but 16 doesn't hurt.
size_t result = gen.current_offset_bytes();
hdr.generate(gen); // 0-64 (inline)
gen.add_word(offset_64); // 64
@@ -71,12 +208,13 @@ size_t JointAnimCompressedFixed::generate(DataObjectGenerator& gen) const {
gen.add_word(offset_16); // 72
gen.add_word(reserved); // 76
// default joint poses (taken from money-idle)
for (size_t i = 0; i < 8; i++) {
for (int i = 0; i < num_data_qw_used; i++) {
gen.add_word_float(data[i].x());
gen.add_word_float(data[i].y());
gen.add_word_float(data[i].z());
gen.add_word_float(data[i].w());
}
// -- not sure what this is, part of the dummy data maybe?
gen.add_word(0);
gen.add_word(0x7fff0000);
gen.add_word(0x2250000);
@@ -90,16 +228,22 @@ size_t JointAnimCompressedFixed::generate(DataObjectGenerator& gen) const {
}
size_t JointAnimCompressedControl::generate(DataObjectGenerator& gen) const {
gen.align(16);
size_t result = gen.current_offset_bytes();
gen.add_word(num_frames); // 0
gen.add_word(fixed_qwc); // 4
gen.add_word(frame_qwc); // 8
auto ja_fixed_slot = gen.add_word(0);
auto ja_frame_slot = gen.add_word(0);
gen.align(4);
std::vector<int> ja_frame_slots;
for (u32 i = 0; i < num_frames; i++) {
ja_frame_slots.push_back(gen.add_word(0));
}
gen.link_word_to_byte(ja_fixed_slot, fixed.generate(gen));
gen.link_word_to_byte(ja_frame_slot, frame[0].generate(gen));
ASSERT(num_frames == frame.size());
for (size_t i = 0; i < num_frames; i++) {
gen.link_word_to_byte(ja_frame_slots[i], frame[i].generate(gen));
}
return result;
}
@@ -161,6 +305,7 @@ size_t ArtJointGeo::generate_mesh(DataObjectGenerator& gen) const {
gen.add_type_tag("collide-mesh"); // 8 (content-type)
content_slots.reserve(cmeshes.size());
for (auto& data : cmeshes) {
(void)data;
content_slots.push_back(gen.add_word(0)); // 12 (data)
}
gen.align(4);
@@ -185,11 +330,11 @@ size_t ArtJointGeo::generate(DataObjectGenerator& gen) {
gen.add_word(0);
gen.add_word(0);
std::vector<size_t> joint_slots;
for (size_t i = 0; i < length; i++) {
for (int i = 0; i < length; i++) {
joint_slots.push_back(gen.add_word(0));
}
gen.align(4);
for (size_t i = 0; i < length; i++) {
for (int i = 0; i < length; i++) {
auto joint = data.at(i).generate(gen);
gen.link_word_to_byte(joint_slots.at(i), joint);
g_joint_map[data.at(i).number] = joint;
@@ -218,12 +363,12 @@ size_t ArtJointAnim::generate(DataObjectGenerator& gen) const {
gen.add_symbol_link("#f"); // 40 (blerc)
auto ctrl_slot = gen.add_word(0);
std::vector<size_t> frame_slots;
for (size_t i = 0; i < length; i++) {
for (int i = 0; i < length; i++) {
frame_slots.push_back(gen.add_word(0));
}
gen.align(4);
gen.link_word_to_byte(ctrl_slot, frames.generate(gen));
for (size_t i = 0; i < length; i++) {
for (int i = 0; i < length; i++) {
gen.link_word_to_byte(frame_slots.at(i), data.at(i).generate(gen));
}
return result;
@@ -351,6 +496,41 @@ size_t gen_dummy_frag_ctrl(DataObjectGenerator& gen) {
return result;
}
size_t gen_dummy_frag_ctrl_for_uploads(DataObjectGenerator& gen, int n) {
size_t result = gen.current_offset_bytes();
std::vector<u8> packed_frag_ctrls;
// this is still a bit of a hack - the dummy_frag_ctrl above has 8 fragments, so we need to
// provide fragment controls for each. The PC merc renderer will de-duplicate bone uploads over
// all effects and fragments, so we just need to have a single fragment that asks for all bones,
// and everything will work out.
for (int k = 0; k < 8; k++) {
packed_frag_ctrls.push_back(0);
packed_frag_ctrls.push_back(0);
packed_frag_ctrls.push_back(0);
if (k == 0) { // for the first frag, do all matrix uploads.
// note that these are bogus destination addresses, but nothing uses it on PC
packed_frag_ctrls.push_back(n);
for (int i = 0; i < n; i++) {
packed_frag_ctrls.push_back(i);
packed_frag_ctrls.push_back(i);
}
} else {
// remaining frags can have empty matrix upload lists.
packed_frag_ctrls.push_back(0);
}
}
std::vector<u32> u32s((packed_frag_ctrls.size() + 3) / 4);
memcpy(u32s.data(), packed_frag_ctrls.data(), packed_frag_ctrls.size());
for (auto x : u32s) {
gen.add_word(x);
}
return result;
}
size_t gen_dummy_extra_info(DataObjectGenerator& gen) {
size_t result = gen.current_offset_bytes();
gen.add_word(0x1);
@@ -369,7 +549,7 @@ size_t generate_dummy_merc_ctrl(DataObjectGenerator& gen, const ArtGroup& ag) {
gen.add_type_tag("merc-ctrl");
size_t result = gen.current_offset_bytes();
// excluding align and prejoint
auto joints = ((ArtJointGeo*)ag.elts.at(0))->length - 2;
auto joints = ((ArtJointGeo*)ag.elts.at(0).get())->length - 2;
gen.add_word(0); // 4
gen.add_ref_to_string_in_pool(ag.name + "-lod0"); // 8
gen.add_word(0); // 12
@@ -406,7 +586,7 @@ size_t generate_dummy_merc_ctrl(DataObjectGenerator& gen, const ArtGroup& ag) {
gen.add_word(0x100011b); // 136
auto extra_info_slot = gen.add_word(0); // 140 (extra-info)
gen.link_word_to_byte(extra_info_slot, gen_dummy_extra_info(gen));
gen.link_word_to_byte(frag_ctrl_slot, gen_dummy_frag_ctrl(gen));
gen.link_word_to_byte(frag_ctrl_slot, gen_dummy_frag_ctrl_for_uploads(gen, joints + 3));
gen.link_word_to_byte(frag_geo_slot, gen_dummy_frag_geo(gen));
return result;
}
@@ -428,15 +608,16 @@ std::vector<u8> ArtGroup::save_object_file() const {
gen.set_word(28 / 4, 0);
if (!elts.empty()) {
if (elts.at(0)) {
auto jgeo = (ArtJointGeo*)elts.at(0);
auto jgeo = (ArtJointGeo*)elts.at(0).get();
gen.link_word_to_byte(32 / 4, jgeo->generate(gen));
}
if (!elts.at(1)) {
gen.link_word_to_byte(36 / 4, generate_dummy_merc_ctrl(gen, *this));
}
if (elts.at(2)) {
auto ja = (ArtJointAnim*)elts.at(2);
gen.link_word_to_byte(40 / 4, ja->generate(gen));
for (size_t i = 2; i < elts.size(); i++) {
auto ja = (ArtJointAnim*)elts.at(i).get();
gen.link_word_to_byte((32 + i * 4) / 4, ja->generate(gen));
}
}
@@ -446,7 +627,7 @@ std::vector<u8> ArtGroup::save_object_file() const {
int ArtGroup::get_joint_idx(const std::string& name) {
for (auto& elt : this->elts) {
if (elt && typeid(*elt) == typeid(ArtJointGeo)) {
auto jgeo = (ArtJointGeo*)elt;
auto jgeo = (ArtJointGeo*)elt.get();
for (auto& joint : jgeo->data) {
if (joint.name == name) {
return joint.number;
@@ -457,6 +638,156 @@ int ArtGroup::get_joint_idx(const std::string& name) {
return -1;
}
/*!
* Load tinygltf::Model from a .glb file (binary format), fatal error if it fails.
*/
tinygltf::Model load_gltf_model(const fs::path& path) {
tinygltf::TinyGLTF loader;
tinygltf::Model model;
std::string err, warn;
bool res = loader.LoadBinaryFromFile(&model, &err, &warn, path.string());
ASSERT_MSG(warn.empty(), warn.c_str());
ASSERT_MSG(err.empty(), err.c_str());
ASSERT_MSG(res, "Failed to load GLTF file!");
return model;
}
struct GltfJoint {
math::Matrix4f bind_pose_T_w; // inverse bind pose
std::string name;
int gltf_node_index = 0;
int parent = -1;
std::vector<int> children;
};
/*!
* Extract the "skeleton" structure from a GLTF model's skin. This requires that the skin's joints
* are topologically sorted (parents always have lower index than children).
*/
std::vector<GltfJoint> extract_skeleton(const tinygltf::Model& model, int skin_idx) {
const auto& skin = model.skins.at(skin_idx);
lg::info("skin name is {}", skin.name);
lg::info("skeleton root is {}", skin.skeleton);
auto inverse_bind_matrices = extract_mat4(model, skin.inverseBindMatrices);
ASSERT(inverse_bind_matrices.size() == skin.joints.size());
std::map<int, int> node_to_joint;
std::map<int, int> joint_to_node;
std::vector<GltfJoint> joints;
for (size_t i = 0; i < skin.joints.size(); i++) {
auto joint_node_idx = skin.joints[i];
const auto& joint_node = model.nodes.at(joint_node_idx);
// auto ibm = inverse_bind_matrices[i];
// lg::info(" joint {}", joint_node_idx);
// lg::info(" {}", joint_node.name);
// lg::info("\n{}", ibm.to_string_aligned());
node_to_joint[joint_node_idx] = i;
joint_to_node[i] = joint_node_idx;
auto& gjoint = joints.emplace_back();
gjoint.bind_pose_T_w = inverse_bind_matrices[i];
gjoint.name = joint_node.name;
gjoint.gltf_node_index = joint_node_idx;
}
for (size_t i = 0; i < skin.joints.size(); i++) {
auto joint_node_idx = skin.joints[i];
const auto& joint_node = model.nodes.at(joint_node_idx);
// set up children
for (int child_node_idx : joint_node.children) {
int child_joint_idx = node_to_joint.at(child_node_idx);
joints.at(i).children.push_back(child_joint_idx);
auto& child = joints.at(child_joint_idx);
ASSERT(child.parent == -1);
child.parent = i;
ASSERT(child_joint_idx > (int)i);
}
}
ASSERT(joints.at(0).parent == -1);
// for (auto& joint : joints) {
// if (joint.parent == -1) {
// lg::warn("parentless {}", joint.name);
// } else {
// lg::info("joint {}, child of {}", joint.name, joints.at(joint.parent).name);
// }
// }
lg::info("total of {} joints", joints.size());
return joints;
}
/*!
* Convert from GLTF joint format to game joint format.
* @param joint_index the index of the joint, in the GLTF file.
* @param prefix_joint_count number of joints to be inserted before GLTF joints in the game
* @param parent_of_gltf the parent game joint of all GLTF joints.
*/
Joint convert_joint(const GltfJoint& joint,
int joint_index,
int prefix_joint_count,
int parent_of_gltf) {
// node matrix is p_T_myself
// p_T_myself = parent_bind_pose_T_w * w_T_bind_pose
int parent;
if (joint.parent == -1) {
parent = parent_of_gltf;
} else {
parent = joint.parent + prefix_joint_count;
}
math::Matrix4f fixed_matrix = joint.bind_pose_T_w;
for (int i = 0; i < 3; i++) {
fixed_matrix(i, 3) *= 4096;
}
return Joint(joint.name, joint_index + prefix_joint_count, parent, fixed_matrix.transposed());
}
constexpr int kGltfToGameJointOffset = 1;
/*!
* Convert GTLF joint list to game joint list.
* Currently, this inserts a single "align" joint and places the root joint of the GLTF as the
* prejoint. However, we might want to change this, to allow GLTF files to specify "align" at some
* point.
*/
std::vector<Joint> convert_joints(const std::vector<GltfJoint>& gjoints) {
std::vector<Joint> joints;
joints.emplace_back("align", 0, -1, math::Matrix4f::identity());
ASSERT(kGltfToGameJointOffset == joints.size());
for (int gjoint_idx = 0; gjoint_idx < int(gjoints.size()); gjoint_idx++) {
// using -1 as the parent index since gltf's shouldn't be child of align.
joints.push_back(convert_joint(gjoints[gjoint_idx], gjoint_idx, kGltfToGameJointOffset, -1));
}
return joints;
}
std::vector<anim::CompressedAnim> process_anim(const tinygltf::Model& model,
const std::vector<GltfJoint>& gjoints) {
if (model.animations.empty()) {
lg::warn("no animations detected!"); // TODO: make up a dummy one
return {};
}
std::map<int, int> node_to_joint;
for (size_t i = 0; i < gjoints.size(); i++) {
node_to_joint[gjoints[i].gltf_node_index] = i + kGltfToGameJointOffset;
}
std::vector<anim::CompressedAnim> ret;
for (auto& anim : model.animations) {
lg::info("Processing animation {}", anim.name);
ret.push_back(
anim::compress_animation(anim::extract_anim_from_gltf(model, anim, node_to_joint, 60)));
}
return ret;
}
/*!
* Build GOAL format data for an actor. This doesn't generate the data for the .FR3.
*/
bool run_build_actor(const std::string& mdl_name,
const std::string& ag_out,
bool gen_collide_mesh) {
@@ -467,51 +798,59 @@ bool run_build_actor(const std::string& mdl_name,
ASSERT_MSG(false, "Model file not found: " + mdl_name);
}
// Load GLTF file to check for a skeleton
tinygltf::Model model = load_gltf_model(file_util::get_jak_project_dir() / mdl_name);
auto all_nodes = flatten_nodes_from_all_scenes(model);
auto skin_idx = find_single_skin(model, all_nodes);
if (skin_idx) {
lg::info("GLTF file contained a skin, this actor will have a real skeleton");
}
std::vector<anim::CompressedAnim> user_anims;
ArtGroup ag(ag_name);
std::vector<Joint> joints;
auto identity = math::Matrix4f::identity();
joints.emplace_back("align", 0, -1, identity);
joints.emplace_back("prejoint", 1, -1, identity);
// matrix stolen from "egg" joint from "money" art group
auto main_pose = math::Matrix4f::zero();
main_pose(0, 0) = 1.0f;
main_pose(0, 1) = -0.0f;
main_pose(0, 2) = 0.0f;
main_pose(0, 3) = -0.0f;
main_pose(1, 0) = -0.0f;
main_pose(1, 1) = 1.0f;
main_pose(1, 2) = -0.0f;
main_pose(1, 3) = 0.0f;
main_pose(2, 0) = 0.0f;
main_pose(2, 1) = -0.0f;
main_pose(2, 2) = 1.0f;
main_pose(2, 3) = -0.0f;
main_pose(3, 0) = -0.0f;
main_pose(3, 1) = -2194.1628418f;
main_pose(3, 2) = -0.0f;
main_pose(3, 3) = 1.0f;
Joint main("main", 2, 1, main_pose);
joints.emplace_back(main);
MercExtractData extract_data;
extract("test", extract_data, model, all_nodes, 0, 0, 0);
// MercSwapData out;
// merc_convert(out, extract_data);
// Set up joints:
if (skin_idx) {
// read skeleton data out of GLTF
auto skeleton_joints = extract_skeleton(model, *skin_idx);
// convert to game format
joints = convert_joints(skeleton_joints);
// get animation from user.
user_anims = process_anim(model, skeleton_joints);
} else {
auto identity = math::Matrix4f::identity();
joints.emplace_back("align", 0, -1, identity);
joints.emplace_back("prejoint", 1, -1, identity);
// matrix stolen from "egg" joint from "money" art group
auto main_pose = math::Matrix4f::identity();
main_pose(3, 1) = -2194.1628418f;
Joint main("main", 2, 1, main_pose);
joints.emplace_back(main);
}
std::vector<CollideMesh> mesh;
if (gen_collide_mesh) {
tinygltf::TinyGLTF loader;
tinygltf::Model model;
std::string err, warn;
std::string path = (file_util::get_jak_project_dir() / mdl_name).string();
bool res = loader.LoadBinaryFromFile(&model, &err, &warn, path);
ASSERT_MSG(warn.empty(), warn.c_str());
ASSERT_MSG(err.empty(), err.c_str());
ASSERT_MSG(res, "Failed to load GLTF file!");
auto all_nodes = flatten_nodes_from_all_scenes(model);
mesh = gen_collide_mesh_from_model(model, all_nodes, 3);
}
ArtJointGeo jgeo(ag.name, mesh, joints);
ArtJointAnim ja(ag.name, joints);
ag.elts.emplace_back(&jgeo);
std::shared_ptr<ArtJointGeo> jgeo = std::make_shared<ArtJointGeo>(ag.name, mesh, joints);
ag.elts.emplace_back(jgeo);
// dummy merc-ctrl
ag.elts.emplace_back(nullptr);
ag.elts.emplace_back(&ja);
if (!user_anims.empty()) {
for (auto& anim : user_anims) {
ag.elts.emplace_back(std::make_shared<ArtJointAnim>(anim, joints));
}
} else {
ag.elts.emplace_back(std::make_shared<ArtJointAnim>(ag.name, joints));
}
ag.length = ag.elts.size();
+36 -29
View File
@@ -2,11 +2,17 @@
#include "common/util/gltf_util.h"
#include "goalc/build_actor/common/animation_processing.h"
#include "goalc/build_actor/common/art_types.h"
#include "goalc/build_level/collide/common/collide_common.h"
namespace jak1 {
// Note: there's some weirdness with the Joint types here - I believe that very early on in
// development, joint animations were stored separately per joint. However, all joint animations use
// the "compressed" format, which combines data for all joints into a single structure.
// By jak 2, they had cleaned this up, but for Jak 1, we have to deal with this weirdness.
struct Joint {
std::string name;
s32 number;
@@ -24,35 +30,22 @@ struct Joint {
};
// basic
struct JointAnim {
// This is one of those weird leftover types.
// There's one of these per-joint, per-animation, and all it's useful for is storing the
// length of the animation. The game only looks at the data for joint 0 and assumes the rest are
// the same. (and by jak 2, this is gone entirely!)
struct JointAnimCompressed {
std::string name;
s16 number;
s16 length;
explicit JointAnim(const Joint& joint) {
this->name = joint.name;
number = joint.number;
length = 1;
}
};
// basic
struct JointAnimCompressed : JointAnim {
std::vector<u32> data;
explicit JointAnimCompressed(const Joint& joint) : JointAnim(joint) {
number = joint.number;
length = 1;
}
size_t generate(DataObjectGenerator& gen) const;
};
struct JointAnimFrame {
math::Matrix4f matrices[2];
std::vector<math::Matrix4f> data;
explicit JointAnimCompressed(const Joint& joint, s16 num_frames)
: name(joint.name), number(joint.number), length(num_frames) {}
size_t generate(DataObjectGenerator& gen) const;
};
// Header for a compressed joint animation - this tells the decompressor how to read
// the data in the animation.
struct JointAnimCompressedHDR {
u32 control_bits[14];
u32 num_joints;
@@ -65,6 +58,9 @@ struct JointAnimCompressedHDR {
num_joints = 1;
matrix_bits = 0;
}
explicit JointAnimCompressedHDR(const anim::CompressedAnim& anim);
size_t generate(DataObjectGenerator& gen) const;
};
@@ -75,6 +71,7 @@ struct JointAnimCompressedFixed {
u32 offset_16;
u32 reserved;
math::Vector4f data[133];
int num_data_qw_used = 0;
JointAnimCompressedFixed() {
offset_64 = 0;
@@ -89,7 +86,11 @@ struct JointAnimCompressedFixed {
data[5] = math::Vector4f(0.0f, 1.0f, 0.0f, 0.0f);
data[6] = math::Vector4f(0.0f, 0.0f, 1.0f, 0.0f);
data[7] = math::Vector4f(0.0f, 0.0f, 0.0f, 1.0f);
num_data_qw_used = 8;
}
JointAnimCompressedFixed(const anim::CompressedAnim& anim);
size_t generate(DataObjectGenerator& gen) const;
};
@@ -99,6 +100,7 @@ struct JointAnimCompressedFrame {
u32 offset_16;
u32 reserved;
math::Vector4f data[133];
u32 num_data_qw_used = 0;
JointAnimCompressedFrame() {
offset_64 = 0;
@@ -107,6 +109,8 @@ struct JointAnimCompressedFrame {
reserved = 0;
}
JointAnimCompressedFrame(const anim::CompressedFrame& frame);
size_t generate(DataObjectGenerator& gen) const;
};
@@ -114,17 +118,18 @@ struct JointAnimCompressedControl {
u32 num_frames;
u32 fixed_qwc;
u32 frame_qwc;
JointAnimCompressedFixed fixed{};
JointAnimCompressedFrame frame[1];
JointAnimCompressedFixed fixed;
std::vector<JointAnimCompressedFrame> frame;
JointAnimCompressedControl() {
num_frames = 1;
fixed_qwc = 0xf;
frame_qwc = 1;
fixed = JointAnimCompressedFixed();
frame[0] = JointAnimCompressedFrame();
frame.emplace_back();
}
JointAnimCompressedControl(const anim::CompressedAnim& anim);
size_t generate(DataObjectGenerator& gen) const;
};
@@ -188,17 +193,19 @@ struct ArtJointAnim : ArtElement {
artist_step = 1.0f;
master_art_group_name = name;
master_art_group_index = 2;
frames = JointAnimCompressedControl();
for (auto& joint : joints) {
data.emplace_back(joint);
data.emplace_back(joint, 1);
}
}
ArtJointAnim(const anim::CompressedAnim& anim, const std::vector<Joint>& joints);
size_t generate(DataObjectGenerator& gen) const;
};
struct ArtGroup : Art {
FileInfo info;
std::vector<ArtElement*> elts;
std::vector<std::shared_ptr<ArtElement>> elts;
std::map<int, size_t> joint_map;
explicit ArtGroup(const std::string& file_name) {
+1 -1
View File
@@ -14,7 +14,7 @@ int main(int argc, char** argv) {
// game version
std::string game, mdl_name, output_file;
bool gen_collide_mesh;
bool gen_collide_mesh = false;
fs::path project_path_override;
// path
+1
View File
@@ -57,6 +57,7 @@ void add_model_to_level(GameVersion version, const std::string& name, tfrag3::Le
for (auto& vert : merc_data.new_vertices) {
lvl.merc_data.vertices.push_back(vert);
}
lvl.merc_data.models.push_back(merc_data.new_model);
lvl.textures.insert(lvl.textures.end(), merc_data.new_textures.begin(),
merc_data.new_textures.end());