Skip to content

FBX export: embed textures (no sidecar .material/.png) - #311

Merged
fernandotonon merged 4 commits into
masterfrom
feat/fbx-embed-textures
Apr 25, 2026
Merged

FBX export: embed textures (no sidecar .material/.png)#311
fernandotonon merged 4 commits into
masterfrom
feat/fbx-embed-textures

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Apr 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Embed texture payloads directly into exported FBX via Video.Content so exports are single-file (Mixamo-style).
  • Improve texture resource resolution by scanning Ogre resource groups.
  • Stop emitting sidecar .material and extracted texture images for FBX exports.

Test plan

  • Run: QtMeshEditor --cli convert <textured_asset> -o out.fbx and verify no .material/.png sidecars are created.
  • Verify FBX contains Objects/Video/Content (raw R payload).
  • Run: UnitTests --gtest_filter=FBXExporter*.

Summary by CodeRabbit

  • New Features

    • FBX exports now embed texture data directly within the FBX file, eliminating separate texture files and material sidecars for FBX outputs.
  • Tests

    • Added tests to verify embedded texture payloads are present, non-empty, and correctly formatted in FBX exports.

Export Video.Content as raw bytes (FBX 'R') so textures are embedded inside
the .fbx, matching Mixamo-style single-file exports. The exporter now
resolves texture resources by scanning resource groups.

Tests: provide a real PNG resource for coverage and assert Video.Content
exists; also guard coverage tests behind canLoadMeshFiles() to avoid
segfaults when no GL context is available.

Made-with: Cursor
FBX exports should be single-file when textures are embedded. Skip
exportMaterial()/exportTextures sidecar dumping for FBX in the entity export
path, the Assimp export path, and pose export path.

Made-with: Cursor
@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds conditional embedding of texture payloads into FBX Video objects by resolving Ogre texture resources to streams, reading bytes into vectors, emitting Video.Content with raw payloads; updates tests to create and assert embedded content; prevents material/texture sidecar emission for FBX exports.

Changes

Cohort / File(s) Summary
FBX Texture Embedding
src/FBX/FBXExporter.cpp
Adds helper to locate and open Ogre texture resources (by group search), read streams into std::vector<uint8_t> (using reported size or chunked reads), and emit Video -> Content with an R property containing raw bytes when available.
Tests: Embedded Video Content
src/FBX/FBXExporter_test.cpp
Creates a real PNG in a temp resource group, initializes only that group, and extends TextureAndVideo test to assert presence, type ('R'), and non-empty payload of exported Video.Content.
Export Behavior Refinement
src/MeshImporterExporter.cpp
Suppresses exportMaterial sidecar emission for the dedicated FBXExporter::exportFBX() path (treat FBX as single-file/embedded); exportMaterial still runs for non-FBX/Assimp paths; return semantics adjusted to -1 on failure.

Sequence Diagram

sequenceDiagram
    participant Exporter as FBXExporter
    participant OgreRM as Ogre Resource Manager
    participant Stream as Resource Stream
    participant FBX as FBX Video Object

    Exporter->>OgreRM: resolve texture name (preferred group → default → all groups)
    OgreRM-->>Exporter: DataStreamPtr (or not found)
    Exporter->>Stream: read bytes (use size or chunked reads)
    Stream-->>Exporter: byte[] payload
    Exporter->>FBX: create Video node
    Exporter->>FBX: emit Content child with property 'R' and payload
    FBX-->>Exporter: Video.Content embedded
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I found the bytes beneath the skin,
Popped them into Video, tucked them in.
No stray sidecars on the grass today—
One file hops off happily away. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: embedding textures in FBX exports and eliminating sidecar material/PNG files.
Description check ✅ Passed The description includes a clear summary of changes and a detailed test plan, covering the three main objectives and how to verify them.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fbx-embed-textures

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b42410863

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 1169 to 1173
bool ok = FBXExporter::exportFBX(e, _uri);
if (ok)
exportMaterial(e, file);
else
// FBXExporter embeds textures (Video.Content) so avoid emitting sidecar
// .material and extracted image files next to the FBX.
if (!ok)
return -1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore texture fallback when FBX embedding is unavailable

This branch now suppresses exportMaterial for every FBX export, but FBXExporter only embeds Video.Content when readOgreResourceBytes can reopen the texture via ResourceGroupManager. Textures created from embedded imports are loaded in-memory (MaterialProcessor::loadTexture uses TextureManager::loadImage/loadRawData) and are not guaranteed to be reopenable as file resources, so Content can be omitted while sidecar images are no longer written. In that case the FBX still references texture names but ships no texture data, causing missing textures on re-import.

Useful? React with 👍 / 👎.

Comment on lines +1507 to +1508
if (fmt != "FBX Binary (*.fbx)")
exportMaterial(entity, file);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep texture export for current-pose FBX path

In exportCurrentPose, FBX output goes through Assimp and this function builds materials with texture path properties only; it does not populate embedded aiTexture payloads. Skipping exportMaterial for fmt == "FBX Binary (*.fbx)" therefore removes the only code path that writes texture files for this export mode, so pose FBX exports can lose textures unless matching files already exist beside the output.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/FBX/FBXExporter_test.cpp (1)

474-500: Assert the embedded payload bytes, not just that Content is non-empty.

Right now this still passes if the exporter resolves a different diffuse_tex.png from another resource group. Comparing content->properties[0].stringVal against the png bytes written in SetUp() would make the test catch wrong-resource regressions.

Also applies to: 1921-1926

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FBX/FBXExporter_test.cpp` around lines 474 - 500, The test currently only
checks that the exported Video.Content is non-empty, which can pass if a
different texture is picked up; modify the FBXExporter_test (the block that
writes the 1x1 PNG into texPath using the png QByteArray) to assert that the
embedded payload equals the exact png bytes written by SetUp(), e.g. compare
content->properties[0].stringVal (or the Video.Content buffer extracted from the
exporter result) against the original png QByteArray, and add the same exact
assertion to the other failing location referenced (around lines 1921-1926) so
the test fails if a wrong resource is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/FBX/FBXExporter.cpp`:
- Around line 1689-1697: When embedding texture bytes via
readOgreResourceBytes(texName) in FBXExporter (the block that calls
m_w.beginNode("Content") / m_w.writePropertyR(bytes)), do not silently continue
when bytes.empty(): call SentryReporter::addBreadcrumb with category
"file.export" (and a message like "Missing texture payload for <texName> during
FBX export") to record the missing Video.Content, and then surface failure by
returning/propagating an export error (e.g., set/export failure flag or return
false / throw an ExportException from the surrounding export function) so the
caller knows the texture was not embedded; ensure the breadcrumb message
includes texName and any context so downstream code (and logs) can correlate the
missing payload.
- Around line 1587-1608: The helper readOgreResourceBytes currently searches all
resource groups by bare resourceName and can return the wrong file when multiple
groups have the same filename; change it to accept an optional preferredGroup
parameter (or overload) and first try rgm.resourceExists(preferredGroup,
resourceName) / rgm.openResource(resourceName, preferredGroup), then fallback to
DEFAULT_RESOURCE_GROUP_NAME, and only then scan all groups; update the caller in
writeTextureObjects to pass the bound material/texture group (e.g. use
mat->getGroup() or the loaded texture resource's getGroup()) so the exporter
resolves bytes from the correct resource before falling back to a global scan.

In `@src/MeshImporterExporter.cpp`:
- Around line 1504-1508: The pose-FBX export path currently skips
exportMaterial() when fmt == "FBX Binary (*.fbx)", which removes the only
exported texture image files because exportCurrentPose() uses Assimp and only
writes texture references; restore texture file export by ensuring
exportMaterial(entity, file) (or at least exportTextures()) is called for FBX in
MeshImporterExporter.cpp so referenced images are written alongside the FBX
(e.g., re-enable exportMaterial in the block that currently checks fmt, or call
a new exportTextures(entity, file) from exportCurrentPose/exportMaterial),
keeping exportCurrentPose(), exportMaterial(), and exportTextures() references
intact so pose-FBXs reopen with textures until a true embedded-texture path is
implemented.

---

Nitpick comments:
In `@src/FBX/FBXExporter_test.cpp`:
- Around line 474-500: The test currently only checks that the exported
Video.Content is non-empty, which can pass if a different texture is picked up;
modify the FBXExporter_test (the block that writes the 1x1 PNG into texPath
using the png QByteArray) to assert that the embedded payload equals the exact
png bytes written by SetUp(), e.g. compare content->properties[0].stringVal (or
the Video.Content buffer extracted from the exporter result) against the
original png QByteArray, and add the same exact assertion to the other failing
location referenced (around lines 1921-1926) so the test fails if a wrong
resource is used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec8fe20f-e5b9-4c49-b165-f11e8dc36c9e

📥 Commits

Reviewing files that changed from the base of the PR and between 6c2f858 and 6b42410.

📒 Files selected for processing (3)
  • src/FBX/FBXExporter.cpp
  • src/FBX/FBXExporter_test.cpp
  • src/MeshImporterExporter.cpp

Comment thread src/FBX/FBXExporter.cpp Outdated
Comment thread src/FBX/FBXExporter.cpp
Comment on lines +1689 to +1697
// Embed texture bytes when the resource is available. Many tools (e.g. Mixamo exports)
// expect texture payloads to be embedded via Video.Content.
const auto bytes = readOgreResourceBytes(texName);
if (!bytes.empty()) {
m_w.beginNode("Content");
m_w.writePropertyR(bytes);
m_w.endProperties();
m_w.endNodeLeaf();
}

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 | 🟠 Major

Don't silently drop Video.Content when embedding fails.

If bytes is empty here, the FBX still exports but the texture payload disappears. Since src/MeshImporterExporter.cpp now skips FBX sidecars, that turns a textured export into an untextured one with no signal to the caller. Please surface this as an export failure or at least log/capture the missing texture instead of silently continuing.
As per coding guidelines: Track all user-facing actions and significant operations with SentryReporter::addBreadcrumb(category, message). Use ui.action for toolbar/menu clicks, ai.tool_call for MCP tool invocations, file.import/file.export for I/O operations

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FBX/FBXExporter.cpp` around lines 1689 - 1697, When embedding texture
bytes via readOgreResourceBytes(texName) in FBXExporter (the block that calls
m_w.beginNode("Content") / m_w.writePropertyR(bytes)), do not silently continue
when bytes.empty(): call SentryReporter::addBreadcrumb with category
"file.export" (and a message like "Missing texture payload for <texName> during
FBX export") to record the missing Video.Content, and then surface failure by
returning/propagating an export error (e.g., set/export failure flag or return
false / throw an ExportException from the surrounding export function) so the
caller knows the texture was not embedded; ensure the breadcrumb message
includes texName and any context so downstream code (and logs) can correlate the
missing payload.

Comment on lines +1504 to +1508
// For FBX, prefer a single-file export. Assimp FBX export may still
// reference textures by name, but our current expectation for FBX is
// "no sidecar dumps" (materials/textures are embedded or handled by importer).
if (fmt != "FBX Binary (*.fbx)")
exportMaterial(entity, file);

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 | 🟠 Major

Keep texture export for pose FBX until this path really embeds textures.

exportCurrentPose() still routes FBX through Assimp (Lines 1468-1470), and that path only writes texture references into the FBX material data. Skipping exportMaterial() here removes the only exported texture files, so textured pose FBXs will reopen without their textures.

Minimal fix
-                if (fmt != "FBX Binary (*.fbx)")
-                    exportMaterial(entity, file);
+                exportMaterial(entity, file);

A cleaner follow-up would be to split exportTextures() from .material emission so pose-FBX can keep referenced image files without reintroducing an unnecessary .material sidecar.

Based on learnings: MeshImporterExporter (src/MeshImporterExporter.h/cpp) provides static methods for .mesh, .obj, .dae, .gltf, .fbx via Assimp. Provides sceneExporter()/sceneImporter() for saving/loading entire scenes. Multi-entity scenes use entity-name-prefixed bones to avoid cross-entity skeleton contamination
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MeshImporterExporter.cpp` around lines 1504 - 1508, The pose-FBX export
path currently skips exportMaterial() when fmt == "FBX Binary (*.fbx)", which
removes the only exported texture image files because exportCurrentPose() uses
Assimp and only writes texture references; restore texture file export by
ensuring exportMaterial(entity, file) (or at least exportTextures()) is called
for FBX in MeshImporterExporter.cpp so referenced images are written alongside
the FBX (e.g., re-enable exportMaterial in the block that currently checks fmt,
or call a new exportTextures(entity, file) from
exportCurrentPose/exportMaterial), keeping exportCurrentPose(),
exportMaterial(), and exportTextures() references intact so pose-FBXs reopen
with textures until a true embedded-texture path is implemented.

Refactor readOgreResourceBytes() to reduce nesting, avoid deprecated
DataStreamPtr::isNull(), and catch specific exceptions to satisfy SonarCloud
maintainability on new code.

Made-with: Cursor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/FBX/FBXExporter.cpp (1)

1699-1706: ⚠️ Potential issue | 🟠 Major

Don't silently skip Video.Content on missing payload — surface the failure.

When readOgreResourceBytes(texName) returns empty, the Content node is omitted but the FBX still completes successfully. Since src/MeshImporterExporter.cpp now skips sidecar .material/image emission for the FBX path on the assumption that this exporter embeds textures, a texture that can't be resolved (e.g., procedural/SD-generated textures registered as GPU textures only, or assets loaded outside a resource group) produces an FBX with neither the embedded payload nor a sidecar — a textured mesh silently exports as untextured with no signal to the caller.

At minimum, log a warning and add a Sentry breadcrumb so the regression is observable; ideally, treat persistent failures as an export error or fall back to extracting bytes from Ogre::TexturePtr::convertToImage() for the bound TextureUnitState before giving up.

🛡️ Suggested direction
-                if (const auto bytes = readOgreResourceBytes(texName); !bytes.empty()) {
-                    m_w.beginNode("Content");
-                    m_w.writePropertyR(bytes);
-                    m_w.endProperties();
-                    m_w.endNodeLeaf();
-                }
+                const auto bytes = readOgreResourceBytes(texName);
+                if (!bytes.empty()) {
+                    m_w.beginNode("Content");
+                    m_w.writePropertyR(bytes);
+                    m_w.endProperties();
+                    m_w.endNodeLeaf();
+                } else {
+                    Ogre::LogManager::getSingleton().logWarning(
+                        "FBXExporter: missing embedded payload for texture '" + texName +
+                        "' — exported FBX will reference '" + texName + "' but contain no Video.Content.");
+                    SentryReporter::addBreadcrumb("file.export",
+                        "Missing FBX Video.Content for texture: " + QString::fromStdString(texName));
+                    // Optionally: fall back to tus->_getTexturePtr()->convertToImage() and encode to PNG bytes here.
+                }

As per coding guidelines: "Track all user-facing actions and significant operations with SentryReporter::addBreadcrumb(category, message). Use … file.import/file.export for I/O operations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FBX/FBXExporter.cpp` around lines 1699 - 1706, The code currently omits
the Video.Content node when readOgreResourceBytes(texName) returns empty —
change this to (1) log a warning and call
SentryReporter::addBreadcrumb("file.export", formatted message including
texName) whenever bytes.empty() is true so missing payloads are observable, (2)
attempt a fallback by resolving the TextureUnitState / Ogre::TexturePtr bound to
that texName and calling convertToImage() to extract bytes to embed before
giving up, and (3) if the fallback also fails, propagate an export failure
(return/throw an error) instead of silently continuing; edit the block around
readOgreResourceBytes(texName), the m_w.beginNode("Content") emission, and add
the SentryReporter::addBreadcrumb and fallback logic so
MeshImporterExporter.cpp’s assumption that FBX exports embed textures remains
valid.
🧹 Nitpick comments (1)
src/FBX/FBXExporter.cpp (1)

1638-1642: Consider logging a warning when an exception swallows a resource read.

Both catch blocks return an empty vector with no log entry, so a transient I/O error or a corrupt resource in Ogre::DataStream becomes indistinguishable from "resource not found". Combined with the silent-skip at the call site (lines 1701-1706), an FBX with a real read error will export as if everything succeeded. At minimum, log via Ogre::LogManager so the failure mode is observable.

♻️ Suggested change
-        } catch (const Ogre::Exception&) {
-            return {};
-        } catch (const std::exception&) {
-            return {};
-        }
+        } catch (const Ogre::Exception& ex) {
+            Ogre::LogManager::getSingleton().logWarning(
+                "FBXExporter: failed to read embedded resource '" + resourceName + "': " + ex.what());
+            return {};
+        } catch (const std::exception& ex) {
+            Ogre::LogManager::getSingleton().logWarning(
+                "FBXExporter: failed to read embedded resource '" + resourceName + "': " + ex.what());
+            return {};
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FBX/FBXExporter.cpp` around lines 1638 - 1642, The two catch blocks that
currently swallow exceptions (catch (const Ogre::Exception&) and catch (const
std::exception&)) should capture the exception by name and log a warning/error
via Ogre::LogManager before returning an empty vector; for example, change to
catch (const Ogre::Exception& e) and use e.getFullDescription() and catch (const
std::exception& e) and use e.what(), and call
Ogre::LogManager::getSingleton().logMessage(...) (include a brief context string
like "FBXExporter: failed to read resource" plus the exception text) so the
transient I/O/corruption errors are observable while keeping the existing return
{} behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/FBX/FBXExporter.cpp`:
- Around line 1699-1706: The code currently omits the Video.Content node when
readOgreResourceBytes(texName) returns empty — change this to (1) log a warning
and call SentryReporter::addBreadcrumb("file.export", formatted message
including texName) whenever bytes.empty() is true so missing payloads are
observable, (2) attempt a fallback by resolving the TextureUnitState /
Ogre::TexturePtr bound to that texName and calling convertToImage() to extract
bytes to embed before giving up, and (3) if the fallback also fails, propagate
an export failure (return/throw an error) instead of silently continuing; edit
the block around readOgreResourceBytes(texName), the m_w.beginNode("Content")
emission, and add the SentryReporter::addBreadcrumb and fallback logic so
MeshImporterExporter.cpp’s assumption that FBX exports embed textures remains
valid.

---

Nitpick comments:
In `@src/FBX/FBXExporter.cpp`:
- Around line 1638-1642: The two catch blocks that currently swallow exceptions
(catch (const Ogre::Exception&) and catch (const std::exception&)) should
capture the exception by name and log a warning/error via Ogre::LogManager
before returning an empty vector; for example, change to catch (const
Ogre::Exception& e) and use e.getFullDescription() and catch (const
std::exception& e) and use e.what(), and call
Ogre::LogManager::getSingleton().logMessage(...) (include a brief context string
like "FBXExporter: failed to read resource" plus the exception text) so the
transient I/O/corruption errors are observable while keeping the existing return
{} behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9834e518-7f12-4143-965a-96ef387c54a4

📥 Commits

Reviewing files that changed from the base of the PR and between 6b42410 and 1ae030e.

📒 Files selected for processing (1)
  • src/FBX/FBXExporter.cpp

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit ba60ae4 into master Apr 25, 2026
19 checks passed
@fernandotonon
fernandotonon deleted the feat/fbx-embed-textures branch April 25, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant