mirror of
https://github.com/Laupetin/OpenAssetTools.git
synced 2026-06-07 17:22:34 +00:00
feat: add binary xanim support for remaining games (#818)
* refactor: use generic loader for iw3 xanims * refactor: use generic dumper for iw3 xanims * chore: use templating on XAnimDumper * chore: use templating on XAnimLoader * feat: dump xanims for T5 * feat: load binary t5 xanims * feat: load and dump t6 xanims * feat: load and dump iw4,iw5 xanims * chore: make sure iw3 and t5 notify about unsupported delta3D * chore: also use CommonVec3U8 and CommonVec3U16 for non delta trans track
This commit is contained in:
@@ -395,6 +395,13 @@ namespace IW4
|
|||||||
PART_TYPE_COUNT
|
PART_TYPE_COUNT
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum XAnimFlags
|
||||||
|
{
|
||||||
|
ANIM_LOOP = 0x1,
|
||||||
|
ANIM_DELTA = 0x2,
|
||||||
|
ANIM_DELTA_3D = 0x4,
|
||||||
|
};
|
||||||
|
|
||||||
struct XAnimParts
|
struct XAnimParts
|
||||||
{
|
{
|
||||||
const char* name;
|
const char* name;
|
||||||
@@ -404,7 +411,7 @@ namespace IW4
|
|||||||
uint16_t randomDataByteCount;
|
uint16_t randomDataByteCount;
|
||||||
uint16_t randomDataIntCount;
|
uint16_t randomDataIntCount;
|
||||||
uint16_t numframes;
|
uint16_t numframes;
|
||||||
char flags;
|
unsigned char flags;
|
||||||
unsigned char boneCount[PART_TYPE_COUNT];
|
unsigned char boneCount[PART_TYPE_COUNT];
|
||||||
unsigned char notifyCount;
|
unsigned char notifyCount;
|
||||||
unsigned char assetType;
|
unsigned char assetType;
|
||||||
|
|||||||
@@ -419,6 +419,13 @@ namespace IW5
|
|||||||
PART_TYPE_COUNT
|
PART_TYPE_COUNT
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum XAnimFlags
|
||||||
|
{
|
||||||
|
ANIM_LOOP = 0x1,
|
||||||
|
ANIM_DELTA = 0x2,
|
||||||
|
ANIM_DELTA_3D = 0x4,
|
||||||
|
};
|
||||||
|
|
||||||
struct XAnimParts
|
struct XAnimParts
|
||||||
{
|
{
|
||||||
const char* name;
|
const char* name;
|
||||||
|
|||||||
@@ -5668,7 +5668,7 @@ namespace T6
|
|||||||
XAnimPartTransData u;
|
XAnimPartTransData u;
|
||||||
};
|
};
|
||||||
|
|
||||||
union XAnimDynamicIndicesDeltaQuat2
|
union XAnimDynamicIndicesQuat
|
||||||
{
|
{
|
||||||
unsigned char _1[1];
|
unsigned char _1[1];
|
||||||
uint16_t _2[1];
|
uint16_t _2[1];
|
||||||
@@ -5682,7 +5682,7 @@ namespace T6
|
|||||||
struct type_align32(4) XAnimDeltaPartQuatDataFrames2
|
struct type_align32(4) XAnimDeltaPartQuatDataFrames2
|
||||||
{
|
{
|
||||||
XQuat2* frames;
|
XQuat2* frames;
|
||||||
XAnimDynamicIndicesDeltaQuat2 indices;
|
XAnimDynamicIndicesQuat indices;
|
||||||
};
|
};
|
||||||
|
|
||||||
union XAnimDeltaPartQuatData2
|
union XAnimDeltaPartQuatData2
|
||||||
@@ -5697,12 +5697,6 @@ namespace T6
|
|||||||
XAnimDeltaPartQuatData2 u;
|
XAnimDeltaPartQuatData2 u;
|
||||||
};
|
};
|
||||||
|
|
||||||
union XAnimDynamicIndicesDeltaQuat
|
|
||||||
{
|
|
||||||
unsigned char _1[1];
|
|
||||||
uint16_t _2[1];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct type_align(4) XQuat
|
struct type_align(4) XQuat
|
||||||
{
|
{
|
||||||
int16_t value[4];
|
int16_t value[4];
|
||||||
@@ -5711,7 +5705,7 @@ namespace T6
|
|||||||
struct type_align32(4) XAnimDeltaPartQuatDataFrames
|
struct type_align32(4) XAnimDeltaPartQuatDataFrames
|
||||||
{
|
{
|
||||||
XQuat* frames;
|
XQuat* frames;
|
||||||
XAnimDynamicIndicesDeltaQuat indices;
|
XAnimDynamicIndicesQuat indices;
|
||||||
};
|
};
|
||||||
|
|
||||||
union XAnimDeltaPartQuatData
|
union XAnimDeltaPartQuatData
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace xanim
|
||||||
|
{
|
||||||
|
enum class CompiledXAnimVersion : uint8_t
|
||||||
|
{
|
||||||
|
// IW3, T4
|
||||||
|
VERSION_17 = 17,
|
||||||
|
// IW4, IW5
|
||||||
|
VERSION_18 = 18,
|
||||||
|
// T5, T6 (slightly different format however)
|
||||||
|
VERSION_19 = 19
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace binary17
|
||||||
|
{
|
||||||
|
constexpr uint8_t FLAG_LOOPED = 0x1;
|
||||||
|
constexpr uint8_t FLAG_DELTA = 0x2;
|
||||||
|
} // namespace binary17
|
||||||
|
|
||||||
|
namespace binary18
|
||||||
|
{
|
||||||
|
constexpr uint8_t FLAG_LOOPED = 0x1;
|
||||||
|
constexpr uint8_t FLAG_DELTA = 0x2;
|
||||||
|
constexpr uint8_t FLAG_DELTA_3D = 0x4;
|
||||||
|
} // namespace binary18
|
||||||
|
|
||||||
|
namespace binary19
|
||||||
|
{
|
||||||
|
constexpr uint8_t FLAG_LOOPED = 0x1;
|
||||||
|
constexpr uint8_t FLAG_DELTA = 0x2;
|
||||||
|
|
||||||
|
constexpr uint8_t FLAG_T5_LEFT_HAND_GRIP_IK = 0x4;
|
||||||
|
constexpr uint8_t FLAG_T5_STREAMABLE = 0x8;
|
||||||
|
|
||||||
|
constexpr uint8_t FLAG_T6_DELTA_3D = 0x4;
|
||||||
|
constexpr uint8_t FLAG_T6_LEFT_HAND_GRIP_IK = 0x8;
|
||||||
|
|
||||||
|
// This flag is not part of the official format.
|
||||||
|
// T5 and T6 use the same XAnim version, even though the format is different:
|
||||||
|
// * Flags have slightly different values
|
||||||
|
// * T6 does not support "streamable"
|
||||||
|
// * T5 does not support delta3D
|
||||||
|
// So this flag value is added to be able to identify whether the file should use
|
||||||
|
// T5 or T6 parsing behavior.
|
||||||
|
constexpr uint8_t FLAG_T6_COMPATIBILITY = 0x80;
|
||||||
|
} // namespace binary19
|
||||||
|
} // namespace xanim
|
||||||
@@ -1,9 +1,146 @@
|
|||||||
#include "XAnimCommon.h"
|
#include "XAnimCommon.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <format>
|
#include <format>
|
||||||
|
#include <numeric>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace xanim
|
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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TransTrack::TransTrack()
|
||||||
|
: m_type(TransType::NO_TRANS),
|
||||||
|
m_mins({}),
|
||||||
|
m_size({}),
|
||||||
|
m_constant({})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonXAnimNotifyInfo::CommonXAnimNotifyInfo()
|
||||||
|
: m_time(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonXAnimNotifyInfo::CommonXAnimNotifyInfo(std::string name, const float time)
|
||||||
|
: m_name(std::move(name)),
|
||||||
|
m_time(time)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CommonDeltaQuatTrack::Is3DTrack() const
|
||||||
|
{
|
||||||
|
return !m_frames.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonDeltaTransTrack::CommonDeltaTransTrack()
|
||||||
|
: m_constant(std::nullopt),
|
||||||
|
m_small_trans(false),
|
||||||
|
m_mins({}),
|
||||||
|
m_size({})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonXAnimParts::CommonXAnimParts()
|
||||||
|
: m_num_frames(0),
|
||||||
|
m_looped(false),
|
||||||
|
m_left_hand_grip_ik(false),
|
||||||
|
m_streamable(false),
|
||||||
|
m_frame_rate(0),
|
||||||
|
m_primed_length(0),
|
||||||
|
m_asset_type(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommonXAnimParts::SortBoneTracksForQuats()
|
||||||
|
{
|
||||||
|
std::vector<size_t> boneOrder(m_bone_tracks.size());
|
||||||
|
std::ranges::iota(boneOrder, 0);
|
||||||
|
|
||||||
|
std::ranges::sort(boneOrder,
|
||||||
|
[this](const size_t i0, const size_t i1)
|
||||||
|
{
|
||||||
|
const auto type0 = std::to_underlying(m_bone_tracks[i0].m_quat.m_type);
|
||||||
|
const auto type1 = std::to_underlying(m_bone_tracks[i1].m_quat.m_type);
|
||||||
|
if (type0 != type1)
|
||||||
|
return type0 < type1;
|
||||||
|
|
||||||
|
return i0 < i1;
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<BoneTrack> boneTrackCopies(m_bone_tracks.size());
|
||||||
|
for (size_t i = 0u; i < boneOrder.size(); ++i)
|
||||||
|
{
|
||||||
|
boneTrackCopies[i] = std::move(m_bone_tracks[boneOrder[i]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_bone_tracks = std::move(boneTrackCopies);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<size_t> CommonXAnimParts::GetBoneTrackOrderForTrans() const
|
||||||
|
{
|
||||||
|
// This assumes the bone tracks were already sorted for quats
|
||||||
|
std::vector<size_t> boneOrder(m_bone_tracks.size());
|
||||||
|
std::ranges::iota(boneOrder, 0);
|
||||||
|
|
||||||
|
std::ranges::sort(boneOrder,
|
||||||
|
[this](const size_t i0, const size_t i1)
|
||||||
|
{
|
||||||
|
const auto type0 = std::to_underlying(m_bone_tracks[i0].m_trans.m_type);
|
||||||
|
const auto type1 = std::to_underlying(m_bone_tracks[i1].m_trans.m_type);
|
||||||
|
if (type0 != type1)
|
||||||
|
return type0 < type1;
|
||||||
|
|
||||||
|
return i0 < i1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return boneOrder;
|
||||||
|
}
|
||||||
|
|
||||||
std::string GetCompiledFileNameForAssetName(const std::string& assetName)
|
std::string GetCompiledFileNameForAssetName(const std::string& assetName)
|
||||||
{
|
{
|
||||||
return std::format("xanim/{}", assetName);
|
return std::format("xanim/{}", assetName);
|
||||||
|
|||||||
@@ -1,8 +1,163 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace xanim
|
namespace xanim
|
||||||
{
|
{
|
||||||
|
enum class QuatType : uint8_t
|
||||||
|
{
|
||||||
|
NO_QUAT = 0,
|
||||||
|
HALF_QUAT = 1,
|
||||||
|
FULL_QUAT = 2,
|
||||||
|
HALF_QUAT_NO_SIZE = 3,
|
||||||
|
FULL_QUAT_NO_SIZE = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TransType : uint8_t
|
||||||
|
{
|
||||||
|
SMALL_TRANS = 5,
|
||||||
|
FULL_TRANS = 6,
|
||||||
|
TRANS_NO_SIZE = 7,
|
||||||
|
NO_TRANS = 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
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];
|
||||||
|
};
|
||||||
|
|
||||||
|
class QuatTrack
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QuatTrack();
|
||||||
|
|
||||||
|
QuatType m_type;
|
||||||
|
std::vector<uint16_t> m_indices;
|
||||||
|
std::vector<CommonXQuat> m_frames;
|
||||||
|
std::vector<CommonXQuat2> m_frames2;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TransTrack
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TransTrack();
|
||||||
|
|
||||||
|
TransType m_type;
|
||||||
|
std::vector<uint16_t> m_indices;
|
||||||
|
std::array<float, 3> m_mins;
|
||||||
|
std::array<float, 3> m_size;
|
||||||
|
std::vector<CommonVec3U8> m_frames_u8;
|
||||||
|
std::vector<CommonVec3U16> m_frames_u16;
|
||||||
|
std::array<float, 3> m_constant;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BoneTrack
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
BoneTrack() = default;
|
||||||
|
|
||||||
|
std::string m_name;
|
||||||
|
QuatTrack m_quat;
|
||||||
|
TransTrack m_trans;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CommonXAnimNotifyInfo
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CommonXAnimNotifyInfo();
|
||||||
|
CommonXAnimNotifyInfo(std::string name, float time);
|
||||||
|
|
||||||
|
std::string m_name;
|
||||||
|
float m_time;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CommonDeltaQuatTrack
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CommonDeltaQuatTrack() = default;
|
||||||
|
|
||||||
|
[[nodiscard]] bool Is3DTrack() const;
|
||||||
|
|
||||||
|
std::vector<uint16_t> m_indices;
|
||||||
|
std::vector<CommonXQuat> m_frames;
|
||||||
|
std::vector<CommonXQuat2> m_frames2;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CommonDeltaTransTrack
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CommonDeltaTransTrack();
|
||||||
|
|
||||||
|
std::optional<std::array<float, 3>> m_constant;
|
||||||
|
|
||||||
|
bool m_small_trans;
|
||||||
|
std::vector<uint16_t> m_indices;
|
||||||
|
std::array<float, 3> m_mins;
|
||||||
|
std::array<float, 3> m_size;
|
||||||
|
std::vector<CommonVec3U8> m_frames_u8;
|
||||||
|
std::vector<CommonVec3U16> m_frames_u16;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CommonXAnimDeltaTrack
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CommonXAnimDeltaTrack() = default;
|
||||||
|
|
||||||
|
std::optional<CommonDeltaQuatTrack> m_quat;
|
||||||
|
std::optional<CommonDeltaTransTrack> m_trans;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CommonXAnimParts
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CommonXAnimParts();
|
||||||
|
|
||||||
|
void SortBoneTracksForQuats();
|
||||||
|
[[nodiscard]] std::vector<size_t> GetBoneTrackOrderForTrans() const;
|
||||||
|
|
||||||
|
size_t m_num_frames;
|
||||||
|
bool m_looped;
|
||||||
|
bool m_left_hand_grip_ik;
|
||||||
|
bool m_streamable;
|
||||||
|
float m_frame_rate;
|
||||||
|
float m_primed_length;
|
||||||
|
uint8_t m_asset_type;
|
||||||
|
std::vector<BoneTrack> m_bone_tracks;
|
||||||
|
std::vector<CommonXAnimNotifyInfo> m_notifies;
|
||||||
|
std::unique_ptr<CommonXAnimDeltaTrack> m_delta_track;
|
||||||
|
};
|
||||||
|
|
||||||
[[nodiscard]] std::string GetCompiledFileNameForAssetName(const std::string& assetName);
|
[[nodiscard]] std::string GetCompiledFileNameForAssetName(const std::string& assetName);
|
||||||
}
|
} // namespace xanim
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "Game/IW3/Image/ImageLoaderExternalIW3.h"
|
#include "Game/IW3/Image/ImageLoaderExternalIW3.h"
|
||||||
#include "Game/IW3/Techset/PixelShaderLoaderIW3.h"
|
#include "Game/IW3/Techset/PixelShaderLoaderIW3.h"
|
||||||
#include "Game/IW3/Techset/VertexShaderLoaderIW3.h"
|
#include "Game/IW3/Techset/VertexShaderLoaderIW3.h"
|
||||||
|
#include "Game/IW3/XAnim/XAnimLoaderIW3.h"
|
||||||
#include "Game/IW3/XModel/LoaderXModelIW3.h"
|
#include "Game/IW3/XModel/LoaderXModelIW3.h"
|
||||||
#include "LightDef/LightDefLoaderIW3.h"
|
#include "LightDef/LightDefLoaderIW3.h"
|
||||||
#include "Localize/AssetLoaderLocalizeIW3.h"
|
#include "Localize/AssetLoaderLocalizeIW3.h"
|
||||||
@@ -18,7 +19,6 @@
|
|||||||
#include "RawFile/AssetLoaderRawFileIW3.h"
|
#include "RawFile/AssetLoaderRawFileIW3.h"
|
||||||
#include "Sound/LoaderSoundCurveIW3.h"
|
#include "Sound/LoaderSoundCurveIW3.h"
|
||||||
#include "StringTable/AssetLoaderStringTableIW3.h"
|
#include "StringTable/AssetLoaderStringTableIW3.h"
|
||||||
#include "XAnim/XAnimLoaderIW3.h"
|
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
|||||||
@@ -1,781 +0,0 @@
|
|||||||
#include "XAnimLoaderIW3.h"
|
|
||||||
|
|
||||||
#include "Utils/Alignment.h"
|
|
||||||
#include "Utils/Logging/Log.h"
|
|
||||||
#include "Utils/StreamUtils.h"
|
|
||||||
#include "XAnim/XAnimCommon.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <cassert>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstring>
|
|
||||||
#include <format>
|
|
||||||
#include <limits>
|
|
||||||
#include <numeric>
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace IW3;
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr uint16_t RAW_VERSION = 17;
|
|
||||||
|
|
||||||
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;
|
|
||||||
enum class QuatType : uint8_t
|
|
||||||
{
|
|
||||||
NO_QUAT = 0,
|
|
||||||
HALF_QUAT = 1,
|
|
||||||
FULL_QUAT = 2,
|
|
||||||
HALF_QUAT_NO_SIZE = 3,
|
|
||||||
FULL_QUAT_NO_SIZE = 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class TransType : uint8_t
|
|
||||||
{
|
|
||||||
SMALL_TRANS = 5,
|
|
||||||
FULL_TRANS = 6,
|
|
||||||
TRANS_NO_SIZE = 7,
|
|
||||||
NO_TRANS = 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct QuatTrack
|
|
||||||
{
|
|
||||||
QuatType type = QuatType::NO_QUAT;
|
|
||||||
std::vector<uint16_t> indices;
|
|
||||||
std::vector<int16_t> values;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct TransTrack
|
|
||||||
{
|
|
||||||
TransType type = TransType::NO_TRANS;
|
|
||||||
std::vector<uint16_t> indices;
|
|
||||||
std::array<float, 3> mins{};
|
|
||||||
std::array<float, 3> size{};
|
|
||||||
std::vector<uint8_t> byteFrames;
|
|
||||||
std::vector<uint16_t> shortFrames;
|
|
||||||
std::array<float, 3> constant{};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct BoneTrack
|
|
||||||
{
|
|
||||||
std::string name;
|
|
||||||
QuatTrack quat;
|
|
||||||
TransTrack trans;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FlatDataWriteCursor
|
|
||||||
{
|
|
||||||
std::vector<uint8_t> dataByte;
|
|
||||||
std::vector<int16_t> dataShort;
|
|
||||||
std::vector<int32_t> dataInt;
|
|
||||||
std::vector<uint8_t> randomDataByte;
|
|
||||||
std::vector<int16_t> randomDataShort;
|
|
||||||
std::vector<uint16_t> indices;
|
|
||||||
};
|
|
||||||
|
|
||||||
void PrintError(const XAnimParts& parts, const std::string& message)
|
|
||||||
{
|
|
||||||
con::error("Cannot load xanim \"{}\": {}", parts.name, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] bool UseByteIndices(const XAnimParts& parts)
|
|
||||||
{
|
|
||||||
return parts.numframes < 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] int FloatBitsToInt(const float value)
|
|
||||||
{
|
|
||||||
union
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
float f;
|
|
||||||
};
|
|
||||||
|
|
||||||
f = value;
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WriteFloat3(FlatDataWriteCursor& writeCursor, const std::array<float, 3>& value)
|
|
||||||
{
|
|
||||||
for (const float f : value)
|
|
||||||
writeCursor.dataInt.emplace_back(FloatBitsToInt(f));
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] float DecodeRawTransSize(const float value, const bool smallTrans)
|
|
||||||
{
|
|
||||||
const auto scale = smallTrans ? HALF_TRANS_SIZE_SCALE : FULL_TRANS_SIZE_SCALE;
|
|
||||||
return value * scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConsumeQuat(std::istream& stream, XQuat& quat)
|
|
||||||
{
|
|
||||||
quat.value[0] = stream::ReadValue<int16_t>(stream);
|
|
||||||
quat.value[1] = stream::ReadValue<int16_t>(stream);
|
|
||||||
quat.value[2] = stream::ReadValue<int16_t>(stream);
|
|
||||||
|
|
||||||
int32_t temp = 0x3FFF0001 - (quat.value[0] * quat.value[0] + quat.value[1] * quat.value[1] + quat.value[2] * quat.value[2]);
|
|
||||||
if (temp <= 0)
|
|
||||||
temp = 0;
|
|
||||||
else
|
|
||||||
temp = static_cast<int32_t>(std::floor(std::sqrt(static_cast<float>(temp)) + 0.5f));
|
|
||||||
|
|
||||||
assert(temp >= std::numeric_limits<int16_t>::min() && temp <= std::numeric_limits<int16_t>::max());
|
|
||||||
quat.value[3] = static_cast<int16_t>(temp);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConsumeQuat2(std::istream& stream, XQuat2& quat2)
|
|
||||||
{
|
|
||||||
quat2.value[0] = stream::ReadValue<int16_t>(stream);
|
|
||||||
|
|
||||||
int32_t temp = 0x3FFF0001 - quat2.value[0] * quat2.value[0];
|
|
||||||
if (temp <= 0)
|
|
||||||
temp = 0;
|
|
||||||
else
|
|
||||||
temp = static_cast<int32_t>(floor(std::sqrt(static_cast<float>(temp)) + 0.5f));
|
|
||||||
|
|
||||||
assert(temp >= std::numeric_limits<int16_t>::min() && temp <= std::numeric_limits<int16_t>::max());
|
|
||||||
quat2.value[1] = static_cast<int16_t>(temp);
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlipQuat(XQuat& quat)
|
|
||||||
{
|
|
||||||
quat.value[0] = static_cast<int16_t>(-quat.value[0]);
|
|
||||||
quat.value[1] = static_cast<int16_t>(-quat.value[1]);
|
|
||||||
quat.value[2] = static_cast<int16_t>(-quat.value[2]);
|
|
||||||
quat.value[3] = static_cast<int16_t>(-quat.value[3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlipQuat2(XQuat2& quat)
|
|
||||||
{
|
|
||||||
quat.value[0] = static_cast<int16_t>(-quat.value[0]);
|
|
||||||
quat.value[1] = static_cast<int16_t>(-quat.value[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
void LoadIndicesIfNeeded(std::istream& stream, T& indices, const uint16_t numIndices, const bool useByteIndices, const uint16_t numLoopFrames)
|
|
||||||
{
|
|
||||||
if (useByteIndices)
|
|
||||||
{
|
|
||||||
// The raw format omits indices when a track covers every loop frame in order.
|
|
||||||
if (numIndices >= numLoopFrames)
|
|
||||||
std::iota(&indices._1[0], &indices._1[numIndices], 0);
|
|
||||||
else
|
|
||||||
stream::Read(stream, indices._1, numIndices * sizeof(uint8_t));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// The raw format omits indices when a track covers every loop frame in order.
|
|
||||||
if (numIndices >= numLoopFrames)
|
|
||||||
std::iota(&indices._2[0], &indices._2[numIndices], 0);
|
|
||||||
else
|
|
||||||
stream::Read(stream, indices._2, numIndices * sizeof(uint16_t));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void LoadIndicesIfNeeded(
|
|
||||||
std::istream& stream, std::vector<uint16_t>& indices, const uint16_t numIndices, const bool useByteIndices, const uint16_t numLoopFrames)
|
|
||||||
{
|
|
||||||
// The raw format omits indices when a track covers every loop frame in order.
|
|
||||||
if (numIndices >= numLoopFrames)
|
|
||||||
{
|
|
||||||
indices.resize(numIndices);
|
|
||||||
std::ranges::iota(indices, 0);
|
|
||||||
}
|
|
||||||
else if (useByteIndices)
|
|
||||||
{
|
|
||||||
indices.reserve(numIndices);
|
|
||||||
for (auto i = 0u; i < numIndices; i++)
|
|
||||||
indices.emplace_back(stream::ReadValue<uint8_t>(stream));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
indices.resize(numIndices);
|
|
||||||
stream::Read(stream, indices.data(), numIndices * sizeof(uint16_t));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReadTransTrack(std::istream& stream, TransTrack& transTrack, const uint16_t numLoopFrames, const bool useByteIndices)
|
|
||||||
{
|
|
||||||
const auto numTransIndices = stream::ReadValue<uint16_t>(stream);
|
|
||||||
if (numTransIndices == 0)
|
|
||||||
{
|
|
||||||
transTrack.type = TransType::NO_TRANS;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numTransIndices == 1)
|
|
||||||
{
|
|
||||||
transTrack.type = TransType::TRANS_NO_SIZE;
|
|
||||||
for (auto& value : transTrack.constant)
|
|
||||||
value = stream::ReadValue<float>(stream);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadIndicesIfNeeded(stream, transTrack.indices, numTransIndices, useByteIndices, numLoopFrames);
|
|
||||||
|
|
||||||
const auto smallTrans = stream::ReadValue<bool>(stream);
|
|
||||||
transTrack.type = smallTrans ? TransType::SMALL_TRANS : TransType::FULL_TRANS;
|
|
||||||
|
|
||||||
for (auto& value : transTrack.mins)
|
|
||||||
value = stream::ReadValue<float>(stream);
|
|
||||||
for (auto& value : transTrack.size)
|
|
||||||
value = DecodeRawTransSize(stream::ReadValue<float>(stream), smallTrans);
|
|
||||||
|
|
||||||
if (smallTrans)
|
|
||||||
{
|
|
||||||
transTrack.byteFrames.resize(numTransIndices * 3);
|
|
||||||
stream::Read(stream, transTrack.byteFrames.data(), numTransIndices * sizeof(uint8_t) * 3);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
transTrack.shortFrames.resize(numTransIndices * 3);
|
|
||||||
stream::Read(stream, transTrack.shortFrames.data(), numTransIndices * sizeof(uint16_t) * 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReadQuatTrack(
|
|
||||||
std::istream& stream, QuatTrack& quatTrack, const uint16_t numLoopFrames, const bool useByteIndices, const bool flipQuat, const bool halfQuat)
|
|
||||||
{
|
|
||||||
const auto numQuatIndices = stream::ReadValue<uint16_t>(stream);
|
|
||||||
if (numQuatIndices == 0)
|
|
||||||
{
|
|
||||||
assert(halfQuat);
|
|
||||||
quatTrack.type = QuatType::NO_QUAT;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numQuatIndices == 1)
|
|
||||||
{
|
|
||||||
quatTrack.type = halfQuat ? QuatType::HALF_QUAT_NO_SIZE : QuatType::FULL_QUAT_NO_SIZE;
|
|
||||||
if (halfQuat)
|
|
||||||
{
|
|
||||||
XQuat2 quat2;
|
|
||||||
ConsumeQuat2(stream, quat2);
|
|
||||||
if (flipQuat)
|
|
||||||
FlipQuat2(quat2);
|
|
||||||
|
|
||||||
quatTrack.values.reserve(2);
|
|
||||||
quatTrack.values.emplace_back(quat2.value[0]);
|
|
||||||
quatTrack.values.emplace_back(quat2.value[1]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
XQuat quat;
|
|
||||||
ConsumeQuat(stream, quat);
|
|
||||||
if (flipQuat)
|
|
||||||
FlipQuat(quat);
|
|
||||||
|
|
||||||
quatTrack.values.reserve(4);
|
|
||||||
quatTrack.values.emplace_back(quat.value[0]);
|
|
||||||
quatTrack.values.emplace_back(quat.value[1]);
|
|
||||||
quatTrack.values.emplace_back(quat.value[2]);
|
|
||||||
quatTrack.values.emplace_back(quat.value[3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadIndicesIfNeeded(stream, quatTrack.indices, numQuatIndices, useByteIndices, numLoopFrames);
|
|
||||||
|
|
||||||
if (halfQuat)
|
|
||||||
{
|
|
||||||
quatTrack.type = QuatType::HALF_QUAT;
|
|
||||||
quatTrack.values.resize(numQuatIndices * 2);
|
|
||||||
auto* quats2 = reinterpret_cast<XQuat2*>(quatTrack.values.data());
|
|
||||||
for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; quatIndexNum++)
|
|
||||||
{
|
|
||||||
auto& curFrame = quats2[quatIndexNum];
|
|
||||||
ConsumeQuat2(stream, curFrame);
|
|
||||||
|
|
||||||
if (quatIndexNum > 0)
|
|
||||||
{
|
|
||||||
const auto& prevFrame = quats2[quatIndexNum - 1];
|
|
||||||
if (prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] < 0)
|
|
||||||
FlipQuat2(curFrame);
|
|
||||||
}
|
|
||||||
else if (flipQuat)
|
|
||||||
FlipQuat2(curFrame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
quatTrack.type = QuatType::FULL_QUAT;
|
|
||||||
quatTrack.values.resize(numQuatIndices * 4);
|
|
||||||
auto* quats = reinterpret_cast<XQuat*>(quatTrack.values.data());
|
|
||||||
for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; quatIndexNum++)
|
|
||||||
{
|
|
||||||
auto& curFrame = quats[quatIndexNum];
|
|
||||||
ConsumeQuat(stream, curFrame);
|
|
||||||
|
|
||||||
if (quatIndexNum > 0)
|
|
||||||
{
|
|
||||||
const auto& prevFrame = quats[quatIndexNum - 1];
|
|
||||||
const auto dot = prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] + prevFrame.value[2] * curFrame.value[2]
|
|
||||||
+ prevFrame.value[3] * curFrame.value[3];
|
|
||||||
if (dot < 0)
|
|
||||||
FlipQuat(curFrame);
|
|
||||||
}
|
|
||||||
else if (flipQuat)
|
|
||||||
FlipQuat(curFrame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ApplyWriteCursorToParts(XAnimParts& parts, const FlatDataWriteCursor& writeCursor, MemoryManager& memory)
|
|
||||||
{
|
|
||||||
if (!writeCursor.dataByte.empty())
|
|
||||||
{
|
|
||||||
parts.dataByteCount = static_cast<uint16_t>(writeCursor.dataByte.size());
|
|
||||||
parts.dataByte = memory.Alloc<uint8_t>(parts.dataByteCount);
|
|
||||||
std::memcpy(parts.dataByte, writeCursor.dataByte.data(), parts.dataByteCount * sizeof(uint8_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!writeCursor.dataShort.empty())
|
|
||||||
{
|
|
||||||
parts.dataShortCount = static_cast<uint16_t>(writeCursor.dataShort.size());
|
|
||||||
parts.dataShort = memory.Alloc<int16_t>(parts.dataShortCount);
|
|
||||||
std::memcpy(parts.dataShort, writeCursor.dataShort.data(), parts.dataShortCount * sizeof(int16_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!writeCursor.dataInt.empty())
|
|
||||||
{
|
|
||||||
parts.dataIntCount = static_cast<uint16_t>(writeCursor.dataInt.size());
|
|
||||||
parts.dataInt = memory.Alloc<int32_t>(parts.dataIntCount);
|
|
||||||
std::memcpy(parts.dataInt, writeCursor.dataInt.data(), parts.dataIntCount * sizeof(int32_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!writeCursor.randomDataByte.empty())
|
|
||||||
{
|
|
||||||
parts.randomDataByteCount = static_cast<uint16_t>(writeCursor.randomDataByte.size());
|
|
||||||
parts.randomDataByte = memory.Alloc<uint8_t>(parts.randomDataByteCount);
|
|
||||||
std::memcpy(parts.randomDataByte, writeCursor.randomDataByte.data(), parts.randomDataByteCount * sizeof(uint8_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!writeCursor.randomDataShort.empty())
|
|
||||||
{
|
|
||||||
parts.randomDataShortCount = static_cast<unsigned int>(writeCursor.randomDataShort.size());
|
|
||||||
parts.randomDataShort = memory.Alloc<int16_t>(parts.randomDataShortCount);
|
|
||||||
std::memcpy(parts.randomDataShort, writeCursor.randomDataShort.data(), parts.randomDataShortCount * sizeof(int16_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!writeCursor.indices.empty())
|
|
||||||
{
|
|
||||||
parts.indexCount = static_cast<unsigned int>(writeCursor.indices.size());
|
|
||||||
parts.indices._2 = memory.Alloc<uint16_t>(parts.indexCount);
|
|
||||||
std::memcpy(parts.indices._2, writeCursor.indices.data(), parts.indexCount * sizeof(uint16_t));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WritePackedIndices(FlatDataWriteCursor& writeCursor, const std::vector<uint16_t>& indices, const bool useByteIndices)
|
|
||||||
{
|
|
||||||
const auto indexCount = indices.size();
|
|
||||||
writeCursor.dataShort.emplace_back(static_cast<int16_t>(indexCount - 1)); // storedSize
|
|
||||||
|
|
||||||
if (useByteIndices)
|
|
||||||
{
|
|
||||||
for (const auto index : indices)
|
|
||||||
{
|
|
||||||
assert(index <= std::numeric_limits<uint8_t>::max());
|
|
||||||
writeCursor.dataByte.emplace_back(static_cast<uint8_t>(index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (indexCount >= 65)
|
|
||||||
{
|
|
||||||
// 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.
|
|
||||||
std::ranges::copy(indices, std::back_inserter(writeCursor.indices));
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
const auto longTableSize = ((indexCount - 2) / 256u) + 1;
|
|
||||||
for (auto i = 0u; i < longTableSize; i++)
|
|
||||||
writeCursor.dataShort.emplace_back(indices[256 * i]);
|
|
||||||
writeCursor.dataShort.emplace_back(indices[indices.size() - 1]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
std::ranges::copy(indices, std::back_inserter(writeCursor.dataShort));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProcessQuatTrack(FlatDataWriteCursor& writeCursor, const QuatTrack& quatTrack, XAnimParts& parts, const bool useByteIndices)
|
|
||||||
{
|
|
||||||
switch (quatTrack.type)
|
|
||||||
{
|
|
||||||
case QuatType::NO_QUAT:
|
|
||||||
parts.boneCount[PART_TYPE_NO_QUAT]++;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case QuatType::HALF_QUAT:
|
|
||||||
parts.boneCount[PART_TYPE_HALF_QUAT]++;
|
|
||||||
WritePackedIndices(writeCursor, quatTrack.indices, useByteIndices);
|
|
||||||
assert(quatTrack.values.size() == quatTrack.indices.size() * 2);
|
|
||||||
std::ranges::copy(quatTrack.values, std::back_inserter(writeCursor.randomDataShort));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case QuatType::FULL_QUAT:
|
|
||||||
parts.boneCount[PART_TYPE_FULL_QUAT]++;
|
|
||||||
WritePackedIndices(writeCursor, quatTrack.indices, useByteIndices);
|
|
||||||
assert(quatTrack.values.size() == quatTrack.indices.size() * 4);
|
|
||||||
std::ranges::copy(quatTrack.values, std::back_inserter(writeCursor.randomDataShort));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case QuatType::HALF_QUAT_NO_SIZE:
|
|
||||||
parts.boneCount[PART_TYPE_HALF_QUAT_NO_SIZE]++;
|
|
||||||
assert(quatTrack.values.size() == 2);
|
|
||||||
std::ranges::copy(quatTrack.values, std::back_inserter(writeCursor.dataShort));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case QuatType::FULL_QUAT_NO_SIZE:
|
|
||||||
parts.boneCount[PART_TYPE_FULL_QUAT_NO_SIZE]++;
|
|
||||||
assert(quatTrack.values.size() == 4);
|
|
||||||
std::ranges::copy(quatTrack.values, std::back_inserter(writeCursor.dataShort));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProcessTransTrack(FlatDataWriteCursor& writeCursor, const TransTrack& transTrack, const size_t boneIndex, XAnimParts& parts, const bool useByteIndices)
|
|
||||||
{
|
|
||||||
assert(boneIndex <= std::numeric_limits<uint8_t>::max());
|
|
||||||
writeCursor.dataByte.emplace_back(static_cast<uint8_t>(boneIndex));
|
|
||||||
|
|
||||||
switch (transTrack.type)
|
|
||||||
{
|
|
||||||
case TransType::SMALL_TRANS:
|
|
||||||
parts.boneCount[PART_TYPE_SMALL_TRANS]++;
|
|
||||||
WritePackedIndices(writeCursor, transTrack.indices, useByteIndices);
|
|
||||||
WriteFloat3(writeCursor, transTrack.mins);
|
|
||||||
WriteFloat3(writeCursor, transTrack.size);
|
|
||||||
assert(transTrack.byteFrames.size() == transTrack.indices.size() * 3);
|
|
||||||
std::ranges::copy(transTrack.byteFrames, std::back_inserter(writeCursor.randomDataByte));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TransType::FULL_TRANS:
|
|
||||||
parts.boneCount[PART_TYPE_TRANS]++;
|
|
||||||
WritePackedIndices(writeCursor, transTrack.indices, useByteIndices);
|
|
||||||
WriteFloat3(writeCursor, transTrack.mins);
|
|
||||||
WriteFloat3(writeCursor, transTrack.size);
|
|
||||||
assert(transTrack.shortFrames.size() == transTrack.indices.size() * 3);
|
|
||||||
std::ranges::copy(transTrack.shortFrames, std::back_inserter(writeCursor.randomDataShort));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TransType::TRANS_NO_SIZE:
|
|
||||||
parts.boneCount[PART_TYPE_TRANS_NO_SIZE]++;
|
|
||||||
WriteFloat3(writeCursor, transTrack.constant);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TransType::NO_TRANS:
|
|
||||||
parts.boneCount[PART_TYPE_NO_TRANS]++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class XAnimLoader final : public AssetCreator<AssetXAnim>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
XAnimLoader(MemoryManager& memory, ISearchPath& searchPath, ZoneScriptStrings& scriptStrings)
|
|
||||||
: m_memory(memory),
|
|
||||||
m_search_path(searchPath),
|
|
||||||
m_script_strings(scriptStrings)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override
|
|
||||||
{
|
|
||||||
const auto file = m_search_path.Open(xanim::GetCompiledFileNameForAssetName(assetName));
|
|
||||||
if (!file.IsOpen())
|
|
||||||
return AssetCreationResult::NoAction();
|
|
||||||
|
|
||||||
auto* parts = m_memory.Alloc<XAnimParts>();
|
|
||||||
parts->name = m_memory.Dup(assetName.c_str());
|
|
||||||
|
|
||||||
AssetRegistration<AssetXAnim> registration(assetName, parts);
|
|
||||||
if (!LoadFromFile(*file.m_stream, *parts, registration))
|
|
||||||
{
|
|
||||||
con::error("Failed to load xanim \"{}\"", assetName);
|
|
||||||
return AssetCreationResult::Failure();
|
|
||||||
}
|
|
||||||
|
|
||||||
return AssetCreationResult::Success(context.AddAsset(std::move(registration)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void ReadNoteTracks(std::istream& stream, XAnimParts& parts, AssetRegistration<AssetXAnim>& registration) const
|
|
||||||
{
|
|
||||||
const auto numDiskNoteTracks = stream::ReadValue<uint8_t>(stream);
|
|
||||||
assert(numDiskNoteTracks + 1 <= std::numeric_limits<uint8_t>::max());
|
|
||||||
|
|
||||||
uint8_t numNoteTracks;
|
|
||||||
if (numDiskNoteTracks == std::numeric_limits<uint8_t>::max())
|
|
||||||
{
|
|
||||||
PrintError(parts, "Could not add \"end\" notify as maximum notify entries were reached");
|
|
||||||
numNoteTracks = numDiskNoteTracks;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
numNoteTracks = numDiskNoteTracks + 1;
|
|
||||||
|
|
||||||
parts.notifyCount = numNoteTracks;
|
|
||||||
parts.notify = m_memory.Alloc<XAnimNotifyInfo>(numNoteTracks);
|
|
||||||
|
|
||||||
for (auto notifyIndex = 0u; notifyIndex < numDiskNoteTracks; notifyIndex++)
|
|
||||||
{
|
|
||||||
auto& notify = parts.notify[notifyIndex];
|
|
||||||
|
|
||||||
const auto notifyName = stream::ReadCString(stream);
|
|
||||||
notify.name = m_script_strings.AddOrGetScriptString(notifyName);
|
|
||||||
registration.AddScriptString(notify.name);
|
|
||||||
|
|
||||||
const auto frame = stream::ReadValue<uint16_t>(stream);
|
|
||||||
notify.time = parts.numframes > 0 ? static_cast<float>(frame) / static_cast<float>(parts.numframes) : 0;
|
|
||||||
assert(notify.time >= 0.0f && notify.time <= 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numNoteTracks > numDiskNoteTracks)
|
|
||||||
{
|
|
||||||
const auto endScriptString = m_script_strings.AddOrGetScriptString("end");
|
|
||||||
registration.AddScriptString(endScriptString);
|
|
||||||
parts.notify[numDiskNoteTracks].name = endScriptString;
|
|
||||||
parts.notify[numDiskNoteTracks].time = 1.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void LoadDeltaQuats(std::istream& stream, XAnimDeltaPart& delta, const bool useByteIndices, const uint16_t numLoopFrames) const
|
|
||||||
{
|
|
||||||
const auto numQuatIndices = stream::ReadValue<uint16_t>(stream);
|
|
||||||
if (numQuatIndices == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (numQuatIndices == 1)
|
|
||||||
{
|
|
||||||
delta.quat = static_cast<XAnimDeltaPartQuat*>(m_memory.AllocRaw(offsetof(XAnimDeltaPartQuat, u) + sizeof(XAnimDeltaPartQuatData::frame0)));
|
|
||||||
delta.quat->size = 0;
|
|
||||||
ConsumeQuat2(stream, delta.quat->u.frame0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto indicesArraySize =
|
|
||||||
useByteIndices ? numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_1) : numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_2);
|
|
||||||
|
|
||||||
delta.quat = static_cast<XAnimDeltaPartQuat*>(
|
|
||||||
m_memory.AllocRaw(offsetof(XAnimDeltaPartQuat, u) + offsetof(XAnimDeltaPartQuatDataFrames, indices) + indicesArraySize));
|
|
||||||
|
|
||||||
auto& quatIndices = delta.quat->u.frames.indices;
|
|
||||||
LoadIndicesIfNeeded(stream, quatIndices, numQuatIndices, useByteIndices, numLoopFrames);
|
|
||||||
|
|
||||||
delta.quat->size = static_cast<uint16_t>(numQuatIndices - 1);
|
|
||||||
delta.quat->u.frames.frames = m_memory.Alloc<XQuat2>(numQuatIndices);
|
|
||||||
|
|
||||||
for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum)
|
|
||||||
{
|
|
||||||
auto& curFrame = delta.quat->u.frames.frames[quatIndexNum];
|
|
||||||
ConsumeQuat2(stream, curFrame);
|
|
||||||
|
|
||||||
if (quatIndexNum > 0)
|
|
||||||
{
|
|
||||||
const auto& prevFrame = delta.quat->u.frames.frames[quatIndexNum - 1];
|
|
||||||
if (prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] < 0)
|
|
||||||
FlipQuat2(curFrame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void LoadDeltaTrans(std::istream& stream, XAnimDeltaPart& delta, const bool useByteIndices, const uint16_t numLoopFrames) const
|
|
||||||
{
|
|
||||||
const auto numTransIndices = stream::ReadValue<uint16_t>(stream);
|
|
||||||
if (numTransIndices == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (numTransIndices == 1)
|
|
||||||
{
|
|
||||||
delta.trans = static_cast<XAnimPartTrans*>(m_memory.AllocRaw(offsetof(XAnimPartTrans, u) + sizeof(XAnimPartTransData::frame0)));
|
|
||||||
delta.trans->size = 0;
|
|
||||||
delta.trans->u.frame0.x = stream::ReadValue<float>(stream);
|
|
||||||
delta.trans->u.frame0.y = stream::ReadValue<float>(stream);
|
|
||||||
delta.trans->u.frame0.z = stream::ReadValue<float>(stream);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto indicesArraySize =
|
|
||||||
useByteIndices ? numTransIndices * sizeof(XAnimDynamicIndicesTrans::_1) : numTransIndices * sizeof(XAnimDynamicIndicesTrans::_2);
|
|
||||||
|
|
||||||
delta.trans =
|
|
||||||
static_cast<XAnimPartTrans*>(m_memory.AllocRaw(offsetof(XAnimPartTrans, u) + offsetof(XAnimPartTransFrames, indices) + indicesArraySize));
|
|
||||||
|
|
||||||
auto& frames = delta.trans->u.frames;
|
|
||||||
LoadIndicesIfNeeded(stream, frames.indices, numTransIndices, useByteIndices, numLoopFrames);
|
|
||||||
|
|
||||||
const auto smallTrans = stream::ReadValue<bool>(stream);
|
|
||||||
delta.trans->smallTrans = smallTrans ? 1 : 0;
|
|
||||||
|
|
||||||
frames.mins.x = stream::ReadValue<float>(stream);
|
|
||||||
frames.mins.y = stream::ReadValue<float>(stream);
|
|
||||||
frames.mins.z = stream::ReadValue<float>(stream);
|
|
||||||
|
|
||||||
frames.size.x = DecodeRawTransSize(stream::ReadValue<float>(stream), smallTrans);
|
|
||||||
frames.size.y = DecodeRawTransSize(stream::ReadValue<float>(stream), smallTrans);
|
|
||||||
frames.size.z = DecodeRawTransSize(stream::ReadValue<float>(stream), smallTrans);
|
|
||||||
|
|
||||||
delta.trans->size = static_cast<uint16_t>(numTransIndices - 1);
|
|
||||||
if (smallTrans)
|
|
||||||
{
|
|
||||||
frames.frames._1 = m_memory.Alloc<ByteVec>(numTransIndices);
|
|
||||||
stream::Read(stream, frames.frames._1, numTransIndices * sizeof(ByteVec));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
frames.frames._2 = m_memory.Alloc<UShortVec>(numTransIndices);
|
|
||||||
stream::Read(stream, frames.frames._2, numTransIndices * sizeof(UShortVec));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void LoadDeltaTrack(std::istream& stream, XAnimParts& parts, const bool useByteIndices, const uint16_t numLoopFrames) const
|
|
||||||
{
|
|
||||||
auto* delta = m_memory.Alloc<XAnimDeltaPart>();
|
|
||||||
parts.deltaPart = delta;
|
|
||||||
|
|
||||||
LoadDeltaQuats(stream, *delta, useByteIndices, numLoopFrames);
|
|
||||||
LoadDeltaTrans(stream, *delta, useByteIndices, numLoopFrames);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool LoadFromFile(std::istream& stream, XAnimParts& parts, AssetRegistration<AssetXAnim>& registration) const
|
|
||||||
{
|
|
||||||
const auto fileVersion = stream::ReadValue<uint16_t>(stream);
|
|
||||||
if (fileVersion != RAW_VERSION)
|
|
||||||
{
|
|
||||||
PrintError(parts, std::format("Unsupported version number {} (expected {})", fileVersion, RAW_VERSION));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto numFrames = stream::ReadValue<uint16_t>(stream);
|
|
||||||
const auto boneCount = stream::ReadValue<uint16_t>(stream);
|
|
||||||
const auto flags = stream::ReadValue<uint8_t>(stream);
|
|
||||||
const auto assetType = stream::ReadValue<uint8_t>(stream);
|
|
||||||
const auto framerate = stream::ReadValue<uint16_t>(stream);
|
|
||||||
if (stream.fail())
|
|
||||||
{
|
|
||||||
PrintError(parts, "Truncated file");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool isLooped = flags & FLAG_LOOPED;
|
|
||||||
const bool hasDelta = flags & FLAG_DELTA;
|
|
||||||
const uint16_t numLoopFrames = isLooped ? numFrames + 1u : numFrames;
|
|
||||||
|
|
||||||
parts.numframes = numLoopFrames - 1;
|
|
||||||
parts.bLoop = isLooped;
|
|
||||||
parts.bDelta = hasDelta;
|
|
||||||
parts.assetType = assetType;
|
|
||||||
parts.framerate = static_cast<float>(framerate);
|
|
||||||
parts.frequency = parts.numframes > 0 ? parts.framerate / static_cast<float>(parts.numframes) : 0;
|
|
||||||
|
|
||||||
const auto useByteIndices = UseByteIndices(parts);
|
|
||||||
|
|
||||||
if (hasDelta)
|
|
||||||
LoadDeltaTrack(stream, parts, useByteIndices, numLoopFrames);
|
|
||||||
|
|
||||||
std::vector<BoneTrack> boneTracks;
|
|
||||||
if (boneCount > 0)
|
|
||||||
{
|
|
||||||
const auto bitmaskSize = utils::Align<size_t>(boneCount, 8u) / 8u;
|
|
||||||
std::vector<uint8_t> flipQuatBits(bitmaskSize, 0);
|
|
||||||
std::vector<uint8_t> halfQuatBits(bitmaskSize, 0);
|
|
||||||
stream::Read(stream, flipQuatBits.data(), bitmaskSize);
|
|
||||||
stream::Read(stream, halfQuatBits.data(), bitmaskSize);
|
|
||||||
|
|
||||||
boneTracks.resize(boneCount);
|
|
||||||
for (size_t boneIndex = 0; boneIndex < boneCount; ++boneIndex)
|
|
||||||
boneTracks[boneIndex].name = stream::ReadCString(stream);
|
|
||||||
|
|
||||||
for (size_t boneIndex = 0; boneIndex < boneCount; ++boneIndex)
|
|
||||||
{
|
|
||||||
auto& boneTrack = boneTracks[boneIndex];
|
|
||||||
|
|
||||||
const bool flipQuat = flipQuatBits[boneIndex / 8u] & static_cast<uint8_t>(1u << (boneIndex % 8u));
|
|
||||||
const bool halfQuat = halfQuatBits[boneIndex / 8u] & static_cast<uint8_t>(1u << (boneIndex % 8u));
|
|
||||||
|
|
||||||
ReadQuatTrack(stream, boneTrack.quat, numLoopFrames, useByteIndices, flipQuat, halfQuat);
|
|
||||||
ReadTransTrack(stream, boneTrack.trans, numLoopFrames, useByteIndices);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadNoteTracks(stream, parts, registration);
|
|
||||||
|
|
||||||
FlatDataWriteCursor writeCursor;
|
|
||||||
|
|
||||||
std::vector<size_t> boneOrder(boneCount);
|
|
||||||
std::ranges::iota(boneOrder, 0);
|
|
||||||
|
|
||||||
std::ranges::sort(boneOrder,
|
|
||||||
[&boneTracks](const size_t i0, const size_t i1)
|
|
||||||
{
|
|
||||||
const auto type0 = std::to_underlying(boneTracks[i0].quat.type);
|
|
||||||
const auto type1 = std::to_underlying(boneTracks[i1].quat.type);
|
|
||||||
if (type0 != type1)
|
|
||||||
return type0 < type1;
|
|
||||||
|
|
||||||
return i0 < i1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// The parts bone indices are based on the quats order
|
|
||||||
std::vector<size_t> boneTrackIndexToPartsBoneIndex(boneCount);
|
|
||||||
parts.names = m_memory.Alloc<ScriptString>(boneCount);
|
|
||||||
for (auto partsBoneIndex = 0u; partsBoneIndex < boneCount; ++partsBoneIndex)
|
|
||||||
{
|
|
||||||
const auto boneTrackIndex = boneOrder[partsBoneIndex];
|
|
||||||
boneTrackIndexToPartsBoneIndex[boneTrackIndex] = partsBoneIndex;
|
|
||||||
ProcessQuatTrack(writeCursor, boneTracks[boneTrackIndex].quat, parts, useByteIndices);
|
|
||||||
|
|
||||||
// Names are based on quats order so apply them here as well
|
|
||||||
const auto scrString = m_script_strings.AddOrGetScriptString(boneTracks[boneTrackIndex].name);
|
|
||||||
parts.names[partsBoneIndex] = scrString;
|
|
||||||
registration.AddScriptString(scrString);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trans are ordered differently
|
|
||||||
std::ranges::sort(boneOrder,
|
|
||||||
[&boneTracks, &boneTrackIndexToPartsBoneIndex](const size_t i0, const size_t i1)
|
|
||||||
{
|
|
||||||
const auto type0 = std::to_underlying(boneTracks[i0].trans.type);
|
|
||||||
const auto type1 = std::to_underlying(boneTracks[i1].trans.type);
|
|
||||||
if (type0 != type1)
|
|
||||||
return type0 < type1;
|
|
||||||
|
|
||||||
return boneTrackIndexToPartsBoneIndex[i0] < boneTrackIndexToPartsBoneIndex[i1];
|
|
||||||
});
|
|
||||||
for (auto partsBoneIndex = 0u; partsBoneIndex < boneCount; ++partsBoneIndex)
|
|
||||||
{
|
|
||||||
const auto boneTrackIndex = boneOrder[partsBoneIndex];
|
|
||||||
ProcessTransTrack(writeCursor, boneTracks[boneTrackIndex].trans, boneTrackIndexToPartsBoneIndex[boneTrackIndex], parts, useByteIndices);
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyWriteCursorToParts(parts, writeCursor, m_memory);
|
|
||||||
parts.boneCount[PART_TYPE_ALL] = static_cast<uint8_t>(boneCount);
|
|
||||||
|
|
||||||
assert(stream.peek() == std::char_traits<char>::eof());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
MemoryManager& m_memory;
|
|
||||||
ISearchPath& m_search_path;
|
|
||||||
ZoneScriptStrings& m_script_strings;
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
namespace xanim
|
|
||||||
{
|
|
||||||
std::unique_ptr<AssetCreator<AssetXAnim>> CreateLoaderIW3(MemoryManager& memory, ISearchPath& searchPath, Zone& zone)
|
|
||||||
{
|
|
||||||
return std::make_unique<XAnimLoader>(memory, searchPath, zone.m_script_strings);
|
|
||||||
}
|
|
||||||
} // namespace xanim
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Asset/IAssetCreator.h"
|
|
||||||
#include "Game/IW3/IW3.h"
|
|
||||||
#include "SearchPath/ISearchPath.h"
|
|
||||||
#include "Utils/MemoryManager.h"
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace xanim
|
|
||||||
{
|
|
||||||
std::unique_ptr<AssetCreator<IW3::AssetXAnim>> CreateLoaderIW3(MemoryManager& memory, ISearchPath& searchPath, Zone& zone);
|
|
||||||
} // namespace xanim
|
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "Game/IW4/Image/ImageLoaderExternalIW4.h"
|
#include "Game/IW4/Image/ImageLoaderExternalIW4.h"
|
||||||
#include "Game/IW4/Techset/PixelShaderLoaderIW4.h"
|
#include "Game/IW4/Techset/PixelShaderLoaderIW4.h"
|
||||||
#include "Game/IW4/Techset/VertexShaderLoaderIW4.h"
|
#include "Game/IW4/Techset/VertexShaderLoaderIW4.h"
|
||||||
|
#include "Game/IW4/XAnim/XAnimLoaderIW4.h"
|
||||||
#include "Game/IW4/XModel/LoaderXModelIW4.h"
|
#include "Game/IW4/XModel/LoaderXModelIW4.h"
|
||||||
#include "Leaderboard/LoaderLeaderboardIW4.h"
|
#include "Leaderboard/LoaderLeaderboardIW4.h"
|
||||||
#include "LightDef/LightDefLoaderIW4.h"
|
#include "LightDef/LightDefLoaderIW4.h"
|
||||||
@@ -125,8 +126,7 @@ namespace
|
|||||||
collection.AddAssetCreator(phys_preset::CreateRawLoaderIW4(memory, searchPath, zone));
|
collection.AddAssetCreator(phys_preset::CreateRawLoaderIW4(memory, searchPath, zone));
|
||||||
collection.AddAssetCreator(phys_preset::CreateGdtLoaderIW4(memory, gdt, zone));
|
collection.AddAssetCreator(phys_preset::CreateGdtLoaderIW4(memory, gdt, zone));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderPhysCollMap>(memory));
|
// collection.AddAssetCreator(std::make_unique<AssetLoaderPhysCollMap>(memory));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderXAnim>(memory));
|
collection.AddAssetCreator(xanim::CreateLoaderIW4(memory, searchPath, zone));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderXModelSurfs>(memory));
|
|
||||||
collection.AddAssetCreator(xmodel::CreateLoaderIW4(memory, searchPath, zone));
|
collection.AddAssetCreator(xmodel::CreateLoaderIW4(memory, searchPath, zone));
|
||||||
collection.AddAssetCreator(material::CreateLoaderIW4(memory, searchPath));
|
collection.AddAssetCreator(material::CreateLoaderIW4(memory, searchPath));
|
||||||
collection.AddAssetCreator(techset::CreateVertexShaderLoaderIW4(memory, searchPath));
|
collection.AddAssetCreator(techset::CreateVertexShaderLoaderIW4(memory, searchPath));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "Game/IW5/Image/ImageLoaderExternalIW5.h"
|
#include "Game/IW5/Image/ImageLoaderExternalIW5.h"
|
||||||
#include "Game/IW5/Techset/PixelShaderLoaderIW5.h"
|
#include "Game/IW5/Techset/PixelShaderLoaderIW5.h"
|
||||||
#include "Game/IW5/Techset/VertexShaderLoaderIW5.h"
|
#include "Game/IW5/Techset/VertexShaderLoaderIW5.h"
|
||||||
|
#include "Game/IW5/XAnim/XAnimLoaderIW5.h"
|
||||||
#include "Game/IW5/XModel/LoaderXModelIW5.h"
|
#include "Game/IW5/XModel/LoaderXModelIW5.h"
|
||||||
#include "Leaderboard/LoaderLeaderboardIW5.h"
|
#include "Leaderboard/LoaderLeaderboardIW5.h"
|
||||||
#include "LightDef/LightDefLoaderIW5.h"
|
#include "LightDef/LightDefLoaderIW5.h"
|
||||||
@@ -132,8 +133,7 @@ namespace
|
|||||||
collection.AddAssetCreator(phys_preset::CreateRawLoaderIW5(memory, searchPath, zone));
|
collection.AddAssetCreator(phys_preset::CreateRawLoaderIW5(memory, searchPath, zone));
|
||||||
collection.AddAssetCreator(phys_preset::CreateGdtLoaderIW5(memory, gdt, zone));
|
collection.AddAssetCreator(phys_preset::CreateGdtLoaderIW5(memory, gdt, zone));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderPhysCollMap>(memory));
|
// collection.AddAssetCreator(std::make_unique<AssetLoaderPhysCollMap>(memory));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderXAnim>(memory));
|
collection.AddAssetCreator(xanim::CreateLoaderIW5(memory, searchPath, zone));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderXModelSurfs>(memory));
|
|
||||||
collection.AddAssetCreator(xmodel::CreateLoaderIW5(memory, searchPath, zone));
|
collection.AddAssetCreator(xmodel::CreateLoaderIW5(memory, searchPath, zone));
|
||||||
collection.AddAssetCreator(material::CreateLoaderIW5(memory, searchPath));
|
collection.AddAssetCreator(material::CreateLoaderIW5(memory, searchPath));
|
||||||
collection.AddAssetCreator(techset::CreateVertexShaderLoaderIW5(memory, searchPath));
|
collection.AddAssetCreator(techset::CreateVertexShaderLoaderIW5(memory, searchPath));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "Game/T5/T5.h"
|
#include "Game/T5/T5.h"
|
||||||
#include "Game/T5/Techset/PixelShaderLoaderT5.h"
|
#include "Game/T5/Techset/PixelShaderLoaderT5.h"
|
||||||
#include "Game/T5/Techset/VertexShaderLoaderT5.h"
|
#include "Game/T5/Techset/VertexShaderLoaderT5.h"
|
||||||
|
#include "Game/T5/XAnim/XAnimLoaderT5.h"
|
||||||
#include "Game/T5/XModel/LoaderXModelT5.h"
|
#include "Game/T5/XModel/LoaderXModelT5.h"
|
||||||
#include "LightDef/LightDefLoaderT5.h"
|
#include "LightDef/LightDefLoaderT5.h"
|
||||||
#include "Localize/LoaderLocalizeT5.h"
|
#include "Localize/LoaderLocalizeT5.h"
|
||||||
@@ -112,7 +113,7 @@ namespace
|
|||||||
collection.AddAssetCreator(phys_preset::CreateGdtLoaderT5(memory, gdt, zone));
|
collection.AddAssetCreator(phys_preset::CreateGdtLoaderT5(memory, gdt, zone));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderPhysConstraints>(memory));
|
// collection.AddAssetCreator(std::make_unique<AssetLoaderPhysConstraints>(memory));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderDestructibleDef>(memory));
|
// collection.AddAssetCreator(std::make_unique<AssetLoaderDestructibleDef>(memory));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderXAnim>(memory));
|
collection.AddAssetCreator(xanim::CreateLoaderT5(memory, searchPath, zone));
|
||||||
collection.AddAssetCreator(xmodel::CreateLoaderT5(memory, searchPath, zone));
|
collection.AddAssetCreator(xmodel::CreateLoaderT5(memory, searchPath, zone));
|
||||||
collection.AddAssetCreator(material::CreateLoaderT5(memory, searchPath));
|
collection.AddAssetCreator(material::CreateLoaderT5(memory, searchPath));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderTechniqueSet>(memory));
|
// collection.AddAssetCreator(std::make_unique<AssetLoaderTechniqueSet>(memory));
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "Game/T6/T6.h"
|
#include "Game/T6/T6.h"
|
||||||
#include "Game/T6/Techset/PixelShaderLoaderT6.h"
|
#include "Game/T6/Techset/PixelShaderLoaderT6.h"
|
||||||
#include "Game/T6/Techset/VertexShaderLoaderT6.h"
|
#include "Game/T6/Techset/VertexShaderLoaderT6.h"
|
||||||
|
#include "Game/T6/XAnim/XAnimLoaderT6.h"
|
||||||
#include "Game/T6/XModel/LoaderXModelT6.h"
|
#include "Game/T6/XModel/LoaderXModelT6.h"
|
||||||
#include "Image/Dx12TextureLoader.h"
|
#include "Image/Dx12TextureLoader.h"
|
||||||
#include "Image/IwiLoader.h"
|
#include "Image/IwiLoader.h"
|
||||||
@@ -386,7 +387,7 @@ namespace T6
|
|||||||
collection.AddAssetCreator(phys_constraints::CreateRawLoaderT6(memory, searchPath, zone));
|
collection.AddAssetCreator(phys_constraints::CreateRawLoaderT6(memory, searchPath, zone));
|
||||||
collection.AddAssetCreator(phys_constraints::CreateGdtLoaderT6(memory, searchPath, gdt, zone));
|
collection.AddAssetCreator(phys_constraints::CreateGdtLoaderT6(memory, searchPath, gdt, zone));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderDestructibleDef>(memory));
|
// collection.AddAssetCreator(std::make_unique<AssetLoaderDestructibleDef>(memory));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderXAnim>(memory));
|
collection.AddAssetCreator(xanim::CreateLoaderT6(memory, searchPath, zone));
|
||||||
collection.AddAssetCreator(xmodel::CreateLoaderT6(memory, searchPath, zone));
|
collection.AddAssetCreator(xmodel::CreateLoaderT6(memory, searchPath, zone));
|
||||||
collection.AddAssetCreator(material::CreateLoaderT6(memory, searchPath));
|
collection.AddAssetCreator(material::CreateLoaderT6(memory, searchPath));
|
||||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderTechniqueSet>(memory));
|
// collection.AddAssetCreator(std::make_unique<AssetLoaderTechniqueSet>(memory));
|
||||||
|
|||||||
@@ -0,0 +1,546 @@
|
|||||||
|
#include "CompiledXAnimLoader.h"
|
||||||
|
|
||||||
|
#include "Utils/Alignment.h"
|
||||||
|
#include "Utils/StreamUtils.h"
|
||||||
|
#include "XAnim/BinaryXAnimCommon.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <format>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
|
using namespace xanim;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
std::expected<CompiledXAnimVersion, std::string> IdentifyVersion(std::istream& stream)
|
||||||
|
{
|
||||||
|
const auto fileVersion = stream::ReadValue<uint16_t>(stream);
|
||||||
|
switch (static_cast<CompiledXAnimVersion>(fileVersion))
|
||||||
|
{
|
||||||
|
case CompiledXAnimVersion::VERSION_17:
|
||||||
|
return CompiledXAnimVersion::VERSION_17;
|
||||||
|
case CompiledXAnimVersion::VERSION_18:
|
||||||
|
return CompiledXAnimVersion::VERSION_18;
|
||||||
|
case CompiledXAnimVersion::VERSION_19:
|
||||||
|
return CompiledXAnimVersion::VERSION_19;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::unexpected(std::format("Version {} is not supported", fileVersion));
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonXQuat ConsumeQuat(std::istream& stream)
|
||||||
|
{
|
||||||
|
CommonXQuat quat{};
|
||||||
|
quat.value[0] = stream::ReadValue<int16_t>(stream);
|
||||||
|
quat.value[1] = stream::ReadValue<int16_t>(stream);
|
||||||
|
quat.value[2] = stream::ReadValue<int16_t>(stream);
|
||||||
|
|
||||||
|
int32_t temp = 0x3FFF0001 - (quat.value[0] * quat.value[0] + quat.value[1] * quat.value[1] + quat.value[2] * quat.value[2]);
|
||||||
|
if (temp <= 0)
|
||||||
|
temp = 0;
|
||||||
|
else
|
||||||
|
temp = static_cast<int32_t>(std::floor(std::sqrt(static_cast<float>(temp)) + 0.5f));
|
||||||
|
|
||||||
|
assert(temp >= std::numeric_limits<int16_t>::min() && temp <= std::numeric_limits<int16_t>::max());
|
||||||
|
quat.value[3] = static_cast<int16_t>(temp);
|
||||||
|
|
||||||
|
return quat;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonXQuat2 ConsumeQuat2(std::istream& stream)
|
||||||
|
{
|
||||||
|
CommonXQuat2 quat2{};
|
||||||
|
quat2.value[0] = stream::ReadValue<int16_t>(stream);
|
||||||
|
|
||||||
|
int32_t temp = 0x3FFF0001 - quat2.value[0] * quat2.value[0];
|
||||||
|
if (temp <= 0)
|
||||||
|
temp = 0;
|
||||||
|
else
|
||||||
|
temp = static_cast<int32_t>(floor(std::sqrt(static_cast<float>(temp)) + 0.5f));
|
||||||
|
|
||||||
|
assert(temp >= std::numeric_limits<int16_t>::min() && temp <= std::numeric_limits<int16_t>::max());
|
||||||
|
quat2.value[1] = static_cast<int16_t>(temp);
|
||||||
|
|
||||||
|
return quat2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlipQuat(CommonXQuat& quat)
|
||||||
|
{
|
||||||
|
quat.value[0] = static_cast<int16_t>(-quat.value[0]);
|
||||||
|
quat.value[1] = static_cast<int16_t>(-quat.value[1]);
|
||||||
|
quat.value[2] = static_cast<int16_t>(-quat.value[2]);
|
||||||
|
quat.value[3] = static_cast<int16_t>(-quat.value[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlipQuat2(CommonXQuat2& quat)
|
||||||
|
{
|
||||||
|
quat.value[0] = static_cast<int16_t>(-quat.value[0]);
|
||||||
|
quat.value[1] = static_cast<int16_t>(-quat.value[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] float DecodeRawTransSize(const float value, const bool smallTrans)
|
||||||
|
{
|
||||||
|
const auto scale = smallTrans ? HALF_TRANS_SIZE_SCALE : FULL_TRANS_SIZE_SCALE;
|
||||||
|
return value * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadIndicesIfNeeded(
|
||||||
|
std::istream& stream, std::vector<uint16_t>& indices, const uint16_t numIndices, const bool useByteIndices, const uint16_t numLoopFrames)
|
||||||
|
{
|
||||||
|
// The raw format omits indices when a track covers every loop frame in order.
|
||||||
|
if (numIndices >= numLoopFrames)
|
||||||
|
{
|
||||||
|
indices.resize(numIndices);
|
||||||
|
std::ranges::iota(indices, 0);
|
||||||
|
}
|
||||||
|
else if (useByteIndices)
|
||||||
|
{
|
||||||
|
indices.reserve(numIndices);
|
||||||
|
for (auto i = 0u; i < numIndices; i++)
|
||||||
|
indices.emplace_back(stream::ReadValue<uint8_t>(stream));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
indices.resize(numIndices);
|
||||||
|
stream::Read(stream, indices.data(), numIndices * sizeof(uint16_t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<std::optional<CommonDeltaQuatTrack>, std::string>
|
||||||
|
LoadDeltaQuatTrack(std::istream& stream, const bool useByteIndices, const uint16_t numLoopFrames)
|
||||||
|
{
|
||||||
|
const auto numQuatIndices = stream::ReadValue<uint16_t>(stream);
|
||||||
|
if (numQuatIndices == 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
CommonDeltaQuatTrack deltaQuatTrack;
|
||||||
|
if (numQuatIndices == 1)
|
||||||
|
{
|
||||||
|
deltaQuatTrack.m_frames.emplace_back(ConsumeQuat(stream));
|
||||||
|
return deltaQuatTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadIndicesIfNeeded(stream, deltaQuatTrack.m_indices, numQuatIndices, useByteIndices, numLoopFrames);
|
||||||
|
|
||||||
|
deltaQuatTrack.m_frames.reserve(numQuatIndices);
|
||||||
|
for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum)
|
||||||
|
{
|
||||||
|
auto& curFrame = deltaQuatTrack.m_frames.emplace_back(ConsumeQuat(stream));
|
||||||
|
|
||||||
|
if (quatIndexNum > 0)
|
||||||
|
{
|
||||||
|
const auto& prevFrame = deltaQuatTrack.m_frames[quatIndexNum - 1];
|
||||||
|
const auto dot = prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] + prevFrame.value[2] * curFrame.value[2]
|
||||||
|
+ prevFrame.value[3] * curFrame.value[3];
|
||||||
|
if (dot < 0)
|
||||||
|
FlipQuat(curFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deltaQuatTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<std::optional<CommonDeltaQuatTrack>, std::string>
|
||||||
|
LoadDeltaQuat2Track(std::istream& stream, const bool useByteIndices, const uint16_t numLoopFrames)
|
||||||
|
{
|
||||||
|
const auto numQuatIndices = stream::ReadValue<uint16_t>(stream);
|
||||||
|
if (numQuatIndices == 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
CommonDeltaQuatTrack deltaQuatTrack;
|
||||||
|
if (numQuatIndices == 1)
|
||||||
|
{
|
||||||
|
deltaQuatTrack.m_frames2.emplace_back(ConsumeQuat2(stream));
|
||||||
|
return deltaQuatTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadIndicesIfNeeded(stream, deltaQuatTrack.m_indices, numQuatIndices, useByteIndices, numLoopFrames);
|
||||||
|
|
||||||
|
deltaQuatTrack.m_frames2.reserve(numQuatIndices);
|
||||||
|
for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum)
|
||||||
|
{
|
||||||
|
auto& curFrame = deltaQuatTrack.m_frames2.emplace_back(ConsumeQuat2(stream));
|
||||||
|
|
||||||
|
if (quatIndexNum > 0)
|
||||||
|
{
|
||||||
|
const auto& prevFrame = deltaQuatTrack.m_frames2[quatIndexNum - 1];
|
||||||
|
if (prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] < 0)
|
||||||
|
FlipQuat2(curFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deltaQuatTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<std::optional<CommonDeltaTransTrack>, std::string>
|
||||||
|
LoadDeltaTransTrack(std::istream& stream, const bool useByteIndices, const uint16_t numLoopFrames)
|
||||||
|
{
|
||||||
|
const auto numTransIndices = stream::ReadValue<uint16_t>(stream);
|
||||||
|
if (numTransIndices == 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
CommonDeltaTransTrack deltaTransTrack;
|
||||||
|
if (numTransIndices == 1)
|
||||||
|
{
|
||||||
|
deltaTransTrack.m_constant.emplace(std::array<float, 3>({
|
||||||
|
stream::ReadValue<float>(stream),
|
||||||
|
stream::ReadValue<float>(stream),
|
||||||
|
stream::ReadValue<float>(stream),
|
||||||
|
}));
|
||||||
|
return deltaTransTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadIndicesIfNeeded(stream, deltaTransTrack.m_indices, numTransIndices, useByteIndices, numLoopFrames);
|
||||||
|
|
||||||
|
const auto smallTrans = stream::ReadValue<bool>(stream);
|
||||||
|
|
||||||
|
deltaTransTrack.m_mins[0] = stream::ReadValue<float>(stream);
|
||||||
|
deltaTransTrack.m_mins[1] = stream::ReadValue<float>(stream);
|
||||||
|
deltaTransTrack.m_mins[2] = stream::ReadValue<float>(stream);
|
||||||
|
|
||||||
|
deltaTransTrack.m_size[0] = DecodeRawTransSize(stream::ReadValue<float>(stream), smallTrans);
|
||||||
|
deltaTransTrack.m_size[1] = DecodeRawTransSize(stream::ReadValue<float>(stream), smallTrans);
|
||||||
|
deltaTransTrack.m_size[2] = DecodeRawTransSize(stream::ReadValue<float>(stream), smallTrans);
|
||||||
|
|
||||||
|
if (smallTrans)
|
||||||
|
{
|
||||||
|
deltaTransTrack.m_frames_u8.resize(numTransIndices);
|
||||||
|
stream::Read(stream, deltaTransTrack.m_frames_u8.data(), numTransIndices * sizeof(CommonVec3U8));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
deltaTransTrack.m_frames_u16.resize(numTransIndices);
|
||||||
|
stream::Read(stream, deltaTransTrack.m_frames_u16.data(), numTransIndices * sizeof(CommonVec3U16));
|
||||||
|
}
|
||||||
|
|
||||||
|
return deltaTransTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<std::unique_ptr<CommonXAnimDeltaTrack>, std::string>
|
||||||
|
LoadDeltaTrack(std::istream& stream, const bool hasDelta3D, const bool useByteIndices, const uint16_t numLoopFrames)
|
||||||
|
{
|
||||||
|
auto delta = std::make_unique<CommonXAnimDeltaTrack>();
|
||||||
|
|
||||||
|
auto maybeLoadedDeltaQuat =
|
||||||
|
hasDelta3D ? LoadDeltaQuatTrack(stream, useByteIndices, numLoopFrames) : LoadDeltaQuat2Track(stream, useByteIndices, numLoopFrames);
|
||||||
|
if (!maybeLoadedDeltaQuat.has_value())
|
||||||
|
return std::unexpected(std::move(maybeLoadedDeltaQuat).error());
|
||||||
|
delta->m_quat = std::move(maybeLoadedDeltaQuat).value();
|
||||||
|
|
||||||
|
auto maybeLoadedDeltaTrans = LoadDeltaTransTrack(stream, useByteIndices, numLoopFrames);
|
||||||
|
if (!maybeLoadedDeltaTrans.has_value())
|
||||||
|
return std::unexpected(std::move(maybeLoadedDeltaTrans).error());
|
||||||
|
delta->m_trans = std::move(maybeLoadedDeltaTrans).value();
|
||||||
|
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
QuatTrack ReadQuatTrack(std::istream& stream, const uint16_t numLoopFrames, const bool useByteIndices, const bool flipQuat, const bool halfQuat)
|
||||||
|
{
|
||||||
|
QuatTrack quatTrack;
|
||||||
|
|
||||||
|
const auto numQuatIndices = stream::ReadValue<uint16_t>(stream);
|
||||||
|
if (numQuatIndices == 0)
|
||||||
|
{
|
||||||
|
assert(halfQuat);
|
||||||
|
quatTrack.m_type = QuatType::NO_QUAT;
|
||||||
|
return quatTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numQuatIndices == 1)
|
||||||
|
{
|
||||||
|
quatTrack.m_type = halfQuat ? QuatType::HALF_QUAT_NO_SIZE : QuatType::FULL_QUAT_NO_SIZE;
|
||||||
|
if (halfQuat)
|
||||||
|
{
|
||||||
|
auto quat2 = ConsumeQuat2(stream);
|
||||||
|
if (flipQuat)
|
||||||
|
FlipQuat2(quat2);
|
||||||
|
|
||||||
|
quatTrack.m_frames2.emplace_back(quat2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto quat = ConsumeQuat(stream);
|
||||||
|
if (flipQuat)
|
||||||
|
FlipQuat(quat);
|
||||||
|
|
||||||
|
quatTrack.m_frames.emplace_back(quat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return quatTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadIndicesIfNeeded(stream, quatTrack.m_indices, numQuatIndices, useByteIndices, numLoopFrames);
|
||||||
|
|
||||||
|
if (halfQuat)
|
||||||
|
{
|
||||||
|
quatTrack.m_type = QuatType::HALF_QUAT;
|
||||||
|
quatTrack.m_frames2.reserve(numQuatIndices);
|
||||||
|
for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; quatIndexNum++)
|
||||||
|
{
|
||||||
|
auto& curFrame = quatTrack.m_frames2.emplace_back(ConsumeQuat2(stream));
|
||||||
|
|
||||||
|
if (quatIndexNum > 0)
|
||||||
|
{
|
||||||
|
const auto& prevFrame = quatTrack.m_frames2[quatIndexNum - 1];
|
||||||
|
if (prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] < 0)
|
||||||
|
FlipQuat2(curFrame);
|
||||||
|
}
|
||||||
|
else if (flipQuat)
|
||||||
|
FlipQuat2(curFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
quatTrack.m_type = QuatType::FULL_QUAT;
|
||||||
|
quatTrack.m_frames.reserve(numQuatIndices);
|
||||||
|
for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; quatIndexNum++)
|
||||||
|
{
|
||||||
|
auto& curFrame = quatTrack.m_frames.emplace_back(ConsumeQuat(stream));
|
||||||
|
|
||||||
|
if (quatIndexNum > 0)
|
||||||
|
{
|
||||||
|
const auto& prevFrame = quatTrack.m_frames[quatIndexNum - 1];
|
||||||
|
const auto dot = prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] + prevFrame.value[2] * curFrame.value[2]
|
||||||
|
+ prevFrame.value[3] * curFrame.value[3];
|
||||||
|
if (dot < 0)
|
||||||
|
FlipQuat(curFrame);
|
||||||
|
}
|
||||||
|
else if (flipQuat)
|
||||||
|
FlipQuat(curFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return quatTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransTrack ReadTransTrack(std::istream& stream, const uint16_t numLoopFrames, const bool useByteIndices)
|
||||||
|
{
|
||||||
|
TransTrack transTrack;
|
||||||
|
|
||||||
|
const auto numTransIndices = stream::ReadValue<uint16_t>(stream);
|
||||||
|
if (numTransIndices == 0)
|
||||||
|
{
|
||||||
|
transTrack.m_type = TransType::NO_TRANS;
|
||||||
|
return transTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numTransIndices == 1)
|
||||||
|
{
|
||||||
|
transTrack.m_type = TransType::TRANS_NO_SIZE;
|
||||||
|
for (auto& value : transTrack.m_constant)
|
||||||
|
value = stream::ReadValue<float>(stream);
|
||||||
|
return transTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadIndicesIfNeeded(stream, transTrack.m_indices, numTransIndices, useByteIndices, numLoopFrames);
|
||||||
|
|
||||||
|
const auto smallTrans = stream::ReadValue<bool>(stream);
|
||||||
|
transTrack.m_type = smallTrans ? TransType::SMALL_TRANS : TransType::FULL_TRANS;
|
||||||
|
|
||||||
|
for (auto& value : transTrack.m_mins)
|
||||||
|
value = stream::ReadValue<float>(stream);
|
||||||
|
for (auto& value : transTrack.m_size)
|
||||||
|
value = DecodeRawTransSize(stream::ReadValue<float>(stream), smallTrans);
|
||||||
|
|
||||||
|
if (smallTrans)
|
||||||
|
{
|
||||||
|
static_assert(sizeof(decltype(transTrack.m_frames_u8)::value_type) == sizeof(uint8_t) * 3u);
|
||||||
|
transTrack.m_frames_u8.resize(numTransIndices);
|
||||||
|
stream::Read(stream, transTrack.m_frames_u8.data(), numTransIndices * sizeof(uint8_t) * 3);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
static_assert(sizeof(decltype(transTrack.m_frames_u16)::value_type) == sizeof(int16_t) * 3u);
|
||||||
|
transTrack.m_frames_u16.resize(numTransIndices);
|
||||||
|
stream::Read(stream, transTrack.m_frames_u16.data(), numTransIndices * sizeof(uint16_t) * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReadNoteTracks(std::istream& stream, CommonXAnimParts& parts)
|
||||||
|
{
|
||||||
|
const auto numNoteTracks = stream::ReadValue<uint8_t>(stream);
|
||||||
|
|
||||||
|
parts.m_notifies.reserve(numNoteTracks + 1);
|
||||||
|
for (auto notifyIndex = 0u; notifyIndex < numNoteTracks; notifyIndex++)
|
||||||
|
{
|
||||||
|
const auto notifyName = stream::ReadCString(stream);
|
||||||
|
|
||||||
|
const auto frame = stream::ReadValue<uint16_t>(stream);
|
||||||
|
const auto time = parts.m_num_frames > 0 ? static_cast<float>(frame) / static_cast<float>(parts.m_num_frames) : 0;
|
||||||
|
assert(time >= 0.0f && time <= 1.0f);
|
||||||
|
parts.m_notifies.emplace_back(notifyName, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This notify is always automatically added
|
||||||
|
parts.m_notifies.emplace_back("end", 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsLooped(const uint8_t flags, const CompiledXAnimVersion version)
|
||||||
|
{
|
||||||
|
switch (version)
|
||||||
|
{
|
||||||
|
case CompiledXAnimVersion::VERSION_17:
|
||||||
|
return (flags & binary17::FLAG_LOOPED) > 0;
|
||||||
|
case CompiledXAnimVersion::VERSION_18:
|
||||||
|
return (flags & binary18::FLAG_LOOPED) > 0;
|
||||||
|
case CompiledXAnimVersion::VERSION_19:
|
||||||
|
return (flags & binary19::FLAG_LOOPED) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasDelta(const uint8_t flags, const CompiledXAnimVersion version)
|
||||||
|
{
|
||||||
|
switch (version)
|
||||||
|
{
|
||||||
|
case CompiledXAnimVersion::VERSION_17:
|
||||||
|
return (flags & binary17::FLAG_DELTA) > 0;
|
||||||
|
case CompiledXAnimVersion::VERSION_18:
|
||||||
|
return (flags & binary18::FLAG_DELTA) > 0;
|
||||||
|
case CompiledXAnimVersion::VERSION_19:
|
||||||
|
return (flags & binary19::FLAG_DELTA) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasDelta3D(const uint8_t flags, const CompiledXAnimVersion version)
|
||||||
|
{
|
||||||
|
switch (version)
|
||||||
|
{
|
||||||
|
case CompiledXAnimVersion::VERSION_18:
|
||||||
|
return (flags & binary18::FLAG_DELTA_3D) > 0;
|
||||||
|
case CompiledXAnimVersion::VERSION_19:
|
||||||
|
return (flags & binary19::FLAG_T6_COMPATIBILITY) > 0 && (flags & binary19::FLAG_T6_DELTA_3D) > 0;
|
||||||
|
case CompiledXAnimVersion::VERSION_17:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsLeftHandGripIk(const uint8_t flags, const CompiledXAnimVersion version)
|
||||||
|
{
|
||||||
|
switch (version)
|
||||||
|
{
|
||||||
|
case CompiledXAnimVersion::VERSION_19:
|
||||||
|
if (flags & binary19::FLAG_T6_COMPATIBILITY)
|
||||||
|
return (flags & binary19::FLAG_T6_LEFT_HAND_GRIP_IK) > 0;
|
||||||
|
return (flags & binary19::FLAG_T5_LEFT_HAND_GRIP_IK) > 0;
|
||||||
|
case CompiledXAnimVersion::VERSION_17:
|
||||||
|
case CompiledXAnimVersion::VERSION_18:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsStreamable(const uint8_t flags, const CompiledXAnimVersion version)
|
||||||
|
{
|
||||||
|
switch (version)
|
||||||
|
{
|
||||||
|
case CompiledXAnimVersion::VERSION_19:
|
||||||
|
if (flags & binary19::FLAG_T6_COMPATIBILITY)
|
||||||
|
return false;
|
||||||
|
return (flags & binary19::FLAG_T5_STREAMABLE) > 0;
|
||||||
|
|
||||||
|
case CompiledXAnimVersion::VERSION_17:
|
||||||
|
case CompiledXAnimVersion::VERSION_18:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace xanim
|
||||||
|
{
|
||||||
|
std::expected<std::unique_ptr<CommonXAnimParts>, std::string> LoadCompiledXAnim(std::istream& stream)
|
||||||
|
{
|
||||||
|
auto maybeVersion = IdentifyVersion(stream);
|
||||||
|
if (!maybeVersion)
|
||||||
|
return std::unexpected(std::move(maybeVersion).error());
|
||||||
|
|
||||||
|
auto parts = std::make_unique<CommonXAnimParts>();
|
||||||
|
|
||||||
|
const auto version = maybeVersion.value();
|
||||||
|
|
||||||
|
const auto numFrames = stream::ReadValue<uint16_t>(stream);
|
||||||
|
const auto boneCount = stream::ReadValue<uint16_t>(stream);
|
||||||
|
const auto flags = stream::ReadValue<uint8_t>(stream);
|
||||||
|
const auto assetType = stream::ReadValue<uint8_t>(stream);
|
||||||
|
const auto framerate = stream::ReadValue<uint16_t>(stream);
|
||||||
|
if (stream.fail())
|
||||||
|
return std::unexpected("Truncated file");
|
||||||
|
|
||||||
|
const bool isLooped = IsLooped(flags, version);
|
||||||
|
const bool hasDelta = HasDelta(flags, version);
|
||||||
|
const bool hasDelta3D = HasDelta3D(flags, version);
|
||||||
|
const bool leftHandGripIk = IsLeftHandGripIk(flags, version);
|
||||||
|
const bool streamable = IsStreamable(flags, version);
|
||||||
|
const uint16_t numLoopFrames = isLooped ? numFrames + 1u : numFrames;
|
||||||
|
|
||||||
|
parts->m_num_frames = numLoopFrames - 1;
|
||||||
|
parts->m_looped = isLooped;
|
||||||
|
parts->m_left_hand_grip_ik = leftHandGripIk;
|
||||||
|
parts->m_streamable = streamable;
|
||||||
|
parts->m_asset_type = assetType;
|
||||||
|
parts->m_frame_rate = static_cast<float>(framerate);
|
||||||
|
|
||||||
|
if (version == CompiledXAnimVersion::VERSION_19 && streamable)
|
||||||
|
parts->m_primed_length = stream::ReadValue<float>(stream);
|
||||||
|
|
||||||
|
const auto useByteIndices = parts->m_num_frames < 256;
|
||||||
|
|
||||||
|
if (hasDelta || hasDelta3D)
|
||||||
|
{
|
||||||
|
auto maybeBoneTrack = LoadDeltaTrack(stream, hasDelta3D, useByteIndices, numLoopFrames);
|
||||||
|
if (!maybeBoneTrack.has_value())
|
||||||
|
return std::unexpected(std::move(maybeBoneTrack).error());
|
||||||
|
|
||||||
|
parts->m_delta_track = std::move(maybeBoneTrack).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boneCount > 0)
|
||||||
|
{
|
||||||
|
const auto bitmaskSize = utils::Align<size_t>(boneCount, 8u) / 8u;
|
||||||
|
std::vector<uint8_t> flipQuatBits(bitmaskSize, 0);
|
||||||
|
std::vector<uint8_t> halfQuatBits(bitmaskSize, 0);
|
||||||
|
stream::Read(stream, flipQuatBits.data(), bitmaskSize);
|
||||||
|
stream::Read(stream, halfQuatBits.data(), bitmaskSize);
|
||||||
|
|
||||||
|
parts->m_bone_tracks.resize(boneCount);
|
||||||
|
for (size_t boneIndex = 0; boneIndex < boneCount; ++boneIndex)
|
||||||
|
parts->m_bone_tracks[boneIndex].m_name = stream::ReadCString(stream);
|
||||||
|
|
||||||
|
for (size_t boneIndex = 0; boneIndex < boneCount; ++boneIndex)
|
||||||
|
{
|
||||||
|
auto& boneTrack = parts->m_bone_tracks[boneIndex];
|
||||||
|
|
||||||
|
const bool flipQuat = flipQuatBits[boneIndex / 8u] & static_cast<uint8_t>(1u << (boneIndex % 8u));
|
||||||
|
const bool halfQuat = halfQuatBits[boneIndex / 8u] & static_cast<uint8_t>(1u << (boneIndex % 8u));
|
||||||
|
|
||||||
|
boneTrack.m_quat = ReadQuatTrack(stream, numLoopFrames, useByteIndices, flipQuat, halfQuat);
|
||||||
|
boneTrack.m_trans = ReadTransTrack(stream, numLoopFrames, useByteIndices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts->SortBoneTracksForQuats();
|
||||||
|
|
||||||
|
ReadNoteTracks(stream, *parts);
|
||||||
|
|
||||||
|
assert(stream.peek() == std::char_traits<char>::eof());
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
} // namespace xanim
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "XAnim/XAnimCommon.h"
|
||||||
|
|
||||||
|
#include <expected>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace xanim
|
||||||
|
{
|
||||||
|
std::expected<std::unique_ptr<CommonXAnimParts>, std::string> LoadCompiledXAnim(std::istream& stream);
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
#include "FlatXAnimDataWriter.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
|
using namespace xanim;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
[[nodiscard]] int FloatBitsToInt(const float value)
|
||||||
|
{
|
||||||
|
union
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
float f;
|
||||||
|
};
|
||||||
|
|
||||||
|
f = value;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteFloat3(FlatData& writeCursor, const std::array<float, 3>& value)
|
||||||
|
{
|
||||||
|
for (const float f : value)
|
||||||
|
writeCursor.m_data_int.emplace_back(FloatBitsToInt(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WritePackedIndices(FlatData& writeCursor, const std::vector<uint16_t>& indices, const bool useByteIndices)
|
||||||
|
{
|
||||||
|
const auto indexCount = indices.size();
|
||||||
|
writeCursor.m_data_short.emplace_back(static_cast<int16_t>(indexCount - 1)); // storedSize
|
||||||
|
|
||||||
|
if (useByteIndices)
|
||||||
|
{
|
||||||
|
for (const auto index : indices)
|
||||||
|
{
|
||||||
|
assert(index <= std::numeric_limits<uint8_t>::max());
|
||||||
|
writeCursor.m_data_byte.emplace_back(static_cast<uint8_t>(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (indexCount >= 65)
|
||||||
|
{
|
||||||
|
// 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.
|
||||||
|
std::ranges::copy(indices, std::back_inserter(writeCursor.m_indices));
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
const auto longTableSize = ((indexCount - 2) / 256u) + 1;
|
||||||
|
for (auto i = 0u; i < longTableSize; i++)
|
||||||
|
writeCursor.m_data_short.emplace_back(indices[256 * i]);
|
||||||
|
writeCursor.m_data_short.emplace_back(indices[indices.size() - 1]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::ranges::copy(indices, std::back_inserter(writeCursor.m_data_short));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessQuatTrack(FlatData& writeCursor, const QuatTrack& quatTrack, const bool useByteIndices)
|
||||||
|
{
|
||||||
|
switch (quatTrack.m_type)
|
||||||
|
{
|
||||||
|
case QuatType::NO_QUAT:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QuatType::HALF_QUAT:
|
||||||
|
WritePackedIndices(writeCursor, quatTrack.m_indices, useByteIndices);
|
||||||
|
assert(quatTrack.m_frames2.size() == quatTrack.m_indices.size());
|
||||||
|
|
||||||
|
writeCursor.m_random_data_short.reserve(writeCursor.m_random_data_short.size() + quatTrack.m_frames2.size() * 2);
|
||||||
|
for (const auto& quat2 : quatTrack.m_frames2)
|
||||||
|
{
|
||||||
|
writeCursor.m_random_data_short.emplace_back(quat2.value[0]);
|
||||||
|
writeCursor.m_random_data_short.emplace_back(quat2.value[1]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QuatType::FULL_QUAT:
|
||||||
|
WritePackedIndices(writeCursor, quatTrack.m_indices, useByteIndices);
|
||||||
|
assert(quatTrack.m_frames.size() == quatTrack.m_indices.size());
|
||||||
|
|
||||||
|
writeCursor.m_random_data_short.reserve(writeCursor.m_random_data_short.size() + quatTrack.m_frames.size() * 4);
|
||||||
|
for (const auto& quat : quatTrack.m_frames)
|
||||||
|
{
|
||||||
|
writeCursor.m_random_data_short.emplace_back(quat.value[0]);
|
||||||
|
writeCursor.m_random_data_short.emplace_back(quat.value[1]);
|
||||||
|
writeCursor.m_random_data_short.emplace_back(quat.value[2]);
|
||||||
|
writeCursor.m_random_data_short.emplace_back(quat.value[3]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QuatType::HALF_QUAT_NO_SIZE:
|
||||||
|
{
|
||||||
|
assert(quatTrack.m_frames2.size() == 1);
|
||||||
|
writeCursor.m_data_short.reserve(writeCursor.m_data_short.size() + 2);
|
||||||
|
|
||||||
|
const auto& quat2 = quatTrack.m_frames2[0];
|
||||||
|
writeCursor.m_data_short.emplace_back(quat2.value[0]);
|
||||||
|
writeCursor.m_data_short.emplace_back(quat2.value[1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case QuatType::FULL_QUAT_NO_SIZE:
|
||||||
|
{
|
||||||
|
assert(quatTrack.m_frames.size() == 1);
|
||||||
|
writeCursor.m_data_short.reserve(writeCursor.m_data_short.size() + 4);
|
||||||
|
|
||||||
|
const auto& quat = quatTrack.m_frames[0];
|
||||||
|
writeCursor.m_data_short.emplace_back(quat.value[0]);
|
||||||
|
writeCursor.m_data_short.emplace_back(quat.value[1]);
|
||||||
|
writeCursor.m_data_short.emplace_back(quat.value[2]);
|
||||||
|
writeCursor.m_data_short.emplace_back(quat.value[3]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessTransTrack(FlatData& writeCursor, const TransTrack& transTrack, const size_t boneIndex, const bool useByteIndices)
|
||||||
|
{
|
||||||
|
assert(boneIndex <= std::numeric_limits<uint8_t>::max());
|
||||||
|
writeCursor.m_data_byte.emplace_back(static_cast<uint8_t>(boneIndex));
|
||||||
|
|
||||||
|
switch (transTrack.m_type)
|
||||||
|
{
|
||||||
|
case TransType::SMALL_TRANS:
|
||||||
|
WritePackedIndices(writeCursor, transTrack.m_indices, useByteIndices);
|
||||||
|
WriteFloat3(writeCursor, transTrack.m_mins);
|
||||||
|
WriteFloat3(writeCursor, transTrack.m_size);
|
||||||
|
assert(transTrack.m_frames_u8.size() == transTrack.m_indices.size());
|
||||||
|
|
||||||
|
writeCursor.m_random_data_byte.reserve(writeCursor.m_random_data_byte.size() + transTrack.m_frames_u8.size() * 3);
|
||||||
|
for (const auto& vec : transTrack.m_frames_u8)
|
||||||
|
{
|
||||||
|
writeCursor.m_random_data_byte.emplace_back(vec.value[0]);
|
||||||
|
writeCursor.m_random_data_byte.emplace_back(vec.value[1]);
|
||||||
|
writeCursor.m_random_data_byte.emplace_back(vec.value[2]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TransType::FULL_TRANS:
|
||||||
|
WritePackedIndices(writeCursor, transTrack.m_indices, useByteIndices);
|
||||||
|
WriteFloat3(writeCursor, transTrack.m_mins);
|
||||||
|
WriteFloat3(writeCursor, transTrack.m_size);
|
||||||
|
assert(transTrack.m_frames_u16.size() == transTrack.m_indices.size());
|
||||||
|
|
||||||
|
writeCursor.m_random_data_short.reserve(writeCursor.m_random_data_short.size() + transTrack.m_frames_u16.size() * 3);
|
||||||
|
for (const auto& vec : transTrack.m_frames_u16)
|
||||||
|
{
|
||||||
|
writeCursor.m_random_data_short.emplace_back(vec.value[0]);
|
||||||
|
writeCursor.m_random_data_short.emplace_back(vec.value[1]);
|
||||||
|
writeCursor.m_random_data_short.emplace_back(vec.value[2]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TransType::TRANS_NO_SIZE:
|
||||||
|
WriteFloat3(writeCursor, transTrack.m_constant);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TransType::NO_TRANS:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace xanim
|
||||||
|
{
|
||||||
|
FlatData CreateFlatDataFromCommonXAnim(const CommonXAnimParts& parts)
|
||||||
|
{
|
||||||
|
FlatData writeCursor;
|
||||||
|
|
||||||
|
const auto useByteIndices = parts.m_num_frames < 256;
|
||||||
|
|
||||||
|
for (const auto& boneTrack : parts.m_bone_tracks)
|
||||||
|
ProcessQuatTrack(writeCursor, boneTrack.m_quat, useByteIndices);
|
||||||
|
|
||||||
|
const auto transBoneOrder = parts.GetBoneTrackOrderForTrans();
|
||||||
|
const auto boneCount = transBoneOrder.size();
|
||||||
|
for (size_t i = 0; i < boneCount; ++i)
|
||||||
|
{
|
||||||
|
const auto boneIndex = transBoneOrder[i];
|
||||||
|
ProcessTransTrack(writeCursor, parts.m_bone_tracks[boneIndex].m_trans, boneIndex, useByteIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeCursor;
|
||||||
|
}
|
||||||
|
} // namespace xanim
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "XAnim/XAnimCommon.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace xanim
|
||||||
|
{
|
||||||
|
class FlatData
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::vector<uint8_t> m_data_byte;
|
||||||
|
std::vector<int16_t> m_data_short;
|
||||||
|
std::vector<int32_t> m_data_int;
|
||||||
|
std::vector<uint8_t> m_random_data_byte;
|
||||||
|
std::vector<int16_t> m_random_data_short;
|
||||||
|
std::vector<uint16_t> m_indices;
|
||||||
|
};
|
||||||
|
|
||||||
|
FlatData CreateFlatDataFromCommonXAnim(const CommonXAnimParts& parts);
|
||||||
|
} // namespace xanim
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
#options GAME(IW3, IW4, IW5, T5, T6)
|
||||||
|
|
||||||
|
#filename "Game/" + GAME + "/XAnim/XAnimLoader" + GAME + ".cpp"
|
||||||
|
|
||||||
|
#set LOADER_HEADER "\"XAnimLoader" + GAME + ".h\""
|
||||||
|
|
||||||
|
#if GAME == "IW3"
|
||||||
|
#define FEATURE_IW3
|
||||||
|
#elif GAME == "IW4"
|
||||||
|
#define FEATURE_IW4
|
||||||
|
#define HAS_DELTA_QUAT_3D
|
||||||
|
#elif GAME == "IW5"
|
||||||
|
#define FEATURE_IW5
|
||||||
|
#define HAS_DELTA_QUAT_3D
|
||||||
|
#elif GAME == "T5"
|
||||||
|
#define FEATURE_T5
|
||||||
|
#elif GAME == "T6"
|
||||||
|
#define FEATURE_T6
|
||||||
|
#define HAS_DELTA_QUAT_3D
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(FEATURE_IW4) || defined(FEATURE_IW5)
|
||||||
|
#define SET_IS_LOOPED() parts.flags |= ANIM_LOOP
|
||||||
|
#define SET_HAS_DELTA() parts.flags |= ANIM_DELTA
|
||||||
|
#define SET_HAS_DELTA_3D() parts.flags |= ANIM_DELTA_3D
|
||||||
|
#else
|
||||||
|
#define SET_IS_LOOPED() parts.bLoop = true
|
||||||
|
#define SET_HAS_DELTA() parts.bDelta = true
|
||||||
|
#define SET_HAS_DELTA_3D() parts.bDelta3D = true
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// This file was templated.
|
||||||
|
// See XAnimLoader.cpp.template.
|
||||||
|
// Do not modify, changes will be lost.
|
||||||
|
|
||||||
|
#include LOADER_HEADER
|
||||||
|
|
||||||
|
#include "Utils/Logging/Log.h"
|
||||||
|
#include "XAnim/CompiledXAnimLoader.h"
|
||||||
|
#include "XAnim/FlatXAnimDataWriter.h"
|
||||||
|
#include "XAnim/XAnimCommon.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <expected>
|
||||||
|
#include <format>
|
||||||
|
#include <limits>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace GAME;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
void ConvertNoteTracks(XAnimParts& parts,
|
||||||
|
const xanim::CommonXAnimParts& commonParts,
|
||||||
|
AssetRegistration<AssetXAnim>& registration,
|
||||||
|
MemoryManager& memory,
|
||||||
|
ZoneScriptStrings& scriptStrings)
|
||||||
|
{
|
||||||
|
if (commonParts.m_notifies.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto numCommonNoteTracks = commonParts.m_notifies.size();
|
||||||
|
const auto numNoteTracks = static_cast<uint8_t>(std::min<size_t>(numCommonNoteTracks, std::numeric_limits<uint8_t>::max()));
|
||||||
|
|
||||||
|
if (numNoteTracks < numCommonNoteTracks)
|
||||||
|
con::error("XAnim {}: Could only fit {} of {} notetracks", parts.name, numNoteTracks, numCommonNoteTracks);
|
||||||
|
|
||||||
|
parts.notifyCount = numNoteTracks;
|
||||||
|
parts.notify = memory.Alloc<XAnimNotifyInfo>(numNoteTracks);
|
||||||
|
|
||||||
|
for (auto notifyIndex = 0u; notifyIndex < numCommonNoteTracks; notifyIndex++)
|
||||||
|
{
|
||||||
|
const auto& commonNotify = commonParts.m_notifies[notifyIndex];
|
||||||
|
auto& notify = parts.notify[notifyIndex];
|
||||||
|
|
||||||
|
notify.name = scriptStrings.AddOrGetScriptString(commonNotify.m_name);
|
||||||
|
registration.AddScriptString(notify.name);
|
||||||
|
|
||||||
|
notify.time = commonNotify.m_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(FEATURE_T5) || defined(FEATURE_T6)
|
||||||
|
const auto loopBegin = std::ranges::find_if(commonParts.m_notifies, [](const xanim::CommonXAnimNotifyInfo& notify)
|
||||||
|
{
|
||||||
|
return notify.m_name == "loop_begin";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loopBegin != commonParts.m_notifies.end())
|
||||||
|
parts.loopEntryTime = loopBegin->m_time;
|
||||||
|
else
|
||||||
|
parts.loopEntryTime = 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T> void ConvertIndices(T& indices, const std::vector<uint16_t>& commonIndices, const bool useByteIndices)
|
||||||
|
{
|
||||||
|
if (useByteIndices)
|
||||||
|
{
|
||||||
|
const auto numIndices = commonIndices.size();
|
||||||
|
for (size_t i = 0u; i < numIndices; i++)
|
||||||
|
{
|
||||||
|
assert(commonIndices[i] <= std::numeric_limits<uint8_t>::max());
|
||||||
|
indices._1[i] = static_cast<uint8_t>(commonIndices[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::memcpy(indices._2, commonIndices.data(), commonIndices.size() * sizeof(uint16_t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CountBoneTrackTypes(XAnimParts& parts, const xanim::BoneTrack& boneTrack)
|
||||||
|
{
|
||||||
|
switch (boneTrack.m_quat.m_type)
|
||||||
|
{
|
||||||
|
case xanim::QuatType::NO_QUAT:
|
||||||
|
parts.boneCount[PART_TYPE_NO_QUAT]++;
|
||||||
|
break;
|
||||||
|
case xanim::QuatType::HALF_QUAT:
|
||||||
|
parts.boneCount[PART_TYPE_HALF_QUAT]++;
|
||||||
|
break;
|
||||||
|
case xanim::QuatType::FULL_QUAT:
|
||||||
|
parts.boneCount[PART_TYPE_FULL_QUAT]++;
|
||||||
|
break;
|
||||||
|
case xanim::QuatType::HALF_QUAT_NO_SIZE:
|
||||||
|
parts.boneCount[PART_TYPE_HALF_QUAT_NO_SIZE]++;
|
||||||
|
break;
|
||||||
|
case xanim::QuatType::FULL_QUAT_NO_SIZE:
|
||||||
|
parts.boneCount[PART_TYPE_FULL_QUAT_NO_SIZE]++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (boneTrack.m_trans.m_type)
|
||||||
|
{
|
||||||
|
case xanim::TransType::SMALL_TRANS:
|
||||||
|
parts.boneCount[PART_TYPE_SMALL_TRANS]++;
|
||||||
|
break;
|
||||||
|
case xanim::TransType::FULL_TRANS:
|
||||||
|
parts.boneCount[PART_TYPE_TRANS]++;
|
||||||
|
break;
|
||||||
|
case xanim::TransType::TRANS_NO_SIZE:
|
||||||
|
parts.boneCount[PART_TYPE_TRANS_NO_SIZE]++;
|
||||||
|
break;
|
||||||
|
case xanim::TransType::NO_TRANS:
|
||||||
|
parts.boneCount[PART_TYPE_NO_TRANS]++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConvertFlatData(XAnimParts& parts, const xanim::FlatData& flatData, MemoryManager& memory)
|
||||||
|
{
|
||||||
|
if (!flatData.m_data_byte.empty())
|
||||||
|
{
|
||||||
|
parts.dataByteCount = static_cast<uint16_t>(flatData.m_data_byte.size());
|
||||||
|
parts.dataByte = memory.Alloc<uint8_t>(parts.dataByteCount);
|
||||||
|
std::memcpy(parts.dataByte, flatData.m_data_byte.data(), parts.dataByteCount * sizeof(uint8_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flatData.m_data_short.empty())
|
||||||
|
{
|
||||||
|
parts.dataShortCount = static_cast<uint16_t>(flatData.m_data_short.size());
|
||||||
|
parts.dataShort = memory.Alloc<int16_t>(parts.dataShortCount);
|
||||||
|
std::memcpy(parts.dataShort, flatData.m_data_short.data(), parts.dataShortCount * sizeof(int16_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flatData.m_data_int.empty())
|
||||||
|
{
|
||||||
|
parts.dataIntCount = static_cast<uint16_t>(flatData.m_data_int.size());
|
||||||
|
parts.dataInt = memory.Alloc<int32_t>(parts.dataIntCount);
|
||||||
|
std::memcpy(parts.dataInt, flatData.m_data_int.data(), parts.dataIntCount * sizeof(int32_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flatData.m_random_data_byte.empty())
|
||||||
|
{
|
||||||
|
parts.randomDataByteCount = static_cast<uint16_t>(flatData.m_random_data_byte.size());
|
||||||
|
parts.randomDataByte = memory.Alloc<uint8_t>(parts.randomDataByteCount);
|
||||||
|
std::memcpy(parts.randomDataByte, flatData.m_random_data_byte.data(), parts.randomDataByteCount * sizeof(uint8_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flatData.m_random_data_short.empty())
|
||||||
|
{
|
||||||
|
parts.randomDataShortCount = static_cast<unsigned int>(flatData.m_random_data_short.size());
|
||||||
|
parts.randomDataShort = memory.Alloc<int16_t>(parts.randomDataShortCount);
|
||||||
|
std::memcpy(parts.randomDataShort, flatData.m_random_data_short.data(), parts.randomDataShortCount * sizeof(int16_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flatData.m_indices.empty())
|
||||||
|
{
|
||||||
|
parts.indexCount = static_cast<unsigned int>(flatData.m_indices.size());
|
||||||
|
parts.indices._2 = memory.Alloc<uint16_t>(parts.indexCount);
|
||||||
|
std::memcpy(parts.indices._2, flatData.m_indices.data(), parts.indexCount * sizeof(uint16_t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAS_DELTA_QUAT_3D
|
||||||
|
#define DELTA_QUAT_2D_MEMBER quat2
|
||||||
|
#define DELTA_QUAT_2D_STRUCT XAnimDeltaPartQuat2
|
||||||
|
#define DELTA_QUAT_2D_DATA_STRUCT XAnimDeltaPartQuatData2
|
||||||
|
#define DELTA_QUAT_2D_FRAMES_STRUCT XAnimDeltaPartQuatDataFrames2
|
||||||
|
#else
|
||||||
|
#define DELTA_QUAT_2D_MEMBER quat
|
||||||
|
#define DELTA_QUAT_2D_STRUCT XAnimDeltaPartQuat
|
||||||
|
#define DELTA_QUAT_2D_DATA_STRUCT XAnimDeltaPartQuatData
|
||||||
|
#define DELTA_QUAT_2D_FRAMES_STRUCT XAnimDeltaPartQuatDataFrames
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAS_DELTA_QUAT_3D
|
||||||
|
void ConvertCommonDeltaQuatPart(XAnimDeltaPart& deltaPart,
|
||||||
|
const xanim::CommonDeltaQuatTrack& commonDeltaQuatTrack,
|
||||||
|
MemoryManager& memory,
|
||||||
|
const bool useByteIndices)
|
||||||
|
{
|
||||||
|
if (commonDeltaQuatTrack.m_frames.size() == 1)
|
||||||
|
{
|
||||||
|
deltaPart.quat = static_cast<XAnimDeltaPartQuat*>(memory.AllocRaw(offsetof(XAnimDeltaPartQuat, u) + sizeof(XAnimDeltaPartQuatData::frame0)));
|
||||||
|
deltaPart.quat->size = 0;
|
||||||
|
|
||||||
|
const auto& commonFrame = commonDeltaQuatTrack.m_frames[0];
|
||||||
|
auto& frame = deltaPart.quat->u.frame0;
|
||||||
|
|
||||||
|
frame.value[0] = commonFrame.value[0];
|
||||||
|
frame.value[1] = commonFrame.value[1];
|
||||||
|
frame.value[2] = commonFrame.value[2];
|
||||||
|
frame.value[3] = commonFrame.value[3];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto numQuatIndices = commonDeltaQuatTrack.m_indices.size();
|
||||||
|
const auto indicesArraySize =
|
||||||
|
useByteIndices ? numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_1) : numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_2);
|
||||||
|
|
||||||
|
deltaPart.quat = static_cast<XAnimDeltaPartQuat*>(
|
||||||
|
memory.AllocRaw(offsetof(XAnimDeltaPartQuat, u) + offsetof(XAnimDeltaPartQuatDataFrames, indices) + indicesArraySize));
|
||||||
|
|
||||||
|
auto& frames = deltaPart.quat->u.frames;
|
||||||
|
ConvertIndices(frames.indices, commonDeltaQuatTrack.m_indices, useByteIndices);
|
||||||
|
|
||||||
|
deltaPart.quat->size = static_cast<uint16_t>(numQuatIndices - 1);
|
||||||
|
deltaPart.quat->u.frames.frames = memory.Alloc<XQuat>(numQuatIndices);
|
||||||
|
|
||||||
|
for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum)
|
||||||
|
{
|
||||||
|
const auto& commonFrame = commonDeltaQuatTrack.m_frames[quatIndexNum];
|
||||||
|
auto& curFrame = deltaPart.quat->u.frames.frames[quatIndexNum];
|
||||||
|
|
||||||
|
curFrame.value[0] = commonFrame.value[0];
|
||||||
|
curFrame.value[1] = commonFrame.value[1];
|
||||||
|
curFrame.value[2] = commonFrame.value[2];
|
||||||
|
curFrame.value[3] = commonFrame.value[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void ConvertCommonDeltaQuat2Part(XAnimDeltaPart& deltaPart,
|
||||||
|
const xanim::CommonDeltaQuatTrack& commonDeltaQuatTrack,
|
||||||
|
MemoryManager& memory,
|
||||||
|
const bool useByteIndices)
|
||||||
|
{
|
||||||
|
if (commonDeltaQuatTrack.m_frames2.size() == 1)
|
||||||
|
{
|
||||||
|
deltaPart.DELTA_QUAT_2D_MEMBER = static_cast<DELTA_QUAT_2D_STRUCT*>(memory.AllocRaw(offsetof(DELTA_QUAT_2D_STRUCT, u) + sizeof(DELTA_QUAT_2D_DATA_STRUCT::frame0)));
|
||||||
|
deltaPart.DELTA_QUAT_2D_MEMBER->size = 0;
|
||||||
|
|
||||||
|
const auto& commonFrame = commonDeltaQuatTrack.m_frames2[0];
|
||||||
|
auto& frame = deltaPart.DELTA_QUAT_2D_MEMBER->u.frame0;
|
||||||
|
|
||||||
|
frame.value[0] = commonFrame.value[0];
|
||||||
|
frame.value[1] = commonFrame.value[1];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto numQuatIndices = commonDeltaQuatTrack.m_indices.size();
|
||||||
|
const auto indicesArraySize =
|
||||||
|
useByteIndices ? numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_1) : numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_2);
|
||||||
|
|
||||||
|
deltaPart.DELTA_QUAT_2D_MEMBER = static_cast<DELTA_QUAT_2D_STRUCT*>(
|
||||||
|
memory.AllocRaw(offsetof(DELTA_QUAT_2D_STRUCT, u) + offsetof(DELTA_QUAT_2D_FRAMES_STRUCT, indices) + indicesArraySize));
|
||||||
|
|
||||||
|
auto& frames = deltaPart.DELTA_QUAT_2D_MEMBER->u.frames;
|
||||||
|
ConvertIndices(frames.indices, commonDeltaQuatTrack.m_indices, useByteIndices);
|
||||||
|
|
||||||
|
deltaPart.DELTA_QUAT_2D_MEMBER->size = static_cast<uint16_t>(numQuatIndices - 1);
|
||||||
|
deltaPart.DELTA_QUAT_2D_MEMBER->u.frames.frames = memory.Alloc<XQuat2>(numQuatIndices);
|
||||||
|
|
||||||
|
for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum)
|
||||||
|
{
|
||||||
|
const auto& commonFrame = commonDeltaQuatTrack.m_frames2[quatIndexNum];
|
||||||
|
auto& curFrame = deltaPart.DELTA_QUAT_2D_MEMBER->u.frames.frames[quatIndexNum];
|
||||||
|
|
||||||
|
curFrame.value[0] = commonFrame.value[0];
|
||||||
|
curFrame.value[1] = commonFrame.value[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConvertCommonDeltaTransPart(XAnimDeltaPart& deltaPart,
|
||||||
|
const xanim::CommonDeltaTransTrack& commonDeltaTransTrack,
|
||||||
|
MemoryManager& memory,
|
||||||
|
const bool useByteIndices)
|
||||||
|
{
|
||||||
|
if (commonDeltaTransTrack.m_constant)
|
||||||
|
{
|
||||||
|
deltaPart.trans = static_cast<XAnimPartTrans*>(memory.AllocRaw(offsetof(XAnimPartTrans, u) + sizeof(XAnimPartTransData::frame0)));
|
||||||
|
deltaPart.trans->size = 0;
|
||||||
|
deltaPart.trans->u.frame0.x = (*commonDeltaTransTrack.m_constant)[0];
|
||||||
|
deltaPart.trans->u.frame0.y = (*commonDeltaTransTrack.m_constant)[1];
|
||||||
|
deltaPart.trans->u.frame0.z = (*commonDeltaTransTrack.m_constant)[2];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto numTransIndices = commonDeltaTransTrack.m_indices.size();
|
||||||
|
const auto indicesArraySize =
|
||||||
|
useByteIndices ? numTransIndices * sizeof(XAnimDynamicIndicesTrans::_1) : numTransIndices * sizeof(XAnimDynamicIndicesTrans::_2);
|
||||||
|
|
||||||
|
deltaPart.trans =
|
||||||
|
static_cast<XAnimPartTrans*>(memory.AllocRaw(offsetof(XAnimPartTrans, u) + offsetof(XAnimPartTransFrames, indices) + indicesArraySize));
|
||||||
|
|
||||||
|
auto& frames = deltaPart.trans->u.frames;
|
||||||
|
ConvertIndices(frames.indices, commonDeltaTransTrack.m_indices, useByteIndices);
|
||||||
|
|
||||||
|
deltaPart.trans->size = static_cast<uint16_t>(numTransIndices - 1);
|
||||||
|
frames.mins.x = commonDeltaTransTrack.m_mins[0];
|
||||||
|
frames.mins.y = commonDeltaTransTrack.m_mins[1];
|
||||||
|
frames.mins.z = commonDeltaTransTrack.m_mins[2];
|
||||||
|
frames.size.x = commonDeltaTransTrack.m_size[0];
|
||||||
|
frames.size.y = commonDeltaTransTrack.m_size[1];
|
||||||
|
frames.size.z = commonDeltaTransTrack.m_size[2];
|
||||||
|
|
||||||
|
if (commonDeltaTransTrack.m_frames_u16.empty())
|
||||||
|
{
|
||||||
|
deltaPart.trans->smallTrans = 1;
|
||||||
|
|
||||||
|
static_assert(sizeof(ByteVec) == sizeof(xanim::CommonVec3U8));
|
||||||
|
frames.frames._1 = memory.Alloc<ByteVec>(numTransIndices);
|
||||||
|
std::memcpy(frames.frames._1, commonDeltaTransTrack.m_frames_u8.data(), numTransIndices * sizeof(ByteVec));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
deltaPart.trans->smallTrans = 0;
|
||||||
|
|
||||||
|
static_assert(sizeof(UShortVec) == sizeof(xanim::CommonVec3U16));
|
||||||
|
frames.frames._2 = memory.Alloc<UShortVec>(numTransIndices);
|
||||||
|
std::memcpy(frames.frames._2, commonDeltaTransTrack.m_frames_u16.data(), numTransIndices * sizeof(UShortVec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<void, std::string> ConvertCommonDeltaPart(XAnimDeltaPart& deltaPart,
|
||||||
|
const xanim::CommonXAnimDeltaTrack& commonXAnimDeltaTrack,
|
||||||
|
MemoryManager& memory,
|
||||||
|
const bool useByteIndices)
|
||||||
|
{
|
||||||
|
#ifdef HAS_DELTA_QUAT_3D
|
||||||
|
if (commonXAnimDeltaTrack.m_quat)
|
||||||
|
{
|
||||||
|
if (commonXAnimDeltaTrack.m_quat->Is3DTrack())
|
||||||
|
ConvertCommonDeltaQuatPart(deltaPart, *commonXAnimDeltaTrack.m_quat, memory, useByteIndices);
|
||||||
|
else
|
||||||
|
ConvertCommonDeltaQuat2Part(deltaPart, *commonXAnimDeltaTrack.m_quat, memory, useByteIndices);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (commonXAnimDeltaTrack.m_quat)
|
||||||
|
{
|
||||||
|
if (commonXAnimDeltaTrack.m_quat->Is3DTrack())
|
||||||
|
return std::unexpected("XAnim uses delta3D which is unsupported in this game");
|
||||||
|
|
||||||
|
ConvertCommonDeltaQuat2Part(deltaPart, *commonXAnimDeltaTrack.m_quat, memory, useByteIndices);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (commonXAnimDeltaTrack.m_trans)
|
||||||
|
ConvertCommonDeltaTransPart(deltaPart, *commonXAnimDeltaTrack.m_trans, memory, useByteIndices);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<void, std::string> ConvertCommonXAnim(XAnimParts& parts,
|
||||||
|
const xanim::CommonXAnimParts& commonParts,
|
||||||
|
AssetRegistration<AssetXAnim>& registration,
|
||||||
|
MemoryManager& memory,
|
||||||
|
ZoneScriptStrings& scriptStrings)
|
||||||
|
{
|
||||||
|
parts.numframes = static_cast<decltype(XAnimParts::numframes)>(commonParts.m_num_frames);
|
||||||
|
if (commonParts.m_looped)
|
||||||
|
SET_IS_LOOPED();
|
||||||
|
#if defined(FEATURE_T5) || defined(FEATURE_T6)
|
||||||
|
parts.bLeftHandGripIK = commonParts.m_left_hand_grip_ik;
|
||||||
|
#endif
|
||||||
|
#if defined(FEATURE_T5)
|
||||||
|
parts.bStreamable = commonParts.m_streamable;
|
||||||
|
#endif
|
||||||
|
parts.assetType = commonParts.m_asset_type;
|
||||||
|
parts.framerate = commonParts.m_frame_rate;
|
||||||
|
parts.frequency = parts.numframes > 0 ? parts.framerate / static_cast<float>(parts.numframes) : 0;
|
||||||
|
#if defined(FEATURE_T5) || defined(FEATURE_T6)
|
||||||
|
parts.primedLength = commonParts.m_primed_length;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const auto useByteIndices = parts.numframes < 256;
|
||||||
|
|
||||||
|
if (commonParts.m_delta_track)
|
||||||
|
{
|
||||||
|
parts.deltaPart = memory.Alloc<XAnimDeltaPart>();
|
||||||
|
#ifdef HAS_DELTA_QUAT_3D
|
||||||
|
if (commonParts.m_delta_track->m_quat && commonParts.m_delta_track->m_quat->Is3DTrack())
|
||||||
|
SET_HAS_DELTA_3D();
|
||||||
|
else
|
||||||
|
SET_HAS_DELTA();
|
||||||
|
#else
|
||||||
|
SET_HAS_DELTA();
|
||||||
|
#endif
|
||||||
|
auto result = ConvertCommonDeltaPart(*parts.deltaPart, *commonParts.m_delta_track, memory, useByteIndices);
|
||||||
|
if (!result.has_value())
|
||||||
|
return std::unexpected(std::move(result).error());
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.names = memory.Alloc<ScriptString>(commonParts.m_bone_tracks.size());
|
||||||
|
for (size_t boneIndex = 0; boneIndex < commonParts.m_bone_tracks.size(); ++boneIndex)
|
||||||
|
{
|
||||||
|
const auto nameScrString = scriptStrings.AddOrGetScriptString(commonParts.m_bone_tracks[boneIndex].m_name);
|
||||||
|
parts.names[boneIndex] = nameScrString;
|
||||||
|
registration.AddScriptString(nameScrString);
|
||||||
|
CountBoneTrackTypes(parts, commonParts.m_bone_tracks[boneIndex]);
|
||||||
|
}
|
||||||
|
parts.boneCount[PART_TYPE_ALL] = static_cast<uint8_t>(commonParts.m_bone_tracks.size());
|
||||||
|
|
||||||
|
ConvertNoteTracks(parts, commonParts, registration, memory, scriptStrings);
|
||||||
|
|
||||||
|
const auto flatData = xanim::CreateFlatDataFromCommonXAnim(commonParts);
|
||||||
|
ConvertFlatData(parts, flatData, memory);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
class XAnimLoader final : public AssetCreator<AssetXAnim>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
XAnimLoader(MemoryManager& memory, ISearchPath& searchPath, ZoneScriptStrings& scriptStrings)
|
||||||
|
: m_memory(memory),
|
||||||
|
m_search_path(searchPath),
|
||||||
|
m_script_strings(scriptStrings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override
|
||||||
|
{
|
||||||
|
const auto file = m_search_path.Open(xanim::GetCompiledFileNameForAssetName(assetName));
|
||||||
|
if (!file.IsOpen())
|
||||||
|
return AssetCreationResult::NoAction();
|
||||||
|
|
||||||
|
auto maybeCommonParts = xanim::LoadCompiledXAnim(*file.m_stream);
|
||||||
|
if (!maybeCommonParts.has_value())
|
||||||
|
{
|
||||||
|
con::error("Failed to load xanim \"{}\": {}", assetName, maybeCommonParts.error());
|
||||||
|
return AssetCreationResult::Failure();
|
||||||
|
}
|
||||||
|
const auto commonParts = std::move(maybeCommonParts).value();
|
||||||
|
|
||||||
|
auto* parts = m_memory.Alloc<XAnimParts>();
|
||||||
|
parts->name = m_memory.Dup(assetName.c_str());
|
||||||
|
|
||||||
|
AssetRegistration<AssetXAnim> registration(assetName, parts);
|
||||||
|
const auto conversionResult = ConvertCommonXAnim(*parts, *commonParts, registration, m_memory, m_script_strings);
|
||||||
|
if (!conversionResult.has_value())
|
||||||
|
{
|
||||||
|
con::error("Failed to load xanim \"{}\": {}", assetName, conversionResult.error());
|
||||||
|
return AssetCreationResult::Failure();
|
||||||
|
}
|
||||||
|
|
||||||
|
return AssetCreationResult::Success(context.AddAsset(std::move(registration)));
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryManager& m_memory;
|
||||||
|
ISearchPath& m_search_path;
|
||||||
|
ZoneScriptStrings& m_script_strings;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#set METHOD_NAME "CreateLoader" + GAME
|
||||||
|
|
||||||
|
namespace xanim
|
||||||
|
{
|
||||||
|
std::unique_ptr<AssetCreator<AssetXAnim>> METHOD_NAME(MemoryManager& memory, ISearchPath& searchPath, Zone& zone)
|
||||||
|
{
|
||||||
|
return std::make_unique<XAnimLoader>(memory, searchPath, zone.m_script_strings);
|
||||||
|
}
|
||||||
|
} // namespace xanim
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#options GAME(IW3, IW4, IW5, T5, T6)
|
||||||
|
|
||||||
|
#filename "Game/" + GAME + "/XAnim/XAnimLoader" + GAME + ".h"
|
||||||
|
|
||||||
|
#set GAME_HEADER "\"Game/" + GAME + "/" + GAME + ".h\""
|
||||||
|
|
||||||
|
// This file was templated.
|
||||||
|
// See XAnimLoader.h.template.
|
||||||
|
// Do not modify, changes will be lost.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Asset/IAssetCreator.h"
|
||||||
|
#include GAME_HEADER
|
||||||
|
#include "SearchPath/ISearchPath.h"
|
||||||
|
#include "Utils/MemoryManager.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#set METHOD_NAME "CreateLoader" + GAME
|
||||||
|
|
||||||
|
namespace xanim
|
||||||
|
{
|
||||||
|
std::unique_ptr<AssetCreator<GAME::AssetXAnim>> METHOD_NAME(MemoryManager& memory, ISearchPath& searchPath, Zone& zone);
|
||||||
|
} // namespace xanim
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "Game/IW3/Image/ImageDumperIW3.h"
|
#include "Game/IW3/Image/ImageDumperIW3.h"
|
||||||
#include "Game/IW3/Material/MaterialJsonDumperIW3.h"
|
#include "Game/IW3/Material/MaterialJsonDumperIW3.h"
|
||||||
#include "Game/IW3/Techset/TechsetDumperIW3.h"
|
#include "Game/IW3/Techset/TechsetDumperIW3.h"
|
||||||
|
#include "Game/IW3/XAnim/XAnimDumperIW3.h"
|
||||||
#include "Game/IW3/XModel/XModelDumperIW3.h"
|
#include "Game/IW3/XModel/XModelDumperIW3.h"
|
||||||
#include "LightDef/LightDefDumperIW3.h"
|
#include "LightDef/LightDefDumperIW3.h"
|
||||||
#include "Localize/LocalizeDumperIW3.h"
|
#include "Localize/LocalizeDumperIW3.h"
|
||||||
@@ -12,7 +13,6 @@
|
|||||||
#include "Sound/LoadedSoundDumperIW3.h"
|
#include "Sound/LoadedSoundDumperIW3.h"
|
||||||
#include "Sound/SndCurveDumperIW3.h"
|
#include "Sound/SndCurveDumperIW3.h"
|
||||||
#include "StringTable/StringTableDumperIW3.h"
|
#include "StringTable/StringTableDumperIW3.h"
|
||||||
#include "XAnim/XAnimDumperIW3.h"
|
|
||||||
|
|
||||||
using namespace IW3;
|
using namespace IW3;
|
||||||
|
|
||||||
|
|||||||
@@ -1,937 +0,0 @@
|
|||||||
#include "XAnimDumperIW3.h"
|
|
||||||
|
|
||||||
#include "Utils/Alignment.h"
|
|
||||||
#include "Utils/StreamUtils.h"
|
|
||||||
#include "XAnim/XAnimCommon.h"
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cassert>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstring>
|
|
||||||
#include <expected>
|
|
||||||
#include <format>
|
|
||||||
#include <limits>
|
|
||||||
#include <optional>
|
|
||||||
#include <ostream>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace IW3;
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr uint16_t RAW_VERSION = 17;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
enum class QuatType : uint8_t
|
|
||||||
{
|
|
||||||
NO_QUAT = 0,
|
|
||||||
HALF_QUAT = 1,
|
|
||||||
FULL_QUAT = 2,
|
|
||||||
HALF_QUAT_NO_SIZE = 3,
|
|
||||||
FULL_QUAT_NO_SIZE = 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class TransType : uint8_t
|
|
||||||
{
|
|
||||||
SMALL_TRANS = 5,
|
|
||||||
FULL_TRANS = 6,
|
|
||||||
TRANS_NO_SIZE = 7,
|
|
||||||
NO_TRANS = 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct QuatTrack
|
|
||||||
{
|
|
||||||
QuatType type = QuatType::NO_QUAT;
|
|
||||||
std::vector<uint16_t> indices;
|
|
||||||
std::vector<int16_t> values;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct TransTrack
|
|
||||||
{
|
|
||||||
TransType type = TransType::NO_TRANS;
|
|
||||||
std::vector<uint16_t> indices;
|
|
||||||
std::array<float, 3> mins{};
|
|
||||||
std::array<float, 3> size{};
|
|
||||||
std::vector<uint8_t> byteFrames;
|
|
||||||
std::vector<uint16_t> shortFrames;
|
|
||||||
std::array<float, 3> constant{};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct BoneTrack
|
|
||||||
{
|
|
||||||
std::string name;
|
|
||||||
QuatTrack quat;
|
|
||||||
TransTrack trans;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FlatDataCursor
|
|
||||||
{
|
|
||||||
const uint8_t* dataByte;
|
|
||||||
const int16_t* dataShort;
|
|
||||||
const int* dataInt;
|
|
||||||
const uint8_t* randomDataByte;
|
|
||||||
const int16_t* randomDataShort;
|
|
||||||
const uint16_t* indices;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DeltaQuatTrack
|
|
||||||
{
|
|
||||||
bool keyframed = false;
|
|
||||||
std::vector<uint16_t> indices;
|
|
||||||
std::vector<int16_t> values;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DeltaTransTrack
|
|
||||||
{
|
|
||||||
bool keyframed = false;
|
|
||||||
bool smallTrans = false;
|
|
||||||
std::vector<uint16_t> indices;
|
|
||||||
std::array<float, 3> mins{};
|
|
||||||
std::array<float, 3> size{};
|
|
||||||
std::vector<uint8_t> byteFrames;
|
|
||||||
std::vector<uint16_t> shortFrames;
|
|
||||||
std::array<float, 3> constant{};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DeltaTrack
|
|
||||||
{
|
|
||||||
std::optional<DeltaQuatTrack> quat;
|
|
||||||
std::optional<DeltaTransTrack> trans;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct EncodedQuatTrack
|
|
||||||
{
|
|
||||||
bool flipQuat = false;
|
|
||||||
std::vector<int16_t> storedValues;
|
|
||||||
};
|
|
||||||
|
|
||||||
[[nodiscard]] const std::string& ResolveScriptString(const XAssetInfo<XAnimParts>& asset, const ScriptString value)
|
|
||||||
{
|
|
||||||
assert(asset.m_zone != nullptr && value < asset.m_zone->m_script_strings.Count());
|
|
||||||
return asset.m_zone->m_script_strings[value];
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] uint16_t GetNumLoopFrames(const XAnimParts& parts)
|
|
||||||
{
|
|
||||||
assert(parts.numframes < 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.numframes + 1u);
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] bool UseByteIndices(const XAnimParts& parts)
|
|
||||||
{
|
|
||||||
return parts.numframes < 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] float IntBitsToFloat(const int value)
|
|
||||||
{
|
|
||||||
union
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
float f;
|
|
||||||
};
|
|
||||||
|
|
||||||
i = value;
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] std::array<float, 3> ReadFloat3(const int*& dataInt)
|
|
||||||
{
|
|
||||||
std::array<float, 3> result{};
|
|
||||||
for (float& i : result)
|
|
||||||
i = IntBitsToFloat(*dataInt++);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T> [[nodiscard]] const T* AdvancePtr(const T* ptr, const size_t count)
|
|
||||||
{
|
|
||||||
if (count == 0uz)
|
|
||||||
return ptr;
|
|
||||||
|
|
||||||
assert(ptr != nullptr);
|
|
||||||
return ptr + count;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] std::vector<uint16_t> ReadPackedIndices(FlatDataCursor& 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.dataByte[i];
|
|
||||||
|
|
||||||
cursor.dataByte += count;
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
for (auto i = 0uz; i < count; i++)
|
|
||||||
result[i] = cursor.indices[i];
|
|
||||||
|
|
||||||
cursor.indices += 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.dataShort += ((count - 2uz) / 256u) + 2uz;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto i = 0uz; i < count; i++)
|
|
||||||
result[i] = static_cast<uint16_t>(cursor.dataShort[i]);
|
|
||||||
|
|
||||||
cursor.dataShort += count;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[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;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] bool QuatTypeUsesHalf(const QuatType type)
|
|
||||||
{
|
|
||||||
return type == QuatType::NO_QUAT || type == QuatType::HALF_QUAT || type == QuatType::HALF_QUAT_NO_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] float EncodeRawTransSize(const float value, const bool smallTrans)
|
|
||||||
{
|
|
||||||
const auto scale = smallTrans ? HALF_TRANS_SIZE_SCALE : FULL_TRANS_SIZE_SCALE;
|
|
||||||
return value / scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] int64_t ComputeQuatDot(const int16_t* lhs, const int16_t* rhs, const size_t componentCount)
|
|
||||||
{
|
|
||||||
int64_t result = 0;
|
|
||||||
for (auto i = 0uz; i < componentCount; i++)
|
|
||||||
result += static_cast<int64_t>(lhs[i]) * static_cast<int64_t>(rhs[i]);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] EncodedQuatTrack EncodeQuatFrames(const int16_t* values, const size_t frameCount, const size_t componentCount, const bool allowFlipQuat)
|
|
||||||
{
|
|
||||||
assert(componentCount == 2uz || componentCount == 4uz);
|
|
||||||
|
|
||||||
EncodedQuatTrack result;
|
|
||||||
if (frameCount == 0uz)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
const auto storedComponentCount = componentCount - 1uz;
|
|
||||||
result.storedValues.reserve(frameCount * storedComponentCount);
|
|
||||||
|
|
||||||
// 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.flipQuat = allowFlipQuat && values[storedComponentCount] < 0;
|
|
||||||
const auto targetNegativeOmitted = result.flipQuat;
|
|
||||||
|
|
||||||
for (auto frameIndex = 0uz; frameIndex < frameCount; frameIndex++)
|
|
||||||
{
|
|
||||||
const auto* frame = &values[frameIndex * componentCount];
|
|
||||||
const auto omittedNegative = frame[storedComponentCount] < 0;
|
|
||||||
|
|
||||||
auto continuityNegated = false;
|
|
||||||
if (frameIndex > 0uz && omittedNegative != targetNegativeOmitted)
|
|
||||||
{
|
|
||||||
const auto* prevFrame = &values[(frameIndex - 1uz) * componentCount];
|
|
||||||
continuityNegated = ComputeQuatDot(prevFrame, frame, componentCount) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto rawNegated = result.flipQuat != continuityNegated;
|
|
||||||
const auto sign = rawNegated ? -1 : 1;
|
|
||||||
|
|
||||||
for (auto componentIndex = 0uz; componentIndex < storedComponentCount; componentIndex++)
|
|
||||||
{
|
|
||||||
const auto value = static_cast<int>(frame[componentIndex]) * sign;
|
|
||||||
assert(value >= std::numeric_limits<int16_t>::min() && value <= std::numeric_limits<int16_t>::max());
|
|
||||||
result.storedValues.emplace_back(static_cast<int16_t>(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] EncodedQuatTrack EncodeQuatTrack(const QuatTrack& quat)
|
|
||||||
{
|
|
||||||
switch (quat.type)
|
|
||||||
{
|
|
||||||
case QuatType::NO_QUAT:
|
|
||||||
return {};
|
|
||||||
|
|
||||||
case QuatType::HALF_QUAT_NO_SIZE:
|
|
||||||
assert(quat.values.size() == 2uz);
|
|
||||||
return EncodeQuatFrames(quat.values.data(), 1uz, 2uz, true);
|
|
||||||
|
|
||||||
case QuatType::FULL_QUAT_NO_SIZE:
|
|
||||||
assert(quat.values.size() == 4uz);
|
|
||||||
return EncodeQuatFrames(quat.values.data(), 1uz, 4uz, true);
|
|
||||||
|
|
||||||
case QuatType::HALF_QUAT:
|
|
||||||
{
|
|
||||||
const auto frameCount = quat.indices.size();
|
|
||||||
assert(quat.values.size() == frameCount * 2uz);
|
|
||||||
return EncodeQuatFrames(quat.values.data(), frameCount, 2uz, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
case QuatType::FULL_QUAT:
|
|
||||||
{
|
|
||||||
const auto frameCount = quat.indices.size();
|
|
||||||
assert(quat.values.size() == frameCount * 4uz);
|
|
||||||
return EncodeQuatFrames(quat.values.data(), frameCount, 4uz, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(false);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] EncodedQuatTrack EncodeDeltaQuatTrack(const DeltaTrack& delta)
|
|
||||||
{
|
|
||||||
if (!delta.quat)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
// Delta quats are serialized without the per-bone flipQuat mask used by normal bone quats.
|
|
||||||
if (!delta.quat->keyframed)
|
|
||||||
{
|
|
||||||
assert(delta.quat->values.size() == 2uz);
|
|
||||||
return EncodeQuatFrames(delta.quat->values.data(), 1uz, 2uz, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto frameCount = delta.quat->indices.size();
|
|
||||||
assert(delta.quat->values.size() == frameCount * 2uz);
|
|
||||||
return EncodeQuatFrames(delta.quat->values.data(), frameCount, 2uz, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string CreateReconstructionError(const XAssetInfo<XAnimParts>& asset, const char* field)
|
|
||||||
{
|
|
||||||
return std::format("IW3 xanim raw reconstruction cursor mismatch for asset \"{}\" in {}", asset.m_name, field);
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] std::expected<std::vector<BoneTrack>, std::string> ReconstructBoneTracks(const XAssetInfo<XAnimParts>& asset)
|
|
||||||
{
|
|
||||||
const auto& parts = *asset.Asset();
|
|
||||||
const auto nameCount = static_cast<size_t>(parts.boneCount[PART_TYPE_ALL]);
|
|
||||||
const auto useByteIndices = UseByteIndices(parts);
|
|
||||||
|
|
||||||
std::vector<BoneTrack> bones(nameCount);
|
|
||||||
for (auto i = 0uz; i < nameCount; i++)
|
|
||||||
bones[i].name = ResolveScriptString(asset, parts.names[i]);
|
|
||||||
|
|
||||||
// Root indices should only ever be used when it is !useByteIndices, therefore we should be safe to always use the short version
|
|
||||||
assert(!useByteIndices || parts.indices._1 == nullptr);
|
|
||||||
|
|
||||||
auto cursor = FlatDataCursor{
|
|
||||||
.dataByte = parts.dataByte,
|
|
||||||
.dataShort = parts.dataShort,
|
|
||||||
.dataInt = parts.dataInt,
|
|
||||||
.randomDataByte = parts.randomDataByte,
|
|
||||||
.randomDataShort = parts.randomDataShort,
|
|
||||||
.indices = parts.indices._2,
|
|
||||||
};
|
|
||||||
|
|
||||||
size_t boneIndex = 0;
|
|
||||||
|
|
||||||
for (auto i = 0u; i < parts.boneCount[PART_TYPE_NO_QUAT]; i++, boneIndex++)
|
|
||||||
bones[boneIndex].quat.type = QuatType::NO_QUAT;
|
|
||||||
|
|
||||||
for (auto i = 0u; i < parts.boneCount[PART_TYPE_HALF_QUAT]; i++, boneIndex++)
|
|
||||||
{
|
|
||||||
auto& quat = bones[boneIndex].quat;
|
|
||||||
quat.type = QuatType::HALF_QUAT;
|
|
||||||
const auto storedSize = static_cast<uint16_t>(*cursor.dataShort++);
|
|
||||||
const auto frameCount = static_cast<size_t>(storedSize) + 1uz;
|
|
||||||
quat.indices = ReadPackedIndices(cursor, storedSize, useByteIndices);
|
|
||||||
quat.values.assign(cursor.randomDataShort, cursor.randomDataShort + frameCount * 2uz);
|
|
||||||
cursor.randomDataShort += frameCount * 2uz;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto i = 0u; i < parts.boneCount[PART_TYPE_FULL_QUAT]; i++, boneIndex++)
|
|
||||||
{
|
|
||||||
auto& quat = bones[boneIndex].quat;
|
|
||||||
quat.type = QuatType::FULL_QUAT;
|
|
||||||
const auto storedSize = static_cast<uint16_t>(*cursor.dataShort++);
|
|
||||||
const auto frameCount = static_cast<size_t>(storedSize) + 1uz;
|
|
||||||
quat.indices = ReadPackedIndices(cursor, storedSize, useByteIndices);
|
|
||||||
quat.values.assign(cursor.randomDataShort, cursor.randomDataShort + frameCount * 4uz);
|
|
||||||
cursor.randomDataShort += frameCount * 4uz;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto i = 0u; i < parts.boneCount[PART_TYPE_HALF_QUAT_NO_SIZE]; i++, boneIndex++)
|
|
||||||
{
|
|
||||||
auto& quat = bones[boneIndex].quat;
|
|
||||||
quat.type = QuatType::HALF_QUAT_NO_SIZE;
|
|
||||||
quat.values.assign(cursor.dataShort, cursor.dataShort + 2);
|
|
||||||
cursor.dataShort += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto i = 0u; i < parts.boneCount[PART_TYPE_FULL_QUAT_NO_SIZE]; i++, boneIndex++)
|
|
||||||
{
|
|
||||||
auto& quat = bones[boneIndex].quat;
|
|
||||||
quat.type = QuatType::FULL_QUAT_NO_SIZE;
|
|
||||||
quat.values.assign(cursor.dataShort, cursor.dataShort + 4);
|
|
||||||
cursor.dataShort += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<bool> transAssigned(nameCount, false);
|
|
||||||
|
|
||||||
for (auto i = 0u; i < parts.boneCount[PART_TYPE_SMALL_TRANS]; i++)
|
|
||||||
{
|
|
||||||
const auto bone = static_cast<size_t>(*cursor.dataByte++);
|
|
||||||
assert(bone < nameCount && !transAssigned[bone]);
|
|
||||||
|
|
||||||
auto& trans = bones[bone].trans;
|
|
||||||
transAssigned[bone] = true;
|
|
||||||
trans.type = TransType::SMALL_TRANS;
|
|
||||||
|
|
||||||
const auto storedSize = static_cast<uint16_t>(*cursor.dataShort++);
|
|
||||||
const auto frameCount = static_cast<size_t>(storedSize) + 1uz;
|
|
||||||
trans.mins = ReadFloat3(cursor.dataInt);
|
|
||||||
trans.size = ReadFloat3(cursor.dataInt);
|
|
||||||
trans.indices = ReadPackedIndices(cursor, storedSize, useByteIndices);
|
|
||||||
trans.byteFrames.assign(cursor.randomDataByte, cursor.randomDataByte + frameCount * 3uz);
|
|
||||||
cursor.randomDataByte += frameCount * 3uz;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto i = 0u; i < parts.boneCount[PART_TYPE_TRANS]; i++)
|
|
||||||
{
|
|
||||||
const auto bone = static_cast<size_t>(*cursor.dataByte++);
|
|
||||||
assert(bone < nameCount && !transAssigned[bone]);
|
|
||||||
|
|
||||||
auto& trans = bones[bone].trans;
|
|
||||||
transAssigned[bone] = true;
|
|
||||||
trans.type = TransType::FULL_TRANS;
|
|
||||||
|
|
||||||
const auto storedSize = static_cast<uint16_t>(*cursor.dataShort++);
|
|
||||||
const auto frameCount = static_cast<size_t>(storedSize) + 1uz;
|
|
||||||
trans.mins = ReadFloat3(cursor.dataInt);
|
|
||||||
trans.size = ReadFloat3(cursor.dataInt);
|
|
||||||
trans.indices = ReadPackedIndices(cursor, storedSize, useByteIndices);
|
|
||||||
trans.shortFrames.reserve(frameCount * 3uz);
|
|
||||||
for (auto frame = 0uz; frame < frameCount * 3uz; frame++)
|
|
||||||
trans.shortFrames.emplace_back(static_cast<uint16_t>(*cursor.randomDataShort++));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto i = 0u; i < parts.boneCount[PART_TYPE_TRANS_NO_SIZE]; i++)
|
|
||||||
{
|
|
||||||
const auto bone = static_cast<size_t>(*cursor.dataByte++);
|
|
||||||
assert(bone < nameCount && !transAssigned[bone]);
|
|
||||||
|
|
||||||
auto& trans = bones[bone].trans;
|
|
||||||
transAssigned[bone] = true;
|
|
||||||
trans.type = TransType::TRANS_NO_SIZE;
|
|
||||||
trans.constant = ReadFloat3(cursor.dataInt);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto i = 0u; i < parts.boneCount[PART_TYPE_NO_TRANS]; i++)
|
|
||||||
{
|
|
||||||
const auto bone = static_cast<size_t>(*cursor.dataByte++);
|
|
||||||
assert(bone < nameCount && !transAssigned[bone]);
|
|
||||||
|
|
||||||
bones[bone].trans.type = TransType::NO_TRANS;
|
|
||||||
transAssigned[bone] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto i = 0uz; i < nameCount; i++)
|
|
||||||
assert(transAssigned[i]);
|
|
||||||
|
|
||||||
const auto dataByteEnd = AdvancePtr(parts.dataByte, parts.dataByteCount);
|
|
||||||
const auto dataShortEnd = AdvancePtr(parts.dataShort, parts.dataShortCount);
|
|
||||||
const auto dataIntEnd = AdvancePtr(parts.dataInt, parts.dataIntCount);
|
|
||||||
const auto randomDataByteEnd = AdvancePtr(parts.randomDataByte, parts.randomDataByteCount);
|
|
||||||
const auto randomDataShortEnd = AdvancePtr(parts.randomDataShort, parts.randomDataShortCount);
|
|
||||||
|
|
||||||
if (cursor.dataByte != dataByteEnd)
|
|
||||||
return std::unexpected(CreateReconstructionError(asset, "dataByte"));
|
|
||||||
if (cursor.dataShort != dataShortEnd)
|
|
||||||
return std::unexpected(CreateReconstructionError(asset, "dataShort"));
|
|
||||||
if (cursor.dataInt != dataIntEnd)
|
|
||||||
return std::unexpected(CreateReconstructionError(asset, "dataInt"));
|
|
||||||
if (cursor.randomDataByte != randomDataByteEnd)
|
|
||||||
return std::unexpected(CreateReconstructionError(asset, "randomDataByte"));
|
|
||||||
if (cursor.randomDataShort != randomDataShortEnd)
|
|
||||||
return std::unexpected(CreateReconstructionError(asset, "randomDataShort"));
|
|
||||||
|
|
||||||
if (!useByteIndices)
|
|
||||||
{
|
|
||||||
const auto indicesEnd = AdvancePtr(parts.indices._2, parts.indexCount);
|
|
||||||
if (cursor.indices != indicesEnd)
|
|
||||||
return std::unexpected(CreateReconstructionError(asset, "indices"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
assert(parts.indexCount == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bones;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] DeltaTrack ReconstructDeltaTrack(const XAnimParts& parts)
|
|
||||||
{
|
|
||||||
DeltaTrack result;
|
|
||||||
|
|
||||||
assert(static_cast<bool>(parts.deltaPart) == static_cast<bool>(parts.bDelta));
|
|
||||||
if (!parts.deltaPart)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
const auto numLoopFrames = GetNumLoopFrames(parts);
|
|
||||||
const auto useByteIndices = UseByteIndices(parts);
|
|
||||||
|
|
||||||
if (const auto* quat = parts.deltaPart->quat; quat)
|
|
||||||
{
|
|
||||||
result.quat.emplace();
|
|
||||||
|
|
||||||
if (quat->size > 0)
|
|
||||||
{
|
|
||||||
result.quat->keyframed = true;
|
|
||||||
const auto frameCount = static_cast<size_t>(quat->size) + 1uz;
|
|
||||||
result.quat->values.reserve(frameCount * 2uz);
|
|
||||||
result.quat->indices.reserve(frameCount);
|
|
||||||
|
|
||||||
for (auto i = 0uz; i < frameCount; i++)
|
|
||||||
{
|
|
||||||
result.quat->values.emplace_back(quat->u.frames.frames[i].value[0]);
|
|
||||||
result.quat->values.emplace_back(quat->u.frames.frames[i].value[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useByteIndices)
|
|
||||||
{
|
|
||||||
for (auto i = 0uz; i < frameCount; i++)
|
|
||||||
result.quat->indices.emplace_back(static_cast<uint8_t>(quat->u.frames.indices._1[i]));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (auto i = 0uz; i < frameCount; i++)
|
|
||||||
result.quat->indices.emplace_back(quat->u.frames.indices._2[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(result.quat->indices.size() <= numLoopFrames);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.quat->values.emplace_back(quat->u.frame0.value[0]);
|
|
||||||
result.quat->values.emplace_back(quat->u.frame0.value[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const auto* trans = parts.deltaPart->trans; trans)
|
|
||||||
{
|
|
||||||
result.trans.emplace();
|
|
||||||
|
|
||||||
if (trans->size > 0)
|
|
||||||
{
|
|
||||||
result.trans->keyframed = true;
|
|
||||||
result.trans->smallTrans = trans->smallTrans;
|
|
||||||
result.trans->mins = {trans->u.frames.mins.x, trans->u.frames.mins.y, trans->u.frames.mins.z};
|
|
||||||
result.trans->size = {trans->u.frames.size.x, trans->u.frames.size.y, trans->u.frames.size.z};
|
|
||||||
|
|
||||||
const auto frameCount = static_cast<size_t>(trans->size) + 1uz;
|
|
||||||
result.trans->indices.reserve(frameCount);
|
|
||||||
if (useByteIndices)
|
|
||||||
{
|
|
||||||
for (auto i = 0uz; i < frameCount; i++)
|
|
||||||
result.trans->indices.emplace_back(static_cast<uint8_t>(trans->u.frames.indices._1[i]));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (auto i = 0uz; i < frameCount; i++)
|
|
||||||
result.trans->indices.emplace_back(trans->u.frames.indices._2[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trans->smallTrans)
|
|
||||||
{
|
|
||||||
result.trans->byteFrames.reserve(frameCount * 3uz);
|
|
||||||
for (auto i = 0uz; i < frameCount; i++)
|
|
||||||
{
|
|
||||||
result.trans->byteFrames.emplace_back(trans->u.frames.frames._1[i][0]);
|
|
||||||
result.trans->byteFrames.emplace_back(trans->u.frames.frames._1[i][1]);
|
|
||||||
result.trans->byteFrames.emplace_back(trans->u.frames.frames._1[i][2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.trans->shortFrames.reserve(frameCount * 3uz);
|
|
||||||
for (auto i = 0uz; i < frameCount; i++)
|
|
||||||
{
|
|
||||||
result.trans->shortFrames.emplace_back(trans->u.frames.frames._2[i][0]);
|
|
||||||
result.trans->shortFrames.emplace_back(trans->u.frames.frames._2[i][1]);
|
|
||||||
result.trans->shortFrames.emplace_back(trans->u.frames.frames._2[i][2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.trans->constant = {trans->u.frame0.v[0], trans->u.frame0.v[1], trans->u.frame0.v[2]};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 WriteQuatTrack(
|
|
||||||
std::ostream& stream, const QuatTrack& quat, const EncodedQuatTrack& encodedQuat, const uint16_t numLoopFrames, const bool useByteIndices)
|
|
||||||
{
|
|
||||||
switch (quat.type)
|
|
||||||
{
|
|
||||||
case QuatType::NO_QUAT:
|
|
||||||
{
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(0));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case QuatType::HALF_QUAT_NO_SIZE:
|
|
||||||
{
|
|
||||||
assert(encodedQuat.storedValues.size() == 1uz);
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(1));
|
|
||||||
stream::WriteValue(stream, encodedQuat.storedValues[0]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case QuatType::FULL_QUAT_NO_SIZE:
|
|
||||||
{
|
|
||||||
assert(encodedQuat.storedValues.size() == 3uz);
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(1));
|
|
||||||
for (const auto value : encodedQuat.storedValues)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case QuatType::HALF_QUAT:
|
|
||||||
{
|
|
||||||
const auto frameCount = quat.indices.size();
|
|
||||||
assert(frameCount > 0uz);
|
|
||||||
assert(quat.values.size() == frameCount * 2uz);
|
|
||||||
assert(encodedQuat.storedValues.size() == frameCount);
|
|
||||||
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(frameCount));
|
|
||||||
WriteIndicesIfNeeded(stream, quat.indices, numLoopFrames, useByteIndices);
|
|
||||||
for (const auto value : encodedQuat.storedValues)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case QuatType::FULL_QUAT:
|
|
||||||
{
|
|
||||||
const auto frameCount = quat.indices.size();
|
|
||||||
assert(frameCount > 0uz);
|
|
||||||
assert(quat.values.size() == frameCount * 4uz);
|
|
||||||
assert(encodedQuat.storedValues.size() == frameCount * 3uz);
|
|
||||||
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(frameCount));
|
|
||||||
WriteIndicesIfNeeded(stream, quat.indices, numLoopFrames, useByteIndices);
|
|
||||||
for (const auto value : encodedQuat.storedValues)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WriteTransTrack(std::ostream& stream, const TransTrack& trans, const uint16_t numLoopFrames, const bool useByteIndices)
|
|
||||||
{
|
|
||||||
switch (trans.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.constant)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case TransType::SMALL_TRANS:
|
|
||||||
{
|
|
||||||
const auto frameCount = trans.indices.size();
|
|
||||||
assert(frameCount > 0uz);
|
|
||||||
assert(trans.byteFrames.size() == frameCount * 3uz);
|
|
||||||
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(frameCount));
|
|
||||||
WriteIndicesIfNeeded(stream, trans.indices, numLoopFrames, useByteIndices);
|
|
||||||
|
|
||||||
constexpr auto smallTrans = static_cast<uint8_t>(1);
|
|
||||||
stream::WriteValue(stream, smallTrans);
|
|
||||||
|
|
||||||
for (const auto value : trans.mins)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
for (const auto value : trans.size)
|
|
||||||
stream::WriteValue(stream, EncodeRawTransSize(value, true));
|
|
||||||
|
|
||||||
stream::Write(stream, trans.byteFrames.data(), trans.byteFrames.size());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case TransType::FULL_TRANS:
|
|
||||||
{
|
|
||||||
const auto frameCount = trans.indices.size();
|
|
||||||
assert(frameCount > 0uz);
|
|
||||||
assert(trans.shortFrames.size() == frameCount * 3uz);
|
|
||||||
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(frameCount));
|
|
||||||
WriteIndicesIfNeeded(stream, trans.indices, numLoopFrames, useByteIndices);
|
|
||||||
|
|
||||||
constexpr auto smallTrans = static_cast<uint8_t>(0);
|
|
||||||
stream::WriteValue(stream, smallTrans);
|
|
||||||
|
|
||||||
for (const auto value : trans.mins)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
for (const auto value : trans.size)
|
|
||||||
stream::WriteValue(stream, EncodeRawTransSize(value, false));
|
|
||||||
|
|
||||||
for (const auto value : trans.shortFrames)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WriteDeltaQuatTrack(std::ostream& stream, const DeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices)
|
|
||||||
{
|
|
||||||
const auto encodedDeltaQuat = EncodeDeltaQuatTrack(delta);
|
|
||||||
|
|
||||||
if (!delta.quat)
|
|
||||||
{
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(0));
|
|
||||||
}
|
|
||||||
else if (!delta.quat->keyframed)
|
|
||||||
{
|
|
||||||
assert(encodedDeltaQuat.storedValues.size() == 1uz);
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(1));
|
|
||||||
stream::WriteValue(stream, encodedDeltaQuat.storedValues[0]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const auto frameCount = delta.quat->indices.size();
|
|
||||||
assert(frameCount > 0uz);
|
|
||||||
assert(delta.quat->values.size() == frameCount * 2uz);
|
|
||||||
assert(encodedDeltaQuat.storedValues.size() == frameCount);
|
|
||||||
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(frameCount));
|
|
||||||
WriteIndicesIfNeeded(stream, delta.quat->indices, numLoopFrames, useByteIndices);
|
|
||||||
for (const auto value : encodedDeltaQuat.storedValues)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WriteDeltaTransTrack(std::ostream& stream, const DeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices)
|
|
||||||
{
|
|
||||||
if (!delta.trans)
|
|
||||||
{
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(0));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!delta.trans->keyframed)
|
|
||||||
{
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(1));
|
|
||||||
for (const auto value : delta.trans->constant)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto frameCount = delta.trans->indices.size();
|
|
||||||
assert(frameCount > 0uz);
|
|
||||||
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(frameCount));
|
|
||||||
WriteIndicesIfNeeded(stream, delta.trans->indices, numLoopFrames, useByteIndices);
|
|
||||||
|
|
||||||
const auto smallTrans = static_cast<uint8_t>(delta.trans->smallTrans ? 1 : 0);
|
|
||||||
stream::WriteValue(stream, smallTrans);
|
|
||||||
for (const auto value : delta.trans->mins)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
|
|
||||||
if (delta.trans->smallTrans)
|
|
||||||
{
|
|
||||||
assert(delta.trans->byteFrames.size() == frameCount * 3uz);
|
|
||||||
for (const auto value : delta.trans->size)
|
|
||||||
stream::WriteValue(stream, EncodeRawTransSize(value, true));
|
|
||||||
|
|
||||||
stream::Write(stream, delta.trans->byteFrames.data(), delta.trans->byteFrames.size());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
assert(delta.trans->shortFrames.size() == frameCount * 3uz);
|
|
||||||
for (const auto value : delta.trans->size)
|
|
||||||
stream::WriteValue(stream, EncodeRawTransSize(value, false));
|
|
||||||
|
|
||||||
for (const auto value : delta.trans->shortFrames)
|
|
||||||
stream::WriteValue(stream, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WriteDeltaTrack(std::ostream& stream, const DeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices)
|
|
||||||
{
|
|
||||||
WriteDeltaQuatTrack(stream, delta, numLoopFrames, useByteIndices);
|
|
||||||
WriteDeltaTransTrack(stream, delta, numLoopFrames, useByteIndices);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WriteNoteTracks(std::ostream& stream, const XAssetInfo<XAnimParts>& asset)
|
|
||||||
{
|
|
||||||
const auto& parts = *asset.Asset();
|
|
||||||
const auto notifyCount = static_cast<size_t>(parts.notifyCount);
|
|
||||||
|
|
||||||
size_t rawNotifyCount = notifyCount;
|
|
||||||
if (notifyCount > 0uz)
|
|
||||||
{
|
|
||||||
const auto& lastName = ResolveScriptString(asset, parts.notify[notifyCount - 1].name);
|
|
||||||
const auto lastTime = parts.notify[notifyCount - 1].time;
|
|
||||||
|
|
||||||
// The linker appends a synthetic "end" notify at 1.0f to the loaded asset state.
|
|
||||||
if (lastName == "end" && std::abs(lastTime - 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++)
|
|
||||||
{
|
|
||||||
stream::WriteCString(stream, ResolveScriptString(asset, parts.notify[i].name));
|
|
||||||
|
|
||||||
uint16_t frame = 0;
|
|
||||||
if (parts.numframes > 0)
|
|
||||||
{
|
|
||||||
const auto scaled = static_cast<long>(std::lround(parts.notify[i].time * static_cast<float>(parts.numframes)));
|
|
||||||
assert(scaled >= 0 && scaled <= std::numeric_limits<uint16_t>::max());
|
|
||||||
frame = static_cast<uint16_t>(scaled);
|
|
||||||
}
|
|
||||||
|
|
||||||
stream::WriteValue(stream, frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
namespace xanim
|
|
||||||
{
|
|
||||||
void DumperIW3::DumpAsset(AssetDumpingContext& context, const XAssetInfo<AssetXAnim::Type>& asset)
|
|
||||||
{
|
|
||||||
const auto* parts = asset.Asset();
|
|
||||||
|
|
||||||
auto maybeBoneTracks = ReconstructBoneTracks(asset);
|
|
||||||
if (!maybeBoneTracks.has_value())
|
|
||||||
{
|
|
||||||
con::error(maybeBoneTracks.error());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto boneTracks = std::move(maybeBoneTracks).value();
|
|
||||||
|
|
||||||
const auto assetFile = context.OpenAssetFile(GetCompiledFileNameForAssetName(asset.m_name));
|
|
||||||
if (!assetFile)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto numLoopFrames = GetNumLoopFrames(*parts);
|
|
||||||
const auto useByteIndices = UseByteIndices(*parts);
|
|
||||||
const auto deltaTrack = ReconstructDeltaTrack(*parts);
|
|
||||||
|
|
||||||
std::vector<EncodedQuatTrack> encodedBoneQuats;
|
|
||||||
encodedBoneQuats.reserve(boneTracks.size());
|
|
||||||
for (const auto& bone : boneTracks)
|
|
||||||
encodedBoneQuats.emplace_back(EncodeQuatTrack(bone.quat));
|
|
||||||
|
|
||||||
auto& stream = *assetFile;
|
|
||||||
|
|
||||||
const auto flags = static_cast<uint8_t>((parts->bLoop ? FLAG_LOOPED : 0u) | (parts->bDelta ? FLAG_DELTA : 0u));
|
|
||||||
const auto boneCount = static_cast<uint16_t>(parts->boneCount[PART_TYPE_ALL]);
|
|
||||||
const auto assetType = static_cast<uint8_t>(parts->assetType);
|
|
||||||
const auto framerate = static_cast<uint16_t>(std::lround(parts->framerate));
|
|
||||||
|
|
||||||
stream::WriteValue(stream, RAW_VERSION);
|
|
||||||
// Looped raws store numframes directly; non-looped raws store numframes + 1.
|
|
||||||
stream::WriteValue(stream, static_cast<uint16_t>(parts->bLoop ? parts->numframes : numLoopFrames));
|
|
||||||
stream::WriteValue(stream, boneCount);
|
|
||||||
stream::WriteValue(stream, flags);
|
|
||||||
stream::WriteValue(stream, assetType);
|
|
||||||
stream::WriteValue(stream, framerate);
|
|
||||||
|
|
||||||
if (parts->bDelta)
|
|
||||||
WriteDeltaTrack(stream, deltaTrack, numLoopFrames, useByteIndices);
|
|
||||||
|
|
||||||
if (!boneTracks.empty())
|
|
||||||
{
|
|
||||||
const auto bitmaskSize = utils::Align<size_t>(boneTracks.size(), 8u) / 8u;
|
|
||||||
std::vector<uint8_t> flipQuat(bitmaskSize, 0);
|
|
||||||
std::vector<uint8_t> halfQuat(bitmaskSize, 0);
|
|
||||||
|
|
||||||
for (size_t i = 0u; i < boneTracks.size(); i++)
|
|
||||||
{
|
|
||||||
if (encodedBoneQuats[i].flipQuat)
|
|
||||||
flipQuat[i / 8u] |= static_cast<uint8_t>(1u << (i % 8u));
|
|
||||||
|
|
||||||
if (QuatTypeUsesHalf(boneTracks[i].quat.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 : boneTracks)
|
|
||||||
stream::WriteCString(stream, bone.name);
|
|
||||||
|
|
||||||
for (auto i = 0uz; i < boneTracks.size(); i++)
|
|
||||||
{
|
|
||||||
WriteQuatTrack(stream, boneTracks[i].quat, encodedBoneQuats[i], numLoopFrames, useByteIndices);
|
|
||||||
WriteTransTrack(stream, boneTracks[i].trans, numLoopFrames, useByteIndices);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteNoteTracks(stream, asset);
|
|
||||||
}
|
|
||||||
} // namespace xanim
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Dumping/AbstractAssetDumper.h"
|
|
||||||
#include "Game/IW3/IW3.h"
|
|
||||||
|
|
||||||
namespace xanim
|
|
||||||
{
|
|
||||||
class DumperIW3 final : public AbstractAssetDumper<IW3::AssetXAnim>
|
|
||||||
{
|
|
||||||
protected:
|
|
||||||
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<IW3::AssetXAnim::Type>& asset) override;
|
|
||||||
};
|
|
||||||
} // namespace xanim
|
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "Game/IW4/Techset/PixelShaderDumperIW4.h"
|
#include "Game/IW4/Techset/PixelShaderDumperIW4.h"
|
||||||
#include "Game/IW4/Techset/TechsetDumperIW4.h"
|
#include "Game/IW4/Techset/TechsetDumperIW4.h"
|
||||||
#include "Game/IW4/Techset/VertexShaderDumperIW4.h"
|
#include "Game/IW4/Techset/VertexShaderDumperIW4.h"
|
||||||
|
#include "Game/IW4/XAnim/XAnimDumperIW4.h"
|
||||||
#include "Game/IW4/XModel/XModelDumperIW4.h"
|
#include "Game/IW4/XModel/XModelDumperIW4.h"
|
||||||
#include "Leaderboard/LeaderboardJsonDumperIW4.h"
|
#include "Leaderboard/LeaderboardJsonDumperIW4.h"
|
||||||
#include "LightDef/LightDefDumperIW4.h"
|
#include "LightDef/LightDefDumperIW4.h"
|
||||||
@@ -30,7 +31,7 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context)
|
|||||||
{
|
{
|
||||||
RegisterAssetDumper(std::make_unique<phys_preset::InfoStringDumperIW4>());
|
RegisterAssetDumper(std::make_unique<phys_preset::InfoStringDumperIW4>());
|
||||||
RegisterAssetDumper(std::make_unique<phys_collmap::DumperIW4>());
|
RegisterAssetDumper(std::make_unique<phys_collmap::DumperIW4>());
|
||||||
// REGISTER_DUMPER(AssetDumperXAnimParts)
|
RegisterAssetDumper(std::make_unique<xanim::DumperIW4>());
|
||||||
RegisterAssetDumper(std::make_unique<xmodel::DumperIW4>());
|
RegisterAssetDumper(std::make_unique<xmodel::DumperIW4>());
|
||||||
RegisterAssetDumper(std::make_unique<material::JsonDumperIW4>());
|
RegisterAssetDumper(std::make_unique<material::JsonDumperIW4>());
|
||||||
#ifdef EXPERIMENTAL_MATERIAL_COMPILATION
|
#ifdef EXPERIMENTAL_MATERIAL_COMPILATION
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "ObjWriterIW5.h"
|
#include "ObjWriterIW5.h"
|
||||||
|
|
||||||
|
#include "Game/IW4/XAnim/XAnimDumperIW4.h"
|
||||||
#include "Game/IW5/Image/ImageDumperIW5.h"
|
#include "Game/IW5/Image/ImageDumperIW5.h"
|
||||||
#include "Game/IW5/Material/MaterialJsonDumperIW5.h"
|
#include "Game/IW5/Material/MaterialJsonDumperIW5.h"
|
||||||
#include "Game/IW5/Techset/PixelShaderDumperIW5.h"
|
#include "Game/IW5/Techset/PixelShaderDumperIW5.h"
|
||||||
@@ -27,8 +28,7 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context)
|
|||||||
{
|
{
|
||||||
RegisterAssetDumper(std::make_unique<phys_preset::InfoStringDumperIW5>());
|
RegisterAssetDumper(std::make_unique<phys_preset::InfoStringDumperIW5>());
|
||||||
// REGISTER_DUMPER(AssetDumperPhysCollmap)
|
// REGISTER_DUMPER(AssetDumperPhysCollmap)
|
||||||
// REGISTER_DUMPER(AssetDumperXAnimParts)
|
RegisterAssetDumper(std::make_unique<xanim::DumperIW4>());
|
||||||
// REGISTER_DUMPER(AssetDumperXModelSurfs)
|
|
||||||
RegisterAssetDumper(std::make_unique<xmodel::DumperIW5>());
|
RegisterAssetDumper(std::make_unique<xmodel::DumperIW5>());
|
||||||
RegisterAssetDumper(std::make_unique<material::JsonDumperIW5>());
|
RegisterAssetDumper(std::make_unique<material::JsonDumperIW5>());
|
||||||
RegisterAssetDumper(std::make_unique<techset::PixelShaderDumperIW5>());
|
RegisterAssetDumper(std::make_unique<techset::PixelShaderDumperIW5>());
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "Game/T5/Image/ImageDumperT5.h"
|
#include "Game/T5/Image/ImageDumperT5.h"
|
||||||
#include "Game/T5/Material/MaterialJsonDumperT5.h"
|
#include "Game/T5/Material/MaterialJsonDumperT5.h"
|
||||||
#include "Game/T5/Techset/TechsetDumperT5.h"
|
#include "Game/T5/Techset/TechsetDumperT5.h"
|
||||||
|
#include "Game/T5/XAnim/XAnimDumperT5.h"
|
||||||
#include "Game/T5/XModel/XModelDumperT5.h"
|
#include "Game/T5/XModel/XModelDumperT5.h"
|
||||||
#include "LightDef/LightDefDumperT5.h"
|
#include "LightDef/LightDefDumperT5.h"
|
||||||
#include "Localize/LocalizeDumperT5.h"
|
#include "Localize/LocalizeDumperT5.h"
|
||||||
@@ -17,7 +18,7 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context)
|
|||||||
RegisterAssetDumper(std::make_unique<phys_preset::InfoStringDumperT5>());
|
RegisterAssetDumper(std::make_unique<phys_preset::InfoStringDumperT5>());
|
||||||
// REGISTER_DUMPER(AssetDumperPhysConstraints, m_phys_constraints)
|
// REGISTER_DUMPER(AssetDumperPhysConstraints, m_phys_constraints)
|
||||||
// REGISTER_DUMPER(AssetDumperDestructibleDef, m_destructible_def)
|
// REGISTER_DUMPER(AssetDumperDestructibleDef, m_destructible_def)
|
||||||
// REGISTER_DUMPER(AssetDumperXAnimParts, m_xanim_parts)
|
RegisterAssetDumper(std::make_unique<xanim::DumperT5>());
|
||||||
RegisterAssetDumper(std::make_unique<xmodel::DumperT5>());
|
RegisterAssetDumper(std::make_unique<xmodel::DumperT5>());
|
||||||
RegisterAssetDumper(std::make_unique<material::JsonDumperT5>());
|
RegisterAssetDumper(std::make_unique<material::JsonDumperT5>());
|
||||||
RegisterAssetDumper(std::make_unique<techset::DumperT5>(
|
RegisterAssetDumper(std::make_unique<techset::DumperT5>(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "Game/T6/Image/ImageDumperT6.h"
|
#include "Game/T6/Image/ImageDumperT6.h"
|
||||||
#include "Game/T6/Material/MaterialJsonDumperT6.h"
|
#include "Game/T6/Material/MaterialJsonDumperT6.h"
|
||||||
#include "Game/T6/Techset/TechsetDumperT6.h"
|
#include "Game/T6/Techset/TechsetDumperT6.h"
|
||||||
|
#include "Game/T6/XAnim/XAnimDumperT6.h"
|
||||||
#include "Game/T6/XModel/XModelDumperT6.h"
|
#include "Game/T6/XModel/XModelDumperT6.h"
|
||||||
#include "Leaderboard/LeaderboardJsonDumperT6.h"
|
#include "Leaderboard/LeaderboardJsonDumperT6.h"
|
||||||
#include "LightDef/LightDefDumperT6.h"
|
#include "LightDef/LightDefDumperT6.h"
|
||||||
@@ -33,7 +34,7 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context)
|
|||||||
RegisterAssetDumper(std::make_unique<phys_preset::InfoStringDumperT6>());
|
RegisterAssetDumper(std::make_unique<phys_preset::InfoStringDumperT6>());
|
||||||
RegisterAssetDumper(std::make_unique<phys_constraints::InfoStringDumperT6>());
|
RegisterAssetDumper(std::make_unique<phys_constraints::InfoStringDumperT6>());
|
||||||
// REGISTER_DUMPER(AssetDumperDestructibleDef, m_destructible_def)
|
// REGISTER_DUMPER(AssetDumperDestructibleDef, m_destructible_def)
|
||||||
// REGISTER_DUMPER(AssetDumperXAnimParts, m_xanim_parts)
|
RegisterAssetDumper(std::make_unique<xanim::DumperT6>());
|
||||||
RegisterAssetDumper(std::make_unique<xmodel::DumperT6>());
|
RegisterAssetDumper(std::make_unique<xmodel::DumperT6>());
|
||||||
RegisterAssetDumper(std::make_unique<material::JsonDumperT6>());
|
RegisterAssetDumper(std::make_unique<material::JsonDumperT6>());
|
||||||
RegisterAssetDumper(std::make_unique<techset::DumperT6>(
|
RegisterAssetDumper(std::make_unique<techset::DumperT6>(
|
||||||
|
|||||||
@@ -0,0 +1,555 @@
|
|||||||
|
#include "CompiledXAnimWriter.h"
|
||||||
|
|
||||||
|
#include "Utils/Alignment.h"
|
||||||
|
#include "Utils/Logging/Log.h"
|
||||||
|
#include "Utils/StreamUtils.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
using namespace xanim;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
uint8_t GetFlagsForVersion(const CompiledXAnimVersion version, const CommonXAnimParts& parts)
|
||||||
|
{
|
||||||
|
uint8_t flags = 0;
|
||||||
|
|
||||||
|
const auto hasDelta3D = parts.m_delta_track && parts.m_delta_track->m_quat && parts.m_delta_track->m_quat->Is3DTrack();
|
||||||
|
switch (version)
|
||||||
|
{
|
||||||
|
case CompiledXAnimVersion::VERSION_17:
|
||||||
|
if (parts.m_looped)
|
||||||
|
flags |= binary17::FLAG_LOOPED;
|
||||||
|
if (parts.m_delta_track)
|
||||||
|
flags |= binary17::FLAG_DELTA;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CompiledXAnimVersion::VERSION_18:
|
||||||
|
if (parts.m_looped)
|
||||||
|
flags |= binary18::FLAG_LOOPED;
|
||||||
|
if (parts.m_delta_track)
|
||||||
|
flags |= hasDelta3D ? binary18::FLAG_DELTA_3D : binary18::FLAG_DELTA;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CompiledXAnimVersion::VERSION_19:
|
||||||
|
{
|
||||||
|
const auto requiresT6Compatibility = hasDelta3D;
|
||||||
|
|
||||||
|
if (requiresT6Compatibility)
|
||||||
|
flags |= binary19::FLAG_T6_COMPATIBILITY;
|
||||||
|
if (parts.m_looped)
|
||||||
|
flags |= binary19::FLAG_LOOPED;
|
||||||
|
if (parts.m_delta_track)
|
||||||
|
flags |= hasDelta3D ? binary19::FLAG_T6_DELTA_3D : binary19::FLAG_DELTA;
|
||||||
|
if (parts.m_left_hand_grip_ik)
|
||||||
|
flags |= requiresT6Compatibility ? binary19::FLAG_T6_LEFT_HAND_GRIP_IK : binary19::FLAG_T5_LEFT_HAND_GRIP_IK;
|
||||||
|
if (parts.m_streamable && !requiresT6Compatibility)
|
||||||
|
flags |= binary19::FLAG_T5_STREAMABLE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[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 CommonDeltaQuatTrack& quat, const uint16_t numLoopFrames, const bool useByteIndices)
|
||||||
|
{
|
||||||
|
const auto numQuatIndices = static_cast<uint16_t>(quat.m_frames.size());
|
||||||
|
assert(numQuatIndices > 0);
|
||||||
|
|
||||||
|
stream::WriteValue(stream, numQuatIndices);
|
||||||
|
|
||||||
|
const auto encodedDeltaQuatFrames = EncodeQuatFrames(quat.m_frames, false);
|
||||||
|
|
||||||
|
if (numQuatIndices == 1)
|
||||||
|
{
|
||||||
|
assert(encodedDeltaQuatFrames.m_stored_values.size() == 3);
|
||||||
|
stream::WriteValue(stream, encodedDeltaQuatFrames.m_stored_values[0]);
|
||||||
|
stream::WriteValue(stream, encodedDeltaQuatFrames.m_stored_values[1]);
|
||||||
|
stream::WriteValue(stream, encodedDeltaQuatFrames.m_stored_values[2]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
assert(numQuatIndices > 1u);
|
||||||
|
assert(quat.m_indices.size() == numQuatIndices);
|
||||||
|
assert(encodedDeltaQuatFrames.m_stored_values.size() == numQuatIndices * 3);
|
||||||
|
|
||||||
|
WriteIndicesIfNeeded(stream, quat.m_indices, numLoopFrames, useByteIndices);
|
||||||
|
for (const auto value : encodedDeltaQuatFrames.m_stored_values)
|
||||||
|
stream::WriteValue(stream, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteDeltaQuat2Track(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)
|
||||||
|
{
|
||||||
|
if (delta.m_quat && delta.m_quat->Is3DTrack())
|
||||||
|
WriteDeltaQuatTrack(stream, *delta.m_quat, numLoopFrames, useByteIndices);
|
||||||
|
else
|
||||||
|
WriteDeltaQuat2Track(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_frames_u8.size() == frameCount);
|
||||||
|
|
||||||
|
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_frames_u8.data(), trans.m_frames_u8.size() * sizeof(CommonVec3U8));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TransType::FULL_TRANS:
|
||||||
|
{
|
||||||
|
const auto frameCount = trans.m_indices.size();
|
||||||
|
assert(frameCount > 0uz);
|
||||||
|
assert(trans.m_frames_u16.size() == frameCount);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
stream::Write(stream, trans.m_frames_u16.data(), trans.m_frames_u16.size() * sizeof(CommonVec3U16));
|
||||||
|
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, CompiledXAnimVersion version)
|
||||||
|
{
|
||||||
|
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 = GetFlagsForVersion(version, parts);
|
||||||
|
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>(version));
|
||||||
|
// 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 (version == CompiledXAnimVersion::VERSION_19 && parts.m_streamable && (flags & binary19::FLAG_T6_COMPATIBILITY) == 0)
|
||||||
|
stream::WriteValue(stream, parts.m_primed_length);
|
||||||
|
|
||||||
|
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,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "XAnim/BinaryXAnimCommon.h"
|
||||||
|
#include "XAnim/XAnimCommon.h"
|
||||||
|
|
||||||
|
#include <ostream>
|
||||||
|
|
||||||
|
namespace xanim
|
||||||
|
{
|
||||||
|
void WriteCompiledXAnim(std::ostream& stream, const CommonXAnimParts& parts, CompiledXAnimVersion version);
|
||||||
|
}
|
||||||
@@ -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_frames_u8)::value_type) == sizeof(uint8_t) * 3u);
|
||||||
|
trans.m_frames_u8.resize(frameCount);
|
||||||
|
cursor.ReadRandomDataByte(trans.m_frames_u8.data(), frameCount * 3u);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_frames_u16)::value_type) == sizeof(int16_t) * 3u);
|
||||||
|
trans.m_frames_u16.resize(frameCount);
|
||||||
|
cursor.ReadRandomDataShort(trans.m_frames_u16.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
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
#options GAME(IW3, IW4, IW5, T5, T6)
|
||||||
|
|
||||||
|
#filename "Game/" + GAME + "/XAnim/XAnimDumper" + GAME + ".cpp"
|
||||||
|
|
||||||
|
#set DUMPER_HEADER "\"XAnimDumper" + GAME + ".h\""
|
||||||
|
|
||||||
|
#if GAME == "IW3"
|
||||||
|
#define FEATURE_IW3
|
||||||
|
#elif GAME == "IW4"
|
||||||
|
#define FEATURE_IW4
|
||||||
|
#define HAS_DELTA_QUAT_3D
|
||||||
|
#elif GAME == "IW5"
|
||||||
|
#define FEATURE_IW5
|
||||||
|
#define HAS_DELTA_QUAT_3D
|
||||||
|
#elif GAME == "T5"
|
||||||
|
#define FEATURE_T5
|
||||||
|
#elif GAME == "T6"
|
||||||
|
#define FEATURE_T6
|
||||||
|
#define HAS_DELTA_QUAT_3D
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(FEATURE_IW4) || defined(FEATURE_IW5)
|
||||||
|
#define IS_LOOPED static_cast<bool>(parts.flags & ANIM_LOOP)
|
||||||
|
#define HAS_DELTA static_cast<bool>(parts.flags & ANIM_DELTA)
|
||||||
|
#define HAS_DELTA_3D static_cast<bool>(parts.flags & ANIM_DELTA_3D)
|
||||||
|
#else
|
||||||
|
#define IS_LOOPED parts.bLoop
|
||||||
|
#define HAS_DELTA parts.bDelta
|
||||||
|
#define HAS_DELTA_3D parts.bDelta3D
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// This file was templated.
|
||||||
|
// See XAnimDumper.cpp.template.
|
||||||
|
// Do not modify, changes will be lost.
|
||||||
|
|
||||||
|
#include DUMPER_HEADER
|
||||||
|
|
||||||
|
#include "XAnim/CompiledXAnimWriter.h"
|
||||||
|
#include "XAnim/FlatXAnimReader.h"
|
||||||
|
#include "XAnim/XAnimCommon.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <expected>
|
||||||
|
#include <format>
|
||||||
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace xanim;
|
||||||
|
using namespace GAME;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
[[nodiscard]] const std::string& ResolveScriptString(const ScriptString value, const XAssetInfo<XAnimParts>& asset)
|
||||||
|
{
|
||||||
|
assert(asset.m_zone != nullptr && value < asset.m_zone->m_script_strings.Count());
|
||||||
|
return asset.m_zone->m_script_strings[value];
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] uint16_t GetNumLoopFrames(const XAnimParts& parts)
|
||||||
|
{
|
||||||
|
assert(parts.numframes < 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.numframes + 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool UseByteIndices(const XAnimParts& parts)
|
||||||
|
{
|
||||||
|
return parts.numframes < 256;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<CommonXAnimNotifyInfo> ConvertNotifies(const XAnimParts& parts, const XAssetInfo<AssetXAnim::Type>& assetInfo)
|
||||||
|
{
|
||||||
|
std::vector<CommonXAnimNotifyInfo> result;
|
||||||
|
if (!parts.notify || parts.notifyCount == 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
for (auto i = 0u; i < parts.notifyCount; i++)
|
||||||
|
{
|
||||||
|
const auto& notify = parts.notify[i];
|
||||||
|
CommonXAnimNotifyInfo commonNotify;
|
||||||
|
|
||||||
|
commonNotify.m_name = ResolveScriptString(notify.name, assetInfo);
|
||||||
|
commonNotify.m_time = notify.time;
|
||||||
|
|
||||||
|
result.emplace_back(std::move(commonNotify));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAS_DELTA_QUAT_3D
|
||||||
|
#define DELTA_QUAT_2D_STRUCT XAnimDeltaPartQuat2
|
||||||
|
#else
|
||||||
|
#define DELTA_QUAT_2D_STRUCT XAnimDeltaPartQuat
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAS_DELTA_QUAT_3D
|
||||||
|
CommonDeltaQuatTrack ConvertDeltaQuatTrack(const XAnimDeltaPartQuat& deltaQuat, const bool useByteIndices, const uint16_t numLoopFrames)
|
||||||
|
{
|
||||||
|
CommonDeltaQuatTrack result;
|
||||||
|
if (deltaQuat.size > 0)
|
||||||
|
{
|
||||||
|
const auto frameCount = static_cast<size_t>(deltaQuat.size) + 1u;
|
||||||
|
result.m_frames.reserve(frameCount);
|
||||||
|
result.m_indices.reserve(frameCount);
|
||||||
|
|
||||||
|
for (size_t i = 0u; i < frameCount; i++)
|
||||||
|
{
|
||||||
|
result.m_frames.emplace_back(deltaQuat.u.frames.frames[i].value[0],
|
||||||
|
deltaQuat.u.frames.frames[i].value[1],
|
||||||
|
deltaQuat.u.frames.frames[i].value[2],
|
||||||
|
deltaQuat.u.frames.frames[i].value[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useByteIndices)
|
||||||
|
{
|
||||||
|
for (size_t i = 0u; i < frameCount; i++)
|
||||||
|
result.m_indices.emplace_back(deltaQuat.u.frames.indices._1[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (size_t i = 0u; i < frameCount; i++)
|
||||||
|
result.m_indices.emplace_back(deltaQuat.u.frames.indices._2[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(result.m_indices.size() <= numLoopFrames);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.m_frames.emplace_back(deltaQuat.u.frame0.value[0], deltaQuat.u.frame0.value[1], deltaQuat.u.frame0.value[2], deltaQuat.u.frame0.value[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
CommonDeltaQuatTrack ConvertDeltaQuat2Track(const DELTA_QUAT_2D_STRUCT& deltaQuat, const bool useByteIndices, const uint16_t numLoopFrames)
|
||||||
|
{
|
||||||
|
CommonDeltaQuatTrack result;
|
||||||
|
if (deltaQuat.size > 0)
|
||||||
|
{
|
||||||
|
const auto frameCount = static_cast<size_t>(deltaQuat.size) + 1u;
|
||||||
|
result.m_frames2.reserve(frameCount);
|
||||||
|
result.m_indices.reserve(frameCount);
|
||||||
|
|
||||||
|
for (size_t i = 0u; i < frameCount; i++)
|
||||||
|
result.m_frames2.emplace_back(deltaQuat.u.frames.frames[i].value[0], deltaQuat.u.frames.frames[i].value[1]);
|
||||||
|
|
||||||
|
if (useByteIndices)
|
||||||
|
{
|
||||||
|
for (size_t i = 0u; i < frameCount; i++)
|
||||||
|
result.m_indices.emplace_back(deltaQuat.u.frames.indices._1[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (size_t i = 0u; i < frameCount; i++)
|
||||||
|
result.m_indices.emplace_back(deltaQuat.u.frames.indices._2[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(result.m_indices.size() <= numLoopFrames);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.m_frames2.emplace_back(deltaQuat.u.frame0.value[0], deltaQuat.u.frame0.value[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonDeltaTransTrack ConvertDeltaTransTrack(const XAnimPartTrans& deltaTrans, const bool useByteIndices)
|
||||||
|
{
|
||||||
|
CommonDeltaTransTrack result;
|
||||||
|
if (deltaTrans.size > 0)
|
||||||
|
{
|
||||||
|
result.m_small_trans = deltaTrans.smallTrans;
|
||||||
|
result.m_mins = {
|
||||||
|
deltaTrans.u.frames.mins.x,
|
||||||
|
deltaTrans.u.frames.mins.y,
|
||||||
|
deltaTrans.u.frames.mins.z,
|
||||||
|
};
|
||||||
|
result.m_size = {
|
||||||
|
deltaTrans.u.frames.size.x,
|
||||||
|
deltaTrans.u.frames.size.y,
|
||||||
|
deltaTrans.u.frames.size.z,
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto frameCount = static_cast<size_t>(deltaTrans.size) + 1u;
|
||||||
|
result.m_indices.reserve(frameCount);
|
||||||
|
if (useByteIndices)
|
||||||
|
{
|
||||||
|
for (size_t i = 0u; i < frameCount; i++)
|
||||||
|
result.m_indices.emplace_back(static_cast<uint8_t>(deltaTrans.u.frames.indices._1[i]));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (size_t i = 0u; i < frameCount; i++)
|
||||||
|
result.m_indices.emplace_back(deltaTrans.u.frames.indices._2[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaTrans.smallTrans)
|
||||||
|
{
|
||||||
|
result.m_frames_u8.reserve(frameCount);
|
||||||
|
for (size_t i = 0u; i < frameCount; i++)
|
||||||
|
{
|
||||||
|
result.m_frames_u8.emplace_back(
|
||||||
|
deltaTrans.u.frames.frames._1[i][0], deltaTrans.u.frames.frames._1[i][1], deltaTrans.u.frames.frames._1[i][2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.m_frames_u16.reserve(frameCount);
|
||||||
|
for (size_t i = 0u; i < frameCount; i++)
|
||||||
|
{
|
||||||
|
result.m_frames_u16.emplace_back(
|
||||||
|
deltaTrans.u.frames.frames._2[i][0], deltaTrans.u.frames.frames._2[i][1], deltaTrans.u.frames.frames._2[i][2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.m_constant = std::array<float, 3>({
|
||||||
|
deltaTrans.u.frame0.v[0],
|
||||||
|
deltaTrans.u.frame0.v[1],
|
||||||
|
deltaTrans.u.frame0.v[2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<CommonXAnimDeltaTrack> ConvertDeltaTrack(const XAnimParts& parts, const bool useByteIndices, const uint16_t numLoopFrames)
|
||||||
|
{
|
||||||
|
if (!parts.deltaPart)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
auto result = std::make_unique<CommonXAnimDeltaTrack>();
|
||||||
|
|
||||||
|
#ifdef HAS_DELTA_QUAT_3D
|
||||||
|
assert(static_cast<bool>(parts.deltaPart) == HAS_DELTA || HAS_DELTA_3D);
|
||||||
|
#else
|
||||||
|
assert(static_cast<bool>(parts.deltaPart) == HAS_DELTA);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!parts.deltaPart)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
#ifdef HAS_DELTA_QUAT_3D
|
||||||
|
if (parts.deltaPart->quat2)
|
||||||
|
{
|
||||||
|
result->m_quat = ConvertDeltaQuat2Track(*parts.deltaPart->quat2, useByteIndices, numLoopFrames);
|
||||||
|
assert(!parts.deltaPart->quat);
|
||||||
|
}
|
||||||
|
else if (parts.deltaPart->quat)
|
||||||
|
{
|
||||||
|
result->m_quat = ConvertDeltaQuatTrack(*parts.deltaPart->quat, useByteIndices, numLoopFrames);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (parts.deltaPart->quat)
|
||||||
|
result->m_quat = ConvertDeltaQuat2Track(*parts.deltaPart->quat, useByteIndices, numLoopFrames);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (parts.deltaPart->trans)
|
||||||
|
result->m_trans = ConvertDeltaTransTrack(*parts.deltaPart->trans, useByteIndices);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#set CLASS_NAME "Dumper" + GAME
|
||||||
|
|
||||||
|
namespace xanim
|
||||||
|
{
|
||||||
|
void CLASS_NAME::DumpAsset(AssetDumpingContext& context, const XAssetInfo<AssetXAnim::Type>& asset)
|
||||||
|
{
|
||||||
|
const auto& parts = *asset.Asset();
|
||||||
|
|
||||||
|
const auto useByteIndices = UseByteIndices(parts);
|
||||||
|
const auto numLoopFrames = GetNumLoopFrames(parts);
|
||||||
|
|
||||||
|
std::vector<std::string> boneNames;
|
||||||
|
if (parts.names)
|
||||||
|
{
|
||||||
|
boneNames.reserve(parts.boneCount[PART_TYPE_ALL]);
|
||||||
|
for (auto i = 0u; i < parts.boneCount[PART_TYPE_ALL]; i++)
|
||||||
|
boneNames.emplace_back(asset.m_zone->m_script_strings.Value(parts.names[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const XAnimBoneCounts boneCounts(parts.boneCount[PART_TYPE_NO_QUAT],
|
||||||
|
parts.boneCount[PART_TYPE_HALF_QUAT],
|
||||||
|
parts.boneCount[PART_TYPE_FULL_QUAT],
|
||||||
|
parts.boneCount[PART_TYPE_HALF_QUAT_NO_SIZE],
|
||||||
|
parts.boneCount[PART_TYPE_FULL_QUAT_NO_SIZE],
|
||||||
|
parts.boneCount[PART_TYPE_SMALL_TRANS],
|
||||||
|
parts.boneCount[PART_TYPE_TRANS],
|
||||||
|
parts.boneCount[PART_TYPE_TRANS_NO_SIZE],
|
||||||
|
parts.boneCount[PART_TYPE_NO_TRANS]);
|
||||||
|
|
||||||
|
// Root indices should only ever be used when it is !useByteIndices, therefore we should be safe to always use the short version
|
||||||
|
assert(!useByteIndices || parts.indices._1 == nullptr);
|
||||||
|
FlatXAnimReadCursor flatData(parts.dataByte,
|
||||||
|
parts.dataByteCount,
|
||||||
|
parts.dataShort,
|
||||||
|
parts.dataShortCount,
|
||||||
|
parts.dataInt,
|
||||||
|
parts.dataIntCount,
|
||||||
|
parts.randomDataByte,
|
||||||
|
parts.randomDataByteCount,
|
||||||
|
parts.randomDataShort,
|
||||||
|
parts.randomDataShortCount,
|
||||||
|
parts.indices._2,
|
||||||
|
parts.indexCount);
|
||||||
|
|
||||||
|
auto maybeBoneTracks = CreateBoneTracksFromFlatData(std::move(boneNames), boneCounts, flatData, useByteIndices);
|
||||||
|
if (!maybeBoneTracks.has_value())
|
||||||
|
{
|
||||||
|
con::error("Failed to reconstruct bone tracks from XAnim {}: {}", parts.name, maybeBoneTracks.error());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto assetFile = context.OpenAssetFile(GetCompiledFileNameForAssetName(asset.m_name));
|
||||||
|
if (!assetFile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CommonXAnimParts commonParts;
|
||||||
|
commonParts.m_num_frames = parts.numframes;
|
||||||
|
commonParts.m_looped = IS_LOOPED;
|
||||||
|
#if defined(FEATURE_T5) || defined(FEATURE_T6)
|
||||||
|
commonParts.m_left_hand_grip_ik = parts.bLeftHandGripIK;
|
||||||
|
#endif
|
||||||
|
#if defined(FEATURE_T5)
|
||||||
|
commonParts.m_streamable = parts.bStreamable;
|
||||||
|
#endif
|
||||||
|
commonParts.m_frame_rate = parts.framerate;
|
||||||
|
#if defined(FEATURE_T5) || defined(FEATURE_T6)
|
||||||
|
commonParts.m_primed_length = parts.primedLength;
|
||||||
|
#endif
|
||||||
|
commonParts.m_asset_type = parts.assetType;
|
||||||
|
commonParts.m_bone_tracks = std::move(maybeBoneTracks).value();
|
||||||
|
commonParts.m_notifies = ConvertNotifies(parts, asset);
|
||||||
|
commonParts.m_delta_track = ConvertDeltaTrack(parts, useByteIndices, numLoopFrames);
|
||||||
|
|
||||||
|
WriteCompiledXAnim(
|
||||||
|
*assetFile,
|
||||||
|
commonParts,
|
||||||
|
#if defined(FEATURE_IW3)
|
||||||
|
CompiledXAnimVersion::VERSION_17
|
||||||
|
#elif defined(FEATURE_IW4) || defined(FEATURE_IW5)
|
||||||
|
// Make sure we dump as many anims as possible in an IW3 modtools compatible way
|
||||||
|
HAS_DELTA_3D ? CompiledXAnimVersion::VERSION_18 : CompiledXAnimVersion::VERSION_17
|
||||||
|
#elif defined(FEATURE_T5) || defined(FEATURE_T6)
|
||||||
|
CompiledXAnimVersion::VERSION_19
|
||||||
|
#endif
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} // namespace xanim
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#options GAME(IW3, IW4, IW5, T5, T6)
|
||||||
|
|
||||||
|
#filename "Game/" + GAME + "/XAnim/XAnimDumper" + GAME + ".h"
|
||||||
|
|
||||||
|
#set GAME_HEADER "\"Game/" + GAME + "/" + GAME + ".h\""
|
||||||
|
|
||||||
|
// This file was templated.
|
||||||
|
// See XAnimDumper.h.template.
|
||||||
|
// Do not modify, changes will be lost.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Dumping/AbstractAssetDumper.h"
|
||||||
|
#include GAME_HEADER
|
||||||
|
|
||||||
|
#set CLASS_NAME "Dumper" + GAME
|
||||||
|
|
||||||
|
namespace xanim
|
||||||
|
{
|
||||||
|
class CLASS_NAME final : public AbstractAssetDumper<GAME::AssetXAnim>
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<GAME::AssetXAnim::Type>& asset) override;
|
||||||
|
};
|
||||||
|
} // namespace xanim
|
||||||
@@ -3,6 +3,8 @@ SystemTests = {}
|
|||||||
function SystemTests:include(includes)
|
function SystemTests:include(includes)
|
||||||
if includes:handle(self:name()) then
|
if includes:handle(self:name()) then
|
||||||
includedirs {
|
includedirs {
|
||||||
|
"%{wks.location}/src/ObjLoading",
|
||||||
|
"%{wks.location}/src/ObjWriting",
|
||||||
path.join(TestFolder(), "SystemTests")
|
path.join(TestFolder(), "SystemTests")
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,71 @@
|
|||||||
|
#include "Game/IW4/XAnim/XAnimDumperIW4.h"
|
||||||
|
#include "Game/IW4/XAnim/XAnimLoaderIW4.h"
|
||||||
|
#include "OatTestPaths.h"
|
||||||
|
#include "SearchPath/MockOutputPath.h"
|
||||||
|
#include "SearchPath/MockSearchPath.h"
|
||||||
|
#include "ZoneLoading.h"
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/generators/catch_generators.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace std::literals;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
TEST_CASE("XAnim Loading/Dumping (IW4)", "[iw4][system]")
|
||||||
|
{
|
||||||
|
MockSearchPath searchPath;
|
||||||
|
|
||||||
|
const auto [animName] = GENERATE(Catch::Generators::table<std::string>({
|
||||||
|
{"test_anim"},
|
||||||
|
{"test_anim2"},
|
||||||
|
}));
|
||||||
|
|
||||||
|
CAPTURE(animName);
|
||||||
|
|
||||||
|
const auto filePath = oat::paths::GetTestDirectory() / std::format("SystemTests/Game/IW4/XAnim/{}", animName);
|
||||||
|
const auto fileSize = static_cast<size_t>(fs::file_size(filePath));
|
||||||
|
|
||||||
|
std::ifstream file(filePath, std::ios::binary);
|
||||||
|
REQUIRE(file.is_open());
|
||||||
|
|
||||||
|
const auto data = std::make_unique<char[]>(fileSize);
|
||||||
|
file.read(data.get(), fileSize);
|
||||||
|
|
||||||
|
searchPath.AddFileData(std::format("xanim/{}", animName), std::string(data.get(), fileSize));
|
||||||
|
|
||||||
|
Zone zone("MockZone", 0, GameId::IW4, GamePlatform::PC);
|
||||||
|
AssetCreatorCollection creatorCollection(zone);
|
||||||
|
IgnoredAssetLookup ignoredAssetLookup;
|
||||||
|
MemoryManager memoryManager;
|
||||||
|
const auto loader = xanim::CreateLoaderIW4(memoryManager, searchPath, zone);
|
||||||
|
AssetCreationContext context(zone, &creatorCollection, &ignoredAssetLookup);
|
||||||
|
|
||||||
|
const auto result = loader->CreateAsset(animName, context);
|
||||||
|
|
||||||
|
REQUIRE(result.HasBeenSuccessful());
|
||||||
|
const auto* assetInfo = reinterpret_cast<XAssetInfo<IW4::AssetXAnim::Type>*>(result.GetAssetInfo());
|
||||||
|
const auto* parts = assetInfo->Asset();
|
||||||
|
|
||||||
|
REQUIRE(parts->name == animName);
|
||||||
|
REQUIRE(parts->numframes > 0);
|
||||||
|
|
||||||
|
MockSearchPath mockObjPath;
|
||||||
|
MockOutputPath mockOutput;
|
||||||
|
xanim::DumperIW4 dumper;
|
||||||
|
AssetDumpingContext dumpingContext(zone, "", mockOutput, mockObjPath, std::nullopt);
|
||||||
|
dumper.Dump(dumpingContext);
|
||||||
|
|
||||||
|
const auto* outAnimFile = mockOutput.GetMockedFile(std::format("xanim/{}", animName));
|
||||||
|
REQUIRE(outAnimFile != nullptr);
|
||||||
|
|
||||||
|
REQUIRE(outAnimFile->m_data.size() == fileSize);
|
||||||
|
REQUIRE(memcmp(outAnimFile->m_data.data(), data.get(), fileSize) == 0);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,71 @@
|
|||||||
|
#include "Game/IW5/XAnim/XAnimDumperIW5.h"
|
||||||
|
#include "Game/IW5/XAnim/XAnimLoaderIW5.h"
|
||||||
|
#include "OatTestPaths.h"
|
||||||
|
#include "SearchPath/MockOutputPath.h"
|
||||||
|
#include "SearchPath/MockSearchPath.h"
|
||||||
|
#include "ZoneLoading.h"
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/generators/catch_generators.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace std::literals;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
TEST_CASE("XAnim Loading/Dumping (IW5)", "[iw5][system]")
|
||||||
|
{
|
||||||
|
MockSearchPath searchPath;
|
||||||
|
|
||||||
|
const auto [animName] = GENERATE(Catch::Generators::table<std::string>({
|
||||||
|
{"test_anim"},
|
||||||
|
{"test_anim2"},
|
||||||
|
}));
|
||||||
|
|
||||||
|
CAPTURE(animName);
|
||||||
|
|
||||||
|
const auto filePath = oat::paths::GetTestDirectory() / std::format("SystemTests/Game/IW5/XAnim/{}", animName);
|
||||||
|
const auto fileSize = static_cast<size_t>(fs::file_size(filePath));
|
||||||
|
|
||||||
|
std::ifstream file(filePath, std::ios::binary);
|
||||||
|
REQUIRE(file.is_open());
|
||||||
|
|
||||||
|
const auto data = std::make_unique<char[]>(fileSize);
|
||||||
|
file.read(data.get(), fileSize);
|
||||||
|
|
||||||
|
searchPath.AddFileData(std::format("xanim/{}", animName), std::string(data.get(), fileSize));
|
||||||
|
|
||||||
|
Zone zone("MockZone", 0, GameId::IW5, GamePlatform::PC);
|
||||||
|
AssetCreatorCollection creatorCollection(zone);
|
||||||
|
IgnoredAssetLookup ignoredAssetLookup;
|
||||||
|
MemoryManager memoryManager;
|
||||||
|
const auto loader = xanim::CreateLoaderIW5(memoryManager, searchPath, zone);
|
||||||
|
AssetCreationContext context(zone, &creatorCollection, &ignoredAssetLookup);
|
||||||
|
|
||||||
|
const auto result = loader->CreateAsset(animName, context);
|
||||||
|
|
||||||
|
REQUIRE(result.HasBeenSuccessful());
|
||||||
|
const auto* assetInfo = reinterpret_cast<XAssetInfo<IW5::AssetXAnim::Type>*>(result.GetAssetInfo());
|
||||||
|
const auto* parts = assetInfo->Asset();
|
||||||
|
|
||||||
|
REQUIRE(parts->name == animName);
|
||||||
|
REQUIRE(parts->numframes > 0);
|
||||||
|
|
||||||
|
MockSearchPath mockObjPath;
|
||||||
|
MockOutputPath mockOutput;
|
||||||
|
xanim::DumperIW5 dumper;
|
||||||
|
AssetDumpingContext dumpingContext(zone, "", mockOutput, mockObjPath, std::nullopt);
|
||||||
|
dumper.Dump(dumpingContext);
|
||||||
|
|
||||||
|
const auto* outAnimFile = mockOutput.GetMockedFile(std::format("xanim/{}", animName));
|
||||||
|
REQUIRE(outAnimFile != nullptr);
|
||||||
|
|
||||||
|
REQUIRE(outAnimFile->m_data.size() == fileSize);
|
||||||
|
REQUIRE(memcmp(outAnimFile->m_data.data(), data.get(), fileSize) == 0);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
Binary file not shown.
@@ -0,0 +1,62 @@
|
|||||||
|
#include "Game/T5/XAnim/XAnimDumperT5.h"
|
||||||
|
#include "Game/T5/XAnim/XAnimLoaderT5.h"
|
||||||
|
#include "OatTestPaths.h"
|
||||||
|
#include "SearchPath/MockOutputPath.h"
|
||||||
|
#include "SearchPath/MockSearchPath.h"
|
||||||
|
#include "ZoneLoading.h"
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace std::literals;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
TEST_CASE("XAnim Loading/Dumping (T5)", "[t5][system]")
|
||||||
|
{
|
||||||
|
MockSearchPath searchPath;
|
||||||
|
|
||||||
|
const auto filePath = oat::paths::GetTestDirectory() / "SystemTests/Game/T5/XAnim/test_anim";
|
||||||
|
const auto fileSize = static_cast<size_t>(fs::file_size(filePath));
|
||||||
|
|
||||||
|
std::ifstream file(filePath, std::ios::binary);
|
||||||
|
REQUIRE(file.is_open());
|
||||||
|
|
||||||
|
const auto data = std::make_unique<char[]>(fileSize);
|
||||||
|
file.read(data.get(), fileSize);
|
||||||
|
|
||||||
|
searchPath.AddFileData("xanim/test_anim", std::string(data.get(), fileSize));
|
||||||
|
|
||||||
|
Zone zone("MockZone", 0, GameId::T5, GamePlatform::PC);
|
||||||
|
AssetCreatorCollection creatorCollection(zone);
|
||||||
|
IgnoredAssetLookup ignoredAssetLookup;
|
||||||
|
MemoryManager memoryManager;
|
||||||
|
const auto loader = xanim::CreateLoaderT5(memoryManager, searchPath, zone);
|
||||||
|
AssetCreationContext context(zone, &creatorCollection, &ignoredAssetLookup);
|
||||||
|
|
||||||
|
const auto result = loader->CreateAsset("test_anim", context);
|
||||||
|
|
||||||
|
REQUIRE(result.HasBeenSuccessful());
|
||||||
|
const auto* assetInfo = reinterpret_cast<XAssetInfo<T5::AssetXAnim::Type>*>(result.GetAssetInfo());
|
||||||
|
const auto* parts = assetInfo->Asset();
|
||||||
|
|
||||||
|
REQUIRE(parts->name == "test_anim"s);
|
||||||
|
REQUIRE(parts->numframes > 0);
|
||||||
|
|
||||||
|
MockSearchPath mockObjPath;
|
||||||
|
MockOutputPath mockOutput;
|
||||||
|
xanim::DumperT5 dumper;
|
||||||
|
AssetDumpingContext dumpingContext(zone, "", mockOutput, mockObjPath, std::nullopt);
|
||||||
|
dumper.Dump(dumpingContext);
|
||||||
|
|
||||||
|
const auto* outAnimFile = mockOutput.GetMockedFile("xanim/test_anim");
|
||||||
|
REQUIRE(outAnimFile != nullptr);
|
||||||
|
|
||||||
|
REQUIRE(outAnimFile->m_data.size() == fileSize);
|
||||||
|
REQUIRE(memcmp(outAnimFile->m_data.data(), data.get(), fileSize) == 0);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,71 @@
|
|||||||
|
#include "Game/T6/XAnim/XAnimDumperT6.h"
|
||||||
|
#include "Game/T6/XAnim/XAnimLoaderT6.h"
|
||||||
|
#include "OatTestPaths.h"
|
||||||
|
#include "SearchPath/MockOutputPath.h"
|
||||||
|
#include "SearchPath/MockSearchPath.h"
|
||||||
|
#include "ZoneLoading.h"
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/generators/catch_generators.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace std::literals;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
TEST_CASE("XAnim Loading/Dumping (T6)", "[t6][system]")
|
||||||
|
{
|
||||||
|
MockSearchPath searchPath;
|
||||||
|
|
||||||
|
const auto [animName] = GENERATE(Catch::Generators::table<std::string>({
|
||||||
|
{"test_anim"},
|
||||||
|
{"test_anim2"},
|
||||||
|
}));
|
||||||
|
|
||||||
|
CAPTURE(animName);
|
||||||
|
|
||||||
|
const auto filePath = oat::paths::GetTestDirectory() / std::format("SystemTests/Game/T6/XAnim/{}", animName);
|
||||||
|
const auto fileSize = static_cast<size_t>(fs::file_size(filePath));
|
||||||
|
|
||||||
|
std::ifstream file(filePath, std::ios::binary);
|
||||||
|
REQUIRE(file.is_open());
|
||||||
|
|
||||||
|
const auto data = std::make_unique<char[]>(fileSize);
|
||||||
|
file.read(data.get(), fileSize);
|
||||||
|
|
||||||
|
searchPath.AddFileData(std::format("xanim/{}", animName), std::string(data.get(), fileSize));
|
||||||
|
|
||||||
|
Zone zone("MockZone", 0, GameId::T6, GamePlatform::PC);
|
||||||
|
AssetCreatorCollection creatorCollection(zone);
|
||||||
|
IgnoredAssetLookup ignoredAssetLookup;
|
||||||
|
MemoryManager memoryManager;
|
||||||
|
const auto loader = xanim::CreateLoaderT6(memoryManager, searchPath, zone);
|
||||||
|
AssetCreationContext context(zone, &creatorCollection, &ignoredAssetLookup);
|
||||||
|
|
||||||
|
const auto result = loader->CreateAsset(animName, context);
|
||||||
|
|
||||||
|
REQUIRE(result.HasBeenSuccessful());
|
||||||
|
const auto* assetInfo = reinterpret_cast<XAssetInfo<T6::AssetXAnim::Type>*>(result.GetAssetInfo());
|
||||||
|
const auto* parts = assetInfo->Asset();
|
||||||
|
|
||||||
|
REQUIRE(parts->name == animName);
|
||||||
|
REQUIRE(parts->numframes > 0);
|
||||||
|
|
||||||
|
MockSearchPath mockObjPath;
|
||||||
|
MockOutputPath mockOutput;
|
||||||
|
xanim::DumperT6 dumper;
|
||||||
|
AssetDumpingContext dumpingContext(zone, "", mockOutput, mockObjPath, std::nullopt);
|
||||||
|
dumper.Dump(dumpingContext);
|
||||||
|
|
||||||
|
const auto* outAnimFile = mockOutput.GetMockedFile(std::format("xanim/{}", animName));
|
||||||
|
REQUIRE(outAnimFile != nullptr);
|
||||||
|
|
||||||
|
REQUIRE(outAnimFile->m_data.size() == fileSize);
|
||||||
|
REQUIRE(memcmp(outAnimFile->m_data.data(), data.get(), fileSize) == 0);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user