-
Notifications
You must be signed in to change notification settings - Fork 0
feat(ps1): Phase 4 mesh reconstruction from capture (#422) #601
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| #include "CaptureSnapshot.h" | ||
| #include "CaptureBuffer.h" | ||
|
|
||
| CaptureSnapshot CaptureSnapshot::fromBuffer(const CaptureBuffer &buffer) | ||
| { | ||
| CaptureSnapshot snap; | ||
| snap.prims = buffer.prims(); | ||
| snap.matrices = buffer.matrices(); | ||
| snap.cameraMatrixId = buffer.cameraMatrixId(); | ||
| return snap; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| #ifndef CAPTURESNAPSHOT_H | ||
| #define CAPTURESNAPSHOT_H | ||
|
|
||
| #include "CaptureTypes.h" | ||
|
|
||
| #include <QMetaType> | ||
| #include <QVector> | ||
|
|
||
| class CaptureBuffer; | ||
|
|
||
| /** Thread-safe copy of a frame capture for main-thread reconstruction (#422). */ | ||
| struct CaptureSnapshot | ||
| { | ||
| QVector<PrimRecord> prims; | ||
| QVector<MatrixRecord> matrices; | ||
| uint32_t cameraMatrixId = UINT32_MAX; | ||
|
|
||
| static CaptureSnapshot fromBuffer(const CaptureBuffer &buffer); | ||
| }; | ||
|
|
||
| Q_DECLARE_METATYPE(CaptureSnapshot) | ||
|
|
||
| #endif // CAPTURESNAPSHOT_H |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| #include "GteInverse.h" | ||
|
|
||
| #include <cmath> | ||
|
|
||
| namespace GteInverse { | ||
|
|
||
| bool screenToModel(const MatrixRecord &matrix, int sx, int sy, int sz, float &mx, float &my, float &mz) | ||
| { | ||
| const double h = matrix.h != 0 ? static_cast<double>(matrix.h) : 4096.0; | ||
| const double ir3 = sz != 0 ? static_cast<double>(sz) : 4096.0; | ||
| const double ir1 = ((static_cast<double>(sx) * 65536.0) - matrix.ofx) * ir3 / h; | ||
| const double ir2 = ((static_cast<double>(sy) * 65536.0) - matrix.ofy) * ir3 / h; | ||
|
|
||
| const double irAdj[3] = {ir1 - matrix.tr[0], ir2 - matrix.tr[1], ir3 - matrix.tr[2]}; | ||
|
Check warning on line 14 in src/PS1/runtime/GteInverse.cpp
|
||
| double vx = 0.0; | ||
| double vy = 0.0; | ||
| double vz = 0.0; | ||
| for (int c = 0; c < 3; ++c) { | ||
| vx += static_cast<double>(matrix.rt.m[c][0]) * irAdj[c]; | ||
| vy += static_cast<double>(matrix.rt.m[c][1]) * irAdj[c]; | ||
| vz += static_cast<double>(matrix.rt.m[c][2]) * irAdj[c]; | ||
| } | ||
|
|
||
| constexpr double kFixedScale = 1.0 / 4096.0; | ||
| mx = static_cast<float>(vx * kFixedScale); | ||
| my = static_cast<float>(vy * kFixedScale); | ||
| mz = static_cast<float>(vz * kFixedScale); | ||
| return std::isfinite(mx) && std::isfinite(my) && std::isfinite(mz); | ||
| } | ||
|
|
||
| void psxScreenToWorld(float sx, float sy, float sz, float &wx, float &wy, float &wz) | ||
| { | ||
| constexpr float kCenterX = 160.0f; | ||
| constexpr float kCenterY = 120.0f; | ||
| constexpr float kScale = 0.01f; | ||
| wx = (sx - kCenterX) * kScale; | ||
| wy = -(sy - kCenterY) * kScale; | ||
| wz = sz * kScale; | ||
| } | ||
|
|
||
| void modelToEditor(float mx, float my, float mz, float &wx, float &wy, float &wz) | ||
| { | ||
| constexpr float kScale = 0.01f; | ||
| wx = mx * kScale; | ||
| wy = -my * kScale; | ||
| wz = -mz * kScale; | ||
| } | ||
|
|
||
| } // namespace GteInverse | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| #ifndef GTEINVERSE_H | ||
| #define GTEINVERSE_H | ||
|
|
||
| #include "CaptureTypes.h" | ||
|
|
||
| /** Approximate inverse of PS1 GTE screen projection (#422). */ | ||
| namespace GteInverse { | ||
|
|
||
| bool screenToModel(const MatrixRecord &matrix, int sx, int sy, int sz, float &mx, float &my, float &mz); | ||
|
|
||
| /** PS1 screen (Y-down) → editor world (Y-up, right-handed). */ | ||
| void psxScreenToWorld(float sx, float sy, float sz, float &wx, float &wy, float &wz); | ||
|
|
||
| /** GTE model space → editor world (handedness flip only, no screen re-centering). */ | ||
| void modelToEditor(float mx, float my, float mz, float &wx, float &wy, float &wz); | ||
|
|
||
| } // namespace GteInverse | ||
|
|
||
| #endif // GTEINVERSE_H |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| #include "GteInverse.h" | ||
|
|
||
| #include <cmath> | ||
| #include <gtest/gtest.h> | ||
|
|
||
| static MatrixRecord identityMatrix() | ||
| { | ||
| MatrixRecord m{}; | ||
| m.rt.m[0][0] = 1 << 12; | ||
| m.rt.m[1][1] = 1 << 12; | ||
| m.rt.m[2][2] = 1 << 12; | ||
| m.h = 256; | ||
| return m; | ||
| } | ||
|
|
||
| TEST(GteInverseTest, ScreenToModelWithTranslation) | ||
| { | ||
| MatrixRecord matrix = identityMatrix(); | ||
| matrix.tr[0] = 4096; | ||
| matrix.tr[1] = 0; | ||
| matrix.tr[2] = 0; | ||
|
|
||
| float mx = 0.0f; | ||
| float my = 0.0f; | ||
| float mz = 0.0f; | ||
| ASSERT_TRUE(GteInverse::screenToModel(matrix, 160, 120, 4096, mx, my, mz)); | ||
| EXPECT_TRUE(std::isfinite(mx)); | ||
| EXPECT_TRUE(std::isfinite(my)); | ||
| EXPECT_TRUE(std::isfinite(mz)); | ||
| EXPECT_NE(mx, 0.0f); | ||
| } | ||
|
|
||
| TEST(GteInverseTest, ScreenToWorldFlipsY) | ||
| { | ||
| float wx = 0.0f; | ||
| float wy = 0.0f; | ||
| float wz = 0.0f; | ||
| GteInverse::psxScreenToWorld(160.0f, 120.0f, 0.0f, wx, wy, wz); | ||
| EXPECT_NEAR(wx, 0.0f, 1e-4f); | ||
| EXPECT_NEAR(wy, 0.0f, 1e-4f); | ||
| GteInverse::psxScreenToWorld(160.0f, 140.0f, 0.0f, wx, wy, wz); | ||
| EXPECT_LT(wy, 0.0f); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| #include "MeshReconstructor.h" | ||
|
|
||
| #include "GteInverse.h" | ||
|
|
||
| #include <OgreColourValue.h> | ||
|
|
||
| #include <QHash> | ||
|
|
||
| namespace { | ||
|
|
||
| uint32_t packDiffuse(uint8_t r, uint8_t g, uint8_t b) | ||
| { | ||
| const Ogre::ColourValue cv(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f); | ||
| return cv.getAsBYTE(); | ||
| } | ||
|
|
||
| QString textureMaterialName(uint16_t tpage, uint16_t clut) | ||
| { | ||
| return QStringLiteral("PS1Rip/tpage_%1_clut_%2") | ||
| .arg(tpage, 4, 16, QChar('0')) | ||
| .arg(clut, 4, 16, QChar('0')); | ||
| } | ||
|
|
||
| quint64 textureGroupKey(uint16_t tpage, uint16_t clut) | ||
| { | ||
| return (static_cast<quint64>(tpage) << 32) | clut; | ||
| } | ||
|
|
||
| struct SubMeshAccumulator { | ||
| QString materialName; | ||
| QVector<ReconstructedVertex> vertices; | ||
| QVector<uint32_t> indices; | ||
|
|
||
| void addTriangle(const ReconstructedVertex &a, const ReconstructedVertex &b, const ReconstructedVertex &c) | ||
| { | ||
| const uint32_t base = static_cast<uint32_t>(vertices.size()); | ||
|
Check warning on line 36 in src/PS1/runtime/MeshReconstructor.cpp
|
||
| vertices.append(a); | ||
| vertices.append(b); | ||
| vertices.append(c); | ||
| indices.append(base); | ||
| indices.append(base + 1); | ||
| indices.append(base + 2); | ||
| } | ||
| }; | ||
|
|
||
| ReconstructedVertex vertexFromPsx(const PsxVertex &v, const MatrixRecord *matrix, bool textured) | ||
| { | ||
| ReconstructedVertex out; | ||
| out.diffuseArgb = packDiffuse(v.r, v.g, v.b); | ||
|
|
||
| float mx = 0.0f; | ||
| float my = 0.0f; | ||
| float mz = 0.0f; | ||
| if (matrix && GteInverse::screenToModel(*matrix, v.x, v.y, v.z, mx, my, mz)) | ||
|
Check warning on line 54 in src/PS1/runtime/MeshReconstructor.cpp
|
||
| GteInverse::modelToEditor(mx, my, mz, out.px, out.py, out.pz); | ||
| else | ||
| GteInverse::psxScreenToWorld(static_cast<float>(v.x), static_cast<float>(v.y), | ||
| static_cast<float>(v.z), out.px, out.py, out.pz); | ||
|
|
||
| if (textured) { | ||
| out.u = static_cast<float>(v.u) / 256.0f; | ||
| out.v = static_cast<float>(v.v) / 256.0f; | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| void emitPrimitive(const PrimRecord &prim, const MatrixRecord *matrix, SubMeshAccumulator &acc) | ||
| { | ||
| const bool textured = prim.kind == PrimKind::TexturedTri || prim.kind == PrimKind::TexturedQuad | ||
| || prim.kind == PrimKind::Sprite; | ||
|
|
||
| auto vtx = [&](int i) { return vertexFromPsx(prim.verts[i], matrix, textured); }; | ||
|
|
||
| if (prim.kind == PrimKind::MonoTri || prim.kind == PrimKind::ShadedTri | ||
| || prim.kind == PrimKind::TexturedTri) { | ||
| if (prim.vertexCount >= 3) | ||
| acc.addTriangle(vtx(0), vtx(1), vtx(2)); | ||
| return; | ||
| } | ||
|
|
||
| if (prim.kind == PrimKind::MonoQuad || prim.kind == PrimKind::ShadedQuad | ||
| || prim.kind == PrimKind::TexturedQuad) { | ||
| if (prim.vertexCount >= 4) { | ||
| acc.addTriangle(vtx(0), vtx(1), vtx(2)); | ||
| acc.addTriangle(vtx(0), vtx(2), vtx(3)); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (prim.kind == PrimKind::Sprite && prim.vertexCount >= 2) { | ||
| ReconstructedVertex a = vtx(0); | ||
| ReconstructedVertex b = vtx(1); | ||
| ReconstructedVertex c = b; | ||
| ReconstructedVertex d = a; | ||
| c.py = b.py + 0.05f; | ||
| d.py = a.py + 0.05f; | ||
| acc.addTriangle(a, b, c); | ||
| acc.addTriangle(a, c, d); | ||
| } | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| ReconstructedMesh MeshReconstructor::reconstruct(const CaptureSnapshot &snapshot) | ||
| { | ||
| ReconstructedMesh result; | ||
| if (snapshot.prims.isEmpty()) | ||
| return result; | ||
|
|
||
| QHash<uint32_t, QHash<quint64, SubMeshAccumulator>> groupsByMatrix; | ||
|
|
||
| for (const PrimRecord &prim : snapshot.prims) { | ||
| const MatrixRecord *matrix = nullptr; | ||
| if (prim.matrixId < static_cast<uint32_t>(snapshot.matrices.size())) | ||
| matrix = &snapshot.matrices[static_cast<int>(prim.matrixId)]; | ||
|
|
||
| const quint64 texKey = textureGroupKey(prim.tpage, prim.clut); | ||
| SubMeshAccumulator &acc = groupsByMatrix[prim.matrixId][texKey]; | ||
| if (acc.materialName.isEmpty()) | ||
| acc.materialName = textureMaterialName(prim.tpage, prim.clut); | ||
| emitPrimitive(prim, matrix, acc); | ||
| } | ||
|
|
||
| int subIndex = 0; | ||
| for (auto matIt = groupsByMatrix.constBegin(); matIt != groupsByMatrix.constEnd(); ++matIt) { | ||
| for (auto texIt = matIt.value().constBegin(); texIt != matIt.value().constEnd(); ++texIt) { | ||
| const SubMeshAccumulator &acc = texIt.value(); | ||
| if (acc.vertices.isEmpty() || acc.indices.size() < 3) | ||
| continue; | ||
|
|
||
| ReconstructedSubMesh sub; | ||
| sub.materialName = acc.materialName; | ||
| sub.vertices = acc.vertices; | ||
| sub.indices = acc.indices; | ||
| result.subMeshes.append(sub); | ||
| result.vertexCount += acc.vertices.size(); | ||
| result.triangleCount += acc.indices.size() / 3; | ||
| ++subIndex; | ||
| } | ||
| } | ||
|
|
||
| result.meshName = QStringLiteral("ps1_capture"); | ||
| return result; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| #ifndef MESHRECONSTRUCTOR_H | ||
| #define MESHRECONSTRUCTOR_H | ||
|
|
||
| #include "CaptureSnapshot.h" | ||
|
|
||
| #include <QString> | ||
| #include <QVector> | ||
|
|
||
| /** One vertex in reconstructed editor space (#422). */ | ||
| struct ReconstructedVertex { | ||
| float px = 0.0f; | ||
| float py = 0.0f; | ||
| float pz = 0.0f; | ||
| float nx = 0.0f; | ||
| float ny = 0.0f; | ||
| float nz = 0.0f; | ||
|
fernandotonon marked this conversation as resolved.
|
||
| float u = 0.0f; | ||
| float v = 0.0f; | ||
| uint32_t diffuseArgb = 0xFFFFFFFFu; | ||
| }; | ||
|
|
||
| struct ReconstructedSubMesh { | ||
| QString materialName; | ||
| QVector<ReconstructedVertex> vertices; | ||
| QVector<uint32_t> indices; | ||
| }; | ||
|
|
||
| struct ReconstructedMesh { | ||
| QString meshName; | ||
| QVector<ReconstructedSubMesh> subMeshes; | ||
| int vertexCount = 0; | ||
| int triangleCount = 0; | ||
|
|
||
| bool isEmpty() const { return subMeshes.isEmpty(); } | ||
| }; | ||
|
|
||
| /** Builds editor-space meshes from a captured primitive stream (#422). */ | ||
| class MeshReconstructor | ||
| { | ||
| public: | ||
| static ReconstructedMesh reconstruct(const CaptureSnapshot &snapshot); | ||
| }; | ||
|
|
||
| #endif // MESHRECONSTRUCTOR_H | ||
Uh oh!
There was an error while loading. Please reload this page.