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..ef4673c 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,14 +722,29 @@ 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); 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); @@ -759,15 +778,269 @@ 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 : 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 offAnimStart = header.offAnimTex; + + + // 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++) { - 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); + 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 + { + if (!animTexDataMap.contains(texOffset-1)) + { + file.seekg(offLev + std::streampos(texOffset-1)); + PSX::AnimTex animTex; + Read(file, animTex); + animTexDataMap[texOffset - 1] = animTex; + + std::vector frameTextureGroupOffset; + for (uint16_t frame = 0; frame < animTex.frameCount; frame++) + { + 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); + + } + animTexFrames[texOffset - 1] = frameTextureGroupOffset; + } + + quadblockFaceToAnimOffset[i][f] = texOffset - 1; + + } + else // Regular Textures + { + file.seekg(offLev + static_cast(texOffset)); + PSX::TextureGroup group = {}; + Read(file, group); + 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[texOffset] = materialCache[key]; + + RawUV rawUV(layout, psxQuad.drawOrderLow, f); + textureToPixelBounds[key].Update(rawUV); + } + } + file.seekg(currentPosQuad); } + // 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 (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)); + 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); + } + } + 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()); + + uint32_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; + + bool allMaterialsFound = true; + + for (const auto& [faceIdx, animOffset] : faceMap) + { + for (uint32_t textureGroupOffset : animTexFrames.at(animOffset)) + { + if (!textureGroupToMaterial.contains(textureGroupOffset)) + { + allMaterialsFound = false; + break; + } + faceFrameMaterials[faceIdx].push_back(textureGroupToMaterial[textureGroupOffset]); + std::streampos savedPos = file.tellg(); + file.seekg(offLev + std::streampos(textureGroupOffset)); + PSX::TextureGroup group = {}; + Read(file, group); + file.seekg(savedPos); + faceFrameLayouts[faceIdx].push_back(group.middle); + } + if (!allMaterialsFound) break; + } + + if (!allMaterialsFound) continue; + + // Create temporary OBJ file + 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); + + 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)); @@ -1917,6 +2190,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;