diff --git a/source/game_sa/ClothesBuilder.cpp b/source/game_sa/ClothesBuilder.cpp index 2ffd53d038..6da2bc23dd 100644 --- a/source/game_sa/ClothesBuilder.cpp +++ b/source/game_sa/ClothesBuilder.cpp @@ -1,49 +1,50 @@ #include "StdInc.h" +#include + #include "ClothesBuilder.h" +#include "PedClothesDesc.h" CDirectory& playerImg = *(CDirectory*)0xBC12C0; CDirectory::DirectoryInfo& playerImgEntries = *(CDirectory::DirectoryInfo*)0xBBCDC8; +auto& gBoneIndices = StaticRef, 0xBBC8C8>(); + +auto& ms_ratiosHaveChanged = StaticRef(); +auto& ms_geometryHasChanged = StaticRef(); +auto& ms_textureHasChanged = StaticRef(); + void CClothesBuilder::InjectHooks() { RH_ScopedClass(CClothesBuilder); RH_ScopedCategoryGlobal(); - RH_ScopedInstall(LoadCdDirectory, 0x5A4190, { .reversed = false }); - RH_ScopedInstall(RequestGeometry, 0x5A41C0, { .reversed = false }); - RH_ScopedInstall(RequestTexture, 0x5A4220, { .reversed = false }); + RH_ScopedInstall(LoadCdDirectory, 0x5A4190); + RH_ScopedInstall(RequestGeometry, 0x5A41C0); + RH_ScopedInstall(RequestTexture, 0x5A4220); //RH_ScopedInstall(nullptr, 0x5A42B0, { .reversed = false }); //RH_ScopedInstall(nullptr, 0x5A4380, { .reversed = false }); AtomicInstanceCB //RH_ScopedInstall(nullptr, 0x5A43A0, { .reversed = false }); //RH_ScopedInstall(nullptr, 0x5A44A0, { .reversed = false }); DestroyTextureCB RH_ScopedInstall(PreprocessClothesDesc, 0x5A44C0, { .reversed = false }); RH_ScopedInstall(ReleaseGeometry, 0x5A47B0, { .reversed = false }); - RH_ScopedGlobalInstall(FindAtomicFromNameCB, 0x5A47E0, { .reversed = false }); - RH_ScopedGlobalInstall(GetAtomicWithName, 0x5A4810, { .reversed = false }); - RH_ScopedInstall(sub_5A4840, 0x5A4840, { .reversed = false }); - RH_ScopedInstall(StoreBoneArray, 0x5A48B0, { .reversed = false }); - // RH_ScopedOverloadedInstall(BlendGeometry, "", 0x5A4940, RpGeometry* (*)(RpClump*, const char*, const char*, const char*, float, float, float)); - // RH_ScopedOverloadedInstall(BlendGeometry, "", 0x5A4F10, RpGeometry* (*)(RpClump*, const char*, const char*, float, float)); + RH_ScopedGlobalInstall(GetAtomicWithName, 0x5A4810); + RH_ScopedInstall(AddWeightToBoneVertex, 0x5A4840); + RH_ScopedInstall(StoreBoneArray, 0x5A48B0); + RH_ScopedOverloadedInstall(BlendGeometry, "3", 0x5A4940, RpGeometry * (*)(RpClump*, const char*, const char*, const char*, float, float, float), { .reversed = false }); + RH_ScopedOverloadedInstall(BlendGeometry, "2", 0x5A4F10, RpGeometry* (*)(RpClump*, const char*, const char*, float, float), {.reversed = false}); RH_ScopedInstall(CopyGeometry, 0x5A5340, { .reversed = false }); - RH_ScopedInstall(ConstructGeometryArray, 0x5A55A0, { .reversed = false }); - RH_ScopedInstall(DestroySkinArrays, 0x5A56C0, { .reversed = false }); - RH_ScopedInstall(BuildBoneIndexConversionTable, 0x5A56E0, { .reversed = false }); - RH_ScopedInstall(CopyTexture, 0x5A5730, { .reversed = false }); - RH_ScopedInstall(PlaceTextureOnTopOfTexture, 0x5A57B0, { .reversed = false }); - // RH_ScopedOverloadedInstall(BlendTextures, "", 0x5A5820, void (*)(RwTexture*, RwTexture*, float, float, int32)); - // RH_ScopedOverloadedInstall(BlendTextures, "", 0x5A59C0, void (*)(RwTexture*, RwTexture*, RwTexture*, float, float, float, int32)); - // RH_ScopedOverloadedInstall(BlendTextures, "", 0x5A5BC0, void (*)(RwTexture*, RwTexture*, RwTexture*, float, float, float, int32, RwTexture*)); - RH_ScopedInstall(InitPaletteOctTree, 0x5A5EB0, { .reversed = false }); - RH_ScopedInstall(ShutdownPaletteOctTree, 0x5A5EE0, { .reversed = false }); - RH_ScopedInstall(ReducePaletteOctTree, 0x5A5EF0, { .reversed = false }); - RH_ScopedInstall(AddColour, 0x5A5F00, { .reversed = false }); - RH_ScopedInstall(FillPalette, 0x5A5F30, { .reversed = false }); - RH_ScopedInstall(FindNearestColour, 0x5A5F40, { .reversed = false }); - RH_ScopedGlobalInstall(GetTextureFromTxdAndLoadNextTxd, 0x5A5F70, { .reversed = false }); + RH_ScopedInstall(ConstructGeometryArray, 0x5A55A0, { .reversed = false }); // Makes the game crash - Probably a register is changed or smth + RH_ScopedInstall(DestroySkinArrays, 0x5A56C0); + RH_ScopedInstall(BuildBoneIndexConversionTable, 0x5A56E0); + RH_ScopedInstall(CopyTexture, 0x5A5730); + RH_ScopedInstall(PlaceTextureOnTopOfTexture, 0x5A57B0); + RH_ScopedOverloadedInstall(BlendTextures, "Dst-Src", 0x5A5820, void (*)(RwTexture*, RwTexture*, float, float, int32)); + RH_ScopedOverloadedInstall(BlendTextures, "Dst-Src1-Src2", 0x5A59C0, void (*)(RwTexture*, RwTexture*, RwTexture*, float, float, float, int32)); + RH_ScopedOverloadedInstall(BlendTextures, "Dst-Src1-Src2-Tat", 0x5A5BC0, void (*)(RwTexture*, RwTexture*, RwTexture*, float, float, float, int32, RwTexture*)); + RH_ScopedGlobalInstall(GetTextureFromTxdAndLoadNextTxd, 0x5A5F70); RH_ScopedInstall(ConstructTextures, 0x5A6040, { .reversed = false }); RH_ScopedInstall(ConstructGeometryAndSkinArrays, 0x5A6530, { .reversed = false }); - RH_ScopedInstall(ReducePaletteSize, 0x5A6870, { .reversed = false }); - RH_ScopedInstall(CreateSkinnedClump, 0x5A69D0, { .reversed = false }); + RH_ScopedInstall(CreateSkinnedClump, 0x5A69D0); } // inlined @@ -54,13 +55,28 @@ void CClothesBuilder::LoadCdDirectory() { } // 0x5A41C0 -void CClothesBuilder::RequestGeometry(int32 modelId, uint32 crc) { - plugin::Call<0x5A41C0, int32, uint32>(modelId, crc); +void CClothesBuilder::RequestGeometry(int32 modelId, uint32 modelNameKey) { + CModelInfo::GetModelInfo(modelId)->bHasComplexHierarchy = true; // TODO/NOTE: Not sure + uint32 offset, size; + VERIFY(playerImg.FindItem(CKeyGen::AppendStringToKey(modelNameKey, ".DFF"), offset, size)); + CStreaming::RequestFile(modelId, offset, size, CClothes::ms_clothesImageId, STREAMING_PRIORITY_REQUEST | STREAMING_GAME_REQUIRED); } // 0x5A4220 -int32 CClothesBuilder::RequestTexture(uint32 crc) { - return plugin::CallAndReturn(crc); +int32 CClothesBuilder::RequestTexture(uint32 txdNameKey) { + if (txdNameKey == 0) { + return -1; + } + + auto& defaultTxdIdx = StaticRef(); + const auto defaultTxd = CTxdStore::defaultTxds[defaultTxdIdx]; + defaultTxdIdx = (defaultTxdIdx + 1) % 4; + + uint32 offset, size; + VERIFY(playerImg.FindItem(CKeyGen::AppendStringToKey(txdNameKey, ".TXD"), offset, size)); + CStreaming::RequestFile(TXDToModelId(defaultTxd), offset, size, CClothes::ms_clothesImageId, STREAMING_PRIORITY_REQUEST | STREAMING_GAME_REQUIRED); + + return defaultTxd; } // 0x5A44C0 @@ -75,34 +91,167 @@ void CClothesBuilder::ReleaseGeometry(int32 numToRelease) { --i; } -// 0x5A47E0 -void FindAtomicFromNameCB(RpAtomic* atomic, void* data) { - plugin::Call<0x5A47E0, RpAtomic*, void*>(atomic, data); -} - // 0x5A4810 -void GetAtomicWithName(RpClump* clump, const char* name) { - plugin::Call<0x5A4810, RpClump*, const char*>(clump, name); +RpAtomic* GetAtomicWithName(RpClump* clump, const char* name) { + struct Context { + notsa::ci_string_view name{}; + RpAtomic* atomic{}; + } c{name}; + RpClumpForAllAtomics(clump, [](RpAtomic* a, void* data) { // 0x5A47E0 + auto& ctx = *static_cast(data); + if (ctx.name == GetFrameNodeName(RpAtomicGetFrame(a))) { + ctx.atomic = a; + } + return a; + }, &c); + return c.atomic; } // 0x5A4840 -void CClothesBuilder::sub_5A4840() { - +void CClothesBuilder::AddWeightToBoneVertex(float (&weights)[8], uint8(&boneVertexIdxs)[8], float weightToAdd, RwUInt32 targetVertexIdx) { // Unknown OG name + if (weightToAdd == 0.f) { + return; + } + for (auto i = 0; i < 8; i++) { + if (weights[i] == 0.f) { // Weight not yet used? + boneVertexIdxs[i] = targetVertexIdx; // Add to list + weights[i] = weightToAdd; + weights[i + 1] = 0.f; // Mark next as unused [Though this step in our case is not necessary as the whole weights array is already zero-inited] + return; + } + if (boneVertexIdxs[i] == targetVertexIdx) { // Already in the list, use that + weights[i] += weightToAdd; + return; + } + } + NOTSA_UNREACHABLE(); // OG code had UB in this case } // 0x5A48B0 -void CClothesBuilder::StoreBoneArray(RpClump* clump, int32 a2) { - plugin::Call<0x5A48B0, RpClump*, int32>(clump, a2); +void CClothesBuilder::StoreBoneArray(RpClump* clump, int32 idx) { + const auto a = GetAtomicWithName(clump, "normal"); + assert(a); + + const auto h = RpSkinAtomicGetHAnimHierarchy(a); + assert(h); + + rng::fill(gBoneIndices[idx], -1); + for (auto i = h->numNodes; i-- > 0;) { + gBoneIndices[idx][i] = static_cast(h->pNodeInfo[i].nodeID); + } +} + +/* +* @notsa +* +* Based on 0x5A4940 +* Blend any number of geometries together +* The result is stored in the 0th frame's geometry. +* +* @arg clump The clump to which the frames belong to +* @arg frames A list of frames whose geometry should be blended together +*/ +template +RpGeometry* BlendGeometry(RpClump* clump, std::pair (&&frameNamesRatios)[N]) { +#ifdef NOTSA_DEBUG + { + float a{}; + for (auto&& v : frameNamesRatios) { + a += v.second; + } + assert(a >= 0.f && a <= 1.f); + } +#endif + + // Process data needed for blending + struct GeoBlendData { + RpAtomic* a; + RpGeometry* g; + const RwUInt8* boneIdxs; + RwMatrixWeights* boneWeights; + RwTexCoords* uvs; + CVector* verts; + CVector* nrmls; + float r; // Blend ratio + } fds[N]; + auto& out = fds[0]; + for (auto&& [i, v] : notsa::enumerate(frameNamesRatios)) { + const auto a = GetAtomicWithName(clump, v.first); + const auto g = RpAtomicGetGeometry(a); + const auto s = RpSkinGeometryGetSkin(g); + const auto mt = RpGeometryGetMorphTarget(g, 0); + fds[i] = { + a, + g, + (const RwUInt8*)RpSkinGetVertexBoneIndices(s), // NOTE: Not sure why it's casted to UInt8, but that really is how the data is stored / TODO: Okay, so actually it seems like something is fucked + RpSkinGetVertexBoneWeights(s), + RpGeometryGetVertexTexCoords(g, 1), + (CVector*)RpMorphTargetGetVertices(mt), + (CVector*)RpMorphTargetGetVertexNormals(mt), + v.second + }; + } + + RpGeometryLock(out.g, rpGEOMETRYLOCKALL); + + for (auto i = 0; i < RpGeometryGetNumVertices(out.g); i++) { + const auto Blend = [&](auto&& Get) { + return multiply_weighted(fds | rng::views::transform([&](auto&& fd) { return WeightedValue{ std::invoke(Get, &fd)[i], fd.r }; })); + }; + out.verts[i] = Blend(&GeoBlendData::verts); + out.nrmls[i] = Blend(&GeoBlendData::nrmls); + out.uvs[i] = Blend(&GeoBlendData::uvs); + + // Helper function to get the weight at a given weight index + const auto RwMatrixWeightsGetWeight = [](RwMatrixWeights& mw, int32 wi) -> float& { + switch (wi) { + case 0: return mw.w0; + case 1: return mw.w1; + case 2: return mw.w2; + case 3: return mw.w3; + default: NOTSA_UNREACHABLE(); + } + }; + + // Calculate bone weights + float weights[8]{}; + uint8 boneVertexIdxs[8]{}; + for (auto& fd : fds) { + for (auto wi = 0; wi < 4; wi++) { + const auto vtxIdx = i + wi; + CClothesBuilder::AddWeightToBoneVertex( + weights, + boneVertexIdxs, + RwMatrixWeightsGetWeight(fd.boneWeights[vtxIdx], wi), + fd.boneIdxs[vtxIdx] + ); + } + } + for (auto b = 0; b < 4; b++) { + const_cast(out.boneIdxs)[i + b] = boneVertexIdxs[b]; + } + const auto t = weights[4] != 0.f + ? 1.f / std::accumulate(weights, weights + 4, 0.f) + : 1.f; + for (auto wi = 0; wi < 4; wi++) { + RwMatrixWeightsGetWeight(out.boneWeights[i + wi], wi) = weights[wi] * t; + } + } + + RpGeometryUnlock(out.g); + out.g->refCount++; // TODO: Function missing + + return out.g; } // 0x5A4940 -RpGeometry* CClothesBuilder::BlendGeometry(RpClump* clump, const char* a2, const char* a3, const char* a4, float a5, float a6, float a7) { - return plugin::CallAndReturn(clump, a2, a3, a4, a5, a6, a7); +RpGeometry* CClothesBuilder::BlendGeometry(RpClump* clump, const char* frameName0, const char* frameName1, const char* frameName2, float r0, float r1, float r2) { + return ::BlendGeometry(clump, { {frameName0, r0}, {frameName1, r1}, {frameName2, r2} }); } // 0x5A4F10 -RpGeometry* CClothesBuilder::BlendGeometry(RpClump* clump, const char* a2, const char* a3, float a4, float a5) { - return plugin::CallAndReturn(clump, a2, a3, a4, a5); +RpGeometry* CClothesBuilder::BlendGeometry(RpClump* clump, const char* frameName0, const char* frameName1, float r0, float r1) { + return ::BlendGeometry(clump, { {frameName0, r0}, {frameName1, r1} }); } // 0x5A5340 @@ -111,97 +260,228 @@ RpGeometry* CClothesBuilder::CopyGeometry(RpClump* clump, const char* a2, const } // 0x5A55A0 -void CClothesBuilder::ConstructGeometryArray(RpGeometry** geometry, uint32* a2, float a3, float a4, float a5) { - plugin::Call<0x5A55A0, RpGeometry**, uint32*, float, float, float>(geometry, a2, a3, a4, a5); +void CClothesBuilder::ConstructGeometryArray(RpGeometry** out, uint32* modelNameKeys, float normal, float fatness, float strength) { + for (auto i = 0; i < 10; i++) { + if (modelNameKeys[i] == 0) { + *out = nullptr; + continue; + } + const auto modelIdx = (eModelID)((int)MODEL_CLOTHES01_ID384 + i); + const auto mi = CModelInfo::GetModelInfo(modelIdx); + + CModelInfo::GetModelInfo(modelIdx)->bHasComplexHierarchy = true; + RequestGeometry(modelIdx, modelNameKeys[i]); + CStreaming::LoadAllRequestedModels(true); + + if (i + 1 < 10 && modelNameKeys[i + 1]) { // Request next model to be loaded in advance + RequestGeometry((eModelID)((int)MODEL_CLOTHES01_ID384 + i + 1), modelNameKeys[i + 1]); + CStreaming::LoadRequestedModels(); + } + + *out = BlendGeometry(mi->m_pRwClump, "normal", "fat", "ripped", normal, fatness, strength); + StoreBoneArray(mi->m_pRwClump, i); + CStreaming::RemoveModel(modelIdx); + } } // inlined, see 0x5A6CE1 // 0x5A56C0 -void CClothesBuilder::DestroySkinArrays(RwMatrixWeights* weights, uint32* a2) { - operator delete(weights); - operator delete(a2); +void CClothesBuilder::DestroySkinArrays(RwMatrixWeights* weights, RwUInt32* bones) { + // TODO: Should this be `delete[]` or `delete`? + delete weights; + delete bones; } // 0x5A56E0 -void CClothesBuilder::BuildBoneIndexConversionTable(uint8* a1, RpHAnimHierarchy* a2, int32 a3) { - plugin::Call<0x5A56E0, uint8*, RpHAnimHierarchy*, int32>(a1, a2, a3); +void CClothesBuilder::BuildBoneIndexConversionTable(uint8* pTable, RpHAnimHierarchy* hier, int32 index) { + for (const auto [tableIdx, boneId] : notsa::enumerate(gBoneIndices[index])) { + if (boneId == -1) { + break; + } + const auto idx = RpHAnimIDGetIndex(hier, boneId); + pTable[tableIdx] = idx == 0xFF ? 0 : idx; + } +} + +void AssertTextureLayouts(std::initializer_list textures) { + assert(textures.size() >= 2); + for (auto i = 0u; i < textures.size() - 1; i++) { + const auto r1 = RwTextureGetRaster(textures.begin()[i]), r2 = RwTextureGetRaster(textures.begin()[i + 1]); + + assert(RwRasterGetWidth(r1) == RwRasterGetWidth(r2)); + assert(RwRasterGetHeight(r1) == RwRasterGetHeight(r2)); + assert(RwRasterGetDepth(r1) == RwRasterGetDepth(r2)); + assert(RwRasterGetDepth(r1) == 32); + } } // 0x5A5730 -RwTexture* CClothesBuilder::CopyTexture(RwTexture* texture) { - return plugin::CallAndReturn(texture); +RwTexture* CClothesBuilder::CopyTexture(RwTexture* srcTex) { + const auto srcRaster = RwTextureGetRaster(srcTex); + + // Create a new raster to which we're going to copy to + const auto dstRaster = RwRasterCreate( + RwRasterGetWidth(srcRaster), + RwRasterGetHeight(srcRaster), + RwRasterGetDepth(srcRaster), + (RwRasterGetFormat(srcRaster) & rwRASTERFORMATPIXELFORMATMASK) | 4 // TODO + ); + + // Copy data from the src raster to this one + const auto srcLck = RwRasterLock(srcRaster, 0, rwRASTERLOCKREAD); + memcpy( + RwRasterLock(dstRaster, 0, rwRASTERLOCKWRITE), + srcLck, + RwRasterGetHeight(srcRaster) * RwRasterGetStride(srcRaster) + ); + RwRasterUnlock(srcRaster); + RwRasterUnlock(dstRaster); + + // Create a texture from the copied raster + const auto dstTex = RwTextureCreate(dstRaster); + RwTextureSetFilterMode(dstTex, rwFILTERLINEAR); + + AssertTextureLayouts({ dstTex, srcTex }); + + return dstTex; } // 0x5A57B0 -void CClothesBuilder::PlaceTextureOnTopOfTexture(RwTexture* texture1, RwTexture* texture2) { - plugin::Call<0x5A57B0, RwTexture*, RwTexture*>(texture1, texture2); +void CClothesBuilder::PlaceTextureOnTopOfTexture(RwTexture* dstTex, RwTexture* srcTex) { + ZoneScoped; + + AssertTextureLayouts({ dstTex, srcTex }); + + const auto dstRaster = RwTextureGetRaster(dstTex); + const auto srcRaster = RwTextureGetRaster(srcTex); + + auto dstIt = (RwUInt32*)RwRasterLock(dstRaster, 0, rwRASTERLOCKREADWRITE); + auto srcIt = (RwUInt32*)RwRasterLock(srcRaster, 0, rwRASTERLOCKREADWRITE); + + // NOTE: They don't skip the stride, but it's fine [This way vectorization should be easier for the compiler] + for (auto i = RwRasterGetHeight(dstRaster) * RwRasterGetWidth(dstRaster); i-- > 0; dstIt++, srcIt++) { + if (*srcIt & 0xFF000000) { // Check alpha != 0 + *dstIt = *srcIt; + } + } + + RwRasterUnlock(dstRaster); + RwRasterUnlock(srcRaster); } // 0x5A5820 -void CClothesBuilder::BlendTextures(RwTexture* t1, RwTexture* t2, float a3, float a4, int32 a5) { - plugin::Call<0x5A5820, RwTexture*, RwTexture*, float, float, int32>(t1, t2, a3, a4, a5); +void CClothesBuilder::BlendTextures(RwTexture* dst, RwTexture* src, float r1, float r2, int32 numColors) { + ZoneScoped; + + AssertTextureLayouts({ dst, src }); + + const auto dstRaster = RwTextureGetRaster(dst); + const auto srcRaster = RwTextureGetRaster(src); + + CTimer::Suspend(); + + auto srcIt = RwRasterLock(srcRaster, 0, rwRASTERLOCKREAD); + auto dstIt = RwRasterLock(dstRaster, 0, rwRASTERLOCKREADWRITE); + + for (auto i = RwRasterGetHeight(dstRaster) * RwRasterGetWidth(dstRaster); i-- > 0; dstIt++, srcIt++) { + for (auto c = 3; i-- > 0; dstIt++, srcIt++) { // Copy RGB, alpha stays the same + *dstIt = multiply_weighted({ { *dstIt, r1 }, { *srcIt, r2 } }); + } + } + + RwRasterUnlock(dstRaster); + RwRasterUnlock(srcRaster); + + CTimer::Resume(); } // 0x5A59C0 -void CClothesBuilder::BlendTextures(RwTexture* t1, RwTexture* t2, RwTexture* t3, float factorA, float factorB, float factorC, int32 a7) { - plugin::Call<0x5A59C0, RwTexture*, RwTexture*, RwTexture*, float, float, float>(t1, t2, t3, factorA, factorB, factorC, a7); +void CClothesBuilder::BlendTextures(RwTexture* dst, RwTexture* src1, RwTexture* src2, float r1, float r2, float r3, int32) { + ZoneScoped; + + AssertTextureLayouts({ dst, src1, src2 }); + + const auto dstRaster = RwTextureGetRaster(dst); + const auto src1Raster = RwTextureGetRaster(src1); + const auto src2Raster = RwTextureGetRaster(src2); + + CTimer::Suspend(); + + auto src1It = RwRasterLock(src1Raster, 0, rwRASTERLOCKREAD); + auto src2It = RwRasterLock(src2Raster, 0, rwRASTERLOCKREAD); + auto dstIt = RwRasterLock(dstRaster, 0, rwRASTERLOCKREADWRITE); + + for (auto i = RwRasterGetHeight(dstRaster) * RwRasterGetWidth(dstRaster); i-- > 0; dstIt++, src1It++, src2It++) { + for (auto c = 3; i-- > 0; dstIt++, src1It++, src2It++) { // Copy RGB, alpha doesn't change + *dstIt = multiply_weighted({ { *dstIt, r1 }, { *src1It, r2 }, { *src2It, r3 } }); + } + } + + RwRasterUnlock(dstRaster); + RwRasterUnlock(src1Raster); + RwRasterUnlock(src2Raster); + + CTimer::Resume(); } // 0x5A5BC0 -void CClothesBuilder::BlendTextures(RwTexture* t1, RwTexture* t2, RwTexture* t3, float factorA, float factorB, float factorC, int32 a7, RwTexture* t4) { - plugin::Call<0x5A5BC0, RwTexture*, RwTexture*, RwTexture*, float, float, float, int32, RwTexture*>(t1, t2, t3, factorA, factorB, factorC, a7, t4); -} +void CClothesBuilder::BlendTextures(RwTexture* dst, RwTexture* src1, RwTexture* src2, float r1, float r2, float r3, int32 numColors, RwTexture* tattoos) { + ZoneScoped; -// unused or inlined -// 0x5A5EB0 -void CClothesBuilder::InitPaletteOctTree(int32 numColors) { - /* - COctTree::InitPool(&PC_Scratch[1024], 15360u); - COctTreeBase::Init(&gOctTreeBase, numColors); - */ -} + AssertTextureLayouts({ dst, src1, src2, tattoos }); -// 0x5A5EE0 -void CClothesBuilder::ShutdownPaletteOctTree() { - // COctTree::ShutdownPool(); -} + const auto dstRaster = RwTextureGetRaster(dst); + const auto src1Raster = RwTextureGetRaster(src1); + const auto src2Raster = RwTextureGetRaster(src2); + const auto tatRaster = RwTextureGetRaster(src2); -// 0x5A5EF0 -void CClothesBuilder::ReducePaletteOctTree(int32 numColorsToReduce) { - // gOctTreeBase.ReduceBranches(newBranchesCount); -} + CTimer::Suspend(); -// 0x5A5F00 -bool CClothesBuilder::AddColour(RwRGBA* color) { - return plugin::CallAndReturn(color); - /* - if (!color->alpha) - gOctTreeBase.m_bHasTransparentPixels = 1; + auto src1It = RwRasterLock(src1Raster, 0, rwRASTERLOCKREAD); + auto src2It = RwRasterLock(src2Raster, 0, rwRASTERLOCKREAD); + auto tatIt = RwRasterLock(tatRaster, 0, rwRASTERLOCKREAD); + auto dstIt = RwRasterLock(dstRaster, 0, rwRASTERLOCKREADWRITE); - return gOctTreeBase.Insert(color); - */ -} + for (auto i = RwRasterGetHeight(dstRaster) * RwRasterGetWidth(dstRaster); i-- > 0; dstIt++, src1It++, src2It++, tatIt++) { + const auto tatAlphaT = (float)tatIt[3] / 255.f; + for (auto c = 3; i-- > 0; dstIt++, src1It++, src2It++, tatIt++) { // Copy RGB, alpha doesn't change + *dstIt = (RwUInt8)lerp(multiply_weighted({ { *dstIt, r1 }, { *src1It, r2 }, { *src2It, r3 } }), *tatIt, tatAlphaT); + } + } -// 0x5A5F30 -void CClothesBuilder::FillPalette(RwRGBA* color) { - // gOctTreeBase.FillPalette(color); -} + RwRasterUnlock(dstRaster); + RwRasterUnlock(src1Raster); + RwRasterUnlock(src2Raster); + RwRasterUnlock(tatRaster); -// unused -// 0x5A5F40 -int32 CClothesBuilder::FindNearestColour(RwRGBA* color) { - return plugin::CallAndReturn(color); - /* - if (color->alpha) - return gOctTreeBase.FindNearestColour(color); - else - return 0; - */ + CTimer::Resume(); } // 0x5A5F70 -RwTexture* GetTextureFromTxdAndLoadNextTxd(RwTexture* destTexture, int32 txdId_withTexture, int32 CRC_nextTxd, int32* nextTxdId) { - return plugin::CallAndReturn(destTexture, txdId_withTexture, CRC_nextTxd, nextTxdId); +RwTexture* GetTextureFromTxdAndLoadNextTxd(RwTexture* dstTex, int32 txdId_withTexture, int32 CRC_nextTxd, int32* nextTxdId) { + if (txdId_withTexture == -1) { + if (CRC_nextTxd) { + *nextTxdId = CClothesBuilder::RequestTexture(CRC_nextTxd); + CStreaming::LoadRequestedModels(); + } else { + *nextTxdId = -1; + } + return dstTex; + } + + CStreaming::LoadAllRequestedModels(true); + if (CRC_nextTxd) { + *nextTxdId = CClothesBuilder::RequestTexture(CRC_nextTxd); + CStreaming::LoadRequestedModels(); + } else { + *nextTxdId = -1; + } + const auto tex = GetFirstTexture(CTxdStore::GetTxd(txdId_withTexture)); + const auto res = dstTex + ? CClothesBuilder::PlaceTextureOnTopOfTexture(dstTex, tex), dstTex + : CClothesBuilder::CopyTexture(tex); + CStreaming::RemoveModel(TXDToModelId(txdId_withTexture)); + return res; } // 0x5A6040 @@ -210,17 +490,172 @@ void CClothesBuilder::ConstructTextures(RwTexDictionary* dict, uint32* hashes, f } // 0x5A6530 -void CClothesBuilder::ConstructGeometryAndSkinArrays(RpHAnimHierarchy* animHierarchy, RpGeometry** geometry1, RwMatrixWeights** weights, uint32** a4, uint32 a5, RpGeometry** geometry2, RpMaterial** material) { - plugin::Call<0x5A6530, RpHAnimHierarchy*, RpGeometry**, RwMatrixWeights**, uint32**, uint32, RpGeometry**, RpMaterial**>(animHierarchy, geometry1, weights, a4, a5, geometry2, material); -} - -// unused -// 0x5A6870 -void CClothesBuilder::ReducePaletteSize(RwTexture* texture, int32 numColorsToReduce) { - plugin::Call<0x5A6870, RwTexture*, int32>(texture, numColorsToReduce); +void CClothesBuilder::ConstructGeometryAndSkinArrays(RpHAnimHierarchy* pBoneHier, RpGeometry** ppGeometry, RwMatrixWeights** ppWeights, uint32** ppIndices, uint32 numModels, RpGeometry** pGeometrys, RpMaterial** pMaterial) { + plugin::Call<0x5A6530>(pBoneHier, ppGeometry, ppWeights, ppIndices, numModels, pGeometrys, pMaterial); } // 0x5A69D0 -RpClump* CClothesBuilder::CreateSkinnedClump(RpClump* clump, RwTexDictionary* dict, CPedClothesDesc& newClothes, const CPedClothesDesc* oldClothes, bool bCutscenePlayer) { - return plugin::CallAndReturn(clump, dict, newClothes, oldClothes, bCutscenePlayer); +RpClump* CClothesBuilder::CreateSkinnedClump(RpClump* bones, RwTexDictionary* dict, CPedClothesDesc& ndscr, const CPedClothesDesc* odscr, bool bCutscenePlayer) { + LoadCdDirectory(); + + const struct { + eClothesModelPart mp; + const char* name; + } parts[]{ + {eClothesModelPart::CLOTHES_MODEL_TORSO, "torso"}, + {eClothesModelPart::CLOTHES_MODEL_HEAD, "head"}, + {eClothesModelPart::CLOTHES_MODEL_HANDS, "hands"}, + {eClothesModelPart::CLOTHES_MODEL_LEGS, "legs"}, + {eClothesModelPart::CLOTHES_MODEL_SHOES, "feet"} + }; + for (const auto [mp, name] : parts) { + if (!ndscr.m_anModelKeys[(int)mp]) { + ndscr.SetModel(name, mp); + } + } + + if (odscr) { + ms_geometryHasChanged = false; + ms_ratiosHaveChanged = false; + if (odscr->m_fFatStat != ndscr.m_fFatStat || odscr->m_fMuscleStat != ndscr.m_fMuscleStat) { + ms_textureHasChanged = true; + ms_geometryHasChanged = true; + } else { + ms_textureHasChanged = true; + } + ms_geometryHasChanged = !rng::equal(ndscr.m_anModelKeys, odscr->m_anModelKeys); + ms_ratiosHaveChanged = !rng::equal(ndscr.m_anTextureKeys, odscr->m_anTextureKeys); + if (!ms_ratiosHaveChanged && !ms_geometryHasChanged && !ms_textureHasChanged) { + return nullptr; + } + } else { + ms_ratiosHaveChanged = ms_geometryHasChanged = ms_textureHasChanged = true; + } + CPedClothesDesc dscr = ndscr; + PreprocessClothesDesc(dscr, bCutscenePlayer); + + //> 0x5A42B0 - Calculate blend ratios + float rNormal, rFatness, rMuscle; + { + rMuscle = std::clamp(CStats::GetStatValue(STAT_MUSCLE) / 1000.f, 0.f, 1.f); + rFatness = std::clamp((dscr.m_fFatStat - 200.f) / 800.f, 0.f, 1.f); + rNormal = 1.f - rMuscle - rFatness; + if (rNormal <= 0.f) { + const auto t = 1.f / (rFatness + rMuscle); + rMuscle *= t; + rFatness *= t; + rNormal = 0.f; + } + } + + if ((ms_textureHasChanged || ms_ratiosHaveChanged) && !bCutscenePlayer) { + RwTexDictionaryForAllTextures(dict, [](RwTexture* t, void* data) { + RwTexDictionaryRemoveTexture(t); + RwTextureDestroy(t); + return t; + }, nullptr); + ConstructTextures(dict, dscr.m_anTextureKeys.data(), rNormal, rFatness, rMuscle); + } + + constexpr auto NO_BODY_PARTS = 10; + + constexpr const char* BODY_PART_TEX_NAMES[NO_BODY_PARTS]{ + "torso", + "head", + "torso", + "legs", + "feet", + "necklace", + "watch", + "glasses", + "hat", + "extra1" + }; + + //> 0x5A6B2C + RpMaterial* ms[NO_BODY_PARTS]; + for (uint32 i{}; const auto name : BODY_PART_TEX_NAMES) { + ms[i++] = [&]() -> RpMaterial* { + if (const auto tex = RwTexDictionaryFindNamedTexture(dict, name)) { + const auto mat = RpMaterialCreate(); + RpMaterialSetTexture(mat, tex); + const auto clr = RwRGBA(0xFF, 0xFF, 0xFF, 0xFF); + RpMaterialSetColor(mat, &clr); + return mat; + } + return nullptr; + }(); + } + + //> 0x5A6C5D + RpGeometry* gs[NO_BODY_PARTS]; + ConstructGeometryArray(gs, dscr.m_anModelKeys.data(), rNormal, rFatness, rMuscle); + + //> 0x5A6C6A + const auto boneAtomic = GetFirstAtomic(bones); + const auto boneSkin = RpSkinGeometryGetSkin(RpAtomicGetGeometry(boneAtomic)); + const auto boneAnimHr = RpSkinAtomicGetHAnimHierarchy(boneAtomic); + + RwMatrixWeights* boneWeights; + RwUInt32* boneIdxs; + RpGeometry* tmpGeo; + ConstructGeometryAndSkinArrays( + boneAnimHr, + &tmpGeo, + &boneWeights, + &boneIdxs, + NO_BODY_PARTS, + gs, + ms + ); + RpSkinGeometrySetSkin( + tmpGeo, + RpSkinCreate( + RpGeometryGetNumVertices(tmpGeo), + RpSkinGetNumBones(boneSkin), + boneWeights, + boneIdxs, + const_cast(RpSkinGetSkinToBoneMatrices(boneSkin)) // TODO + ) + ); + DestroySkinArrays(boneWeights, boneIdxs); + + const auto hier = RpHAnimHierarchyCreateFromHierarchy( + boneAnimHr, + (RpHAnimHierarchyFlag)boneAnimHr->flags, // TODO: Use function to access + boneAnimHr->currentAnim->maxInterpKeyFrameSize // TODO: Use function to access + ); + + const auto childFrame = RwFrameCreate(); + RpHAnimFrameSetHierarchy(childFrame, hier); + + const auto atomic = RpAtomicCreate(); + RpAtomicSetGeometry(atomic, tmpGeo, NULL); + RpSkinAtomicSetHAnimHierarchy(atomic, hier); + RpAtomicSetFrame(atomic, childFrame); + RpSkinAtomicSetType(atomic, rpSKINTYPEGENERIC); + + const auto rootFrame = RwFrameCreate(); + RwFrameAddChild(rootFrame, childFrame); + + const auto clump = RpClumpCreate(); + RpClumpSetFrame(clump, rootFrame); + RpClumpAddAtomic(clump, atomic); + + // Free memory + { + RpGeometryDestroy(tmpGeo); + + RwTexDictionarySetCurrent(dict); + for (auto i = 0; i < NO_BODY_PARTS; i++) { + if (gs[i]) { + RpGeometryDestroy(gs[i]); + } + if (ms[i]) { + RpMaterialDestroy(ms[i]); + } + } + } + + return clump; } diff --git a/source/game_sa/ClothesBuilder.h b/source/game_sa/ClothesBuilder.h index 26d04702a5..56cf37de4b 100644 --- a/source/game_sa/ClothesBuilder.h +++ b/source/game_sa/ClothesBuilder.h @@ -20,31 +20,93 @@ class CClothesBuilder { static int32 RequestTexture(uint32 crc); static void PreprocessClothesDesc(CPedClothesDesc& desc, bool a2); static void ReleaseGeometry(int32 numToRelease); - static void sub_5A4840(); + static void AddWeightToBoneVertex(float(&out_weights)[8], uint8(&bone_vertex_indices)[8], float weight_to_add, RwUInt32 target_vertex); static void StoreBoneArray(RpClump* clump, int32 a2); - static RpGeometry* BlendGeometry(RpClump* clump, const char* a2, const char* a3, const char* a4, float a5, float a6, float a7); - static RpGeometry* BlendGeometry(RpClump* clump, const char* a2, const char* a3, float a4, float a5); + + /*! + * Blend 3 geometries together and store the result in the first one + * + * @addr 0x5A4940 + * + * @arg clump The clump of the named frames [which in turn contain the geometries] + * @arg frameName0 The 0th frame - This is where the result is stored to + * @arg frameName1 The 1st frame + * @arg frameName2 The 2nd frame + * @arg r0 Blend ratio of the corresponding frame + * @arg r1 Blend ratio of the corresponding frame + * @arg r2 Blend ratio of the corresponding frame + */ + static RpGeometry* BlendGeometry(RpClump* pClump, const char* frameName0, const char* frameName1, const char* frameName2, float r0, float r1, float r2); + + /*! + * Blend 2 geometries together and store the result in the first one + * + * @addr 0x5A4F10 + * + * @arg clump The clump of the named frames [which in turn contain the geometries] + * @arg frameName0 The 0th frame - This is where the result is stored to + * @arg frameName1 The 1st frame + * @arg r0 Blend ratio of the corresponding frame + * @arg r1 Blend ratio of the corresponding frame + */ + static RpGeometry* BlendGeometry(RpClump* clump, const char* frameName0, const char* frameName1, float r0, float r1); + static RpGeometry* CopyGeometry(RpClump* clump, const char* a2, const char* a3); - static void ConstructGeometryArray(RpGeometry** geometry, uint32* a2, float a3, float a4, float a5); - static void DestroySkinArrays(RwMatrixWeights* weights, uint32* a2); + static void ConstructGeometryArray(RpGeometry** ppGeometry, uint32* pModelKeys, float normal, float fatness, float strength); + static void DestroySkinArrays(RwMatrixWeights* weights, RwUInt32* bones); static void BuildBoneIndexConversionTable(uint8* a1, RpHAnimHierarchy* a2, int32 a3); + + /*! + * Create a copy of a texture + * + * @addr 0x5A5730 + */ static RwTexture* CopyTexture(RwTexture* texture); static void PlaceTextureOnTopOfTexture(RwTexture* texture1, RwTexture* texture2); - static void BlendTextures(RwTexture* t1, RwTexture* t2, float a3, float a4, int32 a5); - static void BlendTextures(RwTexture* t1, RwTexture* t2, RwTexture* t3, float factorA, float factorB, float factorC, int32 a7); - static void BlendTextures(RwTexture* t1, RwTexture* t2, RwTexture* t3, float factorA, float factorB, float factorC, int32 a7, RwTexture* t4); - static void InitPaletteOctTree(int32 numColors); - static void ShutdownPaletteOctTree(); - static void ReducePaletteOctTree(int32 numColorsToReduce); - static bool AddColour(RwRGBA* color); - static void FillPalette(RwRGBA* color); - static int32 FindNearestColour(RwRGBA* color); + + /*! + * Blend textures with a given blend ratio + * + * @addr 0x5A5820 + * + * @arg dst The texture to which the blended data is written to, and read from + * @arg src The texture that should be blended with the other one + * @arg r1 Ratio of the `dst` texture + * @arg r2 Ratio of the `src` texture + */ + static void BlendTextures(RwTexture* dst, RwTexture* src, float r1, float r2, int32); + + /*! + * Blend textures with a given blend ratio + * + * @addr 0x5A59C0 + * + * @arg dst The texture to which the blended data is written to, and read from + * @arg src1 The texture that should be blended with the other one + * @arg src2 The texture that should be blended with the other one + * @arg r1 Ratio of the `dst` texture + * @arg r2 Ratio of the `src1` texture + * @arg r3 Ratio of the `src2` texture + */ + static void BlendTextures(RwTexture* dst, RwTexture* src1, RwTexture* src2, float r1, float r2, float r3, int32); + + /*! + * Blend textures with a given blend ratio + * + * @addr 0x5A5BC0 + * + * @arg dst The texture to which the blended data is written to, and read from + * @arg src1 The texture that should be blended with the other one + * @arg src2 The texture that should be blended with the other one + * @arg r1 Ratio of the `dst` texture + * @arg r2 Ratio of the `src1` texture + * @arg r3 Ratio of the `src2` texture + */ + static void BlendTextures(RwTexture* dst, RwTexture* src1, RwTexture* src2, float r1, float r2, float r3, int32 numColors, RwTexture* tattoos); static void ConstructTextures(RwTexDictionary* dict, uint32* hashes, float factorA, float factorB, float factorC); static void ConstructGeometryAndSkinArrays(RpHAnimHierarchy* animHierarchy, RpGeometry** geometry1, RwMatrixWeights** weights, uint32** a4, uint32 a5, RpGeometry** geometry2, RpMaterial** material); - static void ReducePaletteSize(RwTexture* texture, int32 numColorsToReduce); static RpClump* CreateSkinnedClump(RpClump* clump, RwTexDictionary* dict, CPedClothesDesc& newClothes, const CPedClothesDesc* oldClothes, bool CutscenePlayer); }; -void FindAtomicFromNameCB(RpAtomic* atomic, void* data); -void GetAtomicWithName(RpClump* clump, const char* name); +RpAtomic* GetAtomicWithName(RpClump* clump, const char* name); RwTexture* GetTextureFromTxdAndLoadNextTxd(RwTexture* destTexture, int32 txdId_withTexture, int32 CRC_nextTxd, int32* nextTxdId); diff --git a/source/game_sa/RenderWare/rw/rwcore.cpp b/source/game_sa/RenderWare/rw/rwcore.cpp index 159f8a71d7..b698a7eff4 100644 --- a/source/game_sa/RenderWare/rw/rwcore.cpp +++ b/source/game_sa/RenderWare/rw/rwcore.cpp @@ -294,6 +294,7 @@ RwRaster* RwRasterShowRaster(RwRaster* raster, void* dev, RwUInt32 flags) { return ((RwRaster*(__cdecl *)(RwRaster*, void*, RwUInt32))0x7FB1A0)(raster, dev, flags); } +//! NOTE: This function is responsible for calculating the `stride` in the `raster` RwUInt8* RwRasterLock(RwRaster* raster, RwUInt8 level, RwInt32 lockMode) { return ((RwUInt8*(__cdecl *)(RwRaster*, RwUInt8, RwInt32))0x7FB2D0)(raster, level, lockMode); }