mirror of
https://github.com/open-goal/jak-project
synced 2026-07-07 14:13:45 -04:00
[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:
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user