diff --git a/src/ObjCommon/XAnim/XAnimCommon.cpp b/src/ObjCommon/XAnim/XAnimCommon.cpp index 539ff141..d305d264 100644 --- a/src/ObjCommon/XAnim/XAnimCommon.cpp +++ b/src/ObjCommon/XAnim/XAnimCommon.cpp @@ -1,9 +1,98 @@ #include "XAnimCommon.h" +#include #include +#include +#include +#include namespace xanim { + 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) + { + } + + CommonDeltaTransTrack::CommonDeltaTransTrack() + : m_constant(std::nullopt), + m_small_trans(false), + m_mins({}), + m_size({}) + { + } + + CommonXAnimParts::CommonXAnimParts() + : m_num_frames(0), + m_looped(false), + m_frame_rate(0), + m_asset_type(0) + { + } + + void CommonXAnimParts::SortBoneTracksForQuats() + { + std::vector 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 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 CommonXAnimParts::GetBoneTrackOrderForTrans() const + { + // This assumes the bone tracks were already sorted for quats + std::vector 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) { return std::format("xanim/{}", assetName); diff --git a/src/ObjCommon/XAnim/XAnimCommon.h b/src/ObjCommon/XAnim/XAnimCommon.h index efa0fb18..6f401a31 100644 --- a/src/ObjCommon/XAnim/XAnimCommon.h +++ b/src/ObjCommon/XAnim/XAnimCommon.h @@ -1,8 +1,151 @@ #pragma once +#include +#include +#include +#include #include +#include namespace xanim { + enum class CompiledXAnimVersion : uint8_t + { + // IW3 + VERSION_17 = 17 + }; + + 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 + { + int16_t value[4]; + }; + + struct CommonXQuat2 + { + int16_t value[2]; + }; + + struct CommonVec3U8 + { + uint8_t value[3]; + }; + + struct CommonVec3U16 + { + uint16_t value[3]; + }; + + class QuatTrack + { + public: + QuatTrack(); + + QuatType m_type; + std::vector m_indices; + std::vector m_frames; + std::vector m_frames2; + }; + + class TransTrack + { + public: + TransTrack(); + + TransType m_type; + std::vector m_indices; + std::array m_mins; + std::array m_size; + std::vector m_byte_frames; + std::vector m_short_frames; + std::array 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; + + std::vector m_indices; + std::vector m_frames2; + }; + + class CommonDeltaTransTrack + { + public: + CommonDeltaTransTrack(); + + std::optional> m_constant; + + bool m_small_trans; + std::vector m_indices; + std::array m_mins; + std::array m_size; + std::vector m_frames_u8; + std::vector m_frames_u16; + }; + + class CommonXAnimDeltaTrack + { + public: + CommonXAnimDeltaTrack() = default; + + std::optional m_quat; + std::optional m_trans; + }; + + class CommonXAnimParts + { + public: + CommonXAnimParts(); + + void SortBoneTracksForQuats(); + [[nodiscard]] std::vector GetBoneTrackOrderForTrans() const; + + size_t m_num_frames; + bool m_looped; + float m_frame_rate; + uint8_t m_asset_type; + std::vector m_bone_tracks; + std::vector m_notifies; + std::unique_ptr m_delta_track; + }; + [[nodiscard]] std::string GetCompiledFileNameForAssetName(const std::string& assetName); -} +} // namespace xanim diff --git a/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp b/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp index e988fecc..9f558373 100644 --- a/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp +++ b/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp @@ -1,19 +1,17 @@ #include "XAnimLoaderIW3.h" -#include "Utils/Alignment.h" #include "Utils/Logging/Log.h" -#include "Utils/StreamUtils.h" +#include "XAnim/CompiledXAnimLoader.h" +#include "XAnim/FlatXAnimDataWriter.h" #include "XAnim/XAnimCommon.h" #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -22,466 +20,272 @@ 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 + void ConvertNoteTracks(XAnimParts& parts, + const xanim::CommonXAnimParts& commonParts, + AssetRegistration& registration, + MemoryManager& memory, + ZoneScriptStrings& scriptStrings) { - NO_QUAT = 0, - HALF_QUAT = 1, - FULL_QUAT = 2, - HALF_QUAT_NO_SIZE = 3, - FULL_QUAT_NO_SIZE = 4, - }; + if (commonParts.m_notifies.empty()) + return; - enum class TransType : uint8_t - { - SMALL_TRANS = 5, - FULL_TRANS = 6, - TRANS_NO_SIZE = 7, - NO_TRANS = 8, - }; + const auto numCommonNoteTracks = commonParts.m_notifies.size(); + const auto numNoteTracks = static_cast(std::min(numCommonNoteTracks, std::numeric_limits::max())); - struct QuatTrack - { - QuatType type = QuatType::NO_QUAT; - std::vector indices; - std::vector values; - }; + if (numNoteTracks < numCommonNoteTracks) + con::error("XAnim {}: Could only fit {} of {} notetracks", parts.name, numNoteTracks, numCommonNoteTracks); - struct TransTrack - { - TransType type = TransType::NO_TRANS; - std::vector indices; - std::array mins{}; - std::array size{}; - std::vector byteFrames; - std::vector shortFrames; - std::array constant{}; - }; + parts.notifyCount = numNoteTracks; + parts.notify = memory.Alloc(numNoteTracks); - struct BoneTrack - { - std::string name; - QuatTrack quat; - TransTrack trans; - }; - - struct FlatDataWriteCursor - { - std::vector dataByte; - std::vector dataShort; - std::vector dataInt; - std::vector randomDataByte; - std::vector randomDataShort; - std::vector 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 + for (auto notifyIndex = 0u; notifyIndex < numCommonNoteTracks; notifyIndex++) { - int i; - float f; - }; + const auto& commonNotify = commonParts.m_notifies[notifyIndex]; + auto& notify = parts.notify[notifyIndex]; - f = value; - return i; + notify.name = scriptStrings.AddOrGetScriptString(commonNotify.m_name); + registration.AddScriptString(notify.name); + + notify.time = commonNotify.m_time; + } } - void WriteFloat3(FlatDataWriteCursor& writeCursor, const std::array& 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(stream); - quat.value[1] = stream::ReadValue(stream); - quat.value[2] = stream::ReadValue(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(std::floor(std::sqrt(static_cast(temp)) + 0.5f)); - - assert(temp >= std::numeric_limits::min() && temp <= std::numeric_limits::max()); - quat.value[3] = static_cast(temp); - } - - void ConsumeQuat2(std::istream& stream, XQuat2& quat2) - { - quat2.value[0] = stream::ReadValue(stream); - - int32_t temp = 0x3FFF0001 - quat2.value[0] * quat2.value[0]; - if (temp <= 0) - temp = 0; - else - temp = static_cast(floor(std::sqrt(static_cast(temp)) + 0.5f)); - - assert(temp >= std::numeric_limits::min() && temp <= std::numeric_limits::max()); - quat2.value[1] = static_cast(temp); - } - - void FlipQuat(XQuat& quat) - { - quat.value[0] = static_cast(-quat.value[0]); - quat.value[1] = static_cast(-quat.value[1]); - quat.value[2] = static_cast(-quat.value[2]); - quat.value[3] = static_cast(-quat.value[3]); - } - - void FlipQuat2(XQuat2& quat) - { - quat.value[0] = static_cast(-quat.value[0]); - quat.value[1] = static_cast(-quat.value[1]); - } - - template - void LoadIndicesIfNeeded(std::istream& stream, T& indices, const uint16_t numIndices, const bool useByteIndices, const uint16_t numLoopFrames) + template void ConvertIndices(T& indices, const std::vector& commonIndices, const bool useByteIndices) { 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& 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(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(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(stream); - return; - } - - LoadIndicesIfNeeded(stream, transTrack.indices, numTransIndices, useByteIndices, numLoopFrames); - - const auto smallTrans = stream::ReadValue(stream); - transTrack.type = smallTrans ? TransType::SMALL_TRANS : TransType::FULL_TRANS; - - for (auto& value : transTrack.mins) - value = stream::ReadValue(stream); - for (auto& value : transTrack.size) - value = DecodeRawTransSize(stream::ReadValue(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(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) + const auto numIndices = commonIndices.size(); + for (size_t i = 0u; i < numIndices; i++) { - 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(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); + assert(commonIndices[i] <= std::numeric_limits::max()); + indices._1[i] = static_cast(commonIndices[i]); } } else { - quatTrack.type = QuatType::FULL_QUAT; - quatTrack.values.resize(numQuatIndices * 4); - auto* quats = reinterpret_cast(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); - } + std::memcpy(indices._2, commonIndices.data(), commonIndices.size() * sizeof(uint16_t)); } } - void ApplyWriteCursorToParts(XAnimParts& parts, const FlatDataWriteCursor& writeCursor, MemoryManager& memory) + void CountBoneTrackTypes(XAnimParts& parts, const xanim::BoneTrack& boneTrack) { - if (!writeCursor.dataByte.empty()) + switch (boneTrack.m_quat.m_type) { - parts.dataByteCount = static_cast(writeCursor.dataByte.size()); - parts.dataByte = memory.Alloc(parts.dataByteCount); - std::memcpy(parts.dataByte, writeCursor.dataByte.data(), parts.dataByteCount * sizeof(uint8_t)); - } - - if (!writeCursor.dataShort.empty()) - { - parts.dataShortCount = static_cast(writeCursor.dataShort.size()); - parts.dataShort = memory.Alloc(parts.dataShortCount); - std::memcpy(parts.dataShort, writeCursor.dataShort.data(), parts.dataShortCount * sizeof(int16_t)); - } - - if (!writeCursor.dataInt.empty()) - { - parts.dataIntCount = static_cast(writeCursor.dataInt.size()); - parts.dataInt = memory.Alloc(parts.dataIntCount); - std::memcpy(parts.dataInt, writeCursor.dataInt.data(), parts.dataIntCount * sizeof(int32_t)); - } - - if (!writeCursor.randomDataByte.empty()) - { - parts.randomDataByteCount = static_cast(writeCursor.randomDataByte.size()); - parts.randomDataByte = memory.Alloc(parts.randomDataByteCount); - std::memcpy(parts.randomDataByte, writeCursor.randomDataByte.data(), parts.randomDataByteCount * sizeof(uint8_t)); - } - - if (!writeCursor.randomDataShort.empty()) - { - parts.randomDataShortCount = static_cast(writeCursor.randomDataShort.size()); - parts.randomDataShort = memory.Alloc(parts.randomDataShortCount); - std::memcpy(parts.randomDataShort, writeCursor.randomDataShort.data(), parts.randomDataShortCount * sizeof(int16_t)); - } - - if (!writeCursor.indices.empty()) - { - parts.indexCount = static_cast(writeCursor.indices.size()); - parts.indices._2 = memory.Alloc(parts.indexCount); - std::memcpy(parts.indices._2, writeCursor.indices.data(), parts.indexCount * sizeof(uint16_t)); - } - } - - void WritePackedIndices(FlatDataWriteCursor& writeCursor, const std::vector& indices, const bool useByteIndices) - { - const auto indexCount = indices.size(); - writeCursor.dataShort.emplace_back(static_cast(indexCount - 1)); // storedSize - - if (useByteIndices) - { - for (const auto index : indices) - { - assert(index <= std::numeric_limits::max()); - writeCursor.dataByte.emplace_back(static_cast(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: + case xanim::QuatType::NO_QUAT: parts.boneCount[PART_TYPE_NO_QUAT]++; break; - - case QuatType::HALF_QUAT: + case xanim::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: + case xanim::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: + case xanim::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: + case xanim::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::max()); - writeCursor.dataByte.emplace_back(static_cast(boneIndex)); - - switch (transTrack.type) + switch (boneTrack.m_trans.m_type) { - case TransType::SMALL_TRANS: + case xanim::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: + case xanim::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: + case xanim::TransType::TRANS_NO_SIZE: parts.boneCount[PART_TYPE_TRANS_NO_SIZE]++; - WriteFloat3(writeCursor, transTrack.constant); break; - - case TransType::NO_TRANS: + 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(flatData.m_data_byte.size()); + parts.dataByte = memory.Alloc(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(flatData.m_data_short.size()); + parts.dataShort = memory.Alloc(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(flatData.m_data_int.size()); + parts.dataInt = memory.Alloc(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(flatData.m_random_data_byte.size()); + parts.randomDataByte = memory.Alloc(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(flatData.m_random_data_short.size()); + parts.randomDataShort = memory.Alloc(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(flatData.m_indices.size()); + parts.indices._2 = memory.Alloc(parts.indexCount); + std::memcpy(parts.indices._2, flatData.m_indices.data(), parts.indexCount * sizeof(uint16_t)); + } + } + + void ConvertCommonDeltaQuatPart(XAnimDeltaPart& deltaPart, + const xanim::CommonDeltaQuatTrack& commonDeltaQuatTrack, + MemoryManager& memory, + const bool useByteIndices) + { + if (commonDeltaQuatTrack.m_frames2.size() == 1) + { + deltaPart.quat = static_cast(memory.AllocRaw(offsetof(XAnimDeltaPartQuat, u) + sizeof(XAnimDeltaPartQuatData::frame0))); + deltaPart.quat->size = 0; + deltaPart.quat->u.frame0.value[0] = commonDeltaQuatTrack.m_frames2[0].value[0]; + deltaPart.quat->u.frame0.value[1] = commonDeltaQuatTrack.m_frames2[0].value[1]; + return; + } + + const auto numQuatIndices = commonDeltaQuatTrack.m_indices.size(); + const auto indicesArraySize = + useByteIndices ? numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_1) : numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_2); + + deltaPart.quat = static_cast( + 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(numQuatIndices - 1); + deltaPart.quat->u.frames.frames = memory.Alloc(numQuatIndices); + + for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum) + { + const auto& commonFrame = commonDeltaQuatTrack.m_frames2[quatIndexNum]; + auto& curFrame = deltaPart.quat->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(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(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(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(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(numTransIndices); + std::memcpy(frames.frames._2, commonDeltaTransTrack.m_frames_u16.data(), numTransIndices * sizeof(UShortVec)); + } + } + + void ConvertCommonDeltaPart(XAnimDeltaPart& deltaPart, + const xanim::CommonXAnimDeltaTrack& commonXAnimDeltaTrack, + MemoryManager& memory, + const bool useByteIndices) + { + if (commonXAnimDeltaTrack.m_quat) + ConvertCommonDeltaQuatPart(deltaPart, *commonXAnimDeltaTrack.m_quat, memory, useByteIndices); + if (commonXAnimDeltaTrack.m_trans) + ConvertCommonDeltaTransPart(deltaPart, *commonXAnimDeltaTrack.m_trans, memory, useByteIndices); + } + + void ConvertCommonXAnim(XAnimParts& parts, + const xanim::CommonXAnimParts& commonParts, + AssetRegistration& registration, + MemoryManager& memory, + ZoneScriptStrings& scriptStrings) + { + parts.numframes = static_cast(commonParts.m_num_frames); + parts.bLoop = commonParts.m_looped; + parts.assetType = commonParts.m_asset_type; + parts.framerate = commonParts.m_frame_rate; + parts.frequency = parts.numframes > 0 ? parts.framerate / static_cast(parts.numframes) : 0; + + const auto useByteIndices = parts.numframes < 256; + + if (commonParts.m_delta_track) + { + parts.deltaPart = memory.Alloc(); + parts.bDelta = true; + ConvertCommonDeltaPart(*parts.deltaPart, *commonParts.m_delta_track, memory, useByteIndices); + } + + parts.names = memory.Alloc(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(commonParts.m_bone_tracks.size()); + + ConvertNoteTracks(parts, commonParts, registration, memory, scriptStrings); + + const auto flatData = xanim::CreateFlatDataFromCommonXAnim(commonParts); + ConvertFlatData(parts, flatData, memory); + } + class XAnimLoader final : public AssetCreator { public: @@ -498,274 +302,23 @@ namespace 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(); parts->name = m_memory.Dup(assetName.c_str()); AssetRegistration registration(assetName, parts); - if (!LoadFromFile(*file.m_stream, *parts, registration)) - { - con::error("Failed to load xanim \"{}\"", assetName); - return AssetCreationResult::Failure(); - } + ConvertCommonXAnim(*parts, *commonParts, registration, m_memory, m_script_strings); return AssetCreationResult::Success(context.AddAsset(std::move(registration))); } - private: - void ReadNoteTracks(std::istream& stream, XAnimParts& parts, AssetRegistration& registration) const - { - const auto numDiskNoteTracks = stream::ReadValue(stream); - assert(numDiskNoteTracks + 1 <= std::numeric_limits::max()); - - uint8_t numNoteTracks; - if (numDiskNoteTracks == std::numeric_limits::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(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(stream); - notify.time = parts.numframes > 0 ? static_cast(frame) / static_cast(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(stream); - if (numQuatIndices == 0) - return; - - if (numQuatIndices == 1) - { - delta.quat = static_cast(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( - 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(numQuatIndices - 1); - delta.quat->u.frames.frames = m_memory.Alloc(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(stream); - if (numTransIndices == 0) - return; - - if (numTransIndices == 1) - { - delta.trans = static_cast(m_memory.AllocRaw(offsetof(XAnimPartTrans, u) + sizeof(XAnimPartTransData::frame0))); - delta.trans->size = 0; - delta.trans->u.frame0.x = stream::ReadValue(stream); - delta.trans->u.frame0.y = stream::ReadValue(stream); - delta.trans->u.frame0.z = stream::ReadValue(stream); - return; - } - const auto indicesArraySize = - useByteIndices ? numTransIndices * sizeof(XAnimDynamicIndicesTrans::_1) : numTransIndices * sizeof(XAnimDynamicIndicesTrans::_2); - - delta.trans = - static_cast(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(stream); - delta.trans->smallTrans = smallTrans ? 1 : 0; - - frames.mins.x = stream::ReadValue(stream); - frames.mins.y = stream::ReadValue(stream); - frames.mins.z = stream::ReadValue(stream); - - frames.size.x = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); - frames.size.y = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); - frames.size.z = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); - - delta.trans->size = static_cast(numTransIndices - 1); - if (smallTrans) - { - frames.frames._1 = m_memory.Alloc(numTransIndices); - stream::Read(stream, frames.frames._1, numTransIndices * sizeof(ByteVec)); - } - else - { - frames.frames._2 = m_memory.Alloc(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(); - parts.deltaPart = delta; - - LoadDeltaQuats(stream, *delta, useByteIndices, numLoopFrames); - LoadDeltaTrans(stream, *delta, useByteIndices, numLoopFrames); - } - - bool LoadFromFile(std::istream& stream, XAnimParts& parts, AssetRegistration& registration) const - { - const auto fileVersion = stream::ReadValue(stream); - if (fileVersion != RAW_VERSION) - { - PrintError(parts, std::format("Unsupported version number {} (expected {})", fileVersion, RAW_VERSION)); - return false; - } - - const auto numFrames = stream::ReadValue(stream); - const auto boneCount = stream::ReadValue(stream); - const auto flags = stream::ReadValue(stream); - const auto assetType = stream::ReadValue(stream); - const auto framerate = stream::ReadValue(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(framerate); - parts.frequency = parts.numframes > 0 ? parts.framerate / static_cast(parts.numframes) : 0; - - const auto useByteIndices = UseByteIndices(parts); - - if (hasDelta) - LoadDeltaTrack(stream, parts, useByteIndices, numLoopFrames); - - std::vector boneTracks; - if (boneCount > 0) - { - const auto bitmaskSize = utils::Align(boneCount, 8u) / 8u; - std::vector flipQuatBits(bitmaskSize, 0); - std::vector 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(1u << (boneIndex % 8u)); - const bool halfQuat = halfQuatBits[boneIndex / 8u] & static_cast(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 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 boneTrackIndexToPartsBoneIndex(boneCount); - parts.names = m_memory.Alloc(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(boneCount); - - assert(stream.peek() == std::char_traits::eof()); - return true; - } - MemoryManager& m_memory; ISearchPath& m_search_path; ZoneScriptStrings& m_script_strings; diff --git a/src/ObjLoading/XAnim/CompiledXAnimLoader.cpp b/src/ObjLoading/XAnim/CompiledXAnimLoader.cpp new file mode 100644 index 00000000..81a4986c --- /dev/null +++ b/src/ObjLoading/XAnim/CompiledXAnimLoader.cpp @@ -0,0 +1,422 @@ +#include "CompiledXAnimLoader.h" + +#include "Utils/Alignment.h" +#include "Utils/StreamUtils.h" + +#include +#include +#include +#include +#include + +using namespace xanim; + +namespace +{ + constexpr uint8_t FLAG_LOOPED = 1u; + constexpr uint8_t FLAG_DELTA = 2u; + + // The linker decodes raw trans size[] with these exact float literals. + // They correspond to 1.0f / 255.0f and 1.0f / 65535.0f, but we keep the + // decompiled values to preserve binary-stable round trips. + constexpr auto HALF_TRANS_SIZE_SCALE = 0.003921568859368563f; + constexpr auto FULL_TRANS_SIZE_SCALE = 0.00001525902189314365f; + + std::expected IdentifyVersion(std::istream& stream) + { + const auto fileVersion = stream::ReadValue(stream); + switch (static_cast(fileVersion)) + { + case CompiledXAnimVersion::VERSION_17: + return CompiledXAnimVersion::VERSION_17; + default: + return std::unexpected(std::format("Version {} is not supported", fileVersion)); + } + } + + CommonXQuat ConsumeQuat(std::istream& stream) + { + CommonXQuat quat{}; + quat.value[0] = stream::ReadValue(stream); + quat.value[1] = stream::ReadValue(stream); + quat.value[2] = stream::ReadValue(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(std::floor(std::sqrt(static_cast(temp)) + 0.5f)); + + assert(temp >= std::numeric_limits::min() && temp <= std::numeric_limits::max()); + quat.value[3] = static_cast(temp); + + return quat; + } + + CommonXQuat2 ConsumeQuat2(std::istream& stream) + { + CommonXQuat2 quat2{}; + quat2.value[0] = stream::ReadValue(stream); + + int32_t temp = 0x3FFF0001 - quat2.value[0] * quat2.value[0]; + if (temp <= 0) + temp = 0; + else + temp = static_cast(floor(std::sqrt(static_cast(temp)) + 0.5f)); + + assert(temp >= std::numeric_limits::min() && temp <= std::numeric_limits::max()); + quat2.value[1] = static_cast(temp); + + return quat2; + } + + void FlipQuat(CommonXQuat& quat) + { + quat.value[0] = static_cast(-quat.value[0]); + quat.value[1] = static_cast(-quat.value[1]); + quat.value[2] = static_cast(-quat.value[2]); + quat.value[3] = static_cast(-quat.value[3]); + } + + void FlipQuat2(CommonXQuat2& quat) + { + quat.value[0] = static_cast(-quat.value[0]); + quat.value[1] = static_cast(-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& 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(stream)); + } + else + { + indices.resize(numIndices); + stream::Read(stream, indices.data(), numIndices * sizeof(uint16_t)); + } + } + + std::expected, std::string> + LoadDeltaQuatTrack(std::istream& stream, const bool useByteIndices, const uint16_t numLoopFrames) + { + const auto numQuatIndices = stream::ReadValue(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::string> + LoadDeltaTransTrack(std::istream& stream, const bool useByteIndices, const uint16_t numLoopFrames) + { + const auto numTransIndices = stream::ReadValue(stream); + if (numTransIndices == 0) + return std::nullopt; + + CommonDeltaTransTrack deltaTransTrack; + if (numTransIndices == 1) + { + deltaTransTrack.m_constant.emplace(std::array({ + stream::ReadValue(stream), + stream::ReadValue(stream), + stream::ReadValue(stream), + })); + return deltaTransTrack; + } + + LoadIndicesIfNeeded(stream, deltaTransTrack.m_indices, numTransIndices, useByteIndices, numLoopFrames); + + const auto smallTrans = stream::ReadValue(stream); + + deltaTransTrack.m_mins[0] = stream::ReadValue(stream); + deltaTransTrack.m_mins[1] = stream::ReadValue(stream); + deltaTransTrack.m_mins[2] = stream::ReadValue(stream); + + deltaTransTrack.m_size[0] = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + deltaTransTrack.m_size[1] = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + deltaTransTrack.m_size[2] = DecodeRawTransSize(stream::ReadValue(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::string> + LoadDeltaTrack(std::istream& stream, const bool useByteIndices, const uint16_t numLoopFrames) + { + auto delta = std::make_unique(); + + auto maybeLoadedDeltaQuat = LoadDeltaQuatTrack(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(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(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(stream); + return transTrack; + } + + LoadIndicesIfNeeded(stream, transTrack.m_indices, numTransIndices, useByteIndices, numLoopFrames); + + const auto smallTrans = stream::ReadValue(stream); + transTrack.m_type = smallTrans ? TransType::SMALL_TRANS : TransType::FULL_TRANS; + + for (auto& value : transTrack.m_mins) + value = stream::ReadValue(stream); + for (auto& value : transTrack.m_size) + value = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + + if (smallTrans) + { + transTrack.m_byte_frames.resize(numTransIndices * 3); + stream::Read(stream, transTrack.m_byte_frames.data(), numTransIndices * sizeof(uint8_t) * 3); + } + else + { + transTrack.m_short_frames.resize(numTransIndices * 3); + stream::Read(stream, transTrack.m_short_frames.data(), numTransIndices * sizeof(uint16_t) * 3); + } + + return transTrack; + } + + void ReadNoteTracks(std::istream& stream, CommonXAnimParts& parts) + { + const auto numNoteTracks = stream::ReadValue(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(stream); + const auto time = parts.m_num_frames > 0 ? static_cast(frame) / static_cast(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); + } +} // namespace + +namespace xanim +{ + std::expected, std::string> LoadCompiledXAnim(std::istream& stream) + { + auto maybeVersion = IdentifyVersion(stream); + if (!maybeVersion) + return std::unexpected(std::move(maybeVersion).error()); + + auto parts = std::make_unique(); + + const auto version = maybeVersion.value(); + assert(version == CompiledXAnimVersion::VERSION_17); + + const auto numFrames = stream::ReadValue(stream); + const auto boneCount = stream::ReadValue(stream); + const auto flags = stream::ReadValue(stream); + const auto assetType = stream::ReadValue(stream); + const auto framerate = stream::ReadValue(stream); + if (stream.fail()) + return std::unexpected("Truncated file"); + + const bool isLooped = flags & FLAG_LOOPED; + const bool hasDelta = flags & FLAG_DELTA; + const uint16_t numLoopFrames = isLooped ? numFrames + 1u : numFrames; + + parts->m_num_frames = numLoopFrames - 1; + parts->m_looped = isLooped; + parts->m_asset_type = assetType; + parts->m_frame_rate = static_cast(framerate); + + const auto useByteIndices = parts->m_num_frames < 256; + + if (hasDelta) + { + auto maybeBoneTrack = LoadDeltaTrack(stream, 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(boneCount, 8u) / 8u; + std::vector flipQuatBits(bitmaskSize, 0); + std::vector 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(1u << (boneIndex % 8u)); + const bool halfQuat = halfQuatBits[boneIndex / 8u] & static_cast(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::eof()); + return parts; + } +} // namespace xanim diff --git a/src/ObjLoading/XAnim/CompiledXAnimLoader.h b/src/ObjLoading/XAnim/CompiledXAnimLoader.h new file mode 100644 index 00000000..add45f75 --- /dev/null +++ b/src/ObjLoading/XAnim/CompiledXAnimLoader.h @@ -0,0 +1,12 @@ +#pragma once + +#include "XAnim/XAnimCommon.h" + +#include +#include +#include + +namespace xanim +{ + std::expected, std::string> LoadCompiledXAnim(std::istream& stream); +} diff --git a/src/ObjLoading/XAnim/FlatXAnimDataWriter.cpp b/src/ObjLoading/XAnim/FlatXAnimDataWriter.cpp new file mode 100644 index 00000000..86f64bfa --- /dev/null +++ b/src/ObjLoading/XAnim/FlatXAnimDataWriter.cpp @@ -0,0 +1,174 @@ +#include "FlatXAnimDataWriter.h" + +#include +#include + +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& value) + { + for (const float f : value) + writeCursor.m_data_int.emplace_back(FloatBitsToInt(f)); + } + + void WritePackedIndices(FlatData& writeCursor, const std::vector& indices, const bool useByteIndices) + { + const auto indexCount = indices.size(); + writeCursor.m_data_short.emplace_back(static_cast(indexCount - 1)); // storedSize + + if (useByteIndices) + { + for (const auto index : indices) + { + assert(index <= std::numeric_limits::max()); + writeCursor.m_data_byte.emplace_back(static_cast(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::max()); + writeCursor.m_data_byte.emplace_back(static_cast(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_byte_frames.size() == transTrack.m_indices.size() * 3); + std::ranges::copy(transTrack.m_byte_frames, std::back_inserter(writeCursor.m_random_data_byte)); + break; + + case TransType::FULL_TRANS: + WritePackedIndices(writeCursor, transTrack.m_indices, useByteIndices); + WriteFloat3(writeCursor, transTrack.m_mins); + WriteFloat3(writeCursor, transTrack.m_size); + assert(transTrack.m_short_frames.size() == transTrack.m_indices.size() * 3); + std::ranges::copy(transTrack.m_short_frames, std::back_inserter(writeCursor.m_random_data_short)); + 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 diff --git a/src/ObjLoading/XAnim/FlatXAnimDataWriter.h b/src/ObjLoading/XAnim/FlatXAnimDataWriter.h new file mode 100644 index 00000000..447ac79e --- /dev/null +++ b/src/ObjLoading/XAnim/FlatXAnimDataWriter.h @@ -0,0 +1,22 @@ +#pragma once + +#include "XAnim/XAnimCommon.h" + +#include +#include + +namespace xanim +{ + class FlatData + { + public: + std::vector m_data_byte; + std::vector m_data_short; + std::vector m_data_int; + std::vector m_random_data_byte; + std::vector m_random_data_short; + std::vector m_indices; + }; + + FlatData CreateFlatDataFromCommonXAnim(const CommonXAnimParts& parts); +} // namespace xanim