From 0988f3ccc7e0618065f62fadc51dda84f443cf4e Mon Sep 17 00:00:00 2001 From: Blounard Date: Mon, 2 Mar 2026 23:06:06 +0100 Subject: [PATCH 1/2] load textures and animtextures Modify LoadLEV() to implement the loading of textures and animated textures --- src/animtexture.cpp | 204 ++++++++++++++++++++++++++ src/animtexture.h | 6 + src/level.cpp | 349 +++++++++++++++++++++++++++++++++++++++++++- src/level.h | 1 + src/psx_types.h | 21 +++ src/quadblock.cpp | 21 +++ src/quadblock.h | 3 + src/texture.cpp | 92 ++++++++++++ src/texture.h | 139 ++++++++++++++++++ 9 files changed, 832 insertions(+), 4 deletions(-) diff --git a/src/animtexture.cpp b/src/animtexture.cpp index 96be76e..8a56924 100644 --- a/src/animtexture.cpp +++ b/src/animtexture.cpp @@ -4,6 +4,9 @@ #include #include #include +#include +#include +#include AnimTexture::AnimTexture(const std::filesystem::path& path, const std::vector& usedNames) { @@ -24,6 +27,195 @@ AnimTexture::AnimTexture(const std::filesystem::path& path, const std::vector, 4>& faceFrameLayouts, + const std::array, 4>& faceFrameMaterials, const std::vector& quadIndices, const std::vector& quadblocks, + const std::unordered_map& textureToPixelBounds, const std::unordered_map& materialToTexture, const PSX::AnimTex& firstAnimData, + const std::vector& animTextures) +{ + std::filesystem::path animDir = tempDir / animName; + std::filesystem::path tempObjPath = animDir / "frames.obj"; + std::ofstream objFile(tempObjPath); + size_t frameCount = firstAnimData.frameCount; + + size_t refQuadIdx = quadIndices[0]; + const Quadblock& refQuad = quadblocks[refQuadIdx]; + bool isTriblock = !refQuad.IsQuadblock(); + + objFile << "mtllib frames.mtl\n"; + + // For each frame, create a quadblock with UVs for all 4 faces + for (size_t frameIdx = 0; frameIdx < frameCount; frameIdx++) + { + std::string objName = "Frame_" + std::to_string(frameIdx + 1); + float z = static_cast(frameIdx); + + // 9 vertices + objFile << "v 0.0 0.0 " << z << " 0.5 0.5 0.5\n"; + objFile << "v 1.0 0.0 " << z << " 0.5 0.5 0.5\n"; + objFile << "v 0.0 1.0 " << z << " 0.5 0.5 0.5\n"; + objFile << "v 1.0 1.0 " << z << " 0.5 0.5 0.5\n"; + objFile << "v 0.5 0.5 " << z << " 0.5 0.5 0.5\n"; + objFile << "v 1.0 0.5 " << z << " 0.5 0.5 0.5\n"; + objFile << "v 0.5 1.0 " << z << " 0.5 0.5 0.5\n"; + objFile << "v 0.0 0.5 " << z << " 0.5 0.5 0.5\n"; + objFile << "v 0.5 0.0 " << z << " 0.5 0.5 0.5\n"; + + // 3 normals + objFile << "vn 0.0 1.0 0.0\n"; + objFile << "vn 0.0 1.0 0.0\n"; + objFile << "vn 0.0 1.0 0.0\n"; + + // Write 16 UVs (4 per face) + for (size_t faceIdx = 0; faceIdx < 4; faceIdx++) + { + if (frameIdx >= faceFrameLayouts[faceIdx].size()) continue; + + const PSX::TextureLayout& layout = faceFrameLayouts[faceIdx][frameIdx]; + + // Get the pixel bounds for this texture + LayoutKey key(layout); + bool crop = true; + float u0 = 0, u1 = 0, u2 = 0, u3 = 0 , v0 = 0, v1 = 0, v2 = 0, v3 = 0; + if (crop) + { + const PixelBounds& bounds = textureToPixelBounds.at(key); + float croppedWidth = static_cast(bounds.maxU - bounds.minU); + float croppedHeight = static_cast(bounds.maxV - bounds.minV); + if (croppedWidth == 0) croppedWidth = 1.0f; + if (croppedHeight == 0) croppedHeight = 1.0f; + u0 = (layout.u0 - bounds.minU) / croppedWidth; + v0 = (layout.v0 - bounds.minV) / croppedHeight; + u1 = (layout.u1 - bounds.minU) / croppedWidth; + v1 = (layout.v1 - bounds.minV) / croppedHeight; + u2 = (layout.u2 - bounds.minU) / croppedWidth; + v2 = (layout.v2 - bounds.minV) / croppedHeight; + u3 = (layout.u3 - bounds.minU) / croppedWidth; + v3 = (layout.v3 - bounds.minV) / croppedHeight; + } + else + { + float pW = (float)(64 * ((layout.texPage.texpageColors == 0) ? 4 : (layout.texPage.texpageColors == 1 ? 2 : 1))); + float pH = 256.0f; + u0 = layout.u0 / pW; + v0 = layout.v0 / pH; + u1 = layout.u1 / pW; + v1 = layout.v1 / pH; + u2 = layout.u2 / pW; + v2 = layout.v2 / pH; + u3 = layout.u3 / pW; + v3 = layout.v3 / pH; + } + + objFile << "vt " << u0 << " " << (1.0f - v0) << "\n"; + objFile << "vt " << u1 << " " << (1.0f - v1) << "\n"; + objFile << "vt " << u2 << " " << (1.0f - v2) << "\n"; + objFile << "vt " << u3 << " " << (1.0f - v3) << "\n"; + } + + objFile << "o " << objName << "\n"; + if (!faceFrameMaterials[0].empty() && frameIdx < faceFrameMaterials[0].size()) + { + objFile << "usemtl " << faceFrameMaterials[0][frameIdx] << "\n"; + } + + int vOffset = static_cast(frameIdx * 9) + 1; + int vtOffset = static_cast(frameIdx * 16) + 1; + int vnOffset = static_cast(frameIdx * 3) + 1; + + if (isTriblock) + { + objFile << "f " << (vOffset + 0) << "/" << (vtOffset + 0) << "/" << (vnOffset + 0) << " " + << (vOffset + 1) << "/" << (vtOffset + 1) << "/" << (vnOffset + 0) << " " + << (vOffset + 4) << "/" << (vtOffset + 2) << "/" << (vnOffset + 0) << "\n"; + objFile << "f " << (vOffset + 1) << "/" << (vtOffset + 4) << "/" << (vnOffset + 1) << " " + << (vOffset + 5) << "/" << (vtOffset + 5) << "/" << (vnOffset + 1) << " " + << (vOffset + 4) << "/" << (vtOffset + 6) << "/" << (vnOffset + 1) << "\n"; + objFile << "f " << (vOffset + 5) << "/" << (vtOffset + 8) << "/" << (vnOffset + 2) << " " + << (vOffset + 3) << "/" << (vtOffset + 9) << "/" << (vnOffset + 2) << " " + << (vOffset + 4) << "/" << (vtOffset + 10) << "/" << (vnOffset + 2) << "\n"; + objFile << "f " << (vOffset + 3) << "/" << (vtOffset + 12) << "/" << (vnOffset + 2) << " " + << (vOffset + 2) << "/" << (vtOffset + 13) << "/" << (vnOffset + 2) << " " + << (vOffset + 4) << "/" << (vtOffset + 14) << "/" << (vnOffset + 2) << "\n"; + } + else + { + // Face 0: Top-Left Quadrant + // Corners: TL(v+0), T_MID(v+7), CENTER(v+8), L_MID(v+5) + objFile << "f " << (vOffset + 0) << "/" << (vtOffset + 0) << "/" << (vnOffset + 0) << " " + << (vOffset + 7) << "/" << (vtOffset + 1) << "/" << (vnOffset + 0) << " " + << (vOffset + 8) << "/" << (vtOffset + 3) << "/" << (vnOffset + 0) << " " + << (vOffset + 5) << "/" << (vtOffset + 2) << "/" << (vnOffset + 0) << "\n"; + + // Face 1: Top-Right Quadrant + // Corners: T_MID(v+7), TR(v+1), R_MID(v+4), CENTER(v+8) + objFile << "f " << (vOffset + 7) << "/" << (vtOffset + 4) << "/" << (vnOffset + 1) << " " + << (vOffset + 1) << "/" << (vtOffset + 5) << "/" << (vnOffset + 1) << " " + << (vOffset + 4) << "/" << (vtOffset + 7) << "/" << (vnOffset + 1) << " " + << (vOffset + 8) << "/" << (vtOffset + 6) << "/" << (vnOffset + 1) << "\n"; + + // Face 2: Bottom-Left Quadrant + // Corners: L_MID(v+5), CENTER(v+8), B_MID(v+6), BL(v+2) + objFile << "f " << (vOffset + 5) << "/" << (vtOffset + 8) << "/" << (vnOffset + 2) << " " + << (vOffset + 8) << "/" << (vtOffset + 9) << "/" << (vnOffset + 2) << " " + << (vOffset + 6) << "/" << (vtOffset + 11) << "/" << (vnOffset + 2) << " " + << (vOffset + 2) << "/" << (vtOffset + 10) << "/" << (vnOffset + 2) << "\n"; + + // Face 3: Bottom-Right Quadrant + // Corners: CENTER(v+8), R_MID(v+4), BR(v+3), B_MID(v+6) + objFile << "f " << (vOffset + 8) << "/" << (vtOffset + 12) << "/" << (vnOffset + 2) << " " + << (vOffset + 4) << "/" << (vtOffset + 13) << "/" << (vnOffset + 2) << " " + << (vOffset + 3) << "/" << (vtOffset + 15) << "/" << (vnOffset + 2) << " " + << (vOffset + 6) << "/" << (vtOffset + 14) << "/" << (vnOffset + 2) << "\n"; + } + } + objFile.close(); + + // Write MTL + std::filesystem::path tempMtlPath = animDir / "frames.mtl"; + std::ofstream mtlFile(tempMtlPath); + std::set writtenMaterials; + + for (const auto& frameMaterials : faceFrameMaterials) + { + for (const std::string& mat : frameMaterials) + { + if (writtenMaterials.count(mat) || mat == "default") continue; + writtenMaterials.insert(mat); + + if (materialToTexture.count(mat)) + { + mtlFile << "newmtl " << mat << "\n"; + mtlFile << "map_Kd " << materialToTexture.at(mat).GetPath().string() << "\n"; + } + } + } + mtlFile.close(); + + // Create AnimTexture + std::vector existingNames; + for (const AnimTexture& at : animTextures) + { + existingNames.push_back(at.GetName()); + } + + m_path = tempObjPath; + std::string origName = tempObjPath.filename().replace_extension().string(); + m_name = origName; + int repetitionCount = 1; + while (true) + { + bool validName = true; + for (const std::string& usedName : existingNames) + { + if (m_name == usedName) { validName = false; break; } + } + if (validName) { break; } + m_name = origName + " (" + std::to_string(repetitionCount++) + ")"; + } + if (!ReadAnimation(tempObjPath)) { ClearAnimation(); } +} + bool AnimTexture::IsEmpty() const { return m_frames.empty(); @@ -196,6 +388,18 @@ void AnimTexture::ClearAnimation() m_lastAppliedMaterialName.clear(); } +void AnimTexture::SetStartFrame(int frame) +{ + m_startAtFrame = frame; + m_renderDirty = true; +} + +void AnimTexture::SetDuration(int duration) +{ + m_duration = duration; + m_renderDirty = true; +} + void AnimTexture::SetDefaultParams() { m_startAtFrame = 0; diff --git a/src/animtexture.h b/src/animtexture.h index 1454bdc..0e0c4ad 100644 --- a/src/animtexture.h +++ b/src/animtexture.h @@ -19,6 +19,10 @@ class AnimTexture public: AnimTexture() {}; AnimTexture(const std::filesystem::path& path, const std::vector& usedNames); + AnimTexture(const std::string& animName, const std::filesystem::path& tempDir, const std::array, 4>& faceFrameLayouts, + const std::array, 4>& faceFrameMaterials, const std::vector& quadIndices, const std::vector& quadblocks, + const std::unordered_map& textureToPixelBounds, const std::unordered_map& materialToTexture, const PSX::AnimTex& firstAnimData, + const std::vector& animTextures); bool IsEmpty() const; bool IsTriblock() const; const std::vector& GetFrames() const; @@ -34,6 +38,8 @@ class AnimTexture void FromJson(const nlohmann::json& json, std::vector& quadblocks, const std::filesystem::path& parentPath); void ToJson(nlohmann::json& json, const std::vector& quadblocks) const; bool IsEquivalent(const AnimTexture& animTex) const; + void SetStartFrame(int frame); + void SetDuration(int duration); bool RenderUI(std::vector& animTexNames, std::vector& quadblocks, const std::map>& materialMap, const std::string& query, std::vector& newTextures); private: diff --git a/src/level.cpp b/src/level.cpp index 5be77a8..ad927de 100644 --- a/src/level.cpp +++ b/src/level.cpp @@ -9,8 +9,12 @@ #include "vistree.h" #include "text3d.h" + +#include +#include #include #include +#include #include #include @@ -718,9 +722,11 @@ void Level::ManageTurbopad(Quadblock& quadblock) } } + bool Level::LoadLEV(const std::filesystem::path& levFile) { std::ifstream file(levFile, std::ios::binary); + if (!file.is_open()) return false; uint32_t offPointerMap; Read(file, offPointerMap); @@ -759,16 +765,306 @@ bool Level::LoadLEV(const std::filesystem::path& levFile) vertices.push_back(vertex); } + + // Loading textures and animated textures and quadblocks + std::filesystem::path vrmPath = levFile; + vrmPath.replace_extension(".vrm"); + std::vector vram = ReadRawVRAM(vrmPath); + int texCounter = 0; + std::unordered_map textureToPixelBounds; // Map Layout key -> Pixels bounds of the texture. + std::unordered_map materialCache; // Layout Key -> matName + std::map> quadblockFaceToAnimOffset; // Map: quadblock index -> face index -> AnimTex offset + std::unordered_map animTexDataMap; // Map : relativeOffset -> AnimTex + std::unordered_map> animTexFrames; // Map : relativeOffset (AnimTex) -> List of TextureGroup Index + std::unordered_map textureGroupToMaterial; // Map : texture group offset -> material name + + std::filesystem::path tempDir = levFile.parent_path() / (levFile.stem().string() + "_textures"); + std::filesystem::create_directories(tempDir); + + bool hasAnimData = header.offAnimTex > 0; + size_t offTextureStart = static_cast(header.offMeshInfo) + sizeof(PSX::MeshInfo); + size_t offTextureEnd = hasAnimData ? header.offAnimTex : meshInfo.offQuadblocks; + size_t offAnimStart = header.offAnimTex; + uint32_t offEndAnimData = meshInfo.offQuadblocks; + + // 1st Pass : Parse all PSX::AnimTex and populate animTexDataMap and animTexFrames, and calculate UV bounds + if (hasAnimData) + { + size_t currentOffset = offAnimStart; + while (currentOffset < offEndAnimData) + { + file.seekg(offLev + std::streampos(currentOffset)); + PSX::AnimTex animTex; + Read(file, animTex); + size_t relativeOffset = currentOffset - header.offAnimTex; + animTexDataMap[relativeOffset] = animTex; + + std::vector frameTextureGroupIndices; + for (uint16_t f = 0; f < animTex.frameCount; f++) + { + uint32_t frameTexOffset; + Read(file, frameTexOffset); + if (frameTexOffset >= offTextureStart && frameTexOffset < offTextureEnd) + { + size_t textureGroupIndex = (frameTexOffset - offTextureStart) / sizeof(PSX::TextureGroup); + frameTextureGroupIndices.push_back(textureGroupIndex); + + std::streampos currentPos = file.tellg(); + file.seekg(offLev + static_cast(frameTexOffset)); + PSX::TextureGroup group = {}; + Read(file, group); + file.seekg(currentPos); + const PSX::TextureLayout& layout = group.middle; + LayoutKey key(layout); + + if (!materialCache.contains(key)) + { + std::string newMatName = "tex_" + std::to_string(texCounter++); + materialCache[key] = newMatName; + } + textureGroupToMaterial[textureGroupIndex] = materialCache[key]; + + RawUV rawUV(layout); + textureToPixelBounds[key].Update(rawUV); + } + else + { + printf(" Frame %u: offset=0x%x OUT OF RANGE (offTextureStart=0x%zx, offAnimTex=0x%x)\n", + f, frameTexOffset, offTextureStart, header.offAnimTex); + } + } + animTexFrames[relativeOffset] = frameTextureGroupIndices; + currentOffset += sizeof(PSX::AnimTex) + (animTex.frameCount * sizeof(uint32_t)); + } + } + + // 2nd pass : Non animated Texture reading and UV bounding boxes file.seekg(offLev + std::streampos(meshInfo.offQuadblocks)); for (uint32_t i = 0; i < meshInfo.numQuadblocks; i++) { - PSX::Quadblock quadblock = {}; - Read(file, quadblock); - m_quadblocks.emplace_back(quadblock, vertices, [this](const Quadblock& qb) { UpdateFilterRenderData(qb); }); - m_materialToQuadblocks["default"].push_back(i); + PSX::Quadblock psxQuad = {}; + Read(file, psxQuad); + for (int f = 0; f < 4; f++) + { + uint32_t texOffset = psxQuad.offMidTextures[f]; + if (texOffset >= offTextureStart && texOffset < offTextureEnd) // Regular textures + { + std::streampos currentPos = file.tellg(); + file.seekg(offLev + static_cast(texOffset)); + PSX::TextureGroup group = {}; + Read(file, group); + file.seekg(currentPos); + const PSX::TextureLayout& layout = group.middle; + LayoutKey key(layout); + + if (!materialCache.contains(key)) + { + std::string newMatName = "tex_" + std::to_string(texCounter++); + materialCache[key] = newMatName; + } + size_t textureGroupIndex = (texOffset - offTextureStart) / sizeof(PSX::TextureGroup); + textureGroupToMaterial[textureGroupIndex] = materialCache[key]; + + RawUV rawUV(layout, psxQuad.drawOrderLow, f); + textureToPixelBounds[key].Update(rawUV); + } + else if (hasAnimData && texOffset >= offAnimStart && texOffset < offEndAnimData) + { + // for AnimTex, texOffset isn't simply the animOffset of the AnimTex. + // We have to find it by looking within which animtex framearray bounds it falls + size_t relativeOffset = texOffset - header.offAnimTex; + size_t foundAnimOffset = 0; + bool found = false; + + for (const auto& [animOffset, animTex] : animTexDataMap) + { + // Calculate where the frame array starts and ends for this AnimTex + size_t frameArrayStart = animOffset + sizeof(PSX::AnimTex); + size_t frameArrayEnd = frameArrayStart + (animTex.frameCount * sizeof(uint32_t)); + + // Check if relativeOffset falls within this frame array + if (relativeOffset >= animOffset && relativeOffset < frameArrayEnd) + { + foundAnimOffset = animOffset; + found = true; + break; + } + } + if (found) + { + quadblockFaceToAnimOffset[i][f] = foundAnimOffset; + } + else + { + printf("WARNING: Could not find owning AnimTex for quadblock %d\n", psxQuad.id); + } + } + } + } + + // 3rd pass : Create PNGs and Materials + for (const auto& [key, bounds] : textureToPixelBounds) + { + std::string newMatName = materialCache[key]; + Texture newTexture(key, bounds, vram, newMatName, tempDir, true); + m_materialToTexture[newMatName] = newTexture; } + // 4th pass : create quadblocks with material, UVs and texture + file.seekg(offLev + std::streampos(meshInfo.offQuadblocks)); + for (uint32_t i = 0; i < meshInfo.numQuadblocks; i++) + { + PSX::Quadblock psxQuad = {}; + Read(file, psxQuad); + Quadblock& qb = m_quadblocks.emplace_back(psxQuad, vertices, [this](const Quadblock& qb) { UpdateFilterRenderData(qb); }); + bool materialAssigned = false; + std::string qbMatName = "default"; + for (int f = 0; f < 4; f++) + { + uint32_t texOffset = psxQuad.offMidTextures[f]; + if (texOffset >= offTextureStart && texOffset < offTextureEnd)// Regular texture + { + std::streampos currentPos = file.tellg(); + file.seekg(offLev + static_cast(texOffset)); + PSX::TextureGroup group = {}; + Read(file, group); + file.seekg(currentPos); + + const PSX::TextureLayout& layout = group.middle; + LayoutKey key(layout); + + if (!materialAssigned) + { + qbMatName = materialCache[key]; + qb.SetMaterial(qbMatName); + qb.SetTexPath(m_materialToTexture[qbMatName].GetPath()); + m_materialToQuadblocks[qbMatName].push_back(i); + materialAssigned = true; + } + + RawUV rawUV(layout, psxQuad.drawOrderLow, f); + const PixelBounds& bounds = textureToPixelBounds[key]; + float croppedWidth = static_cast(bounds.maxU - bounds.minU); + float croppedHeight = static_cast(bounds.maxV - bounds.minV); + if (croppedWidth == 0) croppedWidth = 1.0f; + if (croppedHeight == 0) croppedHeight = 1.0f; + QuadUV uvs = { + Vec2((rawUV.u0 - bounds.minU) / croppedWidth, (rawUV.v0 - bounds.minV) / croppedHeight), + Vec2((rawUV.u1 - bounds.minU) / croppedWidth, (rawUV.v1 - bounds.minV) / croppedHeight), + Vec2((rawUV.u2 - bounds.minU) / croppedWidth, (rawUV.v2 - bounds.minV) / croppedHeight), + Vec2((rawUV.u3 - bounds.minU) / croppedWidth, (rawUV.v3 - bounds.minV) / croppedHeight) + }; + qb.SetFaceUVs(f, uvs); + } + else if (hasAnimData && texOffset >= offAnimStart && texOffset < offEndAnimData) + { + // MATERIAL NOT YET ASSIGNED FOR ANIMATED QUAD. DO IT WHEN CREATING THE .OBJ (or somewhere else) + size_t AnimOffset = quadblockFaceToAnimOffset[i][f]; + qb.SetAnimTextureOffset(AnimOffset, header.offAnimTex, f); + qb.SetAnimated(true); + } + } + if (!materialAssigned) + { + qb.SetMaterial("default"); + m_materialToQuadblocks["default"].push_back(i); + } + } + + //5th pass : Create .obj for AnimText, and assign to quads + if (hasAnimData) + { + std::map, std::set> facePatternToQuadblocks; + for (const auto& [quadIdx, faceMap] : quadblockFaceToAnimOffset) + { + facePatternToQuadblocks[faceMap].insert(quadIdx); + } + + std::set> processedPatterns; + for (const auto& [faceMap, quadSet] : facePatternToQuadblocks) + { + if (processedPatterns.contains(faceMap)) continue; + if (faceMap.empty()) continue; + + std::vector quadIndices(quadSet.begin(), quadSet.end()); + + size_t firstAnimOffset = faceMap.begin()->second; + if (!animTexDataMap.contains(firstAnimOffset)) continue; + + const PSX::AnimTex& firstAnimData = animTexDataMap[firstAnimOffset]; + size_t frameCount = firstAnimData.frameCount; + + // Verify all AnimTex in this pattern have the same frame count + bool validAnimation = true; + for (const auto& [faceIdx, animOffset] : faceMap) + { + if (!animTexDataMap.contains(animOffset) || !animTexFrames.contains(animOffset) || animTexDataMap[animOffset].frameCount != frameCount) + { + validAnimation = false; + break; + } + } + if (!validAnimation) continue; + + + std::array, 4> faceFrameLayouts; + std::array, 4> faceFrameMaterials; + std::array faceHasData = { false, false, false, false }; + + bool allMaterialsFound = true; + + for (const auto& [faceIdx, animOffset] : faceMap) + { + for (size_t textureGroupIndex : animTexFrames[animOffset]) + { + if (!textureGroupToMaterial.contains(textureGroupIndex)) + { + allMaterialsFound = false; + break; + } + faceFrameMaterials[faceIdx].push_back(textureGroupToMaterial[textureGroupIndex]); + size_t frameTexOffset = offTextureStart + (textureGroupIndex * sizeof(PSX::TextureGroup)); + std::streampos savedPos = file.tellg(); + file.seekg(offLev + std::streampos(frameTexOffset)); + PSX::TextureGroup group = {}; + Read(file, group); + file.seekg(savedPos); + faceFrameLayouts[faceIdx].push_back(group.middle); + } + if (!allMaterialsFound) break; + faceHasData[faceIdx] = true; + } + + if (!allMaterialsFound) continue; + + // Create temporary OBJ file + std::string animName = faceFrameMaterials[0][0]; + std::filesystem::path animDir = tempDir / animName; + std::filesystem::create_directories(animDir); + + AnimTexture animTexture(animName, tempDir, faceFrameLayouts, faceFrameMaterials, quadIndices, m_quadblocks, textureToPixelBounds, m_materialToTexture, firstAnimData, m_animTextures); + + if (!animTexture.IsEmpty()) + { + animTexture.SetStartFrame(firstAnimData.startAtFrame); + animTexture.SetDuration(firstAnimData.frameDuration); + + for (size_t quadIdx : quadIndices) + { + animTexture.AddQuadblockIndex(quadIdx); + std::string oldMat = m_quadblocks[quadIdx].GetMaterial(); + auto& v = m_materialToQuadblocks[oldMat]; + v.erase(std::remove(v.begin(), v.end(), quadIdx), v.end()); + m_quadblocks[quadIdx].SetMaterial(animName); + m_materialToQuadblocks[animName].push_back(quadIdx); + } + m_animTextures.push_back(animTexture); + processedPatterns.insert(faceMap); + } + } + } + m_bsp.Clear(); file.seekg(offLev + std::streampos(meshInfo.offBSPNodes)); std::vector bspArray; @@ -1917,6 +2213,51 @@ bool Level::UpdateVRM() return true; } +std::vector Level::ReadRawVRAM(std::filesystem::path vrmPath) +{ + std::vector vram(1024 * 512, 0); + + if (std::filesystem::exists(vrmPath)) + { + std::ifstream vrmFile(vrmPath, std::ios::binary); + + // Read the raw file into temporary memory + vrmFile.seekg(0, std::ios::end); + size_t vrmSize = vrmFile.tellg(); + vrmFile.seekg(0, std::ios::beg); + + std::vector rawVrmData(vrmSize); + vrmFile.read(reinterpret_cast(rawVrmData.data()), vrmSize); + vrmFile.close(); + + const uint8_t* pVrm = rawVrmData.data(); + uint32_t vrmMagic; + memcpy(&vrmMagic, pVrm, sizeof(uint32_t)); + pVrm += sizeof(uint32_t); + + // If magic is 0x20, we have a multi-block VRM (Standard for this level format) + if (vrmMagic == 0x20) { + for (int block = 0; block < 2; block++) { + PSX::VRMHeader blockHead; + memcpy(&blockHead, pVrm, sizeof(PSX::VRMHeader)); + pVrm += sizeof(PSX::VRMHeader); + + for (size_t y = 0; y < blockHead.height; y++) { + // Use the absolute coordinates provided in the VRM header + size_t vramIdx = (blockHead.y + y) * 1024 + blockHead.x; + size_t rowByteSize = blockHead.width * sizeof(uint16_t); + + if (vramIdx + blockHead.width <= vram.size()) { + memcpy(&vram[vramIdx], pVrm, rowByteSize); + } + pVrm += rowByteSize; + } + } + } + } + return vram; +} + bool Level::UpdateAnimTextures(float deltaTime) { bool changed = false; diff --git a/src/level.h b/src/level.h index bf98ad3..3978886 100644 --- a/src/level.h +++ b/src/level.h @@ -77,6 +77,7 @@ class Level bool SaveGhostData(const std::string& emulator, const std::filesystem::path& path); bool SetGhostData(const std::filesystem::path& path, bool tropy); bool UpdateVRM(); + std::vector ReadRawVRAM(std::filesystem::path vrmPath); bool GenerateCheckpoints(); bool GenerateBSP(); diff --git a/src/psx_types.h b/src/psx_types.h index 8988f4e..f5c240a 100644 --- a/src/psx_types.h +++ b/src/psx_types.h @@ -475,3 +475,24 @@ static inline Stars ConvertStars(const PSX::Stars& stars) out.zDepth = stars.zDepth; return out; } + +static inline void ConvertVRAMColorToRGBA(uint16_t vramColor, uint8_t* rgba) +{ + uint8_t r = (vramColor >> 0) & 0x1F; + uint8_t g = (vramColor >> 5) & 0x1F; + uint8_t b = (vramColor >> 10) & 0x1F; + bool stp = (vramColor >> 15) != 0; + + rgba[0] = (r << 3) | (r >> 2); + rgba[1] = (g << 3) | (g >> 2); + rgba[2] = (b << 3) | (b >> 2); + + if (r == 0 && g == 0 && b == 0) + { + rgba[3] = stp ? 255 : 0; + } + else + { + rgba[3] = stp ? 128 : 255; + } +} diff --git a/src/quadblock.cpp b/src/quadblock.cpp index d6c7df9..b2e0592 100644 --- a/src/quadblock.cpp +++ b/src/quadblock.cpp @@ -688,6 +688,27 @@ void Quadblock::SetSpeedImpact(int speed) m_downforce = speed; } +void Quadblock::SetUVs(const QuadUV& uvs) +{ + for (size_t i = 0; i < NUM_FACES_QUADBLOCK + 1; i++) + { + m_uvs[i] = uvs; + } +} + +void Quadblock::SetFaceUVs(size_t faceIndex, const QuadUV& uvs) +{ + if (faceIndex < m_uvs.size()) + { + m_uvs[faceIndex] = uvs; + } +} + +void Quadblock::SetMaterial(const std::string& material) +{ + m_material = material; +} + void Quadblock::Translate(float ratio, const Vec3& direction) { for (size_t i = 0; i < NUM_VERTICES_QUADBLOCK; i++) { m_p[i].m_pos += direction * ratio; } diff --git a/src/quadblock.h b/src/quadblock.h index 805938a..c227446 100644 --- a/src/quadblock.h +++ b/src/quadblock.h @@ -159,6 +159,9 @@ class Quadblock void SetFilter(bool filter); void SetFilterColor(const Color& color); void SetSpeedImpact(int speed); + void SetUVs(const QuadUV& uvs); + void SetFaceUVs(size_t faceIndex, const QuadUV& uvs); + void SetMaterial(const std::string& material); void Translate(float ratio, const Vec3& direction); const BoundingBox& GetBoundingBox() const; std::vector ToGeometry(bool filterTriangles = false, const std::array* overrideUvs = nullptr, const std::filesystem::path* overrideTexturePath = nullptr) const; diff --git a/src/texture.cpp b/src/texture.cpp index ddc9176..8a8c759 100644 --- a/src/texture.cpp +++ b/src/texture.cpp @@ -1,7 +1,12 @@ #include "texture.h" #define STB_IMAGE_IMPLEMENTATION +#define STB_IMAGE_WRITE_IMPLEMENTATION +#pragma warning(push) +#pragma warning(disable: 4996) #include +#include +#pragma warning(pop) static constexpr size_t MIN_CLUT_WIDTH = 16; static constexpr size_t TEXPAGE_WIDTH = 64; @@ -22,6 +27,93 @@ Texture::Texture(const std::filesystem::path& path) if (!CreateTexture()) { ClearTexture(); } } + +Texture::Texture(const LayoutKey& key, const PixelBounds& bounds, const std::vector& vram, const std::string& newMatName, const std::filesystem::path& tempDir, bool crop) + : m_width(0), m_height(0), m_imageX(0), m_imageY(0), m_clutX(0), m_clutY(0), m_blendMode(0), m_semiTransparent(false) +// Constructor that create the PNG file from vram +{ + int bppMode = key.bpp; + int bppMult = (bppMode == 0) ? 4 : (bppMode == 1 ? 2 : 1); + int fullWidth = 64 * bppMult; + int fullHeight = 256; + + int minU = crop ? bounds.minU : 0; + int maxU = crop ? bounds.maxU +1: fullWidth; + int minV = crop ? bounds.minV : 0; + int maxV = crop ? bounds.maxV +1: fullHeight; + + // Boundary check and clamping + if (maxU > fullWidth || maxV > fullHeight) { + maxU = std::min(maxU, fullWidth); + maxV = std::min(maxV, fullHeight); + } + + int croppedWidth = maxU - minU; + int croppedHeight = maxV - minV; + + if (croppedWidth <= 0 || croppedHeight <= 0) return; + + // CLUT Coordinate Mapping + size_t clutX = key.clutX * 16; + size_t clutY = key.clutY; + if (clutX < 512) clutX += 512; + + size_t basePageX = key.pageX; + if (basePageX < 8) basePageX += 8; + + size_t imageXReal = (basePageX % 16) * 64; + size_t imageY = key.pageY * 256; + + // Extract VRAM to RGBA buffer + std::vector rgba(croppedWidth * croppedHeight * 4); + for (int y = 0; y < croppedHeight; y++) { + int srcY = imageY + minV + y; + size_t vramLine = srcY * 1024; + + for (int x = 0; x < croppedWidth; x++) { + int srcU = minU + x; + uint16_t color = 0; + + if (bppMode == 2) { + color = vram[vramLine + imageXReal + srcU]; + } + else { + size_t hOffset = imageXReal + (srcU / bppMult); + uint16_t word = vram[vramLine + hOffset]; + int bits = (bppMode == 0) ? 4 : 8; + int val = (word >> (bits * (srcU % bppMult))) & ((1 << bits) - 1); + + size_t pIdx = clutY * 1024 + clutX + val; + color = (pIdx < vram.size()) ? vram[pIdx] : 0; + } + ConvertVRAMColorToRGBA(color, &rgba[(y * croppedWidth + x) * 4]); + } + } + + // Save to temporary file + m_path = tempDir / (newMatName + ".png"); + if (stbi_write_png(m_path.string().c_str(), croppedWidth, croppedHeight, 4, rgba.data(), croppedWidth * 4)) { + // Initialize the rest of the VRAM metadata + m_imageX = imageXReal - 512; + m_imageY = imageY; + if (bppMode < 2) { + m_clutX = clutX - 512; + m_clutY = clutY; + } + m_blendMode = key.blendMode; + + // Load the PNG back into the class buffers (m_image, m_width, m_height, etc.) + if (!CreateTexture()) { + ClearTexture(); + } + } + else { + printf("ERROR: Failed to write PNG for %s\n", newMatName.c_str()); + ClearTexture(); + } +} + + void Texture::UpdateTexture(const std::filesystem::path& path) { uint16_t blendMode = m_blendMode; diff --git a/src/texture.h b/src/texture.h index 24d0440..0c557ca 100644 --- a/src/texture.h +++ b/src/texture.h @@ -11,6 +11,144 @@ typedef std::unordered_set Shape; + +//Helper structs for reading texture from .lev/.vrm. Maybe move to psx_types.h ? +struct RawUV { + uint8_t u0, v0, u1, v1, u2, v2, u3, v3; + + RawUV() = default; + + RawUV(const PSX::TextureLayout& layout) + : u0(layout.u0), v0(layout.v0) + , u1(layout.u1), v1(layout.v1) + , u2(layout.u2), v2(layout.v2) + , u3(layout.u3), v3(layout.v3) + { + } + + RawUV(const PSX::TextureLayout& layout, uint32_t drawOrderLow, int f) + : RawUV(layout) + { + auto SwapUV = [](uint8_t& u1, uint8_t& v1, uint8_t& u2, uint8_t& v2) { + uint8_t tmpU = u1, tmpV = v1; + u1 = u2; v1 = v2; + u2 = tmpU; v2 = tmpV; + }; + auto Rotate90 = [&](RawUV& u) { + uint8_t tmp_u = u.u0, tmp_v = u.v0; + u.u0 = u.u2; u.v0 = u.v2; + u.u2 = u.u3; u.v2 = u.v3; + u.u3 = u.u1; u.v3 = u.v1; + u.u1 = tmp_u; u.v1 = tmp_v; + }; + auto Flip = [&](RawUV& u) { + SwapUV(u.u0, u.v0, u.u1, u.v1); + SwapUV(u.u2, u.v2, u.u3, u.v3); + }; + + uint32_t shift = 8 + (f * 5); + uint32_t rotateFlip = (drawOrderLow >> shift) & 0x7; + + switch (rotateFlip) + { + case 1: Rotate90(*this); break; + case 2: Rotate90(*this); Rotate90(*this); break; + case 3: Rotate90(*this); Rotate90(*this); Rotate90(*this); break; + case 4: Flip(*this); Rotate90(*this); Rotate90(*this); Rotate90(*this); break; + case 5: Flip(*this); Rotate90(*this); Rotate90(*this); break; + case 6: Flip(*this); Rotate90(*this); break; + case 7: Flip(*this); break; + default: break; + } + } +}; + + + +struct PixelBounds +{ + uint8_t minU = 255, minV = 255; + uint8_t maxU = 0, maxV = 0; + + void Update(const RawUV& uvs) + { + if (uvs.u0 < minU) minU = uvs.u0; + if (uvs.u1 < minU) minU = uvs.u1; + if (uvs.u2 < minU) minU = uvs.u2; + if (uvs.u3 < minU) minU = uvs.u3; + + if (uvs.v0 < minV) minV = uvs.v0; + if (uvs.v1 < minV) minV = uvs.v1; + if (uvs.v2 < minV) minV = uvs.v2; + if (uvs.v3 < minV) minV = uvs.v3; + + if (uvs.u0 > maxU) maxU = uvs.u0; + if (uvs.u1 > maxU) maxU = uvs.u1; + if (uvs.u2 > maxU) maxU = uvs.u2; + if (uvs.u3 > maxU) maxU = uvs.u3; + + if (uvs.v0 > maxV) maxV = uvs.v0; + if (uvs.v1 > maxV) maxV = uvs.v1; + if (uvs.v2 > maxV) maxV = uvs.v2; + if (uvs.v3 > maxV) maxV = uvs.v3; + } +}; + +struct LayoutKey // 2 PSX::TextureLayout have the same LayoutKey if they use the same vram page and colors. Roughly correspond to materials +{ + uint16_t pageX; + uint16_t pageY; + uint16_t bpp; + uint16_t clutX; + uint16_t clutY; + uint16_t blendMode; + + LayoutKey() = default; + + LayoutKey(const PSX::TextureLayout& layout) + : pageX(layout.texPage.x) + , pageY(layout.texPage.y) + , bpp(layout.texPage.texpageColors) + , clutX(layout.clut.x) + , clutY(layout.clut.y) + , blendMode(layout.texPage.blendMode) + { + } + + bool operator==(const LayoutKey& other) const + { + return pageX == other.pageX && + pageY == other.pageY && + bpp == other.bpp && + clutX == other.clutX && + clutY == other.clutY && + blendMode == other.blendMode; + } +}; + +namespace std +{ + template<> + struct hash + { + size_t operator()(const LayoutKey& key) const + { + size_t h1 = std::hash{}(key.pageX); + size_t h2 = std::hash{}(key.pageY); + size_t h3 = std::hash{}(key.bpp); + size_t h4 = std::hash{}(key.clutX); + size_t h5 = std::hash{}(key.clutY); + size_t h6 = std::hash{}(key.blendMode); + + return h1 ^ (h2 << 1) ^ (h3 << 2) ^ (h4 << 3) ^ (h5 << 4) ^ (h6 << 5); + } + }; +} + + + + + class Texture { public: @@ -20,6 +158,7 @@ class Texture }; Texture() : m_width(0), m_height(0), m_imageX(0), m_imageY(0), m_clutX(0), m_clutY(0), m_blendMode(0), m_semiTransparent(false) {}; Texture(const std::filesystem::path& path); + Texture(const LayoutKey& key, const PixelBounds& bounds, const std::vector& vram, const std::string& newMatName, const std::filesystem::path& tempDir, bool crop = true); void UpdateTexture(const std::filesystem::path& path); Texture::BPP GetBPP() const; int GetWidth() const; From ff74b817aa61e5548dc992fb83b8cac44b917997 Mon Sep 17 00:00:00 2001 From: Blounard Date: Thu, 16 Apr 2026 19:30:50 +0200 Subject: [PATCH 2/2] Improved Texture Parsing Improved the texture parsing to try to be closer to what CTR does. It no longer assumes that textures and animTextures are all in order after MeshInfo and before Quadblocks. Instead it parses quadblock exclusively, and determines if a texture is animated or not with PointerMap, and only header.offAnimText --- src/level.cpp | 205 ++++++++++++++++++++++---------------------------- 1 file changed, 91 insertions(+), 114 deletions(-) diff --git a/src/level.cpp b/src/level.cpp index ad927de..ef4673c 100644 --- a/src/level.cpp +++ b/src/level.cpp @@ -732,6 +732,19 @@ bool Level::LoadLEV(const std::filesystem::path& levFile) Read(file, offPointerMap); std::streampos offLev = file.tellg(); + + std::set pointerMap; + file.seekg(offLev + std::streampos(offPointerMap)); + uint32_t pointerMapSize; + Read(file, pointerMapSize); + for (size_t i = 0; i < pointerMapSize / sizeof(uint32_t); i++) + { + uint32_t pointer; + Read(file, pointer); + pointerMap.insert(pointer); + } + + file.seekg(offLev); PSX::LevHeader header = {}; Read(file, header); @@ -773,87 +786,75 @@ bool Level::LoadLEV(const std::filesystem::path& levFile) int texCounter = 0; std::unordered_map textureToPixelBounds; // Map Layout key -> Pixels bounds of the texture. std::unordered_map materialCache; // Layout Key -> matName - std::map> quadblockFaceToAnimOffset; // Map: quadblock index -> face index -> AnimTex offset - std::unordered_map animTexDataMap; // Map : relativeOffset -> AnimTex - std::unordered_map> animTexFrames; // Map : relativeOffset (AnimTex) -> List of TextureGroup Index - std::unordered_map textureGroupToMaterial; // Map : texture group offset -> material name + std::map> quadblockFaceToAnimOffset; // Map: quadblock index -> face index -> AnimTex offset + std::unordered_map animTexDataMap; // Map : Absolute Offset -> AnimTex + std::unordered_map> animTexFrames; // Map : Absolute Offset (AnimTex) -> List of TextureGroup Absolute Offset + std::unordered_map textureGroupToMaterial; // Map : texture group offset -> material name std::filesystem::path tempDir = levFile.parent_path() / (levFile.stem().string() + "_textures"); std::filesystem::create_directories(tempDir); bool hasAnimData = header.offAnimTex > 0; - size_t offTextureStart = static_cast(header.offMeshInfo) + sizeof(PSX::MeshInfo); - size_t offTextureEnd = hasAnimData ? header.offAnimTex : meshInfo.offQuadblocks; size_t offAnimStart = header.offAnimTex; - uint32_t offEndAnimData = meshInfo.offQuadblocks; - // 1st Pass : Parse all PSX::AnimTex and populate animTexDataMap and animTexFrames, and calculate UV bounds - if (hasAnimData) + + // 1st pass : Parse Quadblock, find TextureGroups, and caclulate UV bounds + // Take care of all texture group for static quad and animated quads + file.seekg(offLev + std::streampos(meshInfo.offQuadblocks)); + for (uint32_t i = 0; i < meshInfo.numQuadblocks; i++) { - size_t currentOffset = offAnimStart; - while (currentOffset < offEndAnimData) - { - file.seekg(offLev + std::streampos(currentOffset)); - PSX::AnimTex animTex; - Read(file, animTex); - size_t relativeOffset = currentOffset - header.offAnimTex; - animTexDataMap[relativeOffset] = animTex; - - std::vector frameTextureGroupIndices; - for (uint16_t f = 0; f < animTex.frameCount; f++) + PSX::Quadblock psxQuad = {}; + Read(file, psxQuad); + std::streampos currentPosQuad = file.tellg(); + for (int f = 0; f < 4; f++) + { + uint32_t texOffset = psxQuad.offMidTextures[f]; + if (hasAnimData && texOffset >= offAnimStart && pointerMap.contains(texOffset - 1)) // Anim Textures { - uint32_t frameTexOffset; - Read(file, frameTexOffset); - if (frameTexOffset >= offTextureStart && frameTexOffset < offTextureEnd) + if (!animTexDataMap.contains(texOffset-1)) { - size_t textureGroupIndex = (frameTexOffset - offTextureStart) / sizeof(PSX::TextureGroup); - frameTextureGroupIndices.push_back(textureGroupIndex); + file.seekg(offLev + std::streampos(texOffset-1)); + PSX::AnimTex animTex; + Read(file, animTex); + animTexDataMap[texOffset - 1] = animTex; - std::streampos currentPos = file.tellg(); - file.seekg(offLev + static_cast(frameTexOffset)); - PSX::TextureGroup group = {}; - Read(file, group); - file.seekg(currentPos); - const PSX::TextureLayout& layout = group.middle; - LayoutKey key(layout); - - if (!materialCache.contains(key)) + std::vector frameTextureGroupOffset; + for (uint16_t frame = 0; frame < animTex.frameCount; frame++) { - std::string newMatName = "tex_" + std::to_string(texCounter++); - materialCache[key] = newMatName; - } - textureGroupToMaterial[textureGroupIndex] = materialCache[key]; + uint32_t frameTexOffset; + Read(file, frameTexOffset); + frameTextureGroupOffset.push_back(frameTexOffset); + + std::streampos currentPos = file.tellg(); + file.seekg(offLev + static_cast(frameTexOffset)); + PSX::TextureGroup group = {}; + Read(file, group); + file.seekg(currentPos); + const PSX::TextureLayout& layout = group.middle; + LayoutKey key(layout); + + if (!materialCache.contains(key)) + { + std::string newMatName = "tex_" + std::to_string(texCounter++); + materialCache[key] = newMatName; + } + textureGroupToMaterial[frameTexOffset] = materialCache[key]; - RawUV rawUV(layout); - textureToPixelBounds[key].Update(rawUV); - } - else - { - printf(" Frame %u: offset=0x%x OUT OF RANGE (offTextureStart=0x%zx, offAnimTex=0x%x)\n", - f, frameTexOffset, offTextureStart, header.offAnimTex); + RawUV rawUV(layout); + textureToPixelBounds[key].Update(rawUV); + + } + animTexFrames[texOffset - 1] = frameTextureGroupOffset; } - } - animTexFrames[relativeOffset] = frameTextureGroupIndices; - currentOffset += sizeof(PSX::AnimTex) + (animTex.frameCount * sizeof(uint32_t)); - } - } - // 2nd pass : Non animated Texture reading and UV bounding boxes - file.seekg(offLev + std::streampos(meshInfo.offQuadblocks)); - for (uint32_t i = 0; i < meshInfo.numQuadblocks; i++) - { - PSX::Quadblock psxQuad = {}; - Read(file, psxQuad); - for (int f = 0; f < 4; f++) - { - uint32_t texOffset = psxQuad.offMidTextures[f]; - if (texOffset >= offTextureStart && texOffset < offTextureEnd) // Regular textures + quadblockFaceToAnimOffset[i][f] = texOffset - 1; + + } + else // Regular Textures { - std::streampos currentPos = file.tellg(); file.seekg(offLev + static_cast(texOffset)); PSX::TextureGroup group = {}; Read(file, group); - file.seekg(currentPos); const PSX::TextureLayout& layout = group.middle; LayoutKey key(layout); @@ -862,44 +863,13 @@ bool Level::LoadLEV(const std::filesystem::path& levFile) std::string newMatName = "tex_" + std::to_string(texCounter++); materialCache[key] = newMatName; } - size_t textureGroupIndex = (texOffset - offTextureStart) / sizeof(PSX::TextureGroup); - textureGroupToMaterial[textureGroupIndex] = materialCache[key]; + textureGroupToMaterial[texOffset] = materialCache[key]; RawUV rawUV(layout, psxQuad.drawOrderLow, f); textureToPixelBounds[key].Update(rawUV); } - else if (hasAnimData && texOffset >= offAnimStart && texOffset < offEndAnimData) - { - // for AnimTex, texOffset isn't simply the animOffset of the AnimTex. - // We have to find it by looking within which animtex framearray bounds it falls - size_t relativeOffset = texOffset - header.offAnimTex; - size_t foundAnimOffset = 0; - bool found = false; - - for (const auto& [animOffset, animTex] : animTexDataMap) - { - // Calculate where the frame array starts and ends for this AnimTex - size_t frameArrayStart = animOffset + sizeof(PSX::AnimTex); - size_t frameArrayEnd = frameArrayStart + (animTex.frameCount * sizeof(uint32_t)); - - // Check if relativeOffset falls within this frame array - if (relativeOffset >= animOffset && relativeOffset < frameArrayEnd) - { - foundAnimOffset = animOffset; - found = true; - break; - } - } - if (found) - { - quadblockFaceToAnimOffset[i][f] = foundAnimOffset; - } - else - { - printf("WARNING: Could not find owning AnimTex for quadblock %d\n", psxQuad.id); - } - } } + file.seekg(currentPosQuad); } // 3rd pass : Create PNGs and Materials @@ -923,7 +893,15 @@ bool Level::LoadLEV(const std::filesystem::path& levFile) for (int f = 0; f < 4; f++) { uint32_t texOffset = psxQuad.offMidTextures[f]; - if (texOffset >= offTextureStart && texOffset < offTextureEnd)// Regular texture + if (hasAnimData && texOffset >= offAnimStart && pointerMap.contains(texOffset - 1)) // Anim Texture + { + // MATERIAL NOT YET ASSIGNED FOR ANIMATED QUAD. DO IT WHEN CREATING THE .OBJ (or somewhere else) + //size_t AnimOffset = quadblockFaceToAnimOffset[i][f]; // TODO VERIFY WHAT TO USE THERE BECAUSE ITS OFFSET NOT INDEX + //qb.SetAnimTextureOffset(AnimOffset, header.offAnimTex, f); + qb.SetAnimated(true); + } + + else { std::streampos currentPos = file.tellg(); file.seekg(offLev + static_cast(texOffset)); @@ -957,13 +935,6 @@ bool Level::LoadLEV(const std::filesystem::path& levFile) }; qb.SetFaceUVs(f, uvs); } - else if (hasAnimData && texOffset >= offAnimStart && texOffset < offEndAnimData) - { - // MATERIAL NOT YET ASSIGNED FOR ANIMATED QUAD. DO IT WHEN CREATING THE .OBJ (or somewhere else) - size_t AnimOffset = quadblockFaceToAnimOffset[i][f]; - qb.SetAnimTextureOffset(AnimOffset, header.offAnimTex, f); - qb.SetAnimated(true); - } } if (!materialAssigned) { @@ -975,13 +946,13 @@ bool Level::LoadLEV(const std::filesystem::path& levFile) //5th pass : Create .obj for AnimText, and assign to quads if (hasAnimData) { - std::map, std::set> facePatternToQuadblocks; + std::map, std::set> facePatternToQuadblocks; for (const auto& [quadIdx, faceMap] : quadblockFaceToAnimOffset) { facePatternToQuadblocks[faceMap].insert(quadIdx); } - std::set> processedPatterns; + std::set> processedPatterns; for (const auto& [faceMap, quadSet] : facePatternToQuadblocks) { if (processedPatterns.contains(faceMap)) continue; @@ -989,7 +960,7 @@ bool Level::LoadLEV(const std::filesystem::path& levFile) std::vector quadIndices(quadSet.begin(), quadSet.end()); - size_t firstAnimOffset = faceMap.begin()->second; + uint32_t firstAnimOffset = faceMap.begin()->second; if (!animTexDataMap.contains(firstAnimOffset)) continue; const PSX::AnimTex& firstAnimData = animTexDataMap[firstAnimOffset]; @@ -1010,36 +981,42 @@ bool Level::LoadLEV(const std::filesystem::path& levFile) std::array, 4> faceFrameLayouts; std::array, 4> faceFrameMaterials; - std::array faceHasData = { false, false, false, false }; bool allMaterialsFound = true; for (const auto& [faceIdx, animOffset] : faceMap) { - for (size_t textureGroupIndex : animTexFrames[animOffset]) + for (uint32_t textureGroupOffset : animTexFrames.at(animOffset)) { - if (!textureGroupToMaterial.contains(textureGroupIndex)) + if (!textureGroupToMaterial.contains(textureGroupOffset)) { allMaterialsFound = false; break; } - faceFrameMaterials[faceIdx].push_back(textureGroupToMaterial[textureGroupIndex]); - size_t frameTexOffset = offTextureStart + (textureGroupIndex * sizeof(PSX::TextureGroup)); + faceFrameMaterials[faceIdx].push_back(textureGroupToMaterial[textureGroupOffset]); std::streampos savedPos = file.tellg(); - file.seekg(offLev + std::streampos(frameTexOffset)); + file.seekg(offLev + std::streampos(textureGroupOffset)); PSX::TextureGroup group = {}; Read(file, group); file.seekg(savedPos); faceFrameLayouts[faceIdx].push_back(group.middle); } if (!allMaterialsFound) break; - faceHasData[faceIdx] = true; } if (!allMaterialsFound) continue; // Create temporary OBJ file - std::string animName = faceFrameMaterials[0][0]; + std::string animName = ""; + for (size_t faceIdx = 0; faceIdx < 4; faceIdx++) + { + if (faceFrameMaterials[faceIdx].size() != 0) + { + animName = faceFrameMaterials[faceIdx][0]; + break; + } + } + std::filesystem::path animDir = tempDir / animName; std::filesystem::create_directories(animDir);