mirror of
https://github.com/Laupetin/OpenAssetTools.git
synced 2026-06-03 07:12:33 +00:00
feat: IW3 xanim dumping/loading in CoD4 Mod Tools raw binary format (#768)
* feat: IW3 dump xanim to cod4 mod tools compatible binary * chore: add XAnimPartType enum to game headers * chore: use XAnimPartType in XAnimDumperIW3 * chore: extract xanim filename into XAnimCommon * chore: prefer emplace_back over push_back * chore: small code style improvements * chore: use proper unsigned types for XAnimParts structs * chore: use better understandable calculations for bitfields * chore: use game names for parts * chore: rename method to WriteNoteTracks * chore: adds comments and improve clearity of what the game does * chore: extract stream writing methods into StreamUtils * chore: use vec3 for XAnimPartTransFrames mins and size * chore: properly differ between XQuat and XQuat2 structs * chore: use constants for xanim flags * chore: use optional for delta track quats and trans * chore: split delta track writing methods into quat and trans * chore: add assertion for bDelta * chore: simplify quat frame encoding indexing * chore: simplify float to int bit casting * chore: do not throw exception on failing to reconstruct bone tracks * feat: add xanim loader for iw3 * fix: make sure to sort quats and trans like the game * chore: prevent empty dumped files on bad xanim data * chore: ensure no exception on zero frames in xanim notifies * test: add system test for iw3 xanims --------- Co-authored-by: Jan Laupetin <jan@laupetin.net>
This commit is contained in:
@@ -12,7 +12,7 @@ The following section specify which assets are supported to be dumped to disk (u
|
||||
| Asset Type | Dumping Support | Loading Support | Notes |
|
||||
| -------------------- | --------------- | --------------- | ---------------------------------------------------------------------------- |
|
||||
| PhysPreset | ✅ | ✅ | |
|
||||
| XAnimParts | ❌ | ❌ | |
|
||||
| XAnimParts | ✅ | ✅ | |
|
||||
| XModel | ✅ | ✅ | Model data can be exported to `XMODEL_EXPORT/XMODEL_BIN`, `OBJ`, `GLB/GLTF`. |
|
||||
| Material | ✅ | ✅ | |
|
||||
| MaterialTechniqueSet | ✅ | ✅ | For shaders: only dumps/loads shader bytecode. |
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace IW3
|
||||
|
||||
union XAnimIndices
|
||||
{
|
||||
char* _1;
|
||||
unsigned char* _1;
|
||||
uint16_t* _2;
|
||||
void* data;
|
||||
};
|
||||
@@ -197,7 +197,7 @@ namespace IW3
|
||||
|
||||
union XAnimDynamicIndicesTrans
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
@@ -209,8 +209,8 @@ namespace IW3
|
||||
|
||||
struct XAnimPartTransFrames
|
||||
{
|
||||
float mins[3];
|
||||
float size[3];
|
||||
vec3_t mins;
|
||||
vec3_t size;
|
||||
XAnimDynamicFrames frames;
|
||||
XAnimDynamicIndicesTrans indices;
|
||||
};
|
||||
@@ -224,31 +224,36 @@ namespace IW3
|
||||
struct XAnimPartTrans
|
||||
{
|
||||
uint16_t size;
|
||||
char smallTrans;
|
||||
unsigned char smallTrans;
|
||||
XAnimPartTransData u;
|
||||
};
|
||||
|
||||
struct type_align(4) XQuat
|
||||
{
|
||||
int16_t value[4];
|
||||
};
|
||||
|
||||
struct type_align(4) XQuat2
|
||||
{
|
||||
int16_t value[2];
|
||||
};
|
||||
|
||||
union XAnimDynamicIndicesQuat
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
struct XAnimDeltaPartQuatDataFrames
|
||||
{
|
||||
XQuat* frames;
|
||||
XQuat2* frames;
|
||||
XAnimDynamicIndicesQuat indices;
|
||||
};
|
||||
|
||||
union XAnimDeltaPartQuatData
|
||||
{
|
||||
XAnimDeltaPartQuatDataFrames frames;
|
||||
XQuat frame0;
|
||||
XQuat2 frame0;
|
||||
};
|
||||
|
||||
struct XAnimDeltaPartQuat
|
||||
@@ -263,6 +268,22 @@ namespace IW3
|
||||
XAnimDeltaPartQuat* quat;
|
||||
};
|
||||
|
||||
enum XAnimPartType
|
||||
{
|
||||
PART_TYPE_NO_QUAT = 0x0,
|
||||
PART_TYPE_HALF_QUAT = 0x1,
|
||||
PART_TYPE_FULL_QUAT = 0x2,
|
||||
PART_TYPE_HALF_QUAT_NO_SIZE = 0x3,
|
||||
PART_TYPE_FULL_QUAT_NO_SIZE = 0x4,
|
||||
PART_TYPE_SMALL_TRANS = 0x5,
|
||||
PART_TYPE_TRANS = 0x6,
|
||||
PART_TYPE_TRANS_NO_SIZE = 0x7,
|
||||
PART_TYPE_NO_TRANS = 0x8,
|
||||
PART_TYPE_ALL = 0x9,
|
||||
|
||||
PART_TYPE_COUNT
|
||||
};
|
||||
|
||||
struct XAnimParts
|
||||
{
|
||||
const char* name;
|
||||
@@ -274,20 +295,20 @@ namespace IW3
|
||||
uint16_t numframes;
|
||||
bool bLoop;
|
||||
bool bDelta;
|
||||
unsigned char boneCount[10];
|
||||
char notifyCount;
|
||||
char assetType;
|
||||
unsigned char boneCount[PART_TYPE_COUNT];
|
||||
unsigned char notifyCount;
|
||||
unsigned char assetType;
|
||||
bool isDefault;
|
||||
unsigned int randomDataShortCount;
|
||||
unsigned int indexCount;
|
||||
float framerate;
|
||||
float frequency;
|
||||
ScriptString* names;
|
||||
char* dataByte;
|
||||
unsigned char* dataByte;
|
||||
int16_t* dataShort;
|
||||
int* dataInt;
|
||||
int16_t* randomDataShort;
|
||||
char* randomDataByte;
|
||||
unsigned char* randomDataByte;
|
||||
int* randomDataInt;
|
||||
XAnimIndices indices;
|
||||
XAnimNotifyInfo* notify;
|
||||
|
||||
@@ -267,7 +267,7 @@ namespace IW4
|
||||
|
||||
union XAnimIndices
|
||||
{
|
||||
char* _1;
|
||||
unsigned char* _1;
|
||||
uint16_t* _2;
|
||||
void* data;
|
||||
};
|
||||
@@ -289,14 +289,14 @@ namespace IW4
|
||||
|
||||
union XAnimDynamicIndicesTrans
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
struct type_align32(4) XAnimPartTransFrames
|
||||
{
|
||||
float mins[3];
|
||||
float size[3];
|
||||
vec3_t mins;
|
||||
vec3_t size;
|
||||
XAnimDynamicFrames frames;
|
||||
XAnimDynamicIndicesTrans indices;
|
||||
};
|
||||
@@ -310,13 +310,13 @@ namespace IW4
|
||||
struct XAnimPartTrans
|
||||
{
|
||||
uint16_t size;
|
||||
char smallTrans;
|
||||
unsigned char smallTrans;
|
||||
XAnimPartTransData u;
|
||||
};
|
||||
|
||||
union XAnimDynamicIndicesQuat2
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
@@ -345,7 +345,7 @@ namespace IW4
|
||||
|
||||
union XAnimDynamicIndicesQuat
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
@@ -379,6 +379,22 @@ namespace IW4
|
||||
XAnimDeltaPartQuat* quat;
|
||||
};
|
||||
|
||||
enum XAnimPartType
|
||||
{
|
||||
PART_TYPE_NO_QUAT = 0x0,
|
||||
PART_TYPE_HALF_QUAT = 0x1,
|
||||
PART_TYPE_FULL_QUAT = 0x2,
|
||||
PART_TYPE_HALF_QUAT_NO_SIZE = 0x3,
|
||||
PART_TYPE_FULL_QUAT_NO_SIZE = 0x4,
|
||||
PART_TYPE_SMALL_TRANS = 0x5,
|
||||
PART_TYPE_TRANS = 0x6,
|
||||
PART_TYPE_TRANS_NO_SIZE = 0x7,
|
||||
PART_TYPE_NO_TRANS = 0x8,
|
||||
PART_TYPE_ALL = 0x9,
|
||||
|
||||
PART_TYPE_COUNT
|
||||
};
|
||||
|
||||
struct XAnimParts
|
||||
{
|
||||
const char* name;
|
||||
@@ -389,20 +405,20 @@ namespace IW4
|
||||
uint16_t randomDataIntCount;
|
||||
uint16_t numframes;
|
||||
char flags;
|
||||
unsigned char boneCount[10];
|
||||
char notifyCount;
|
||||
char assetType;
|
||||
unsigned char boneCount[PART_TYPE_COUNT];
|
||||
unsigned char notifyCount;
|
||||
unsigned char assetType;
|
||||
bool isDefault;
|
||||
unsigned int randomDataShortCount;
|
||||
unsigned int indexCount;
|
||||
float framerate;
|
||||
float frequency;
|
||||
ScriptString* names;
|
||||
char* dataByte;
|
||||
unsigned char* dataByte;
|
||||
int16_t* dataShort;
|
||||
int* dataInt;
|
||||
int16_t* randomDataShort;
|
||||
char* randomDataByte;
|
||||
unsigned char* randomDataByte;
|
||||
int* randomDataInt;
|
||||
XAnimIndices indices;
|
||||
XAnimNotifyInfo* notify;
|
||||
|
||||
@@ -313,14 +313,14 @@ namespace IW5
|
||||
|
||||
union XAnimDynamicIndicesTrans
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
struct type_align32(4) XAnimPartTransFrames
|
||||
{
|
||||
float mins[3];
|
||||
float size[3];
|
||||
vec3_t mins;
|
||||
vec3_t size;
|
||||
XAnimDynamicFrames frames;
|
||||
XAnimDynamicIndicesTrans indices;
|
||||
};
|
||||
@@ -340,7 +340,7 @@ namespace IW5
|
||||
|
||||
union XAnimDynamicIndicesQuat2
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
@@ -369,7 +369,7 @@ namespace IW5
|
||||
|
||||
union XAnimDynamicIndicesQuat
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
@@ -403,6 +403,22 @@ namespace IW5
|
||||
XAnimDeltaPartQuat* quat;
|
||||
};
|
||||
|
||||
enum XAnimPartType
|
||||
{
|
||||
PART_TYPE_NO_QUAT = 0x0,
|
||||
PART_TYPE_HALF_QUAT = 0x1,
|
||||
PART_TYPE_FULL_QUAT = 0x2,
|
||||
PART_TYPE_HALF_QUAT_NO_SIZE = 0x3,
|
||||
PART_TYPE_FULL_QUAT_NO_SIZE = 0x4,
|
||||
PART_TYPE_SMALL_TRANS = 0x5,
|
||||
PART_TYPE_TRANS = 0x6,
|
||||
PART_TYPE_TRANS_NO_SIZE = 0x7,
|
||||
PART_TYPE_NO_TRANS = 0x8,
|
||||
PART_TYPE_ALL = 0x9,
|
||||
|
||||
PART_TYPE_COUNT
|
||||
};
|
||||
|
||||
struct XAnimParts
|
||||
{
|
||||
const char* name;
|
||||
@@ -413,7 +429,7 @@ namespace IW5
|
||||
unsigned short randomDataIntCount;
|
||||
unsigned short numframes;
|
||||
unsigned char flags;
|
||||
unsigned char boneCount[10];
|
||||
unsigned char boneCount[PART_TYPE_COUNT];
|
||||
unsigned char notifyCount;
|
||||
unsigned char assetType;
|
||||
bool isDefault;
|
||||
|
||||
@@ -303,7 +303,7 @@ namespace T5
|
||||
|
||||
union XAnimIndices
|
||||
{
|
||||
char* _1;
|
||||
unsigned char* _1;
|
||||
uint16_t* _2;
|
||||
void* data;
|
||||
};
|
||||
@@ -325,14 +325,14 @@ namespace T5
|
||||
|
||||
union XAnimDynamicIndicesTrans
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
struct XAnimPartTransFrames
|
||||
{
|
||||
float mins[3];
|
||||
float size[3];
|
||||
vec3_t mins;
|
||||
vec3_t size;
|
||||
XAnimDynamicFrames frames;
|
||||
XAnimDynamicIndicesTrans indices;
|
||||
};
|
||||
@@ -346,31 +346,36 @@ namespace T5
|
||||
struct XAnimPartTrans
|
||||
{
|
||||
uint16_t size;
|
||||
char smallTrans;
|
||||
unsigned char smallTrans;
|
||||
XAnimPartTransData u;
|
||||
};
|
||||
|
||||
struct type_align(4) XQuat
|
||||
{
|
||||
int16_t value[4];
|
||||
};
|
||||
|
||||
struct type_align(4) XQuat2
|
||||
{
|
||||
int16_t value[2];
|
||||
};
|
||||
|
||||
union XAnimDynamicIndicesQuat
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
struct XAnimDeltaPartQuatDataFrames
|
||||
{
|
||||
XQuat* frames;
|
||||
XQuat2* frames;
|
||||
XAnimDynamicIndicesQuat indices;
|
||||
};
|
||||
|
||||
union XAnimDeltaPartQuatData
|
||||
{
|
||||
XAnimDeltaPartQuatDataFrames frames;
|
||||
XQuat frame0;
|
||||
XQuat2 frame0;
|
||||
};
|
||||
|
||||
struct XAnimDeltaPartQuat
|
||||
@@ -385,6 +390,22 @@ namespace T5
|
||||
XAnimDeltaPartQuat* quat;
|
||||
};
|
||||
|
||||
enum XAnimPartType
|
||||
{
|
||||
PART_TYPE_NO_QUAT = 0x0,
|
||||
PART_TYPE_HALF_QUAT = 0x1,
|
||||
PART_TYPE_FULL_QUAT = 0x2,
|
||||
PART_TYPE_HALF_QUAT_NO_SIZE = 0x3,
|
||||
PART_TYPE_FULL_QUAT_NO_SIZE = 0x4,
|
||||
PART_TYPE_SMALL_TRANS = 0x5,
|
||||
PART_TYPE_TRANS = 0x6,
|
||||
PART_TYPE_TRANS_NO_SIZE = 0x7,
|
||||
PART_TYPE_NO_TRANS = 0x8,
|
||||
PART_TYPE_ALL = 0x9,
|
||||
|
||||
PART_TYPE_COUNT
|
||||
};
|
||||
|
||||
struct XAnimParts
|
||||
{
|
||||
const char* name;
|
||||
@@ -399,9 +420,9 @@ namespace T5
|
||||
bool bLeftHandGripIK;
|
||||
bool bStreamable;
|
||||
unsigned int streamedFileSize;
|
||||
unsigned char boneCount[10];
|
||||
unsigned char boneCount[PART_TYPE_COUNT];
|
||||
unsigned char notifyCount;
|
||||
char assetType;
|
||||
unsigned char assetType;
|
||||
bool isDefault;
|
||||
unsigned int randomDataShortCount;
|
||||
unsigned int indexCount;
|
||||
@@ -410,11 +431,11 @@ namespace T5
|
||||
float primedLength;
|
||||
float loopEntryTime;
|
||||
uint16_t* names;
|
||||
char* dataByte;
|
||||
unsigned char* dataByte;
|
||||
int16_t* dataShort;
|
||||
int* dataInt;
|
||||
int16_t* randomDataShort;
|
||||
char* randomDataByte;
|
||||
unsigned char* randomDataByte;
|
||||
int* randomDataInt;
|
||||
XAnimIndices indices;
|
||||
XAnimNotifyInfo* notify;
|
||||
|
||||
@@ -468,11 +468,27 @@ namespace T6
|
||||
|
||||
union XAnimIndices
|
||||
{
|
||||
char* _1;
|
||||
unsigned char* _1;
|
||||
uint16_t* _2;
|
||||
void* data;
|
||||
};
|
||||
|
||||
enum XAnimPartType
|
||||
{
|
||||
PART_TYPE_NO_QUAT = 0x0,
|
||||
PART_TYPE_HALF_QUAT = 0x1,
|
||||
PART_TYPE_FULL_QUAT = 0x2,
|
||||
PART_TYPE_HALF_QUAT_NO_SIZE = 0x3,
|
||||
PART_TYPE_FULL_QUAT_NO_SIZE = 0x4,
|
||||
PART_TYPE_SMALL_TRANS = 0x5,
|
||||
PART_TYPE_TRANS = 0x6,
|
||||
PART_TYPE_TRANS_NO_SIZE = 0x7,
|
||||
PART_TYPE_NO_TRANS = 0x8,
|
||||
PART_TYPE_ALL = 0x9,
|
||||
|
||||
PART_TYPE_COUNT
|
||||
};
|
||||
|
||||
struct XAnimParts
|
||||
{
|
||||
const char* name;
|
||||
@@ -487,9 +503,9 @@ namespace T6
|
||||
bool bDelta3D;
|
||||
bool bLeftHandGripIK;
|
||||
unsigned int streamedFileSize;
|
||||
unsigned char boneCount[10];
|
||||
unsigned char boneCount[PART_TYPE_COUNT];
|
||||
unsigned char notifyCount;
|
||||
char assetType;
|
||||
unsigned char assetType;
|
||||
bool isDefault;
|
||||
unsigned int randomDataShortCount;
|
||||
unsigned int indexCount;
|
||||
@@ -498,11 +514,11 @@ namespace T6
|
||||
float primedLength;
|
||||
float loopEntryTime;
|
||||
uint16_t* names;
|
||||
char* dataByte;
|
||||
unsigned char* dataByte;
|
||||
int16_t* dataShort;
|
||||
int* dataInt;
|
||||
int16_t* randomDataShort;
|
||||
char* randomDataByte;
|
||||
unsigned char* randomDataByte;
|
||||
int* randomDataInt;
|
||||
XAnimIndices indices;
|
||||
XAnimNotifyInfo* notify;
|
||||
@@ -5627,7 +5643,7 @@ namespace T6
|
||||
|
||||
union XAnimDynamicIndicesTrans
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
@@ -5648,13 +5664,13 @@ namespace T6
|
||||
struct XAnimPartTrans
|
||||
{
|
||||
uint16_t size;
|
||||
char smallTrans;
|
||||
unsigned char smallTrans;
|
||||
XAnimPartTransData u;
|
||||
};
|
||||
|
||||
union XAnimDynamicIndicesDeltaQuat2
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
@@ -5683,7 +5699,7 @@ namespace T6
|
||||
|
||||
union XAnimDynamicIndicesDeltaQuat
|
||||
{
|
||||
char _1[1];
|
||||
unsigned char _1[1];
|
||||
uint16_t _2[1];
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "XAnimCommon.h"
|
||||
|
||||
#include <format>
|
||||
|
||||
namespace xanim
|
||||
{
|
||||
std::string GetCompiledFileNameForAssetName(const std::string& assetName)
|
||||
{
|
||||
return std::format("xanim/{}", assetName);
|
||||
}
|
||||
} // namespace xanim
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xanim
|
||||
{
|
||||
[[nodiscard]] std::string GetCompiledFileNameForAssetName(const std::string& assetName);
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "RawFile/AssetLoaderRawFileIW3.h"
|
||||
#include "Sound/LoaderSoundCurveIW3.h"
|
||||
#include "StringTable/AssetLoaderStringTableIW3.h"
|
||||
#include "XAnim/XAnimLoaderIW3.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -99,7 +100,7 @@ namespace
|
||||
|
||||
collection.AddAssetCreator(phys_preset::CreateRawLoaderIW3(memory, searchPath, zone));
|
||||
collection.AddAssetCreator(phys_preset::CreateGdtLoaderIW3(memory, gdt, zone));
|
||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderXAnim>(memory));
|
||||
collection.AddAssetCreator(xanim::CreateLoaderIW3(memory, searchPath, zone));
|
||||
collection.AddAssetCreator(xmodel::CreateLoaderIW3(memory, searchPath, zone));
|
||||
collection.AddAssetCreator(material::CreateLoaderIW3(memory, searchPath));
|
||||
// collection.AddAssetCreator(std::make_unique<AssetLoaderTechniqueSet>(memory));
|
||||
|
||||
@@ -0,0 +1,781 @@
|
||||
#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
|
||||
@@ -0,0 +1,13 @@
|
||||
#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
|
||||
@@ -12,13 +12,14 @@
|
||||
#include "Sound/LoadedSoundDumperIW3.h"
|
||||
#include "Sound/SndCurveDumperIW3.h"
|
||||
#include "StringTable/StringTableDumperIW3.h"
|
||||
#include "XAnim/XAnimDumperIW3.h"
|
||||
|
||||
using namespace IW3;
|
||||
|
||||
void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context)
|
||||
{
|
||||
RegisterAssetDumper(std::make_unique<phys_preset::InfoStringDumperIW3>());
|
||||
// REGISTER_DUMPER(AssetDumperXAnimParts)
|
||||
RegisterAssetDumper(std::make_unique<xanim::DumperIW3>());
|
||||
RegisterAssetDumper(std::make_unique<xmodel::DumperIW3>());
|
||||
RegisterAssetDumper(std::make_unique<material::JsonDumperIW3>());
|
||||
RegisterAssetDumper(std::make_unique<techset::DumperIW3>(
|
||||
|
||||
@@ -0,0 +1,937 @@
|
||||
#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
|
||||
@@ -0,0 +1,13 @@
|
||||
#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
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "StreamUtils.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace stream
|
||||
{
|
||||
size_t Read(std::istream& stream, void* out, const size_t outSize)
|
||||
{
|
||||
stream.read(static_cast<char*>(out), static_cast<std::streamsize>(outSize));
|
||||
return static_cast<size_t>(stream.gcount());
|
||||
}
|
||||
|
||||
std::string ReadCString(std::istream& stream)
|
||||
{
|
||||
std::string result;
|
||||
std::getline(stream, result, '\0');
|
||||
return result;
|
||||
}
|
||||
|
||||
void Write(std::ostream& stream, const void* in, const size_t inSize)
|
||||
{
|
||||
stream.write(static_cast<const char*>(in), static_cast<std::streamsize>(inSize));
|
||||
}
|
||||
|
||||
void WriteCString(std::ostream& stream, const std::string& value)
|
||||
{
|
||||
stream.write(value.c_str(), static_cast<std::streamsize>(value.size() + 1));
|
||||
}
|
||||
|
||||
void WriteCString(std::ostream& stream, const char* value)
|
||||
{
|
||||
stream.write(value, static_cast<std::streamsize>(std::strlen(value) + 1));
|
||||
}
|
||||
} // namespace stream
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace stream
|
||||
{
|
||||
size_t Read(std::istream& stream, void* out, size_t outSize);
|
||||
|
||||
template<typename T> T ReadValue(std::istream& stream)
|
||||
{
|
||||
T value{};
|
||||
stream.read(reinterpret_cast<char*>(&value), static_cast<std::streamsize>(sizeof(value)));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
template<typename T> void ReadValue(std::istream& stream, T& value)
|
||||
{
|
||||
stream.read(reinterpret_cast<char*>(&value), static_cast<std::streamsize>(sizeof(value)));
|
||||
}
|
||||
|
||||
std::string ReadCString(std::istream& stream);
|
||||
|
||||
void Write(std::ostream& stream, const void* in, size_t inSize);
|
||||
|
||||
template<typename T> void WriteValue(std::ostream& stream, const T& value)
|
||||
{
|
||||
stream.write(reinterpret_cast<const char*>(&value), static_cast<std::streamsize>(sizeof(value)));
|
||||
}
|
||||
|
||||
void WriteCString(std::ostream& stream, const std::string& value);
|
||||
void WriteCString(std::ostream& stream, const char* value);
|
||||
} // namespace stream
|
||||
@@ -41,6 +41,7 @@ function SystemTests:project()
|
||||
Utils:include(includes)
|
||||
ZoneLoading:include(includes)
|
||||
ZoneWriting:include(includes)
|
||||
ObjCommonTestUtils:include(includes)
|
||||
ObjLoading:include(includes)
|
||||
ObjCompiling:include(includes)
|
||||
ObjWriting:include(includes)
|
||||
@@ -53,6 +54,7 @@ function SystemTests:project()
|
||||
links:linkto(Utils)
|
||||
links:linkto(ZoneLoading)
|
||||
links:linkto(ZoneWriting)
|
||||
links:linkto(ObjCommonTestUtils)
|
||||
links:linkto(ObjLoading)
|
||||
links:linkto(ObjCompiling)
|
||||
links:linkto(ObjWriting)
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,62 @@
|
||||
#include "Game/IW3/XAnim/XAnimDumperIW3.h"
|
||||
#include "Game/IW3/XAnim/XAnimLoaderIW3.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 (IW3)", "[iw3][system]")
|
||||
{
|
||||
MockSearchPath searchPath;
|
||||
|
||||
const auto filePath = oat::paths::GetTestDirectory() / "SystemTests/Game/IW3/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::IW3, GamePlatform::PC);
|
||||
AssetCreatorCollection creatorCollection(zone);
|
||||
IgnoredAssetLookup ignoredAssetLookup;
|
||||
MemoryManager memoryManager;
|
||||
const auto loader = xanim::CreateLoaderIW3(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<IW3::AssetXAnim::Type>*>(result.GetAssetInfo());
|
||||
const auto* parts = assetInfo->Asset();
|
||||
|
||||
REQUIRE(parts->name == "test_anim"s);
|
||||
REQUIRE(parts->numframes > 0);
|
||||
|
||||
MockSearchPath mockObjPath;
|
||||
MockOutputPath mockOutput;
|
||||
xanim::DumperIW3 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
|
||||
Reference in New Issue
Block a user