#include #include #include "IXModel.hpp" namespace Assets { void IXModel::load(Game::XAssetHeader* header, const std::string& name, Components::ZoneBuilder::Zone* builder) { header->model = builder->getIW4OfApi()->read(Game::XAssetType::ASSET_TYPE_XMODEL, name); if (header->model) { // ??? if (header->model->physCollmap) { Components::AssetHandler::StoreTemporaryAsset(Game::XAssetType::ASSET_TYPE_PHYSCOLLMAP, { header->model->physCollmap }); } if (header->model->physPreset) { Components::AssetHandler::StoreTemporaryAsset(Game::XAssetType::ASSET_TYPE_PHYSPRESET, { header->model->physPreset }); } for (size_t i = 0; i < header->model->numLods; i++) { const auto& info = header->model->lodInfo[i]; Components::AssetHandler::StoreTemporaryAsset(Game::XAssetType::ASSET_TYPE_XMODEL_SURFS, { info.modelSurfs }); } } } void IXModel::mark(Game::XAssetHeader header, Components::ZoneBuilder::Zone* builder) { Game::XModel* asset = header.model; if (asset->boneNames) { for (char i = 0; i < asset->numBones; ++i) { builder->addScriptString(asset->boneNames[i]); } } if (asset->materialHandles) { for (unsigned char i = 0; i < asset->numsurfs; ++i) { if (asset->materialHandles[i]) { builder->loadAsset(Game::XAssetType::ASSET_TYPE_MATERIAL, asset->materialHandles[i]); } } } for (int i = 0; i < 4; ++i) { if (asset->lodInfo[i].modelSurfs) { builder->loadAsset(Game::XAssetType::ASSET_TYPE_XMODEL_SURFS, asset->lodInfo[i].modelSurfs); } } if (asset->physPreset) { builder->loadAsset(Game::XAssetType::ASSET_TYPE_PHYSPRESET, asset->physPreset); } if (asset->physCollmap) { builder->loadAsset(Game::XAssetType::ASSET_TYPE_PHYSCOLLMAP, asset->physCollmap); } } void IXModel::save(Game::XAssetHeader header, Components::ZoneBuilder::Zone* builder) { AssertSize(Game::XModel, 304); Utils::Stream* buffer = builder->getBuffer(); Game::XModel* asset = header.model; Game::XModel* dest = buffer->dest(); buffer->save(asset); buffer->pushBlock(Game::XFILE_BLOCK_VIRTUAL); if (asset->name) { buffer->saveString(builder->getAssetName(this->getType(), asset->name)); Utils::Stream::ClearPointer(&dest->name); } if (asset->boneNames) { buffer->align(Utils::Stream::ALIGN_2); unsigned short* destBoneNames = buffer->dest(); buffer->saveArray(asset->boneNames, asset->numBones); for (char i = 0; i < asset->numBones; ++i) { builder->mapScriptString(destBoneNames[i]); } Utils::Stream::ClearPointer(&dest->boneNames); } if (asset->parentList) { if (builder->hasPointer(asset->parentList)) { dest->parentList = builder->getPointer(asset->parentList); } else { builder->storePointer(asset->parentList); buffer->save(asset->parentList, asset->numBones - asset->numRootBones); Utils::Stream::ClearPointer(&dest->parentList); } } if (asset->quats) { if (builder->hasPointer(asset->quats)) { dest->quats = builder->getPointer(asset->quats); } else { buffer->align(Utils::Stream::ALIGN_2); builder->storePointer(asset->quats); buffer->saveArray(asset->quats, (asset->numBones - asset->numRootBones) * 4); Utils::Stream::ClearPointer(&dest->quats); } } if (asset->trans) { if (builder->hasPointer(asset->trans)) { dest->trans = builder->getPointer(asset->trans); } else { buffer->align(Utils::Stream::ALIGN_4); builder->storePointer(asset->trans); buffer->saveArray(asset->trans, (asset->numBones - asset->numRootBones) * 3); Utils::Stream::ClearPointer(&dest->trans); } } if (asset->partClassification) { if (builder->hasPointer(asset->partClassification)) { dest->partClassification = builder->getPointer(asset->partClassification); } else { builder->storePointer(asset->partClassification); buffer->save(asset->partClassification, asset->numBones); Utils::Stream::ClearPointer(&dest->partClassification); } } if (asset->baseMat) { AssertSize(Game::DObjAnimMat, 32); if (builder->hasPointer(asset->baseMat)) { dest->baseMat = builder->getPointer(asset->baseMat); } else { buffer->align(Utils::Stream::ALIGN_4); builder->storePointer(asset->baseMat); buffer->saveArray(asset->baseMat, asset->numBones); Utils::Stream::ClearPointer(&dest->baseMat); } } if (asset->materialHandles) { buffer->align(Utils::Stream::ALIGN_4); Game::Material** destMaterials = buffer->dest(); buffer->saveArray(asset->materialHandles, asset->numsurfs); for (unsigned char i = 0; i < asset->numsurfs; ++i) { if (asset->materialHandles[i]) { destMaterials[i] = builder->saveSubAsset(Game::XAssetType::ASSET_TYPE_MATERIAL, asset->materialHandles[i]).material; } } Utils::Stream::ClearPointer(&dest->materialHandles); } // Save_XModelLodInfoArray { AssertSize(Game::XModelLodInfo, 44); for (int i = 0; i < 4; ++i) { if (asset->lodInfo[i].modelSurfs) { dest->lodInfo[i].modelSurfs = builder->saveSubAsset(Game::XAssetType::ASSET_TYPE_XMODEL_SURFS, asset->lodInfo[i].modelSurfs).modelSurfs; } } } // Save_XModelCollSurfArray if (asset->collSurfs) { AssertSize(Game::XModelCollSurf_s, 44); buffer->align(Utils::Stream::ALIGN_4); Game::XModelCollSurf_s* destColSurfs = buffer->dest(); buffer->saveArray(asset->collSurfs, asset->numCollSurfs); for (int i = 0; i < asset->numCollSurfs; ++i) { Game::XModelCollSurf_s* destCollSurf = &destColSurfs[i]; Game::XModelCollSurf_s* collSurf = &asset->collSurfs[i]; if (collSurf->collTris) { buffer->align(Utils::Stream::ALIGN_4); buffer->save(collSurf->collTris, 48, collSurf->numCollTris); Utils::Stream::ClearPointer(&destCollSurf->collTris); } } Utils::Stream::ClearPointer(&dest->collSurfs); } if (asset->boneInfo) { AssertSize(Game::XBoneInfo, 28); buffer->align(Utils::Stream::ALIGN_4); buffer->saveArray(asset->boneInfo, asset->numBones); Utils::Stream::ClearPointer(&dest->boneInfo); } if (asset->physPreset) { dest->physPreset = builder->saveSubAsset(Game::XAssetType::ASSET_TYPE_PHYSPRESET, asset->physPreset).physPreset; } if (asset->physCollmap) { dest->physCollmap = builder->saveSubAsset(Game::XAssetType::ASSET_TYPE_PHYSCOLLMAP, asset->physCollmap).physCollmap; } buffer->popBlock(); } uint8_t IXModel::GetIndexOfBone(const Game::XModel* model, std::string name) { for (uint8_t i = 0; i < model->numBones; i++) { const auto bone = model->boneNames[i]; const auto boneName = Game::SL_ConvertToString(bone); if (name == boneName) { return i; } } return static_cast(UCHAR_MAX); }; uint8_t IXModel::GetParentIndexOfBone(const Game::XModel* model, uint8_t index) { const auto parentIndex = index - model->parentList[index - model->numRootBones]; return static_cast(parentIndex); }; void IXModel::SetParentIndexOfBone(Game::XModel* model, uint8_t boneIndex, uint8_t parentIndex) { if (boneIndex == SCHAR_MAX) { return; } model->parentList[boneIndex - model->numRootBones] = boneIndex - parentIndex; }; std::string IXModel::GetParentOfBone(Game::XModel* model, uint8_t index) { assert(index > 0); const auto parentIndex = GetParentIndexOfBone(model, index); const auto boneName = Game::SL_ConvertToString(model->boneNames[parentIndex]); return boneName; }; uint8_t IXModel::GetHighestAffectingBoneIndex(const Game::XModelLodInfo* lod) { uint8_t highestBoneIndex = 0; constexpr auto LENGTH = 6; { for (auto surfIndex = 0; surfIndex < lod->numsurfs; surfIndex++) { const auto surface = &lod->surfs[surfIndex]; auto vertsBlendOffset = 0; int rebuiltPartBits[LENGTH]{}; std::unordered_set affectingBones{}; const auto registerBoneAffectingSurface = [&](unsigned int offset) { uint8_t index = static_cast(surface->vertInfo.vertsBlend[offset] / sizeof(Game::DObjSkelMat)); highestBoneIndex = std::max(highestBoneIndex, index); }; // 1 bone weight for (unsigned int vertIndex = 0; vertIndex < surface->vertInfo.vertCount[0]; vertIndex++) { registerBoneAffectingSurface(vertsBlendOffset + 0); vertsBlendOffset += 1; } // 2 bone weights for (unsigned int vertIndex = 0; vertIndex < surface->vertInfo.vertCount[1]; vertIndex++) { registerBoneAffectingSurface(vertsBlendOffset + 0); registerBoneAffectingSurface(vertsBlendOffset + 1); vertsBlendOffset += 3; } // 3 bone weights for (unsigned int vertIndex = 0; vertIndex < surface->vertInfo.vertCount[2]; vertIndex++) { registerBoneAffectingSurface(vertsBlendOffset + 0); registerBoneAffectingSurface(vertsBlendOffset + 1); registerBoneAffectingSurface(vertsBlendOffset + 3); vertsBlendOffset += 5; } // 4 bone weights for (unsigned int vertIndex = 0; vertIndex < surface->vertInfo.vertCount[3]; vertIndex++) { registerBoneAffectingSurface(vertsBlendOffset + 0); registerBoneAffectingSurface(vertsBlendOffset + 1); registerBoneAffectingSurface(vertsBlendOffset + 3); registerBoneAffectingSurface(vertsBlendOffset + 5); vertsBlendOffset += 7; } for (unsigned int vertListIndex = 0; vertListIndex < surface->vertListCount; vertListIndex++) { highestBoneIndex = std::max(highestBoneIndex, static_cast(surface->vertList[vertListIndex].boneOffset / sizeof(Game::DObjSkelMat))); } } } return highestBoneIndex; }; void IXModel::RebuildPartBits(Game::XModel* model) { constexpr auto LENGTH = 6; for (auto i = 0; i < model->numLods; i++) { const auto lod = &model->lodInfo[i]; int lodPartBits[6]{}; for (auto surfIndex = 0; surfIndex < lod->numsurfs; surfIndex++) { const auto surface = &lod->surfs[surfIndex]; auto vertsBlendOffset = 0; int rebuiltPartBits[6]{}; std::unordered_set affectingBones{}; const auto registerBoneAffectingSurface = [&](unsigned int offset) { uint8_t index = static_cast(surface->vertInfo.vertsBlend[offset] / sizeof(Game::DObjSkelMat)); assert(index >= 0); assert(index < model->numBones); affectingBones.emplace(index); }; // 1 bone weight for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[0]; vertIndex++) { registerBoneAffectingSurface(vertsBlendOffset + 0); vertsBlendOffset += 1; } // 2 bone weights for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[1]; vertIndex++) { registerBoneAffectingSurface(vertsBlendOffset + 0); registerBoneAffectingSurface(vertsBlendOffset + 1); vertsBlendOffset += 3; } // 3 bone weights for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[2]; vertIndex++) { registerBoneAffectingSurface(vertsBlendOffset + 0); registerBoneAffectingSurface(vertsBlendOffset + 1); registerBoneAffectingSurface(vertsBlendOffset + 3); vertsBlendOffset += 5; } // 4 bone weights for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[3]; vertIndex++) { registerBoneAffectingSurface(vertsBlendOffset + 0); registerBoneAffectingSurface(vertsBlendOffset + 1); registerBoneAffectingSurface(vertsBlendOffset + 3); registerBoneAffectingSurface(vertsBlendOffset + 5); vertsBlendOffset += 7; } for (auto vertListIndex = 0; vertListIndex < surface->vertListCount; vertListIndex++) { affectingBones.emplace(static_cast(surface->vertList[vertListIndex].boneOffset / sizeof(Game::DObjSkelMat))); } // Actually rebuilding for (const auto& boneIndex : affectingBones) { const auto bitPosition = 31 - boneIndex % 32; const auto groupIndex = boneIndex / 32; assert(groupIndex < 6); assert(groupIndex >= 0); rebuiltPartBits[groupIndex] |= 1 << bitPosition; lodPartBits[groupIndex] |= 1 << bitPosition; } std::memcpy(surface->partBits, rebuiltPartBits, 6 * sizeof(int32_t)); } std::memcpy(lod->partBits, lodPartBits, 6 * sizeof(int32_t)); std::memcpy(lod->modelSurfs->partBits, lodPartBits, 6 * sizeof(int32_t)); // here's a little lesson in trickery: // We set the 192nd part bit to TRUE because it has no consequences // but allows us to find out whether that surf was already converted in the past or not lod->partBits[LENGTH - 1] |= 0x1; lod->modelSurfs->partBits[LENGTH - 1] |= 0x1; } }; uint8_t IXModel::InsertBone(Game::XModel* model, const std::string& boneName, const std::string& parentName, Utils::Memory::Allocator& allocator) { assert(GetIndexOfBone(model, boneName) == UCHAR_MAX); constexpr auto MAX_BONES = 192; assert(model->numBones < MAX_BONES); // Start with backing up parent links that we will have to restore // We'll restore them at the end std::map parentsToRestore{}; for (int i = model->numRootBones; i < model->numBones; i++) { parentsToRestore[Game::SL_ConvertToString(model->boneNames[i])] = GetParentOfBone(model, i); } const uint8_t newBoneCount = model->numBones + 1; const uint8_t newBoneCountMinusRoot = newBoneCount - model->numRootBones; const auto parentIndex = GetIndexOfBone(model, parentName); assert(parentIndex != UCHAR_MAX); const uint8_t atPosition = parentIndex + 1; const uint8_t newBoneIndex = atPosition; const uint8_t newBoneIndexMinusRoot = atPosition - model->numRootBones; Components::Logger::Print("Inserting bone {} at position {} (between {} and {})\n", boneName, atPosition, Game::SL_ConvertToString(model->boneNames[atPosition - 1]), Game::SL_ConvertToString(model->boneNames[atPosition + 1])); // Reallocate const auto newBoneNames = allocator.allocateArray(newBoneCount); const auto newMats = allocator.allocateArray(newBoneCount); const auto newBoneInfo = allocator.allocateArray(newBoneCount); const auto newPartsClassification = allocator.allocateArray(newBoneCount); const auto newQuats = allocator.allocateArray(4 * newBoneCountMinusRoot); const auto newTrans = allocator.allocateArray(3 * newBoneCountMinusRoot); const auto newParentList = allocator.allocateArray(newBoneCountMinusRoot); const uint8_t lengthOfFirstPart = atPosition; const uint8_t lengthOfSecondPart = model->numBones - atPosition; const uint8_t lengthOfFirstPartM1 = atPosition - model->numRootBones; const uint8_t lengthOfSecondPartM1 = model->numBones - model->numRootBones - (atPosition - model->numRootBones); const uint8_t atPositionM1 = atPosition - model->numRootBones; // should be equal to model->numBones int total = lengthOfFirstPart + lengthOfSecondPart; assert(total = model->numBones); // should be equal to model->numBones - model->numRootBones int totalM1 = lengthOfFirstPartM1 + lengthOfSecondPartM1; assert(totalM1 == model->numBones - model->numRootBones); // Copy before if (lengthOfFirstPart > 0) { std::memcpy(newBoneNames, model->boneNames, sizeof(uint16_t) * lengthOfFirstPart); std::memcpy(newMats, model->baseMat, sizeof(Game::DObjAnimMat) * lengthOfFirstPart); std::memcpy(newPartsClassification, model->partClassification, lengthOfFirstPart); std::memcpy(newBoneInfo, model->boneInfo, sizeof(Game::XBoneInfo) * lengthOfFirstPart); std::memcpy(newQuats, model->quats, sizeof(uint16_t) * 4 * lengthOfFirstPartM1); std::memcpy(newTrans, model->trans, sizeof(float) * 3 * lengthOfFirstPartM1); } // Insert new bone { unsigned int name = Game::SL_GetString(boneName.data(), 0); Game::XBoneInfo boneInfo{}; Game::DObjAnimMat mat{}; // It's ABSOLUTE! mat = model->baseMat[parentIndex]; boneInfo = model->boneInfo[parentIndex]; // It's RELATIVE ! uint16_t quat[4]{}; quat[3] = SHRT_MAX; // 0 0 0 1 float trans[3]{}; mat.transWeight = 1.9999f; // Should be 1.9999 like everybody? newMats[newBoneIndex] = mat; newBoneInfo[newBoneIndex] = boneInfo; newBoneNames[newBoneIndex] = static_cast(name); // TODO parts Classification std::memcpy(&newQuats[newBoneIndexMinusRoot * 4], quat, ARRAYSIZE(quat) * sizeof(uint16_t)); std::memcpy(&newTrans[newBoneIndexMinusRoot * 3], trans, ARRAYSIZE(trans) * sizeof(float)); } // Copy after if (lengthOfSecondPart > 0) { std::memcpy(&newBoneNames[atPosition + 1], &model->boneNames[atPosition], sizeof(uint16_t) * lengthOfSecondPart); std::memcpy(&newMats[atPosition + 1], &model->baseMat[atPosition], sizeof(Game::DObjAnimMat) * lengthOfSecondPart); std::memcpy(&newPartsClassification[atPosition + 1], &model->partClassification[atPosition], lengthOfSecondPart); std::memcpy(&newBoneInfo[atPosition + 1], &model->boneInfo[atPosition], sizeof(Game::XBoneInfo) * lengthOfSecondPart); std::memcpy(&newQuats[(atPositionM1 + 1) * 4], &model->quats[atPositionM1 * 4], sizeof(uint16_t) * 4 * lengthOfSecondPartM1); std::memcpy(&newTrans[(atPositionM1 + 1) * 3], &model->trans[atPositionM1 * 3], sizeof(float) * 3 * lengthOfSecondPartM1); } //Game::Z_VirtualFree(model->baseMat); //Game::Z_VirtualFree(model->boneInfo); //Game::Z_VirtualFree(model->boneNames); //Game::Z_VirtualFree(model->quats); //Game::Z_VirtualFree(model->trans); //Game::Z_VirtualFree(model->parentList); // Assign reallocated model->baseMat = newMats; model->boneInfo = newBoneInfo; model->boneNames = newBoneNames; model->quats = newQuats; model->trans = newTrans; model->parentList = newParentList; model->numBones = newBoneCount; // Update vertex weight for (uint8_t lodIndex = 0; lodIndex < model->numLods; lodIndex++) { const auto lod = &model->lodInfo[lodIndex]; if ((lod->partBits[5] & 0x1) == 0x1) { // surface lod already converted (more efficient) continue; } if (GetHighestAffectingBoneIndex(lod) >= model->numBones) { // surface lod already converted (more accurate) continue; } for (int surfIndex = 0; surfIndex < lod->modelSurfs->numsurfs; surfIndex++) { auto vertsBlendOffset = 0u; const auto surface = &lod->modelSurfs->surfs[surfIndex]; static_assert(sizeof(Game::DObjSkelMat) == 64); { const auto fixVertexBlendIndex = [&](unsigned int offset) { int index = static_cast(surface->vertInfo.vertsBlend[offset] / sizeof(Game::DObjSkelMat)); if (index >= atPosition) { index++; if (index < 0 || index >= model->numBones) { Components::Logger::Print("Unexpected 'bone index' {} out of {} bones while working vertex blend of model {} lod {} surf {}\n", index, model->numBones, model->name, lodIndex, surfIndex); assert(false); } surface->vertInfo.vertsBlend[offset] = static_cast(index * sizeof(Game::DObjSkelMat)); } }; // Fix bone offsets if (surface->vertList) { for (auto vertListIndex = 0u; vertListIndex < surface->vertListCount; vertListIndex++) { const auto vertList = &surface->vertList[vertListIndex]; auto index = vertList->boneOffset / sizeof(Game::DObjSkelMat); if (index >= atPosition) { index++; if (index < 0 || index >= model->numBones) { Components::Logger::Print("Unexpected 'bone index' {} out of {} bones while working list blend of model {} lod {} surf {}\n", index, model->numBones, model->name, lodIndex, surfIndex); assert(false); } vertList->boneOffset = static_cast(index * sizeof(Game::DObjSkelMat)); } } } // 1 bone weight for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[0]; vertIndex++) { fixVertexBlendIndex(vertsBlendOffset + 0); vertsBlendOffset += 1; } // 2 bone weights for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[1]; vertIndex++) { fixVertexBlendIndex(vertsBlendOffset + 0); fixVertexBlendIndex(vertsBlendOffset + 1); vertsBlendOffset += 3; } // 3 bone weights for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[2]; vertIndex++) { fixVertexBlendIndex(vertsBlendOffset + 0); fixVertexBlendIndex(vertsBlendOffset + 1); fixVertexBlendIndex(vertsBlendOffset + 3); vertsBlendOffset += 5; } // 4 bone weights for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[3]; vertIndex++) { fixVertexBlendIndex(vertsBlendOffset + 0); fixVertexBlendIndex(vertsBlendOffset + 1); fixVertexBlendIndex(vertsBlendOffset + 3); fixVertexBlendIndex(vertsBlendOffset + 5); vertsBlendOffset += 7; } } } } SetParentIndexOfBone(model, atPosition, parentIndex); // Restore parents for (const auto& kv : parentsToRestore) { // Fix parents const auto key = kv.first; const auto beforeVal = kv.second; const auto parentIndex = GetIndexOfBone(model, beforeVal); const auto index = GetIndexOfBone(model, key); SetParentIndexOfBone(model, index, parentIndex); } #if DEBUG // check for (const auto& kv : parentsToRestore) { const auto key = kv.first; const auto beforeVal = kv.second; const auto index = GetIndexOfBone(model, key); const auto afterVal = GetParentOfBone(model, index); if (beforeVal != afterVal) { printf(""); } } // #endif return atPosition; // Bone index of added bone }; void IXModel::TransferWeights(Game::XModel* model, const uint8_t origin, const uint8_t destination) { const auto from = Game::SL_ConvertToString(model->boneNames[origin]); const auto to = Game::SL_ConvertToString(model->boneNames[destination]); Components::Logger::Print("Transferring bone weights from {} to {}\n", from, to); const auto originalWeights = model->baseMat[origin].transWeight; model->baseMat[origin].transWeight = model->baseMat[destination].transWeight; model->baseMat[destination].transWeight = originalWeights; for (int i = 0; i < model->numLods; i++) { const auto lod = &model->lodInfo[i]; if ((lod->partBits[5] & 0x1) == 0x1) { // surface lod already converted (more efficient) continue; } for (int surfIndex = 0; surfIndex < lod->modelSurfs->numsurfs; surfIndex++) { auto vertsBlendOffset = 0u; const auto surface = &lod->modelSurfs->surfs[surfIndex]; { const auto transferVertexBlendIndex = [&](unsigned int offset) { int index = static_cast(surface->vertInfo.vertsBlend[offset] / sizeof(Game::DObjSkelMat)); if (index == origin) { index = destination; if (index < 0 || index >= model->numBones) { assert(false); } surface->vertInfo.vertsBlend[offset] = static_cast(index * sizeof(Game::DObjSkelMat)); } }; // Fix bone offsets if (surface->vertList) { for (auto vertListIndex = 0u; vertListIndex < surface->vertListCount; vertListIndex++) { const auto vertList = &surface->vertList[vertListIndex]; auto index = vertList->boneOffset / sizeof(Game::DObjSkelMat); if (index == origin) { if (index < 0 || index >= model->numBones) { assert(false); } index = destination; vertList->boneOffset = static_cast(index * sizeof(Game::DObjSkelMat)); } } } // 1 bone weight for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[0]; vertIndex++) { transferVertexBlendIndex(vertsBlendOffset + 0); vertsBlendOffset += 1; } // 2 bone weights for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[1]; vertIndex++) { transferVertexBlendIndex(vertsBlendOffset + 0); transferVertexBlendIndex(vertsBlendOffset + 1); vertsBlendOffset += 3; } // 3 bone weights for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[2]; vertIndex++) { transferVertexBlendIndex(vertsBlendOffset + 0); transferVertexBlendIndex(vertsBlendOffset + 1); transferVertexBlendIndex(vertsBlendOffset + 3); vertsBlendOffset += 5; } // 4 bone weights for (auto vertIndex = 0; vertIndex < surface->vertInfo.vertCount[3]; vertIndex++) { transferVertexBlendIndex(vertsBlendOffset + 0); transferVertexBlendIndex(vertsBlendOffset + 1); transferVertexBlendIndex(vertsBlendOffset + 3); transferVertexBlendIndex(vertsBlendOffset + 5); vertsBlendOffset += 7; } } } } }; void IXModel::SetBoneTrans(Game::XModel* model, uint8_t boneIndex, bool baseMat, float x, float y, float z) { if (baseMat) { model->baseMat[boneIndex].trans[0] = x; model->baseMat[boneIndex].trans[1] = y; model->baseMat[boneIndex].trans[2] = z; } else { const auto index = boneIndex - model->numRootBones; assert(index >= 0); model->trans[index * 3 + 0] = x; model->trans[index * 3 + 1] = y; model->trans[index * 3 + 2] = z; } } void IXModel::SetBoneQuaternion(Game::XModel* model, uint8_t boneIndex, bool baseMat, float x, float y, float z, float w) { if (baseMat) { model->baseMat[boneIndex].quat[0] = x; model->baseMat[boneIndex].quat[1] = y; model->baseMat[boneIndex].quat[2] = z; model->baseMat[boneIndex].quat[3] = w; } else { const auto index = boneIndex - model->numRootBones; assert(index >= 0); model->quats[index * 4 + 0] = static_cast(x * SHRT_MAX); model->quats[index * 4 + 1] = static_cast(y * SHRT_MAX); model->quats[index * 4 + 2] = static_cast(z * SHRT_MAX); model->quats[index * 4 + 3] = static_cast(w * SHRT_MAX); } } void IXModel::ConvertPlayerModelFromSingleplayerToMultiplayer(Game::XModel* model, Utils::Memory::Allocator& allocator) { auto indexOfSpine = GetIndexOfBone(model, "j_spinelower"); if (indexOfSpine < UCHAR_MAX) // It has a spine so it must be some sort of humanoid { const auto nameOfParent = GetParentOfBone(model, indexOfSpine); if (GetIndexOfBone(model, "torso_stabilizer") == UCHAR_MAX) // Singleplayer model is likely { Components::Logger::Print("Converting {}\n", model->name); // No stabilizer - let's do surgery // We're trying to get there: // tag_origin // j_main_root // pelvis // j_hip_le // j_hip_ri // tag_stowed_hip_rear // torso_stabilizer // j_spinelower // back_low // j_spineupper // back_mid // j_spine4 const auto root = GetIndexOfBone(model, "j_mainroot"); if (root < UCHAR_MAX) { // Add pelvis #if false const uint8_t backLow = InsertBone(model, "back_low", "j_spinelower", allocator); TransferWeights(model, GetIndexOfBone(model, "j_spinelower"), backLow); SetParentIndexOfBone(model, GetIndexOfBone(model, "j_spineupper"), backLow); #else const uint8_t indexOfPelvis = InsertBone(model, "pelvis", "j_mainroot", allocator); SetBoneQuaternion(model, indexOfPelvis, true, -0.494f, -0.506f, -0.506f, 0.494); TransferWeights(model, root, indexOfPelvis); SetParentIndexOfBone(model, GetIndexOfBone(model, "j_hip_le"), indexOfPelvis); SetParentIndexOfBone(model, GetIndexOfBone(model, "j_hip_ri"), indexOfPelvis); SetParentIndexOfBone(model, GetIndexOfBone(model, "tag_stowed_hip_rear"), indexOfPelvis); // These two are optional if (GetIndexOfBone(model, "j_coatfront_le") == UCHAR_MAX) { InsertBone(model, "j_coatfront_le", "pelvis", allocator); } if (GetIndexOfBone(model, "j_coatfront_ri") == UCHAR_MAX) { InsertBone(model, "j_coatfront_ri", "pelvis", allocator); } const uint8_t torsoStabilizer = InsertBone(model, "torso_stabilizer", "pelvis", allocator); const uint8_t lowerSpine = GetIndexOfBone(model, "j_spinelower"); SetParentIndexOfBone(model, lowerSpine, torsoStabilizer); const uint8_t backLow = InsertBone(model, "back_low", "j_spinelower", allocator); TransferWeights(model, lowerSpine, backLow); SetParentIndexOfBone(model, GetIndexOfBone(model, "j_spineupper"), backLow); const uint8_t backMid = InsertBone(model, "back_mid", "j_spineupper", allocator); TransferWeights(model, GetIndexOfBone(model, "j_spineupper"), backMid); SetParentIndexOfBone(model, GetIndexOfBone(model, "j_spine4"), backMid); assert(root == GetIndexOfBone(model, "j_mainroot")); assert(indexOfPelvis == GetIndexOfBone(model, "pelvis")); assert(backLow == GetIndexOfBone(model, "back_low")); assert(backMid == GetIndexOfBone(model, "back_mid")); // Twister bone SetBoneQuaternion(model, lowerSpine, false, -0.492f, -0.507f, -0.507f, 0.492f); SetBoneQuaternion(model, torsoStabilizer, false, 0.494f, 0.506f, 0.506f, 0.494f); // This doesn't feel like it should be necessary // It is, on singleplayer models unfortunately. Could we add an extra bone to compensate this? // Or compensate it another way? SetBoneTrans(model, GetIndexOfBone(model, "j_spinelower"), false, 0.07, 0.0f, 5.2f); // These are often messed up on civilian models, but there is no obvious way to tell from code const auto stowedBack = GetIndexOfBone(model, "tag_stowed_back"); if (stowedBack != UCHAR_MAX) { SetBoneTrans(model, stowedBack, false, -0.32f, -6.27f, -2.65F); SetBoneQuaternion(model, stowedBack, false, -0.044, 0.088, -0.995, 0.025); SetBoneTrans(model, stowedBack, true, -9.571f, -2.654f, 51.738f); SetBoneQuaternion(model, stowedBack, true, -0.071f, 0.0f, -0.997f, 0.0f); } if (stowedBack != UCHAR_MAX) { const auto stowedRear = GetIndexOfBone(model, "tag_stowed_hip_rear"); SetBoneTrans(model, stowedRear, false, -0.75f, -6.45f, -4.99f); SetBoneQuaternion(model, stowedRear, false, -0.553f, -0.062f, -0.049f, 0.830f); SetBoneTrans(model, stowedBack, true, -9.866f, -4.989f, 36.315f); SetBoneQuaternion(model, stowedRear, true, -0.054, -0.025f, -0.975f, 0.214f); } #endif RebuildPartBits(model); } } printf(""); } } }