mirror of
https://github.com/Laupetin/OpenAssetTools.git
synced 2026-06-06 00:32:34 +00:00
refactor: use generic dumper for iw3 xanims
This commit is contained in:
@@ -8,6 +8,46 @@
|
||||
|
||||
namespace xanim
|
||||
{
|
||||
CommonXQuat::CommonXQuat()
|
||||
: value{}
|
||||
{
|
||||
}
|
||||
|
||||
CommonXQuat::CommonXQuat(const int16_t v0, const int16_t v1, const int16_t v2, const int16_t v3)
|
||||
: value{v0, v1, v2, v3}
|
||||
{
|
||||
}
|
||||
|
||||
CommonXQuat2::CommonXQuat2()
|
||||
: value{}
|
||||
{
|
||||
}
|
||||
|
||||
CommonXQuat2::CommonXQuat2(const int16_t v0, const int16_t v1)
|
||||
: value{v0, v1}
|
||||
{
|
||||
}
|
||||
|
||||
CommonVec3U8::CommonVec3U8()
|
||||
: value{}
|
||||
{
|
||||
}
|
||||
|
||||
CommonVec3U8::CommonVec3U8(const uint8_t x, const uint8_t y, const uint8_t z)
|
||||
: value{x, y, z}
|
||||
{
|
||||
}
|
||||
|
||||
CommonVec3U16::CommonVec3U16()
|
||||
: value{}
|
||||
{
|
||||
}
|
||||
|
||||
CommonVec3U16::CommonVec3U16(const uint16_t x, const uint16_t y, const uint16_t z)
|
||||
: value{x, y, z}
|
||||
{
|
||||
}
|
||||
|
||||
QuatTrack::QuatTrack()
|
||||
: m_type(QuatType::NO_QUAT)
|
||||
{
|
||||
|
||||
@@ -34,21 +34,33 @@ namespace xanim
|
||||
|
||||
struct CommonXQuat
|
||||
{
|
||||
CommonXQuat();
|
||||
CommonXQuat(int16_t v0, int16_t v1, int16_t v2, int16_t v3);
|
||||
|
||||
int16_t value[4];
|
||||
};
|
||||
|
||||
struct CommonXQuat2
|
||||
{
|
||||
CommonXQuat2();
|
||||
CommonXQuat2(int16_t v0, int16_t v1);
|
||||
|
||||
int16_t value[2];
|
||||
};
|
||||
|
||||
struct CommonVec3U8
|
||||
{
|
||||
CommonVec3U8();
|
||||
CommonVec3U8(uint8_t x, uint8_t y, uint8_t z);
|
||||
|
||||
uint8_t value[3];
|
||||
};
|
||||
|
||||
struct CommonVec3U16
|
||||
{
|
||||
CommonVec3U16();
|
||||
CommonVec3U16(uint16_t x, uint16_t y, uint16_t z);
|
||||
|
||||
uint16_t value[3];
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,484 @@
|
||||
#include "CompiledXAnimWriter.h"
|
||||
|
||||
#include "Utils/Alignment.h"
|
||||
#include "Utils/Logging/Log.h"
|
||||
#include "Utils/StreamUtils.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
|
||||
using namespace xanim;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint8_t FLAG_LOOPED = 1u;
|
||||
constexpr uint8_t FLAG_DELTA = 2u;
|
||||
|
||||
// The linker decodes raw trans size[] with these exact float literals.
|
||||
// They correspond to 1.0f / 255.0f and 1.0f / 65535.0f, but we keep the
|
||||
// decompiled values to preserve binary-stable round trips.
|
||||
constexpr auto HALF_TRANS_SIZE_SCALE = 0.003921568859368563f;
|
||||
constexpr auto FULL_TRANS_SIZE_SCALE = 0.00001525902189314365f;
|
||||
|
||||
class EncodedQuatTrack
|
||||
{
|
||||
public:
|
||||
bool m_flip_quat = false;
|
||||
std::vector<int16_t> m_stored_values;
|
||||
};
|
||||
|
||||
[[nodiscard]] uint16_t GetNumLoopFrames(const CommonXAnimParts& parts)
|
||||
{
|
||||
assert(parts.m_num_frames < std::numeric_limits<uint16_t>::max());
|
||||
|
||||
// Raw non-looped xanims store numframes + 1 in keyed track counts/header fields.
|
||||
return static_cast<uint16_t>(parts.m_num_frames + 1u);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool QuatTypeUsesHalf(const QuatType type)
|
||||
{
|
||||
return type == QuatType::NO_QUAT || type == QuatType::HALF_QUAT || type == QuatType::HALF_QUAT_NO_SIZE;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsSequentialCoverage(const std::vector<uint16_t>& indices, const uint16_t numLoopFrames)
|
||||
{
|
||||
if (indices.size() != numLoopFrames)
|
||||
return false;
|
||||
|
||||
for (auto i = 0uz; i < indices.size(); i++)
|
||||
{
|
||||
if (indices[i] != i)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
concept XQuatOrXQuat2 = std::is_array_v<decltype(T::value)> && std::is_integral_v<std::remove_extent_t<decltype(T::value)>>;
|
||||
|
||||
template<XQuatOrXQuat2 T> [[nodiscard]] int64_t ComputeQuatDot(const T& lhs, const T& rhs)
|
||||
{
|
||||
int64_t result = 0;
|
||||
for (auto i = 0uz; i < std::extent_v<decltype(lhs.value)>; i++)
|
||||
result += static_cast<int64_t>(lhs.value[i]) * static_cast<int64_t>(rhs.value[i]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
template<XQuatOrXQuat2 T> [[nodiscard]] EncodedQuatTrack EncodeQuatFrames(const std::vector<T>& frames, const bool allowFlipQuat)
|
||||
{
|
||||
constexpr auto COMPONENT_COUNT = std::extent_v<decltype(T::value)>;
|
||||
constexpr auto STORED_COMPONENT_COUNT = COMPONENT_COUNT - 1;
|
||||
|
||||
EncodedQuatTrack result;
|
||||
if (frames.empty())
|
||||
return result;
|
||||
|
||||
const auto frameCount = frames.size();
|
||||
|
||||
// Raw IW3 xanims store only N-1 quat components. The loader reconstructs the
|
||||
// final component with a positive sqrt, applies the per-bone flip bit, and then
|
||||
// continuity-corrects subsequent frames by optionally negating whole quats.
|
||||
result.m_stored_values.reserve(frameCount * STORED_COMPONENT_COUNT);
|
||||
|
||||
result.m_flip_quat = allowFlipQuat && frames[0].value[COMPONENT_COUNT - 1] < 0;
|
||||
const auto targetNegativeOmitted = result.m_flip_quat;
|
||||
|
||||
for (size_t frameIndex = 0; frameIndex < frameCount; frameIndex++)
|
||||
{
|
||||
const auto& frame = frames[frameIndex];
|
||||
const auto omittedNegative = frame.value[COMPONENT_COUNT - 1] < 0;
|
||||
|
||||
auto continuityNegated = false;
|
||||
if (frameIndex > 0u && omittedNegative != targetNegativeOmitted)
|
||||
{
|
||||
const auto& prevFrame = frames[(frameIndex - 1u)];
|
||||
continuityNegated = ComputeQuatDot(prevFrame, frame) > 0;
|
||||
}
|
||||
|
||||
const auto rawNegated = result.m_flip_quat != continuityNegated;
|
||||
const auto sign = rawNegated ? -1 : 1;
|
||||
|
||||
for (size_t componentIndex = 0; componentIndex < STORED_COMPONENT_COUNT; componentIndex++)
|
||||
{
|
||||
const auto value = static_cast<int>(frame.value[componentIndex]) * sign;
|
||||
assert(value >= std::numeric_limits<int16_t>::min() && value <= std::numeric_limits<int16_t>::max());
|
||||
result.m_stored_values.emplace_back(static_cast<int16_t>(value));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] EncodedQuatTrack EncodeQuatTrack(const QuatTrack& quat)
|
||||
{
|
||||
switch (quat.m_type)
|
||||
{
|
||||
case QuatType::NO_QUAT:
|
||||
return {};
|
||||
|
||||
case QuatType::HALF_QUAT_NO_SIZE:
|
||||
assert(quat.m_frames2.size() == 1);
|
||||
return EncodeQuatFrames(quat.m_frames2, true);
|
||||
|
||||
case QuatType::FULL_QUAT_NO_SIZE:
|
||||
assert(quat.m_frames.size() == 1);
|
||||
return EncodeQuatFrames(quat.m_frames, true);
|
||||
|
||||
case QuatType::HALF_QUAT:
|
||||
assert(quat.m_frames2.size() == quat.m_indices.size());
|
||||
return EncodeQuatFrames(quat.m_frames2, true);
|
||||
|
||||
case QuatType::FULL_QUAT:
|
||||
assert(quat.m_frames.size() == quat.m_indices.size());
|
||||
return EncodeQuatFrames(quat.m_frames, true);
|
||||
}
|
||||
|
||||
assert(false);
|
||||
return {};
|
||||
}
|
||||
|
||||
void WriteIndicesIfNeeded(std::ostream& stream, const std::vector<uint16_t>& indices, const uint16_t numLoopFrames, const bool useByteIndices)
|
||||
{
|
||||
if (indices.empty())
|
||||
return;
|
||||
|
||||
// The raw format omits indices when a track covers every loop frame in order.
|
||||
if (indices.size() >= numLoopFrames)
|
||||
{
|
||||
assert(IsSequentialCoverage(indices, numLoopFrames));
|
||||
return;
|
||||
}
|
||||
|
||||
if (useByteIndices)
|
||||
{
|
||||
for (const auto index : indices)
|
||||
{
|
||||
assert(index <= std::numeric_limits<uint8_t>::max());
|
||||
const auto asByte = static_cast<uint8_t>(index);
|
||||
stream::WriteValue(stream, asByte);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto index : indices)
|
||||
stream::WriteValue(stream, index);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteDeltaQuatTrack(std::ostream& stream, const CommonXAnimDeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices)
|
||||
{
|
||||
if (!delta.m_quat)
|
||||
{
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(0));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto numQuatIndices = static_cast<uint16_t>(delta.m_quat->m_frames2.size());
|
||||
assert(numQuatIndices > 0);
|
||||
|
||||
stream::WriteValue(stream, numQuatIndices);
|
||||
|
||||
const auto encodedDeltaQuatFrames = EncodeQuatFrames(delta.m_quat->m_frames2, false);
|
||||
|
||||
if (numQuatIndices == 1)
|
||||
{
|
||||
assert(encodedDeltaQuatFrames.m_stored_values.size() == 1);
|
||||
stream::WriteValue(stream, encodedDeltaQuatFrames.m_stored_values[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(numQuatIndices > 1u);
|
||||
assert(delta.m_quat->m_indices.size() == numQuatIndices);
|
||||
assert(encodedDeltaQuatFrames.m_stored_values.size() == numQuatIndices);
|
||||
|
||||
WriteIndicesIfNeeded(stream, delta.m_quat->m_indices, numLoopFrames, useByteIndices);
|
||||
for (const auto value : encodedDeltaQuatFrames.m_stored_values)
|
||||
stream::WriteValue(stream, value);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] float EncodeRawTransSize(const float value, const bool smallTrans)
|
||||
{
|
||||
const auto scale = smallTrans ? HALF_TRANS_SIZE_SCALE : FULL_TRANS_SIZE_SCALE;
|
||||
return value / scale;
|
||||
}
|
||||
|
||||
void WriteDeltaTransTrack(std::ostream& stream, const CommonXAnimDeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices)
|
||||
{
|
||||
if (!delta.m_trans)
|
||||
{
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(0));
|
||||
return;
|
||||
}
|
||||
|
||||
if (delta.m_trans->m_constant)
|
||||
{
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(1));
|
||||
for (const auto value : *delta.m_trans->m_constant)
|
||||
stream::WriteValue(stream, value);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto numTransIndices = static_cast<uint16_t>(delta.m_trans->m_indices.size());
|
||||
assert(numTransIndices > 1);
|
||||
|
||||
stream::WriteValue(stream, numTransIndices);
|
||||
WriteIndicesIfNeeded(stream, delta.m_trans->m_indices, numLoopFrames, useByteIndices);
|
||||
|
||||
const auto smallTrans = !delta.m_trans->m_frames_u8.empty();
|
||||
stream::WriteValue(stream, static_cast<uint8_t>(smallTrans ? 1 : 0));
|
||||
for (const auto value : delta.m_trans->m_mins)
|
||||
stream::WriteValue(stream, value);
|
||||
|
||||
if (smallTrans)
|
||||
{
|
||||
assert(delta.m_trans->m_frames_u8.size() == numTransIndices);
|
||||
for (const auto value : delta.m_trans->m_size)
|
||||
stream::WriteValue(stream, EncodeRawTransSize(value, true));
|
||||
|
||||
for (const auto vec3U8 : delta.m_trans->m_frames_u8)
|
||||
{
|
||||
stream::WriteValue(stream, vec3U8.value[0]);
|
||||
stream::WriteValue(stream, vec3U8.value[1]);
|
||||
stream::WriteValue(stream, vec3U8.value[2]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(delta.m_trans->m_frames_u16.size() == numTransIndices);
|
||||
for (const auto value : delta.m_trans->m_size)
|
||||
stream::WriteValue(stream, EncodeRawTransSize(value, false));
|
||||
|
||||
for (const auto vec3U16 : delta.m_trans->m_frames_u16)
|
||||
{
|
||||
stream::WriteValue(stream, vec3U16.value[0]);
|
||||
stream::WriteValue(stream, vec3U16.value[1]);
|
||||
stream::WriteValue(stream, vec3U16.value[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WriteDeltaTrack(std::ostream& stream, const CommonXAnimDeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices)
|
||||
{
|
||||
WriteDeltaQuatTrack(stream, delta, numLoopFrames, useByteIndices);
|
||||
WriteDeltaTransTrack(stream, delta, numLoopFrames, useByteIndices);
|
||||
}
|
||||
|
||||
void WriteQuatTrack(
|
||||
std::ostream& stream, const QuatTrack& quat, const EncodedQuatTrack& encodedQuat, const uint16_t numLoopFrames, const bool useByteIndices)
|
||||
{
|
||||
switch (quat.m_type)
|
||||
{
|
||||
case QuatType::NO_QUAT:
|
||||
{
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(0));
|
||||
break;
|
||||
}
|
||||
|
||||
case QuatType::HALF_QUAT_NO_SIZE:
|
||||
{
|
||||
assert(encodedQuat.m_stored_values.size() == 1uz);
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(1));
|
||||
stream::WriteValue(stream, encodedQuat.m_stored_values[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
case QuatType::FULL_QUAT_NO_SIZE:
|
||||
{
|
||||
assert(encodedQuat.m_stored_values.size() == 3uz);
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(1));
|
||||
for (const auto value : encodedQuat.m_stored_values)
|
||||
stream::WriteValue(stream, value);
|
||||
break;
|
||||
}
|
||||
|
||||
case QuatType::HALF_QUAT:
|
||||
{
|
||||
const auto frameCount = quat.m_indices.size();
|
||||
assert(frameCount > 0uz);
|
||||
assert(quat.m_frames2.size() == frameCount);
|
||||
assert(encodedQuat.m_stored_values.size() == frameCount);
|
||||
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(frameCount));
|
||||
WriteIndicesIfNeeded(stream, quat.m_indices, numLoopFrames, useByteIndices);
|
||||
for (const auto value : encodedQuat.m_stored_values)
|
||||
stream::WriteValue(stream, value);
|
||||
break;
|
||||
}
|
||||
|
||||
case QuatType::FULL_QUAT:
|
||||
{
|
||||
const auto frameCount = quat.m_indices.size();
|
||||
assert(frameCount > 0uz);
|
||||
assert(quat.m_frames.size() == frameCount);
|
||||
assert(encodedQuat.m_stored_values.size() == frameCount * 3uz);
|
||||
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(frameCount));
|
||||
WriteIndicesIfNeeded(stream, quat.m_indices, numLoopFrames, useByteIndices);
|
||||
for (const auto value : encodedQuat.m_stored_values)
|
||||
stream::WriteValue(stream, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WriteTransTrack(std::ostream& stream, const TransTrack& trans, const uint16_t numLoopFrames, const bool useByteIndices)
|
||||
{
|
||||
switch (trans.m_type)
|
||||
{
|
||||
case TransType::NO_TRANS:
|
||||
{
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(0));
|
||||
break;
|
||||
}
|
||||
|
||||
case TransType::TRANS_NO_SIZE:
|
||||
{
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(1));
|
||||
for (const auto value : trans.m_constant)
|
||||
stream::WriteValue(stream, value);
|
||||
break;
|
||||
}
|
||||
|
||||
case TransType::SMALL_TRANS:
|
||||
{
|
||||
const auto frameCount = trans.m_indices.size();
|
||||
assert(frameCount > 0uz);
|
||||
assert(trans.m_byte_frames.size() == frameCount * 3uz);
|
||||
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(frameCount));
|
||||
WriteIndicesIfNeeded(stream, trans.m_indices, numLoopFrames, useByteIndices);
|
||||
|
||||
constexpr auto smallTrans = static_cast<uint8_t>(1);
|
||||
stream::WriteValue(stream, smallTrans);
|
||||
|
||||
for (const auto value : trans.m_mins)
|
||||
stream::WriteValue(stream, value);
|
||||
for (const auto value : trans.m_size)
|
||||
stream::WriteValue(stream, EncodeRawTransSize(value, true));
|
||||
|
||||
stream::Write(stream, trans.m_byte_frames.data(), trans.m_byte_frames.size());
|
||||
break;
|
||||
}
|
||||
|
||||
case TransType::FULL_TRANS:
|
||||
{
|
||||
const auto frameCount = trans.m_indices.size();
|
||||
assert(frameCount > 0uz);
|
||||
assert(trans.m_short_frames.size() == frameCount * 3uz);
|
||||
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(frameCount));
|
||||
WriteIndicesIfNeeded(stream, trans.m_indices, numLoopFrames, useByteIndices);
|
||||
|
||||
constexpr auto smallTrans = static_cast<uint8_t>(0);
|
||||
stream::WriteValue(stream, smallTrans);
|
||||
|
||||
for (const auto value : trans.m_mins)
|
||||
stream::WriteValue(stream, value);
|
||||
for (const auto value : trans.m_size)
|
||||
stream::WriteValue(stream, EncodeRawTransSize(value, false));
|
||||
|
||||
for (const auto value : trans.m_short_frames)
|
||||
stream::WriteValue(stream, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WriteNoteTracks(std::ostream& stream, const CommonXAnimParts& parts)
|
||||
{
|
||||
const auto notifyCount = parts.m_notifies.size();
|
||||
|
||||
auto rawNotifyCount = notifyCount;
|
||||
if (notifyCount > 0uz)
|
||||
{
|
||||
const auto& lastNotify = parts.m_notifies[notifyCount - 1];
|
||||
|
||||
// The linker appends a synthetic "end" notify at 1.0f to the loaded asset state.
|
||||
if (lastNotify.m_name == "end" && std::abs(lastNotify.m_time - 1.0f) < 0.0001f)
|
||||
rawNotifyCount--;
|
||||
}
|
||||
|
||||
assert(rawNotifyCount < 255uz);
|
||||
const auto rawNotifyCountByte = static_cast<uint8_t>(rawNotifyCount);
|
||||
stream::WriteValue(stream, rawNotifyCountByte);
|
||||
|
||||
for (auto i = 0uz; i < rawNotifyCount; i++)
|
||||
{
|
||||
const auto& notify = parts.m_notifies[i];
|
||||
stream::WriteCString(stream, notify.m_name);
|
||||
|
||||
uint16_t frame = 0;
|
||||
if (parts.m_num_frames > 0)
|
||||
{
|
||||
const auto scaled = static_cast<long>(std::lround(notify.m_time * static_cast<float>(parts.m_num_frames)));
|
||||
assert(scaled >= 0 && scaled <= std::numeric_limits<uint16_t>::max());
|
||||
frame = static_cast<uint16_t>(scaled);
|
||||
}
|
||||
|
||||
stream::WriteValue(stream, frame);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace xanim
|
||||
{
|
||||
void WriteCompiledXAnim(std::ostream& stream, const CommonXAnimParts& parts)
|
||||
{
|
||||
const auto numLoopFrames = GetNumLoopFrames(parts);
|
||||
const auto useByteIndices = parts.m_num_frames < 256;
|
||||
|
||||
std::vector<EncodedQuatTrack> encodedBoneQuats;
|
||||
encodedBoneQuats.reserve(parts.m_bone_tracks.size());
|
||||
for (const auto& bone : parts.m_bone_tracks)
|
||||
encodedBoneQuats.emplace_back(EncodeQuatTrack(bone.m_quat));
|
||||
|
||||
const auto flags = static_cast<uint8_t>((parts.m_looped ? FLAG_LOOPED : 0u) | (parts.m_delta_track ? FLAG_DELTA : 0u));
|
||||
const auto boneCount = static_cast<uint16_t>(parts.m_bone_tracks.size());
|
||||
const auto assetType = static_cast<uint8_t>(parts.m_asset_type);
|
||||
const auto framerate = static_cast<uint16_t>(std::lround(parts.m_frame_rate));
|
||||
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(CompiledXAnimVersion::VERSION_17));
|
||||
// Looped raws store numframes directly; non-looped raws store numframes + 1.
|
||||
stream::WriteValue(stream, static_cast<uint16_t>(parts.m_looped ? parts.m_num_frames : numLoopFrames));
|
||||
stream::WriteValue(stream, boneCount);
|
||||
stream::WriteValue(stream, flags);
|
||||
stream::WriteValue(stream, assetType);
|
||||
stream::WriteValue(stream, framerate);
|
||||
|
||||
if (parts.m_delta_track)
|
||||
WriteDeltaTrack(stream, *parts.m_delta_track, numLoopFrames, useByteIndices);
|
||||
|
||||
if (!parts.m_bone_tracks.empty())
|
||||
{
|
||||
const auto bitmaskSize = utils::Align<size_t>(boneCount, 8u) / 8u;
|
||||
std::vector<uint8_t> flipQuat(bitmaskSize, 0);
|
||||
std::vector<uint8_t> halfQuat(bitmaskSize, 0);
|
||||
|
||||
for (auto i = 0u; i < boneCount; i++)
|
||||
{
|
||||
if (encodedBoneQuats[i].m_flip_quat)
|
||||
flipQuat[i / 8u] |= static_cast<uint8_t>(1u << (i % 8u));
|
||||
|
||||
if (QuatTypeUsesHalf(parts.m_bone_tracks[i].m_quat.m_type))
|
||||
halfQuat[i / 8u] |= static_cast<uint8_t>(1u << (i % 8u));
|
||||
}
|
||||
|
||||
stream::Write(stream, flipQuat.data(), flipQuat.size());
|
||||
stream::Write(stream, halfQuat.data(), halfQuat.size());
|
||||
|
||||
for (const auto& bone : parts.m_bone_tracks)
|
||||
stream::WriteCString(stream, bone.m_name);
|
||||
|
||||
for (auto i = 0u; i < boneCount; i++)
|
||||
{
|
||||
WriteQuatTrack(stream, parts.m_bone_tracks[i].m_quat, encodedBoneQuats[i], numLoopFrames, useByteIndices);
|
||||
WriteTransTrack(stream, parts.m_bone_tracks[i].m_trans, numLoopFrames, useByteIndices);
|
||||
}
|
||||
}
|
||||
|
||||
WriteNoteTracks(stream, parts);
|
||||
}
|
||||
} // namespace xanim
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "XAnim/XAnimCommon.h"
|
||||
|
||||
#include <ostream>
|
||||
|
||||
namespace xanim
|
||||
{
|
||||
void WriteCompiledXAnim(std::ostream& stream, const CommonXAnimParts& parts);
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
#include "FlatXAnimReader.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <utility>
|
||||
|
||||
using namespace xanim;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
[[nodiscard]] std::vector<uint16_t> ReadPackedIndices(FlatXAnimReadCursor& cursor, const uint16_t storedSize, const bool useByteIndices)
|
||||
{
|
||||
const auto count = static_cast<size_t>(storedSize) + 1uz;
|
||||
std::vector<uint16_t> result(count);
|
||||
|
||||
if (useByteIndices)
|
||||
{
|
||||
for (auto i = 0uz; i < count; i++)
|
||||
result[i] = cursor.PopDataByte();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// The linker moves 16-bit frame indices into the top-level indices pool only when
|
||||
// the in-memory stored size is at least 64, i.e. frameCount >= 65.
|
||||
if (storedSize >= 64u)
|
||||
{
|
||||
cursor.ReadIndices(result.data(), count);
|
||||
|
||||
// The game inserts checkpoint values in dataShort
|
||||
// Those checkpoint values are copied from positions in the full index list: the first entry, then every 256th entry, and always the final entry.
|
||||
// The final entry is included even when it does not land exactly on a 256-entry boundary.
|
||||
cursor.SkipDataShort(((count - 2uz) / 256u) + 2uz);
|
||||
return result;
|
||||
}
|
||||
|
||||
cursor.ReadDataShort(result.data(), count);
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] float IntBitsToFloat(const int value)
|
||||
{
|
||||
union
|
||||
{
|
||||
int i;
|
||||
float f;
|
||||
};
|
||||
|
||||
i = value;
|
||||
return f;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::array<float, 3> ReadFloat3(FlatXAnimReadCursor& cursor)
|
||||
{
|
||||
std::array<float, 3> result{};
|
||||
for (float& i : result)
|
||||
i = IntBitsToFloat(cursor.PopDataInt());
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace xanim
|
||||
{
|
||||
XAnimBoneCounts::XAnimBoneCounts(const size_t noQuatCount,
|
||||
const size_t halfQuatCount,
|
||||
const size_t fullQuatCount,
|
||||
const size_t halfQuatNoSizeCount,
|
||||
const size_t fullQuatNoSizeCount,
|
||||
const size_t smallTransCount,
|
||||
const size_t fullTransCount,
|
||||
const size_t transNoSizeCount,
|
||||
const size_t noTransCount)
|
||||
: m_counts({
|
||||
noQuatCount,
|
||||
halfQuatCount,
|
||||
fullQuatCount,
|
||||
halfQuatNoSizeCount,
|
||||
fullQuatNoSizeCount,
|
||||
smallTransCount,
|
||||
fullTransCount,
|
||||
transNoSizeCount,
|
||||
noTransCount,
|
||||
})
|
||||
{
|
||||
assert(m_counts[std::to_underlying(QuatType::NO_QUAT)] == noQuatCount);
|
||||
assert(m_counts[std::to_underlying(QuatType::HALF_QUAT)] == halfQuatCount);
|
||||
assert(m_counts[std::to_underlying(QuatType::FULL_QUAT)] == fullQuatCount);
|
||||
assert(m_counts[std::to_underlying(QuatType::HALF_QUAT_NO_SIZE)] == halfQuatNoSizeCount);
|
||||
assert(m_counts[std::to_underlying(QuatType::FULL_QUAT_NO_SIZE)] == fullQuatNoSizeCount);
|
||||
|
||||
assert(m_counts[std::to_underlying(TransType::SMALL_TRANS)] == smallTransCount);
|
||||
assert(m_counts[std::to_underlying(TransType::FULL_TRANS)] == fullTransCount);
|
||||
assert(m_counts[std::to_underlying(TransType::TRANS_NO_SIZE)] == transNoSizeCount);
|
||||
assert(m_counts[std::to_underlying(TransType::NO_TRANS)] == noTransCount);
|
||||
|
||||
assert(noQuatCount + halfQuatCount + fullQuatCount + halfQuatNoSizeCount + fullQuatNoSizeCount
|
||||
== smallTransCount + fullTransCount + transNoSizeCount + noTransCount);
|
||||
}
|
||||
|
||||
size_t XAnimBoneCounts::GetCountForQuatType(const QuatType quatType) const
|
||||
{
|
||||
return m_counts[std::to_underlying(quatType)];
|
||||
}
|
||||
|
||||
size_t XAnimBoneCounts::GetCountForTransType(const TransType transType) const
|
||||
{
|
||||
return m_counts[std::to_underlying(transType)];
|
||||
}
|
||||
|
||||
FlatDataReadException::FlatDataReadException(std::string message)
|
||||
: m_message(std::move(message))
|
||||
{
|
||||
}
|
||||
|
||||
const char* FlatDataReadException::what() const noexcept
|
||||
{
|
||||
return m_message.c_str();
|
||||
}
|
||||
|
||||
const std::string& FlatDataReadException::message() const
|
||||
{
|
||||
return m_message;
|
||||
}
|
||||
|
||||
FlatXAnimReadCursor::FlatXAnimReadCursor(uint8_t* dataByte,
|
||||
const size_t dataByteCount,
|
||||
int16_t* dataShort,
|
||||
const size_t dataShortCount,
|
||||
int32_t* dataInt,
|
||||
const size_t dataIntCount,
|
||||
uint8_t* randomDataByte,
|
||||
const size_t randomDataByteCount,
|
||||
int16_t* randomDataShort,
|
||||
const size_t randomDataShortCount,
|
||||
uint16_t* indices,
|
||||
const size_t indicesCount)
|
||||
: m_data_byte(dataByte),
|
||||
m_data_byte_count(dataByteCount),
|
||||
m_data_short(dataShort),
|
||||
m_data_short_count(dataShortCount),
|
||||
m_data_int(dataInt),
|
||||
m_data_int_count(dataIntCount),
|
||||
m_random_data_byte(randomDataByte),
|
||||
m_random_data_byte_count(randomDataByteCount),
|
||||
m_random_data_short(randomDataShort),
|
||||
m_random_data_short_count(randomDataShortCount),
|
||||
m_indices(indices),
|
||||
m_indices_count(indicesCount)
|
||||
{
|
||||
}
|
||||
|
||||
#define DATA_EXHAUSTED_ERROR(name, readCount, remainingCount) \
|
||||
FlatDataReadException(std::format("Exhausted {} while trying to read {} entries ({} remaining)", name, readCount, remainingCount))
|
||||
|
||||
uint8_t FlatXAnimReadCursor::PopDataByte()
|
||||
{
|
||||
if (m_data_byte_count < 1)
|
||||
throw DATA_EXHAUSTED_ERROR("dataByte", 1, m_data_byte_count);
|
||||
|
||||
const auto result = m_data_byte[0];
|
||||
m_data_byte++;
|
||||
m_data_byte_count--;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int16_t FlatXAnimReadCursor::PopDataShort()
|
||||
{
|
||||
if (m_data_short_count < 1)
|
||||
throw DATA_EXHAUSTED_ERROR("dataShort", 1, m_data_short_count);
|
||||
|
||||
const auto result = m_data_short[0];
|
||||
m_data_short++;
|
||||
m_data_short_count--;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void FlatXAnimReadCursor::ReadDataShort(void* dst, const size_t count)
|
||||
{
|
||||
if (m_data_short_count < count)
|
||||
throw DATA_EXHAUSTED_ERROR("dataShort", count, m_data_short_count);
|
||||
|
||||
std::memcpy(dst, m_data_short, count * sizeof(int16_t));
|
||||
|
||||
m_data_short += count;
|
||||
m_data_short_count -= count;
|
||||
}
|
||||
|
||||
void FlatXAnimReadCursor::SkipDataShort(const size_t count)
|
||||
{
|
||||
if (m_data_short_count < count)
|
||||
throw DATA_EXHAUSTED_ERROR("dataShort", count, m_data_short_count);
|
||||
|
||||
m_data_short += count;
|
||||
m_data_short_count -= count;
|
||||
}
|
||||
|
||||
int32_t FlatXAnimReadCursor::PopDataInt()
|
||||
{
|
||||
if (m_data_int_count < 1)
|
||||
throw DATA_EXHAUSTED_ERROR("dataInt", 1, m_data_int_count);
|
||||
|
||||
const auto result = m_data_int[0];
|
||||
m_data_int++;
|
||||
m_data_int_count--;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void FlatXAnimReadCursor::ReadRandomDataByte(void* dst, const size_t count)
|
||||
{
|
||||
if (m_random_data_byte_count < count)
|
||||
throw DATA_EXHAUSTED_ERROR("randomDataByte", count, m_random_data_byte_count);
|
||||
|
||||
std::memcpy(dst, m_random_data_byte, count * sizeof(uint8_t));
|
||||
|
||||
m_random_data_byte += count;
|
||||
m_random_data_byte_count -= count;
|
||||
}
|
||||
|
||||
void FlatXAnimReadCursor::ReadRandomDataShort(void* dst, const size_t count)
|
||||
{
|
||||
if (m_random_data_short_count < count)
|
||||
throw DATA_EXHAUSTED_ERROR("randomDataShort", count, m_random_data_short_count);
|
||||
|
||||
std::memcpy(dst, m_random_data_short, count * sizeof(int16_t));
|
||||
|
||||
m_random_data_short += count;
|
||||
m_random_data_short_count -= count;
|
||||
}
|
||||
|
||||
void FlatXAnimReadCursor::ReadIndices(void* dst, const size_t count)
|
||||
{
|
||||
if (m_indices_count < count)
|
||||
throw DATA_EXHAUSTED_ERROR("indices", count, m_indices_count);
|
||||
|
||||
std::memcpy(dst, m_indices, count * sizeof(uint16_t));
|
||||
|
||||
m_indices += count;
|
||||
m_indices_count -= count;
|
||||
}
|
||||
|
||||
std::expected<void, std::string> FlatXAnimReadCursor::ExpectEndOfData() const
|
||||
{
|
||||
#define END_OF_DATA_ERROR(name, size) std::unexpected(std::format("Expected {} to be exhausted but {} bytes remain", name, size))
|
||||
#define CHECK_END_OF_DATA_ERROR(name, size) \
|
||||
if ((size) > 0) \
|
||||
return END_OF_DATA_ERROR(name, size);
|
||||
|
||||
CHECK_END_OF_DATA_ERROR("dataByte", m_data_byte_count)
|
||||
CHECK_END_OF_DATA_ERROR("dataShort", m_data_short_count)
|
||||
CHECK_END_OF_DATA_ERROR("dataInt", m_data_int_count)
|
||||
CHECK_END_OF_DATA_ERROR("randomDataByte", m_random_data_byte_count)
|
||||
CHECK_END_OF_DATA_ERROR("randomDataShort", m_random_data_short_count)
|
||||
CHECK_END_OF_DATA_ERROR("indices", m_indices_count)
|
||||
|
||||
return {};
|
||||
|
||||
#undef END_OF_DATA_ERROR
|
||||
#undef CHECK_END_OF_DATA_ERROR
|
||||
}
|
||||
|
||||
std::expected<std::vector<BoneTrack>, std::string> CreateBoneTracksFromFlatData(std::vector<std::string> boneNames,
|
||||
const XAnimBoneCounts& boneCounts,
|
||||
FlatXAnimReadCursor& cursor,
|
||||
const bool useByteIndices)
|
||||
{
|
||||
const auto boneCount = boneNames.size();
|
||||
std::vector<BoneTrack> boneTracks(boneCount);
|
||||
for (size_t i = 0; i < boneCount; i++)
|
||||
boneTracks[i].m_name = std::move(boneNames[i]);
|
||||
|
||||
size_t boneIndex = 0;
|
||||
|
||||
try
|
||||
{
|
||||
for (auto i = 0u; i < boneCounts.GetCountForQuatType(QuatType::NO_QUAT); i++, boneIndex++)
|
||||
boneTracks[boneIndex].m_quat.m_type = QuatType::NO_QUAT;
|
||||
|
||||
for (auto i = 0u; i < boneCounts.GetCountForQuatType(QuatType::HALF_QUAT); i++, boneIndex++)
|
||||
{
|
||||
auto& quat = boneTracks[boneIndex].m_quat;
|
||||
quat.m_type = QuatType::HALF_QUAT;
|
||||
const auto storedSize = static_cast<uint16_t>(cursor.PopDataShort());
|
||||
const auto frameCount = static_cast<size_t>(storedSize) + 1uz;
|
||||
quat.m_indices = ReadPackedIndices(cursor, storedSize, useByteIndices);
|
||||
|
||||
static_assert(sizeof(decltype(quat.m_frames2)::value_type) == sizeof(int16_t) * 2);
|
||||
quat.m_frames2.resize(frameCount);
|
||||
cursor.ReadRandomDataShort(quat.m_frames2.data(), frameCount * 2);
|
||||
}
|
||||
|
||||
for (auto i = 0u; i < boneCounts.GetCountForQuatType(QuatType::FULL_QUAT); i++, boneIndex++)
|
||||
{
|
||||
auto& quat = boneTracks[boneIndex].m_quat;
|
||||
quat.m_type = QuatType::FULL_QUAT;
|
||||
const auto storedSize = static_cast<uint16_t>(cursor.PopDataShort());
|
||||
const auto frameCount = static_cast<size_t>(storedSize) + 1uz;
|
||||
quat.m_indices = ReadPackedIndices(cursor, storedSize, useByteIndices);
|
||||
|
||||
static_assert(sizeof(decltype(quat.m_frames)::value_type) == sizeof(int16_t) * 4);
|
||||
quat.m_frames.resize(frameCount);
|
||||
cursor.ReadRandomDataShort(quat.m_frames.data(), frameCount * 4);
|
||||
}
|
||||
|
||||
for (auto i = 0u; i < boneCounts.GetCountForQuatType(QuatType::HALF_QUAT_NO_SIZE); i++, boneIndex++)
|
||||
{
|
||||
auto& quat = boneTracks[boneIndex].m_quat;
|
||||
quat.m_type = QuatType::HALF_QUAT_NO_SIZE;
|
||||
|
||||
static_assert(sizeof(decltype(quat.m_frames2)::value_type) == sizeof(int16_t) * 2);
|
||||
quat.m_frames2.resize(1);
|
||||
cursor.ReadDataShort(quat.m_frames2.data(), 2);
|
||||
}
|
||||
|
||||
for (auto i = 0u; i < boneCounts.GetCountForQuatType(QuatType::FULL_QUAT_NO_SIZE); i++, boneIndex++)
|
||||
{
|
||||
auto& quat = boneTracks[boneIndex].m_quat;
|
||||
quat.m_type = QuatType::FULL_QUAT_NO_SIZE;
|
||||
|
||||
static_assert(sizeof(decltype(quat.m_frames)::value_type) == sizeof(int16_t) * 4);
|
||||
quat.m_frames.resize(1);
|
||||
cursor.ReadDataShort(quat.m_frames.data(), 4);
|
||||
}
|
||||
|
||||
std::vector<bool> transAssigned(boneCount, false);
|
||||
|
||||
for (auto i = 0u; i < boneCounts.GetCountForTransType(TransType::SMALL_TRANS); i++)
|
||||
{
|
||||
const auto bone = static_cast<size_t>(cursor.PopDataByte());
|
||||
assert(bone < boneCount && !transAssigned[bone]);
|
||||
|
||||
auto& trans = boneTracks[bone].m_trans;
|
||||
transAssigned[bone] = true;
|
||||
trans.m_type = TransType::SMALL_TRANS;
|
||||
|
||||
const auto storedSize = static_cast<uint16_t>(cursor.PopDataShort());
|
||||
const auto frameCount = static_cast<size_t>(storedSize) + 1uz;
|
||||
trans.m_mins = ReadFloat3(cursor);
|
||||
trans.m_size = ReadFloat3(cursor);
|
||||
trans.m_indices = ReadPackedIndices(cursor, storedSize, useByteIndices);
|
||||
|
||||
static_assert(sizeof(decltype(trans.m_byte_frames)::value_type) == sizeof(uint8_t));
|
||||
trans.m_byte_frames.resize(frameCount * 3uz);
|
||||
cursor.ReadRandomDataByte(trans.m_byte_frames.data(), frameCount * 3uz);
|
||||
}
|
||||
|
||||
for (auto i = 0u; i < boneCounts.GetCountForTransType(TransType::FULL_TRANS); i++)
|
||||
{
|
||||
const auto bone = static_cast<size_t>(cursor.PopDataByte());
|
||||
assert(bone < boneCount && !transAssigned[bone]);
|
||||
|
||||
auto& trans = boneTracks[bone].m_trans;
|
||||
transAssigned[bone] = true;
|
||||
trans.m_type = TransType::FULL_TRANS;
|
||||
|
||||
const auto storedSize = static_cast<uint16_t>(cursor.PopDataShort());
|
||||
const auto frameCount = static_cast<size_t>(storedSize) + 1uz;
|
||||
trans.m_mins = ReadFloat3(cursor);
|
||||
trans.m_size = ReadFloat3(cursor);
|
||||
trans.m_indices = ReadPackedIndices(cursor, storedSize, useByteIndices);
|
||||
|
||||
static_assert(sizeof(decltype(trans.m_short_frames)::value_type) == sizeof(int16_t));
|
||||
trans.m_short_frames.resize(frameCount * 3uz);
|
||||
cursor.ReadRandomDataShort(trans.m_short_frames.data(), frameCount * 3uz);
|
||||
}
|
||||
|
||||
for (auto i = 0u; i < boneCounts.GetCountForTransType(TransType::TRANS_NO_SIZE); i++)
|
||||
{
|
||||
const auto bone = static_cast<size_t>(cursor.PopDataByte());
|
||||
assert(bone < boneCount && !transAssigned[bone]);
|
||||
|
||||
auto& trans = boneTracks[bone].m_trans;
|
||||
transAssigned[bone] = true;
|
||||
trans.m_type = TransType::TRANS_NO_SIZE;
|
||||
trans.m_constant = ReadFloat3(cursor);
|
||||
}
|
||||
|
||||
for (auto i = 0u; i < boneCounts.GetCountForTransType(TransType::NO_TRANS); i++)
|
||||
{
|
||||
const auto bone = static_cast<size_t>(cursor.PopDataByte());
|
||||
assert(bone < boneCount && !transAssigned[bone]);
|
||||
|
||||
boneTracks[bone].m_trans.m_type = TransType::NO_TRANS;
|
||||
transAssigned[bone] = true;
|
||||
}
|
||||
|
||||
for (auto i = 0uz; i < boneCount; i++)
|
||||
assert(transAssigned[i]);
|
||||
}
|
||||
catch (const FlatDataReadException& exception)
|
||||
{
|
||||
return std::unexpected(exception.message());
|
||||
}
|
||||
|
||||
auto maybeError = cursor.ExpectEndOfData();
|
||||
if (!maybeError.has_value())
|
||||
return std::unexpected(std::move(maybeError).error());
|
||||
|
||||
return boneTracks;
|
||||
}
|
||||
} // namespace xanim
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include "XAnim/XAnimCommon.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xanim
|
||||
{
|
||||
class XAnimBoneCounts
|
||||
{
|
||||
public:
|
||||
XAnimBoneCounts(size_t noQuatCount,
|
||||
size_t halfQuatCount,
|
||||
size_t fullQuatCount,
|
||||
size_t halfQuatNoSizeCount,
|
||||
size_t fullQuatNoSizeCount,
|
||||
size_t smallTransCount,
|
||||
size_t fullTransCount,
|
||||
size_t transNoSizeCount,
|
||||
size_t noTransCount);
|
||||
|
||||
[[nodiscard]] size_t GetCountForQuatType(QuatType quatType) const;
|
||||
[[nodiscard]] size_t GetCountForTransType(TransType transType) const;
|
||||
|
||||
private:
|
||||
std::array<size_t, 9> m_counts;
|
||||
};
|
||||
|
||||
class FlatDataReadException : public std::exception
|
||||
{
|
||||
public:
|
||||
explicit FlatDataReadException(std::string message);
|
||||
|
||||
[[nodiscard]] const char* what() const noexcept override;
|
||||
[[nodiscard]] const std::string& message() const;
|
||||
|
||||
private:
|
||||
std::string m_message;
|
||||
};
|
||||
|
||||
class FlatXAnimReadCursor
|
||||
{
|
||||
public:
|
||||
FlatXAnimReadCursor(uint8_t* dataByte,
|
||||
size_t dataByteCount,
|
||||
int16_t* dataShort,
|
||||
size_t dataShortCount,
|
||||
int32_t* dataInt,
|
||||
size_t dataIntCount,
|
||||
uint8_t* randomDataByte,
|
||||
size_t randomDataByteCount,
|
||||
int16_t* randomDataShort,
|
||||
size_t randomDataShortCount,
|
||||
uint16_t* indices,
|
||||
size_t indicesCount);
|
||||
|
||||
uint8_t PopDataByte();
|
||||
int16_t PopDataShort();
|
||||
void ReadDataShort(void* dst, size_t count);
|
||||
void SkipDataShort(size_t count);
|
||||
int32_t PopDataInt();
|
||||
void ReadRandomDataByte(void* dst, size_t count);
|
||||
void ReadRandomDataShort(void* dst, size_t count);
|
||||
void ReadIndices(void* dst, size_t count);
|
||||
|
||||
[[nodiscard]] std::expected<void, std::string> ExpectEndOfData() const;
|
||||
|
||||
private:
|
||||
uint8_t* m_data_byte;
|
||||
size_t m_data_byte_count;
|
||||
|
||||
int16_t* m_data_short;
|
||||
size_t m_data_short_count;
|
||||
|
||||
int32_t* m_data_int;
|
||||
size_t m_data_int_count;
|
||||
|
||||
uint8_t* m_random_data_byte;
|
||||
size_t m_random_data_byte_count;
|
||||
|
||||
int16_t* m_random_data_short;
|
||||
size_t m_random_data_short_count;
|
||||
|
||||
uint16_t* m_indices;
|
||||
size_t m_indices_count;
|
||||
};
|
||||
|
||||
std::expected<std::vector<BoneTrack>, std::string>
|
||||
CreateBoneTracksFromFlatData(std::vector<std::string> boneNames, const XAnimBoneCounts& boneCounts, FlatXAnimReadCursor& cursor, bool useByteIndices);
|
||||
} // namespace xanim
|
||||
Reference in New Issue
Block a user