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
2 changes: 1 addition & 1 deletion .github/actions/qtmesh/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: 'Run qtmesh CLI for 3D mesh operations (info, convert, fix, anim, s

inputs:
command:
description: 'Subcommand: info, fix, convert, anim, validate, lod, pose, scan'
description: 'Subcommand: info, fix, convert, anim, validate, lod, pose, turntable, scan, material, optimize, …'
required: true
input-file:
description: 'Directory or file to scan (relative to workspace). Defaults to .'
Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ qtmesh anim model.fbx --bake-fps 30 -o uniform.fbx # re-grid every track to
qtmesh anim model.fbx --bake-fps 60 --animation "Run" -o out.fbx # bake one animation at 60 FPS
qtmesh pose model.fbx --animation "Walk" --time 0.5 -o posed.stl # export single frame
qtmesh pose model.fbx --animation "Dance" --count 4 -o pose_%02d.stl # export N evenly spaced frames
qtmesh turntable model.fbx -o turntable.png # PNG sprite sheet (12 frames default)
qtmesh turntable model.fbx -o frame_%02d.png --frames 24 --axis y --camera-height 25
qtmesh validate model.fbx # validate mesh (exit 1 if errors found)
qtmesh validate model.fbx --json # validation results as JSON
qtmesh lod model.fbx --info # show LOD levels
Expand Down Expand Up @@ -85,7 +87,7 @@ qtmesh optimize character.fbx --target-tris 5000 --simplify-rotation-deg-tol 1.0
qtmesh optimize character.fbx --simplify-preset aggressive -o lo.fbx # 1e-2/1°/1e-2 — ~20× key reduction, visible drift
```

CLI mode is activated by: (1) invoking via the `qtmesh` symlink, (2) passing `--cli`, or (3) using a recognized subcommand (`info`, `fix`, `convert`, `anim`, `validate`, `lod`, `pose`, `scan`, `material`, `pack-textures`, `normal-from-height`, `atlas`, `atlas-apply`, `memory`, `analyze`, `vertex-cache`, `decimate`, `optimize`) as the first argument. Use `--verbose` to see Ogre/engine debug output. Use `--no-telemetry` to permanently opt out of anonymous usage data collection.
CLI mode is activated by: (1) invoking via the `qtmesh` symlink, (2) passing `--cli`, or (3) using a recognized subcommand (`info`, `fix`, `convert`, `anim`, `validate`, `lod`, `pose`, `turntable`, `scan`, `material`, `pack-textures`, `normal-from-height`, `atlas`, `atlas-apply`, `memory`, `analyze`, `vertex-cache`, `decimate`, `optimize`) as the first argument. Use `--verbose` to see Ogre/engine debug output. Use `--no-telemetry` to permanently opt out of anonymous usage data collection.

If Xcode SDK is updated, clear CMake cache (`rm build_local/CMakeCache.txt`) and reconfigure.

Expand Down Expand Up @@ -188,7 +190,7 @@ Three singletons manage core state. All run on the main thread. Access via `Clas
### CLI Pipeline

- **CLIPipeline** (`src/CLIPipeline.h/cpp`): Headless command-line interface for mesh operations. All static methods — entry point is `CLIPipeline::run(argc, argv)`.
- Subcommands: `info`, `fix`, `convert`, `anim` (list/rename/merge), `validate`, `lod`, `pose`, `scan`, `material`, `pack-textures`, `normal-from-height`, `memory`, `analyze`, `vertex-cache`, `decimate`, `atlas`, `atlas-apply`, `optimize`.
- Subcommands: `info`, `fix`, `convert`, `anim` (list/rename/merge), `validate`, `lod`, `pose`, `turntable`, `scan`, `material`, `pack-textures`, `normal-from-height`, `memory`, `analyze`, `vertex-cache`, `decimate`, `atlas`, `atlas-apply`, `optimize`.
- Activated via `qtmesh` symlink (created at build time), `--cli` flag, or recognized subcommand as first arg.
- Redirects stdout to stderr (Ogre/Qt noise) and writes CLI output to the original stdout fd. Uses `_exit()` to avoid Ogre static destructor crashes on macOS.
- **AnimationMerger** (`src/AnimationMerger.h/cpp`): Public `renameAnimation()` static method used by both CLI and GUI for animation renaming.
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ branding:

inputs:
command:
description: 'Subcommand: scan, info, validate, convert, fix, anim, lod, pose'
description: 'Subcommand: scan, info, validate, convert, fix, anim, lod, pose, turntable'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the command list in sync with actual CLI coverage.

Line 11 still documents only a subset of supported subcommands, which can mislead action users about what’s valid. Please either list all supported commands or explicitly label this as “common examples”.

📝 Suggested doc update
-    description: 'Subcommand: scan, info, validate, convert, fix, anim, lod, pose, turntable'
+    description: 'Subcommand (examples): scan, info, validate, convert, fix, anim, lod, pose, turntable, material, pack-textures, normal-from-height, atlas, atlas-apply, optimize'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
description: 'Subcommand: scan, info, validate, convert, fix, anim, lod, pose, turntable'
description: 'Subcommand (examples): scan, info, validate, convert, fix, anim, lod, pose, turntable, material, pack-textures, normal-from-height, atlas, atlas-apply, optimize'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` at line 11, The description field in action.yml currently lists
only a subset of subcommands ("Subcommand: scan, info, validate, convert, fix,
anim, lod, pose, turntable") which may be out of sync with the CLI; update the
description value to either enumerate all supported subcommands exactly as
implemented in the CLI or change the text to indicate these are “common
examples” (e.g., "Subcommands (examples): ...") so users aren’t misled—edit the
description entry in action.yml to reflect the full, accurate command set or to
clearly mark it as examples.

required: true
input-file:
description: 'Directory or file to scan (relative to workspace). Defaults to . (workspace root).'
Expand Down
100 changes: 50 additions & 50 deletions src/Assimp/MaterialProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
return materials.size();
}

Ogre::MaterialPtr MaterialProcessor::processMaterial(const aiMaterial *material, const aiScene* scene)

Check warning on line 46 in src/Assimp/MaterialProcessor.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function should be declared "const".

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ5FhHn4wxx8uFGPaG9u&open=AZ5FhHn4wxx8uFGPaG9u&pullRequest=643
{
std::string materialName = material->GetName().C_Str();
if(materialName.empty()) materialName="importedMaterial" + std::to_string(materials.size());
Expand All @@ -57,26 +57,33 @@
Ogre::Pass* xPass = ensureFirstPass(existingMaterial);
if (!xPass) return existingMaterial;

// Normal map (legacy DIFFUSE/HEIGHT/NORMAL_CAMERA path → RTSS).
aiString existingNormalPath;
if(AI_SUCCESS == material->GetTexture(aiTextureType_NORMALS, 0, &existingNormalPath)
|| AI_SUCCESS == material->GetTexture(aiTextureType_HEIGHT, 0, &existingNormalPath)
|| AI_SUCCESS == material->GetTexture(aiTextureType_NORMAL_CAMERA, 0, &existingNormalPath)) {
std::string normalTexPath = existingNormalPath.C_Str();
std::string normalFilename = normalTexPath.substr(normalTexPath.find_last_of("/\\") + 1);
Ogre::TexturePtr normalTexPtr = Ogre::TextureManager::getSingleton().getByName(normalFilename);
if(!normalTexPtr) {
Ogre::String stagedNormalTex;
auto stageNormalFromAssimp = [&](aiTextureType type) -> bool {

Check warning on line 61 in src/Assimp/MaterialProcessor.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the redundant return type of this lambda.

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

Check warning on line 61 in src/Assimp/MaterialProcessor.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This lambda has 22 lines, which is greater than the 20 lines authorized. Split it into several lambdas or functions, or make it a named function.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ5FhHn4wxx8uFGPaG9x&open=AZ5FhHn4wxx8uFGPaG9x&pullRequest=643
aiString path;
if (material->GetTexture(type, 0, &path) != AI_SUCCESS)
return false;
const std::string texPath = path.C_Str();
const std::string filename =
texPath.substr(texPath.find_last_of("/\\") + 1);
if (filename.empty())
return false;
Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton().getByName(filename);
if (!tex) {
try {
normalTexPtr = loadTexture(normalFilename, existingNormalPath, scene);
tex = loadTexture(filename, path, scene);
} catch (...) {
Ogre::LogManager::getSingleton().logMessage("MaterialProcessor: Failed to load normal map '" + normalFilename + "' for existing material '" + materialName + "'");
return false;
}
}
if(normalTexPtr) {
Ogre::LogManager::getSingleton().logMessage("MaterialProcessor: Applying RTSS normal map '" + normalFilename + "' to existing material '" + materialName + "'");
applyRTSSNormalMap(existingMaterial, normalTexPtr->getName());
}
}
if (!tex)
return false;
stagedNormalTex = tex->getName();
return true;
};
if (!stageNormalFromAssimp(aiTextureType_NORMALS))
stageNormalFromAssimp(aiTextureType_NORMAL_CAMERA);
if (stagedNormalTex.empty())
stageNormalFromAssimp(aiTextureType_HEIGHT);

// PBR slots: add any that are missing on the existing material.
// Without this, reimporting an FBX whose material already exists
Expand Down Expand Up @@ -115,13 +122,8 @@
addMissingSlot(aiTextureType_EMISSION_COLOR, "emissive");
addMissingSlot(aiTextureType_BASE_COLOR, "albedo");

// Wire FFP slot operations on the augmented material so the
// newly-added PBR TUS render the same as they would after a
// no-op Apply in the Material Editor. Guarded behind the render-
// system check (lightweight test fixture has Ogre::Root only).
if (Ogre::Root::getSingletonPtr() && Ogre::Root::getSingletonPtr()->getRenderSystem()) {
RTShaderHelper::wirePbrSlotsForFFP(existingMaterial.get());
existingMaterial->compile();
RTShaderHelper::finalizeShaderGenMaterial(existingMaterial, stagedNormalTex);
}

return existingMaterial;
Expand Down Expand Up @@ -178,24 +180,32 @@
// reimport, so this lets a Maya-Stingray-styled
// FBX round-trip its normal map back into our
// RTSS pipeline.
aiString normalPath;
bool hasNormalMap = (AI_SUCCESS == material->GetTexture(aiTextureType_NORMALS, 0, &normalPath))
|| (AI_SUCCESS == material->GetTexture(aiTextureType_HEIGHT, 0, &normalPath))
|| (AI_SUCCESS == material->GetTexture(aiTextureType_NORMAL_CAMERA, 0, &normalPath));
if(hasNormalMap) {
std::string normalTexPath = normalPath.C_Str();
std::string normalFilename = normalTexPath.substr(normalTexPath.find_last_of("/\\") + 1);
Ogre::TexturePtr normalTexPtr = Ogre::TextureManager::getSingleton().getByName(normalFilename);
if(!normalTexPtr) {
Ogre::String stagedNormalTex;
auto stageNormalFromAssimp = [&](aiTextureType type) -> bool {

Check warning on line 184 in src/Assimp/MaterialProcessor.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the redundant return type of this lambda.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ5FhHn4wxx8uFGPaG9y&open=AZ5FhHn4wxx8uFGPaG9y&pullRequest=643
aiString path;

Check warning on line 185 in src/Assimp/MaterialProcessor.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Declaration shadows a local variable "path" in the outer scope.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ5FhHn4wxx8uFGPaG9v&open=AZ5FhHn4wxx8uFGPaG9v&pullRequest=643
if (material->GetTexture(type, 0, &path) != AI_SUCCESS)
return false;
const std::string texPath = path.C_Str();
const std::string filename = texPath.substr(texPath.find_last_of("/\\") + 1);
if (filename.empty())
return false;
Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton().getByName(filename);
if (!tex) {
try {
normalTexPtr = loadTexture(normalFilename, normalPath, scene);
tex = loadTexture(filename, path, scene);
} catch (...) {
Ogre::LogManager::getSingleton().logMessage("MaterialProcessor: Failed to load normal map '" + normalFilename + "'");
return false;
}
}
if(normalTexPtr)
applyRTSSNormalMap(ogreMaterial, normalTexPtr->getName());
}
if (!tex)
return false;
stagedNormalTex = tex->getName();
return true;
};
if (!stageNormalFromAssimp(aiTextureType_NORMALS))
stageNormalFromAssimp(aiTextureType_NORMAL_CAMERA);
if (stagedNormalTex.empty())
stageNormalFromAssimp(aiTextureType_HEIGHT);

// Slice F3: read PBR-specific texture types from Assimp and bind
// them to the slice E canonical slot names so the user can see
Expand Down Expand Up @@ -349,8 +359,7 @@
// subsequent compile both walk paths that segfault on that fixture.
// In real app use the render system is always up by import time.
if (Ogre::Root::getSingletonPtr() && Ogre::Root::getSingletonPtr()->getRenderSystem()) {
RTShaderHelper::wirePbrSlotsForFFP(ogreMaterial.get());
ogreMaterial->compile();
RTShaderHelper::finalizeShaderGenMaterial(ogreMaterial, stagedNormalTex);
}

return ogreMaterial;
Expand Down Expand Up @@ -410,21 +419,12 @@

void MaterialProcessor::applyRTSSNormalMap(Ogre::MaterialPtr mat, const Ogre::String& normalMapName)
{
RTShaderHelper::applyNormalMap(mat, normalMapName);

// Stash the normal-map texture name on the material's first pass user-
// object bindings so the FBX exporter can find it on round-trip. Without
// this, when MaterialProcessor and the export pipeline reach the
// material through different resource-group instances of the same name
// (common when a .material script and an FBX both define the material),
// the export-side `sub->getMaterial()` returns the script-loaded
// instance — which never had the RTSS normal-map TUS added — and the
// normal map is silently dropped on export. The UOB hint survives the
// resource-group disagreement because it's keyed on the *material* name
// and re-applied by the importer on every load. Issue #508.
// Legacy entry point — full RTSS wiring happens in finalizeShaderGenMaterial
// after all PBR slots exist. Keep UOB for export round-trip (#508).
if (mat && mat->getNumTechniques() > 0 && mat->getTechnique(0)->getNumPasses() > 0) {
Ogre::Pass* pass = mat->getTechnique(0)->getPass(0);
pass->getUserObjectBindings().setUserAny(
"qtme.normal_map", Ogre::Any(normalMapName));
}
RTShaderHelper::finalizeShaderGenMaterial(mat, normalMapName);
}
Loading
Loading