Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 54 additions & 21 deletions src/MeshImporterExporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1672,9 +1672,7 @@
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) {
Expand All @@ -1686,6 +1684,12 @@
}
}
}
// 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);
Expand All @@ -1695,10 +1699,9 @@
} 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) {

Check failure on line 1702 in src/MeshImporterExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ4ev9OzZ9ExVh-4SOBy&open=AZ4ev9OzZ9ExVh-4SOBy&pullRequest=500
// 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<PS1PLY::FaceMaterial> faceMats(rsdMatEntries.size());
for (int fi = 0; fi < rsdMatEntries.size(); ++fi) {
const PS1MAT::MatEntry& me = rsdMatEntries[fi];
Expand Down Expand Up @@ -1732,11 +1735,10 @@
} 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)
Expand Down Expand Up @@ -1778,9 +1780,8 @@
}
} else if (useTexturedMatPath) {
// Textured PLY path emits one submesh per texture group with material names
// shaped `PLY/<meshName>_texN` or `PLY/<meshName>_solid`. Bind the matching
// RSD texture slot to each textured submesh's first texture unit; untextured
// submeshes keep their vertex-colour material.
// shaped `PLY/<meshName>_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<Ogre::SubEntity*>(en->getSubEntity(si));
Ogre::MaterialPtr mat = se->getMaterial();
Expand All @@ -1793,33 +1794,27 @@

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<int>(rsdTexSlots.size())
|| rsdTexSlots[slot].resourceName.isEmpty())
continue;

// 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();
}
}
Expand Down Expand Up @@ -2200,6 +2195,7 @@
std::vector<OutTex> rsdOutTextures;
std::unordered_map<int, int> submeshToTexSlot; ///< submeshIndex -> rsd slot.
std::unordered_map<std::string, int> resourceToSlot;
std::unordered_map<int, bool> 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);
Expand Down Expand Up @@ -2253,6 +2249,36 @@
submeshToTexSlot[static_cast<int>(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"))

Check warning on line 2263 in src/MeshImporterExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "texM" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ4fPOetVABA2dd3vr__&open=AZ4fPOetVABA2dd3vr__&pullRequest=500
unlit = true;
else {
const auto solM = kPsyqSolidUnlitSuffix.match(mname);
if (solM.hasMatch() && solM.captured(1) == QStringLiteral("_nl"))

Check failure on line 2267 in src/MeshImporterExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ4fPOetVABA2dd3vsAB&open=AZ4fPOetVABA2dd3vsAB&pullRequest=500
unlit = true;
}
if (!unlit) {
Ogre::MaterialPtr mat = se->getMaterial();
if (!mat.isNull() && mat->getNumTechniques() > 0

Check failure on line 2272 in src/MeshImporterExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ4fPOetVABA2dd3vsAC&open=AZ4fPOetVABA2dd3vsAC&pullRequest=500

Check warning on line 2272 in src/MeshImporterExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'isNull' is deprecated

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ4fPOetVABA2dd3vr_-&open=AZ4fPOetVABA2dd3vr_-&pullRequest=500
&& mat->getTechnique(0)->getNumPasses() > 0) {
const Ogre::Pass* pass = mat->getTechnique(0)->getPass(0);
if (pass && !pass->getLightingEnabled())
unlit = true;
}
}
submeshUnlit[static_cast<int>(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)
Expand Down Expand Up @@ -2296,6 +2322,13 @@
const bool gotCornerColors = eft.hasCornerColors;
const bool smoothShade = gotCornerColors && !cornersUniform(eft);

const int smIdx = (fi < faceTexInfos.size()) ? faceTexInfos[fi].submeshIndex : -1;
if (smIdx >= 0) {

Check warning on line 2326 in src/MeshImporterExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "smIdx" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ4fPOetVABA2dd3vsAA&open=AZ4fPOetVABA2dd3vsAA&pullRequest=500

Check failure on line 2326 in src/MeshImporterExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ4fPOetVABA2dd3vsAD&open=AZ4fPOetVABA2dd3vsAD&pullRequest=500
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;
Expand Down
47 changes: 37 additions & 10 deletions src/PS1/PS1MAT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,27 @@
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();
Expand Down Expand Up @@ -195,7 +209,11 @@
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)
Expand Down Expand Up @@ -230,6 +248,16 @@

} // 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);

Check warning on line 258 in src/PS1/PS1MAT.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested conditional operator into an independent statement.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ4fPOaSVABA2dd3vr_3&open=AZ4fPOaSVABA2dd3vr_3&pullRequest=500
}

bool parseMatFile(const QString& matPath, QVector<MatEntry>& outEntries, QString* outError)
{
outEntries.clear();
Expand Down Expand Up @@ -310,15 +338,15 @@

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) {
Expand Down Expand Up @@ -413,4 +441,3 @@
}

} // namespace PS1MAT

13 changes: 10 additions & 3 deletions src/PS1/PS1MAT.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ The MIT License
* <polyIndex> <flag> <normalMode> <typeChar> <payload...>
*
* 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)
Expand All @@ -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<UV> uvs; ///< 0, 3 or 4 entries (textured polygons only).
Expand All @@ -58,8 +64,9 @@ bool parseMatFile(const QString& matPath, QVector<MatEntry>& outEntries, QString

/// Write a minimal Psy-Q MAT file with one RGB entry per face.
///
/// Entries are serialised as `<idx> 1 <shading> <type> <payload>` matching the
/// canonical Psy-Q / Blender-RSD ASCII layout. Textured entries (`textureIndex >= 0`
/// Entries are serialised as `<idx> <flag> <shading> <type> <payload>` where `<flag>`
/// 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<MatEntry>& entries, QString* outError = nullptr);
Expand Down
Loading
Loading