diff --git a/src/MeshImporterExporter.cpp b/src/MeshImporterExporter.cpp index 52c771d21..084938d3a 100755 --- a/src/MeshImporterExporter.cpp +++ b/src/MeshImporterExporter.cpp @@ -1672,9 +1672,7 @@ void MeshImporterExporter::importer(const QStringList &_uriList, unsigned int ad continue; } - // Decide whether to use the textured import path. We do so when MAT entries were - // parsed AND at least one of them is textured (T/H/D) — otherwise we fall back to - // the simpler vertex-color path for performance and to keep submesh count low. + // `useTexturedMatPath`: after import, bind TIM slots to `_texN` submesh materials. bool useTexturedMatPath = false; if (!rsdMatEntries.isEmpty()) { for (const auto& me : rsdMatEntries) { @@ -1686,6 +1684,12 @@ void MeshImporterExporter::importer(const QStringList &_uriList, unsigned int ad } } } + // Psy-Q `G` / `H` carry per-corner RGB. Those live in `MatEntry::vertColors`, while + // `MatEntry::rgb` is only the first corner (back-compat / flat preview). If we skip + // `importPsyqPlyWithFaceMaterials` whenever no face is textured, untextured smooth + // MAT rows collapse to `importPsyqPlyWithFaceColors(rsdFaceColors)` — one flat colour + // per face — which is wrong for Blender's Example Project cube (all `F G` rows). + const bool useFaceMaterialsImport = !rsdMatEntries.isEmpty(); Ogre::MeshPtr mesh; const QFileInfo geomFi(geomPath); @@ -1695,10 +1699,9 @@ void MeshImporterExporter::importer(const QStringList &_uriList, unsigned int ad } else if (!geomFi.suffix().compare(QStringLiteral("ply"), Qt::CaseInsensitive) && PS1PLY::isPsyqPlyFile(geomPath)) { const std::string meshName = (file.baseName() + QStringLiteral("_rsd_ply")).toStdString(); - if (useTexturedMatPath) { - // Convert the Psy-Q MAT entries into per-face PLY material bindings: each MAT - // entry maps 1:1 to a PLY face (in declaration order). UVs are normalised by - // the bound texture's width/height; colours retain the PS1 corner ordering. + if (useFaceMaterialsImport) { + // Each MAT entry maps 1:1 to a PLY face (declaration order). UVs are + // normalised by the bound texture's width/height when textured. QVector faceMats(rsdMatEntries.size()); for (int fi = 0; fi < rsdMatEntries.size(); ++fi) { const PS1MAT::MatEntry& me = rsdMatEntries[fi]; @@ -1732,11 +1735,10 @@ void MeshImporterExporter::importer(const QStringList &_uriList, unsigned int ad } else if (me.rgb.isValid()) { fm.color = me.rgb; } + fm.unlit = me.unlit; faceMats[fi] = fm; } mesh = PS1PLY::importPsyqPlyWithFaceMaterials(geomPath, meshName, faceMats); - // Fall back to the simpler vertex-colour path if the textured importer - // rejected the file (e.g. face-count mismatch). if (!mesh) mesh = rsdFaceColors.isEmpty() ? PS1PLY::importPsyqPly(geomPath, meshName) @@ -1778,9 +1780,8 @@ void MeshImporterExporter::importer(const QStringList &_uriList, unsigned int ad } } else if (useTexturedMatPath) { // Textured PLY path emits one submesh per texture group with material names - // shaped `PLY/_texN` or `PLY/_solid`. Bind the matching - // RSD texture slot to each textured submesh's first texture unit; untextured - // submeshes keep their vertex-colour material. + // shaped `PLY/_texN` or `_texN_nl` (unlit); untextured use `_solid` / `_solid_nl`. + // Bind the matching RSD texture slot to each textured submesh's first texture unit. for (unsigned int si = 0; si < en->getNumSubEntities(); ++si) { Ogre::SubEntity* se = const_cast(en->getSubEntity(si)); Ogre::MaterialPtr mat = se->getMaterial(); @@ -1793,12 +1794,13 @@ void MeshImporterExporter::importer(const QStringList &_uriList, unsigned int ad const QString mname = QString::fromStdString(mat->getName()); static const QRegularExpression kTexSlotRe( - QStringLiteral("_tex(\\d+)$")); + QStringLiteral("_tex(\\d+)(_nl)?$")); const auto m = kTexSlotRe.match(mname); if (!m.hasMatch()) continue; // untextured submesh — leave as is. bool ok = false; const int slot = m.captured(1).toInt(&ok); + const bool slotUnlit = m.captured(2) == QStringLiteral("_nl"); if (!ok || slot < 0 || slot >= static_cast(rsdTexSlots.size()) || rsdTexSlots[slot].resourceName.isEmpty()) continue; @@ -1806,20 +1808,13 @@ void MeshImporterExporter::importer(const QStringList &_uriList, unsigned int ad // Replace any existing texture unit states so re-imports refresh cleanly. pass->removeAllTextureUnitStates(); pass->createTextureUnitState(rsdTexSlots[slot].resourceName.toStdString()); - pass->setLightingEnabled(true); - pass->setAmbient(1.0f, 1.0f, 1.0f); - pass->setDiffuse(1.0f, 1.0f, 1.0f, 1.0f); - // Preserve per-corner colours that the textured PLY import already baked - // into the submesh (Psy-Q H/D/G shaded materials encode their tint via - // vertex colour). Force TVC_NONE only when the submesh has no VES_DIFFUSE - // stream, otherwise the texture would render as a plain unshaded image. const Ogre::SubMesh* sm = en->getMesh()->getSubMesh(si); const Ogre::VertexData* vdSm = (sm && sm->useSharedVertices) ? en->getMesh()->sharedVertexData : (sm ? sm->vertexData : nullptr); const bool hasVC = vdSm && vdSm->vertexDeclaration && vdSm->vertexDeclaration->findElementBySemantic(Ogre::VES_DIFFUSE); - pass->setVertexColourTracking(hasVC ? Ogre::TVC_DIFFUSE : Ogre::TVC_NONE); + PS1PLY::configurePsyqRsdMaterialPass(pass, hasVC, slotUnlit); mat->compile(); } } @@ -2200,6 +2195,7 @@ int MeshImporterExporter::exporter(const Ogre::SceneNode *_sn, const QString &_u std::vector rsdOutTextures; std::unordered_map submeshToTexSlot; ///< submeshIndex -> rsd slot. std::unordered_map resourceToSlot; + std::unordered_map submeshUnlit; ///< Psy-Q MAT no-light bit per submesh. for (unsigned int si = 0; si < e->getNumSubEntities(); ++si) { const Ogre::SubEntity* se = e->getSubEntity(si); @@ -2253,6 +2249,36 @@ int MeshImporterExporter::exporter(const Ogre::SceneNode *_sn, const QString &_u submeshToTexSlot[static_cast(si)] = slot; } + static const QRegularExpression kPsyqTexUnlitSuffix( + QStringLiteral("_tex\\d+(_nl)?$")); + static const QRegularExpression kPsyqSolidUnlitSuffix( + QStringLiteral("_solid(_nl)?$")); + for (unsigned int si = 0; si < e->getNumSubEntities(); ++si) { + const Ogre::SubEntity* se = e->getSubEntity(si); + if (!se) + continue; + bool unlit = false; + const QString mname = QString::fromStdString(se->getMaterialName()); + const auto texM = kPsyqTexUnlitSuffix.match(mname); + if (texM.hasMatch() && texM.captured(1) == QStringLiteral("_nl")) + unlit = true; + else { + const auto solM = kPsyqSolidUnlitSuffix.match(mname); + if (solM.hasMatch() && solM.captured(1) == QStringLiteral("_nl")) + unlit = true; + } + if (!unlit) { + Ogre::MaterialPtr mat = se->getMaterial(); + if (!mat.isNull() && mat->getNumTechniques() > 0 + && mat->getTechnique(0)->getNumPasses() > 0) { + const Ogre::Pass* pass = mat->getTechnique(0)->getPass(0); + if (pass && !pass->getLightingEnabled()) + unlit = true; + } + } + submeshUnlit[static_cast(si)] = unlit; + } + // Synthesise MAT entries: one per output PLY face. Pick the most specific Psy-Q // type that preserves the source data: // * Textured + smooth corner colours -> 'H' (textured smooth, per-corner tint) @@ -2296,6 +2322,13 @@ int MeshImporterExporter::exporter(const Ogre::SceneNode *_sn, const QString &_u const bool gotCornerColors = eft.hasCornerColors; const bool smoothShade = gotCornerColors && !cornersUniform(eft); + const int smIdx = (fi < faceTexInfos.size()) ? faceTexInfos[fi].submeshIndex : -1; + if (smIdx >= 0) { + const auto uit = submeshUnlit.find(smIdx); + if (uit != submeshUnlit.end()) + me.unlit = uit->second; + } + const int slotIt = (eft.textured && submeshToTexSlot.count(eft.submeshIndex)) ? submeshToTexSlot[eft.submeshIndex] : -1; diff --git a/src/PS1/PS1MAT.cpp b/src/PS1/PS1MAT.cpp index f399d1146..05063d01b 100644 --- a/src/PS1/PS1MAT.cpp +++ b/src/PS1/PS1MAT.cpp @@ -159,13 +159,27 @@ bool tryParseEntryLine(const QString& line, MatEntry& outEntry) if (!okIdx) return false; - // Find the type char: scan from the start past the leading numerics and pick the - // SECOND single-letter token (first is shadingChar, second is typeChar). + // Optional packed flag integer (Blender exporter) before the two letter tokens. + int scanStart = 1; + int matFlags = 0; + bool haveMatFlags = false; + if (parts.size() > 2) { + bool okF = false; + const int maybe = parts[1].toInt(&okF, 10); + if (okF && maybe >= 0 && maybe <= 0xFFFFFF) { + matFlags = maybe; + haveMatFlags = true; + scanStart = 2; + } + } + + // Find the type char: scan from `scanStart` and pick the SECOND single-letter token + // (first is shadingChar, second is typeChar). int letterCount = 0; int typeCharIdx = -1; char shadingChar = 'F'; char typeChar = 'C'; - for (int i = 1; i < parts.size(); ++i) { + for (int i = scanStart; i < parts.size(); ++i) { const QString& p = parts[i]; if (isSingleLetter(p)) { const char c = p.at(0).toUpper().toLatin1(); @@ -195,7 +209,11 @@ bool tryParseEntryLine(const QString& line, MatEntry& outEntry) outEntry = MatEntry{}; outEntry.shadingChar = shadingChar; outEntry.typeChar = typeChar; - return decodePayload(outEntry, trailing); + if (haveMatFlags) + outEntry.unlit = (matFlags & 1) != 0; + if (!decodePayload(outEntry, trailing)) + return false; + return true; } bool readExpectedCount(const QStringList& lines, int& outCount, bool& outSawHeader) @@ -230,6 +248,16 @@ bool readExpectedCount(const QStringList& lines, int& outCount, bool& outSawHead } // namespace +/** Blender `Playstation RSD Exporter.py` materialFlag("000",0,0,unlit) → decimal int. */ +static int encodeBlenderMaterialFlagBits(bool unlit) +{ + const QString bits = QStringLiteral("00") + QStringLiteral("000") + QLatin1Char('0') + + QLatin1Char('0') + QLatin1Char(unlit ? '1' : '0'); + bool ok = false; + const int v = bits.toInt(&ok, 2); + return ok ? v : (unlit ? 1 : 0); +} + bool parseMatFile(const QString& matPath, QVector& outEntries, QString* outError) { outEntries.clear(); @@ -310,15 +338,15 @@ bool writeMatFile(const QString& matPath, const QVector& entries, QStr for (int i = 0; i < entries.size(); ++i) { const MatEntry& e = entries[i]; - const char shading = (e.shadingChar == 'S' || e.shadingChar == 'G') ? 'S' : 'F'; - ts << i << " 1 " << shading << " "; - - // Effective type char: keep what the caller provided unless it's nonsensical. char typeChar = e.typeChar; if (typeChar != 'C' && typeChar != 'G' && typeChar != 'T' && typeChar != 'D' && typeChar != 'H') typeChar = 'C'; - ts << typeChar; + const char shading = + (e.shadingChar == 'F' || e.shadingChar == 'G' || e.shadingChar == 'S') + ? e.shadingChar + : 'F'; + ts << i << " " << encodeBlenderMaterialFlagBits(e.unlit) << " " << shading << " " << typeChar; auto writeUvs = [&]() { if (e.uvs.size() >= 4) { @@ -413,4 +441,3 @@ bool writeMatFile(const QString& matPath, const QVector& entries, QStr } } // namespace PS1MAT - diff --git a/src/PS1/PS1MAT.h b/src/PS1/PS1MAT.h index aa882724a..f9e719b1b 100644 --- a/src/PS1/PS1MAT.h +++ b/src/PS1/PS1MAT.h @@ -23,6 +23,11 @@ The MIT License * * * Where: + * - Integer after poly index: packed flags from the Blender RSD exporter's + * `materialFlag("000",0,0,unlit)` — the **least significant bit is unlit** (no + * hardware lighting / "full bright") when other bits are zero. Omitted legacy + * lines (`0 F G ...`) default to **lit** (unlit=false). + * - Next letter: Blender mesh smooth (`G`) vs flat (`F`) shading — stored as `shadingChar`. * - typeChar 'C' = flat solid color (payload: R G B) * - typeChar 'G' = smooth (Gouraud) color (payload: 3*RGB for tri or 4*RGB for quad) * - typeChar 'T' = textured, no color (payload: texIndex u0 v0 u1 v1 u2 v2 u3 v3) @@ -46,8 +51,9 @@ struct UV { struct MatEntry { QColor rgb; ///< representative RGB (back-compat); first vert colour for smooth shaded entries. - char shadingChar = 'F'; ///< 'F' (flat) or 'S' / 'G' (smooth) - Psy-Q ASCII field, kept verbatim. + char shadingChar = 'F'; ///< Blender exporter: poly flat (`F`) vs smooth (`G`) normals — kept verbatim. char typeChar = 'C'; ///< 'C', 'G', 'T', 'H', or 'D' - see header doc. + bool unlit = false; ///< MAT flag LSB: no scene lighting (Blender "Unlit" / PS1 no-light style). bool textured = false; ///< true for T/H/D. int textureIndex = -1; ///< RSD TEX[i] index for textured polygons; -1 otherwise. QVector uvs; ///< 0, 3 or 4 entries (textured polygons only). @@ -58,8 +64,9 @@ bool parseMatFile(const QString& matPath, QVector& outEntries, QString /// Write a minimal Psy-Q MAT file with one RGB entry per face. /// -/// Entries are serialised as ` 1 ` matching the -/// canonical Psy-Q / Blender-RSD ASCII layout. Textured entries (`textureIndex >= 0` +/// Entries are serialised as ` ` where `` +/// matches the Blender exporter's `materialFlag(..., unlit)` (LSB = unlit). +/// Textured entries (`textureIndex >= 0` /// and `uvs.size() >= 3`) write the texture index + 8 UV ints, padding with the /// final UV (or `0 0`) for triangles to keep the on-disk shape stable. bool writeMatFile(const QString& matPath, const QVector& entries, QString* outError = nullptr); diff --git a/src/PS1/PS1MAT_test.cpp b/src/PS1/PS1MAT_test.cpp index eee418047..34fae42e7 100644 --- a/src/PS1/PS1MAT_test.cpp +++ b/src/PS1/PS1MAT_test.cpp @@ -28,9 +28,9 @@ TEST(PS1MAT, ParseFlatColorEntries) const QByteArray body = "@MAT940801\n" "3\n" - "0 1 F C 207 207 207\n" - "1 1 F C 58 58 58\n" - "2 1 F C 152 152 152\n"; + "0 0 F C 207 207 207\n" + "1 0 F C 58 58 58\n" + "2 0 F C 152 152 152\n"; const QString path = writeMatFile(dir.path(), body); QVector entries; @@ -45,6 +45,9 @@ TEST(PS1MAT, ParseFlatColorEntries) ASSERT_EQ(entries[0].vertColors.size(), 1); EXPECT_EQ(entries[0].vertColors[0], QColor(207, 207, 207)); EXPECT_EQ(entries[0].rgb, QColor(207, 207, 207)); + EXPECT_FALSE(entries[0].unlit); + EXPECT_FALSE(entries[1].unlit); + EXPECT_FALSE(entries[2].unlit); } TEST(PS1MAT, ParseSmoothColorQuadEntries) @@ -56,7 +59,7 @@ TEST(PS1MAT, ParseSmoothColorQuadEntries) const QByteArray body = "@MAT940801\n" "1\n" - "0 1 F G 152 152 152 197 197 197 84 84 84 120 120 120\n"; + "0 0 F G 152 152 152 197 197 197 84 84 84 120 120 120\n"; const QString path = writeMatFile(dir.path(), body); QVector entries; @@ -74,6 +77,27 @@ TEST(PS1MAT, ParseSmoothColorQuadEntries) EXPECT_EQ(e.vertColors[3], QColor(120, 120, 120)); // Back-compat rgb is the first vert colour. EXPECT_EQ(e.rgb, QColor(152, 152, 152)); + EXPECT_FALSE(e.unlit); +} + +TEST(PS1MAT, ParseMaterialFlag_UnlitUsesLsb) +{ + QTemporaryDir dir; + ASSERT_TRUE(dir.isValid()); + + const QByteArray body = + "@MAT940801\n" + "2\n" + "0 0 F C 255 0 0\n" + "1 1 F C 0 255 0\n"; + + const QString path = writeMatFile(dir.path(), body); + QVector entries; + QString err; + ASSERT_TRUE(PS1MAT::parseMatFile(path, entries, &err)) << err.toStdString(); + ASSERT_EQ(entries.size(), 2); + EXPECT_FALSE(entries[0].unlit); + EXPECT_TRUE(entries[1].unlit); } TEST(PS1MAT, ParseTexturedQuadEntry_HType) @@ -85,7 +109,7 @@ TEST(PS1MAT, ParseTexturedQuadEntry_HType) const QByteArray body = "@MAT940801\n" "1\n" - "0 1 F H 0 0 127 0 0 127 127 127 0 72 72 72 72 72 72 79 79 79 80 80 80\n"; + "0 0 F H 0 0 127 0 0 127 127 127 0 72 72 72 72 72 72 79 79 79 80 80 80\n"; const QString path = writeMatFile(dir.path(), body); QVector entries; @@ -116,7 +140,7 @@ TEST(PS1MAT, ParseTexturedTriEntry_TType) const QByteArray body = "@MAT940801\n" "1\n" - "0 1 F T 2 10 20 30 40 50 60 0 0\n"; + "0 0 F T 2 10 20 30 40 50 60 0 0\n"; const QString path = writeMatFile(dir.path(), body); QVector entries; @@ -155,6 +179,7 @@ TEST(PS1MAT, WriteAndRoundTrip_MixedEntries) t.textured = true; t.textureIndex = 1; t.uvs = { {0, 0}, {127, 0}, {127, 127}, {0, 127} }; + t.unlit = true; in.push_back(t); } { @@ -193,6 +218,39 @@ TEST(PS1MAT, WriteAndRoundTrip_MixedEntries) ASSERT_EQ(out[2].vertColors.size(), 4); EXPECT_EQ(out[2].vertColors[0], QColor(255, 0, 0)); EXPECT_EQ(out[2].vertColors[2], QColor(0, 0, 255)); + EXPECT_FALSE(out[0].unlit); + EXPECT_TRUE(out[1].unlit); + EXPECT_FALSE(out[2].unlit); +} + +TEST(PS1MAT, WriteAndRoundTrip_PreservesSmoothShadingToken) +{ + QTemporaryDir dir; + ASSERT_TRUE(dir.isValid()); + const QString path = QDir(dir.path()).filePath(QStringLiteral("smooth.mat")); + + QVector in; + PS1MAT::MatEntry g; + g.typeChar = 'G'; + g.shadingChar = 'G'; + g.vertColors = { + QColor(255, 0, 0), QColor(0, 255, 0), QColor(0, 0, 255), QColor(255, 255, 255) + }; + g.rgb = g.vertColors.first(); + in.push_back(g); + + QString err; + ASSERT_TRUE(PS1MAT::writeMatFile(path, in, &err)) << err.toStdString(); + + QFile f(path); + ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text)); + const QString text = QString::fromLatin1(f.readAll()); + EXPECT_TRUE(text.contains(QStringLiteral("0 0 G G "))) << text.toStdString(); + + QVector out; + ASSERT_TRUE(PS1MAT::parseMatFile(path, out, &err)) << err.toStdString(); + ASSERT_EQ(out.size(), 1); + EXPECT_EQ(out[0].shadingChar, 'G'); } TEST(PS1MAT, WritesGouraudTriWithFourCornerPadding) @@ -222,7 +280,7 @@ TEST(PS1MAT, WritesGouraudTriWithFourCornerPadding) QFile f(path); ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text)); const QString text = QString::fromLatin1(f.readAll()); - EXPECT_TRUE(text.contains(QStringLiteral("0 1 F G 245 245 245 107 107 107 84 84 84 0 0 0"))) + EXPECT_TRUE(text.contains(QStringLiteral("0 0 F G 245 245 245 107 107 107 84 84 84 0 0 0"))) << "tri G entry must be padded to 4 corners with a trailing 0 0 0 -- got:\n" << text.toStdString(); diff --git a/src/PS1/PS1PLY.cpp b/src/PS1/PS1PLY.cpp index caf041bc4..e5465837b 100644 --- a/src/PS1/PS1PLY.cpp +++ b/src/PS1/PS1PLY.cpp @@ -36,6 +36,10 @@ The MIT License #include #include +namespace PS1PLY { +void configurePsyqRsdMaterialPass(Ogre::Pass* pass, bool hasVertexColour, bool matUnlit); +} + namespace { struct TriSoup { @@ -537,13 +541,8 @@ static Ogre::MeshPtr buildMeshFromTriSoup(const std::string& meshName, const Tri mat->load(); if (mat->getNumTechniques() > 0 && mat->getTechnique(0)->getNumPasses() > 0) { Ogre::Pass* p0 = mat->getTechnique(0)->getPass(0); - if (p0) { - p0->setLightingEnabled(true); - p0->setAmbient(1.0f, 1.0f, 1.0f); - p0->setDiffuse(1.0f, 1.0f, 1.0f, 1.0f); - p0->setEmissive(0.0f, 0.0f, 0.0f); - p0->setVertexColourTracking(haveColors ? (Ogre::TVC_AMBIENT | Ogre::TVC_DIFFUSE) : Ogre::TVC_NONE); - } + if (p0) + PS1PLY::configurePsyqRsdMaterialPass(p0, haveColors, false); } } } catch (...) { @@ -1045,6 +1044,45 @@ static void mergeSubmeshTrisToQuads(const std::vector& I0, namespace PS1PLY { +void configurePsyqRsdMaterialPass(Ogre::Pass* p0, bool hasVertexColour, bool matUnlit) +{ + if (!p0) + return; + if (!hasVertexColour) { + if (matUnlit) { + p0->setLightingEnabled(false); + p0->setAmbient(0.0f, 0.0f, 0.0f); + p0->setDiffuse(1.0f, 1.0f, 1.0f, 1.0f); + p0->setSpecular(0.0f, 0.0f, 0.0f, 0.0f); + p0->setEmissive(0.0f, 0.0f, 0.0f); + p0->setVertexColourTracking(Ogre::TVC_NONE); + } else { + p0->setLightingEnabled(true); + p0->setAmbient(1.0f, 1.0f, 1.0f); + p0->setDiffuse(1.0f, 1.0f, 1.0f, 1.0f); + p0->setSpecular(0.0f, 0.0f, 0.0f, 0.0f); + p0->setEmissive(0.0f, 0.0f, 0.0f); + p0->setVertexColourTracking(Ogre::TVC_NONE); + } + return; + } + if (matUnlit) { + p0->setLightingEnabled(false); + p0->setAmbient(0.0f, 0.0f, 0.0f); + p0->setDiffuse(1.0f, 1.0f, 1.0f, 1.0f); + p0->setSpecular(0.0f, 0.0f, 0.0f, 0.0f); + p0->setEmissive(0.0f, 0.0f, 0.0f); + p0->setVertexColourTracking(Ogre::TVC_DIFFUSE); + } else { + p0->setLightingEnabled(true); + p0->setAmbient(1.0f, 1.0f, 1.0f); + p0->setDiffuse(1.0f, 1.0f, 1.0f, 1.0f); + p0->setSpecular(0.0f, 0.0f, 0.0f, 0.0f); + p0->setEmissive(0.0f, 0.0f, 0.0f); + p0->setVertexColourTracking(Ogre::TVC_DIFFUSE); + } +} + bool isPsyqPlyFile(const QString& filePath) { QFile f(filePath); @@ -1232,6 +1270,7 @@ struct TexturedTriProvenance { struct TexturedSubmeshSoup { int textureIndex = -1; ///< -1 = untextured submesh + bool unlit = false; ///< MAT / Blender no-light bit (material name `_nl` suffix). bool hasColor = false; std::vector corners; ///< multiple of 3 (triangle list). std::vector triProv; ///< One entry per tri (corners.size() / 3). @@ -1458,12 +1497,13 @@ static Ogre::MeshPtr buildMeshFromTexturedSoups( editableSubMeshes.push_back(recoverPolygonsFromProvenance(soup, indices)); Ogre::SubMesh* sm = mesh->createSubMesh(); + const bool nl = soup.unlit; const std::string slotSuffix = textured - ? std::string("_tex") + std::to_string(soup.textureIndex) - : std::string("_solid"); + ? (std::string("_tex") + std::to_string(soup.textureIndex) + (nl ? std::string("_nl") : std::string())) + : (std::string("_solid") + (nl ? std::string("_nl") : std::string())); const std::string matName = std::string("PLY/") + meshName + slotSuffix; // Ensure a fresh material exists with this name so the post-import RSD texture-binding - // pass (in MeshImporterExporter) can resolve `_texN` submeshes by regex. Clone from + // pass (in MeshImporterExporter) can resolve `_texN` / `_texN_nl` submesh materials. // BaseMaterial when available, otherwise create one outright so the CLI path (which // does not preload BaseMaterial) still gets unique submesh materials. try { @@ -1528,21 +1568,15 @@ static Ogre::MeshPtr buildMeshFromTexturedSoups( vbuf->unlock(); bind->setBinding(0, vbuf); - // Configure cloned material so vertex colours track diffuse (matches buildMeshFromTriSoup). + // Configure cloned material from MAT lit/unlit + vertex colour presence. try { if (auto mat = Ogre::MaterialManager::getSingleton().getByName(matName)) { if (!mat->isLoaded()) mat->load(); if (mat->getNumTechniques() > 0 && mat->getTechnique(0)->getNumPasses() > 0) { Ogre::Pass* p0 = mat->getTechnique(0)->getPass(0); - if (p0) { - p0->setLightingEnabled(true); - p0->setAmbient(1.0f, 1.0f, 1.0f); - p0->setDiffuse(1.0f, 1.0f, 1.0f, 1.0f); - p0->setEmissive(0.0f, 0.0f, 0.0f); - p0->setVertexColourTracking(hasColor ? (Ogre::TVC_AMBIENT | Ogre::TVC_DIFFUSE) - : Ogre::TVC_NONE); - } + if (p0) + configurePsyqRsdMaterialPass(p0, hasColor, soup.unlit); } } } catch (...) {} @@ -1587,11 +1621,9 @@ static Ogre::MeshPtr buildMeshFromTexturedSoups( return mesh; } -} // namespace - -Ogre::MeshPtr importPsyqPlyWithFaceMaterials(const QString& filePath, - const std::string& meshName, - const QVector& faceMaterials) +Ogre::MeshPtr importPsyqPlyWithFaceMaterialsImpl(const QString& filePath, + const std::string& meshName, + const QVector& faceMaterials) { const QString fileName = QFileInfo(filePath).fileName(); QFile f(filePath); @@ -1612,19 +1644,37 @@ Ogre::MeshPtr importPsyqPlyWithFaceMaterials(const QString& filePath, if (faceMaterials.size() != static_cast(faces.size())) return {}; - // Bucket faces by texture index (-1 = untextured). Keep insertion order so untextured - // submesh appears first when present and material assignment is deterministic. std::vector soups; - std::unordered_map texToSoup; - auto getSoup = [&](int texIndex) -> TexturedSubmeshSoup& { - const auto it = texToSoup.find(texIndex); + // Bucket faces by texture index (-1 = untextured) and MAT lit/unlit (material `_nl`). + // submesh appears first when present and material assignment is deterministic. + struct SoupKey { + int texIndex = -1; + bool unlit = false; + bool operator==(const SoupKey& o) const noexcept + { + return texIndex == o.texIndex && unlit == o.unlit; + } + }; + struct SoupKeyHash { + size_t operator()(const SoupKey& k) const noexcept + { + const uint64_t u = (static_cast(static_cast(k.texIndex + 0x8000)) << 1u) + | (k.unlit ? 1ull : 0ull); + return static_cast(u); + } + }; + std::unordered_map texToSoup; + auto getSoup = [&](int texIndex, bool unlitFace) -> TexturedSubmeshSoup& { + const SoupKey key{texIndex, unlitFace}; + const auto it = texToSoup.find(key); if (it != texToSoup.end()) return soups[it->second]; TexturedSubmeshSoup s; s.textureIndex = texIndex; + s.unlit = unlitFace; const size_t idx = soups.size(); soups.push_back(std::move(s)); - texToSoup.emplace(texIndex, idx); + texToSoup.emplace(key, idx); return soups[idx]; }; @@ -1634,7 +1684,7 @@ Ogre::MeshPtr importPsyqPlyWithFaceMaterials(const QString& filePath, // Treat textured-with-invalid-index as untextured to avoid silently binding // every malformed face to slot 0; -1 routes the face to the solid bucket. const int submeshKey = (fm.textured && fm.textureIndex >= 0) ? fm.textureIndex : -1; - TexturedSubmeshSoup& soup = getSoup(submeshKey); + TexturedSubmeshSoup& soup = getSoup(submeshKey, fm.unlit); // Psy-Q MAT G/H entries are typically written with 4 RGB triples regardless // of face shape — the Blender RSD exporter pads triangles with a trailing @@ -1700,6 +1750,15 @@ Ogre::MeshPtr importPsyqPlyWithFaceMaterials(const QString& filePath, return outMesh; } +} // namespace + +Ogre::MeshPtr importPsyqPlyWithFaceMaterials(const QString& filePath, + const std::string& meshName, + const QVector& faceMaterials) +{ + return importPsyqPlyWithFaceMaterialsImpl(filePath, meshName, faceMaterials); +} + bool exportPsyqPlyFromEntity(const Ogre::Entity* entity, const QString& plyPath, QVector* outFaceColors, diff --git a/src/PS1/PS1PLY.h b/src/PS1/PS1PLY.h index 220e57e3a..764deea5c 100644 --- a/src/PS1/PS1PLY.h +++ b/src/PS1/PS1PLY.h @@ -13,6 +13,10 @@ The MIT License #include #include + +namespace Ogre { +class Pass; +} #include #include #include @@ -38,6 +42,9 @@ constexpr float kPsyqPlyEditorUniformScale = 1.0f; bool isPsyqPlyFile(const QString& filePath); +/** FFP pass for Psy-Q PLY / RSD materials (`matUnlit` = Blender MAT flag LSB). */ +void configurePsyqRsdMaterialPass(Ogre::Pass* pass, bool hasVertexColour, bool matUnlit); + Ogre::MeshPtr importPsyqPly(const QString& filePath, const std::string& meshName); /** Import with optional per-face colors (size must match face count). */ @@ -53,15 +60,16 @@ struct FaceMaterial { std::array v{}; ///< per-corner V (normalised 0..1, top-origin). QColor color; ///< per-face flat colour fallback (used when no vertex colours are supplied). QVector vertColors; ///< 0, 3 or 4 per-corner colours (in PLY corner order); empty when N/A. + bool unlit = false; ///< Blender MAT flag LSB: no scene lighting (full-bright / PS1 no-light). }; /** * Import with per-face material binding (UVs + texture index + colours). * - * The mesh is split into one submesh per `textureIndex` group, plus one - * submesh for untextured faces. The caller is responsible for binding a - * texture-aware material to each submesh after creation; this routine - * only stores UVs on textured submesh vertices. + * The mesh is split into one submesh per distinct (`textureIndex`, `unlit`) group. + * Material names use suffix `_nl` when the MAT no-light (Blender "Unlit") bit is set. + * The caller binds RSD textures to `_texN` / `_texN_nl` submeshes after import; this + * routine only stores UVs on textured submesh vertices. * * `faceMaterials` length must match the PLY face count. */