Skip to content

Turntable PNG export (CLI + renderer) - #643

Merged
fernandotonon merged 14 commits into
masterfrom
feat/issue-294-turntable-png
May 20, 2026
Merged

Turntable PNG export (CLI + renderer)#643
fernandotonon merged 14 commits into
masterfrom
feat/issue-294-turntable-png

Conversation

@fernandotonon

@fernandotonon fernandotonon commented May 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds qtmesh turntable for PNG sprite sheets / frame sequences from OBJ/glb/fbx/etc., with --axis, --json, --columns, framing/lighting fixes, and pivot-camera orbit so vertical meshes frame consistently.
  • Removes redundant RTSS/material mutation during turntable renders so imported normal maps render correctly.
  • Adds unit tests covering ModelTurntableRenderer (parseAxis, composeSpriteSheet, orbit axes, null guards, double shutdown) and CLIPipeline::cmdTurntable integration paths.

Closes #294.

Test plan

  • cmake --build build_local --target UnitTests && ./build_local/bin/UnitTests --gtest_filter='ModelTurntableRenderer*:CLIPipelineCmdTurntable*:CLIPipelineCmdTurntableError*'
  • Manual: qtmesh turntable model.obj -o sheet.png --frames 8 --size 512

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added a turntable CLI command to render model turntables, outputting per-frame PNGs or composed sprite sheets with configurable frames, axis, camera height, resolution, and layout.
  • Documentation

    • CLI docs and action metadata updated to list turntable and its parameters.
  • Tests

    • Added unit and end-to-end tests covering turntable argument validation and rendering outcomes.
  • Bug Fixes

    • Improved normal-map handling so rendered materials avoid duplicate normal contributions and produce more consistent shading.

Review Change Stack

fernandotonon and others added 8 commits May 19, 2026 23:14
Headless Ogre render-to-texture captures orbit frames from imported meshes.
New `qtmesh turntable` writes a sprite sheet or a %02d frame sequence.

Co-authored-by: Cursor <cursoragent@cursor.com>
Qualify ModelTurntableRenderer::shutdown() so ensureRenderTarget compiles.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use white ambient plus directional light, hide Ogre selection bounding
boxes, orbit on a single selectable axis (y/x/z), and accept
--camera-height as an elevation alias.

Co-authored-by: Cursor <cursoragent@cursor.com>
Orbit camera with a stable world-up view matrix instead of lookAt roll,
and render through the ShaderGenerator viewport scheme with per-material
RTSS validation so normal maps shade correctly.

Co-authored-by: Cursor <cursoragent@cursor.com>
Recenter imported meshes at the world origin before capture and compute
orbit distance from all eight bounding-box corners so the model stays
framed in the viewport at any elevation.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use a pivot node so the camera always looks at the orbit point, dedupe
scene-node translation on multi-entity imports, and apply a small
vertical framing bias for upright assets.

Co-authored-by: Cursor <cursoragent@cursor.com>
Import already wires RTSS like the editor; re-applying wirePbrSlotsForFFP
and applyNormalMap could leave the normal map in the FFP texture stack
while RTSS also sampled it. Rely on MSN_SHADERGEN viewport + imported materials.

Co-authored-by: Cursor <cursoragent@cursor.com>
Cover cmdTurntable success paths (sprite sheet, sequence, columns, JSON),
parseAxis edge cases, composeSpriteSheet layouts, axis orbit variants,
and null output rejection.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@fernandotonon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 21 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4e9ab90-52be-49b1-8944-04c7fcea18a2

📥 Commits

Reviewing files that changed from the base of the PR and between 52bca5c and d00bf79.

📒 Files selected for processing (14)
  • .github/actions/qtmesh/action.yml
  • src/Assimp/MaterialProcessor.cpp
  • src/CLIPipeline.cpp
  • src/CLIPipeline_test.cpp
  • src/CMakeLists.txt
  • src/MeshImporterExporter.cpp
  • src/ModelTurntableRenderer.cpp
  • src/ModelTurntableRenderer_test.cpp
  • src/RTShaderHelper.cpp
  • src/RTShaderHelper.h
  • tests/CMakeLists.txt
  • website/src/App.jsx
  • website/src/DocsApp.jsx
  • website/src/data/content.js
📝 Walkthrough

Walkthrough

This PR adds a new turntable CLI subcommand that renders a 3D model to PNG frames or a sprite sheet using headless Ogre render-to-texture. It includes a core turntable renderer with axis-selectable orbiting camera, full CLI integration with argument parsing and file I/O, comprehensive unit tests, and updated documentation.

Changes

Turntable Rendering Feature

Layer / File(s) Summary
Renderer type contract and public API
src/ModelTurntableRenderer.h
Defines TurntableAxis enum (Y/X/Z), TurntableOptions struct with frame count/resolution/elevation/background, and ModelTurntableRenderer static API for rendering frames, managing resources, composing sprite sheets, and parsing axis strings.
Renderer Ogre RTT implementation
src/ModelTurntableRenderer.cpp
Implements headless render-to-texture pipeline: RTT management, entity bounds computation and recentering, orbiting camera with elevation control, temporary turntable lighting, per-frame capture into QImage, and sprite-sheet composition with configurable column layout.
Renderer unit & integration tests
src/ModelTurntableRenderer_test.cpp, src/CLIPipeline_test.cpp
Validates input handling (null/empty entities), dimension and frame-count clamping, successful rendering from generated primitives, axis-specific rendering (X/Y/Z), sprite-sheet composition and layout, parseAxis string parsing, shutdown idempotence, and CLI command error/success cases.
Build configuration for renderer
src/CMakeLists.txt, tests/CMakeLists.txt
Adds ModelTurntableRenderer.cpp and ModelTurntableRenderer.h to main and test build source/header lists so implementation and tests compile.
CLI command declaration
src/CLIPipeline.h
Declares new cmdTurntable(int argc, char* argv[]) static method for rendering PNG frames or a horizontal sprite sheet.
CLI command implementation, dispatch, and tests
src/CLIPipeline.cpp, src/main.cpp, src/CLIPipeline_test.cpp
Implements cmdTurntable with full argument parsing (--output, --frames, --size, --columns, --axis, --elevation/--camera-height, --json), headless Ogre initialization, model loading, renderer invocation, output handling for single/sequence/sprite-sheet PNG modes, JSON/text reporting, updated dispatch in CLIPipeline::run() and main(), help text, and tests for error/success cases.
Normal-map RTSS integration
src/RTShaderHelper.cpp, src/RTShaderHelper.h, src/MeshImporterExporter.cpp, src/Assimp/MaterialProcessor.cpp
Adds helpers to detect canonical normal-map units, mark normal texture units non-FFP, remove duplicate normal-texture units, refresh RTSS normal-map SRS texture index, and calls excludeNormalMapFromFfpChain after applying normal maps in importer/material flows.
Documentation and CI updates
CLAUDE.md, action.yml
Adds qtmesh turntable CLI usage examples and includes turntable in the documented list of recognized subcommands; updates GitHub Actions input documentation to list turntable among supported command values.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • #466: RTShaderHelper-related changes overlap with this PR's RTSS normal-map exclusion and may be relevant to coordinate with.

Possibly related PRs

Poem

🐰 I spin a mesh upon a dish of light,
Snap frames that twinkle dark to bright,
From axis X to Y to Z,
I stitch a sheet for all to see —
A rabbit's view, in PNG delight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.86% 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 directly describes the main change: adding a turntable PNG export feature with both CLI and renderer implementation.
Description check ✅ Passed The description includes summary, technical details, test plan, and closing statement; follows template structure with feature highlights and issue reference.
Linked Issues check ✅ Passed All code changes implement the primary objective from #294 to allow converting 3D models to PNG turntables with configurable axis, layout, and size options.
Out of Scope Changes check ✅ Passed The normal-map FFP chain exclusion in RTShaderHelper and MeshImporterExporter is directly scoped to fixing turntable rendering and is mentioned in PR objectives.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-294-turntable-png

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: d5fdbd9216

ℹ️ 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 thread src/ModelTurntableRenderer.cpp Outdated
restDir = cameraRestOffset(axis, 1.0f, 0.0f);
restDir.normalise();

const Ogre::Real distance = fitOrbitDistance(bounds, pivotPoint, restDir, st.camera, paddingFactor);

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 Compute fit distance using the current orbit angle

placeCameraOnAxis always calls fitOrbitDistance with restDir (the unrotated view direction), so every frame uses a radius sized for angle 0 instead of the actual camera angle. For non-square bounds this underestimates distance on parts of the orbit (for example, Y-axis orbit when Z-extent is larger than X-extent), which crops frames in the generated turntable. The fit calculation should use the rotated per-frame view direction (or precompute the max radius over all sampled angles).

Useful? React with 👍 / 👎.

Comment thread src/CLIPipeline.cpp Outdated
Comment on lines +2918 to +2924
const bool sequenceOutput = outputPath.contains(QLatin1Char('%'));
QStringList writtenPaths;

if (sequenceOutput) {
for (int f = 0; f < frames.size(); ++f) {
char buf[2048];
snprintf(buf, sizeof(buf), outputPath.toUtf8().constData(), f);

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 Validate output pattern before passing it to snprintf

Any % in -o switches to sequence mode, and the raw user string is then used as a snprintf format string. A filename like "100%done.png" (or any unsupported specifier) is treated as a format pattern, producing undefined behavior and potentially failing/crashing instead of writing files. Restrict sequence mode to an explicit validated placeholder (e.g. %0Nd/%d) and escape or reject other % usages.

Useful? React with 👍 / 👎.

Cover --size WxH parsing, --cli flag handling, camera_height alias, null-only
entity lists, and turntable option clamping.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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: 6

🧹 Nitpick comments (1)
src/ModelTurntableRenderer_test.cpp (1)

13-17: ⚡ Quick win

Align fixture prerequisites with Ogre test convention

On Line 15, add ASSERT_TRUE(canLoadMeshFiles()) alongside tryInitOgre() in SetUp() to keep Ogre-dependent CI prerequisites explicit.

Suggested fix
     void SetUp() override
     {
         ASSERT_TRUE(tryInitOgre()) << "Ogre init failed (Xvfb/GL required in CI)";
+        ASSERT_TRUE(canLoadMeshFiles()) << "Required mesh resources are unavailable";
         ModelTurntableRenderer::shutdown();
     }
Based on learnings: In QtMeshEditor tests that depend on Ogre, fixture SetUp must fail loudly in CI by using `ASSERT_TRUE(tryInitOgre())` and `ASSERT_TRUE(canLoadMeshFiles())`.
🤖 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 `@src/ModelTurntableRenderer_test.cpp` around lines 13 - 17, The SetUp fixture
must assert both Ogre initialization and mesh-loading capability: inside the
overridden SetUp() in ModelTurntableRenderer_test.cpp, after calling
ASSERT_TRUE(tryInitOgre()) and before ModelTurntableRenderer::shutdown(), add
ASSERT_TRUE(canLoadMeshFiles()) so tests that depend on Ogre + mesh files fail
loudly in CI; reference functions: SetUp(), tryInitOgre(), canLoadMeshFiles(),
and ModelTurntableRenderer::shutdown().
🤖 Prompt for all review comments with 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.

Inline comments:
In `@action.yml`:
- 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.

In `@src/CLIPipeline.cpp`:
- Around line 2816-2849: Numeric flag parsing currently uses
QString::toInt()/toFloat without validation (e.g., parsing into frameCount,
columns, width, height, elevation, and the sizeArg parts), causing malformed
inputs to coerce to 0; change each conversion to use the ok boolean overload
(QString::toInt(bool *ok) / toFloat(bool *ok)), validate ok and for the "--size"
branch validate both left and right parts (or single value) before assigning,
and if any parse fails print a usage/error and return exit code 2 instead of
silently using 0; update both "--elevation" and ("--camera-height" ||
"--camera_height") handling to validate the float parse the same way.
- Around line 2886-2949: The code logs a generic "cli.turntable" breadcrumb but
doesn't emit file I/O breadcrumbs for the actual model import and image exports;
add SentryReporter::addBreadcrumb("file.import", QString("Import
%1").arg(fi.absoluteFilePath())) immediately before calling
MeshImporterExporter::importer({fi.absoluteFilePath()}) and add
SentryReporter::addBreadcrumb("file.export", QString("Export
%1").arg(framePath)) inside the sequenceOutput loop (before frames.at(f).save),
plus a file.export breadcrumb before the single-image save (before
frames.first().save(outputPath)) and before the sprite-sheet save (before
sheet.save(outputPath)) so Sentry captures the concrete input/output paths for
MeshImporterExporter::importer,
ModelTurntableRenderer::renderToImages/composeSpriteSheet and the image write
operations.

In `@src/ModelTurntableRenderer.cpp`:
- Around line 94-100: The fast-path that returns early when st.renderTarget
exists and st.rttWidth == width && st.rttHeight == height skips refreshing the
render target background; update the code in ModelTurntableRenderer.cpp so
before the early return you call st.renderTarget->setBackgroundColour(bg) (using
the same bg derived from TurntableOptions.background) and, if needed, also
propagate the color to the existing viewport (via
st.renderTarget->getViewport(0)->setBackgroundColour(bg)) so background changes
are applied even on the same-size RTT reuse.
- Around line 407-482: Add Sentry breadcrumbs for the turntable render lifecycle
in ModelTurntableRenderer::renderToImages: call SentryReporter::addBreadcrumb
with an appropriate category (e.g., "turntable") and messages for start
("renderToImages:start" or similar) immediately at the top of the function
(after null checks and before heavy work), for success
("renderToImages:success") just before returning true, and for failures in both
catch blocks ("renderToImages:error" with the exception description for the
Ogre::Exception catch and a generic failure message for the catch-all). Also add
an intermediate breadcrumb if desired inside the main loop after each frame or
before state().renderTarget->update() to indicate progress (e.g.,
"renderToImages:frame:N"). Ensure you reference the existing functions/state
used here (renderToImages, state().renderTarget, placeCameraOnAxis,
readRenderTarget) when inserting the SentryReporter::addBreadcrumb calls.
- Around line 376-404: The cleanup currently only runs when sm is non-null,
leaving cached pointers (st.light, st.lightNode, st.camera, st.cameraNode,
st.pivotNode, st.hasSavedAmbient/st.savedAmbient) unchanged if sceneMgr() is
null; change the logic so any calls that require sm (e.g., sm->setAmbientLight,
detachObject, destroySceneNode, destroyLight, destroyCamera) are guarded by if
(sm) but the cached pointer resets and flag clears always run afterwards—i.e.,
move st.light = nullptr, st.lightNode = nullptr, st.camera = nullptr,
st.cameraNode = nullptr, st.pivotNode = nullptr and st.hasSavedAmbient = false
out of the sm guard so pointers are nulled even when sm is null.

---

Nitpick comments:
In `@src/ModelTurntableRenderer_test.cpp`:
- Around line 13-17: The SetUp fixture must assert both Ogre initialization and
mesh-loading capability: inside the overridden SetUp() in
ModelTurntableRenderer_test.cpp, after calling ASSERT_TRUE(tryInitOgre()) and
before ModelTurntableRenderer::shutdown(), add ASSERT_TRUE(canLoadMeshFiles())
so tests that depend on Ogre + mesh files fail loudly in CI; reference
functions: SetUp(), tryInitOgre(), canLoadMeshFiles(), and
ModelTurntableRenderer::shutdown().
🪄 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: 2277fbe7-e0c8-4465-a7e1-1fb53f366680

📥 Commits

Reviewing files that changed from the base of the PR and between a372ab9 and 10c9db6.

📒 Files selected for processing (11)
  • CLAUDE.md
  • action.yml
  • src/CLIPipeline.cpp
  • src/CLIPipeline.h
  • src/CLIPipeline_test.cpp
  • src/CMakeLists.txt
  • src/ModelTurntableRenderer.cpp
  • src/ModelTurntableRenderer.h
  • src/ModelTurntableRenderer_test.cpp
  • src/main.cpp
  • tests/CMakeLists.txt

Comment thread action.yml
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.

Comment thread src/CLIPipeline.cpp
Comment thread src/CLIPipeline.cpp
Comment thread src/ModelTurntableRenderer.cpp
Comment thread src/ModelTurntableRenderer.cpp
Comment thread src/ModelTurntableRenderer.cpp
Strip duplicate normal-map texture units from the FFP multitexture chain
while keeping SRS_NORMALMAP sampling, and run that sync before turntable
capture. Also resolve normal maps via qtme.normal_map UOB when TUS names
are missing (typical on FBX imports like Jump.fbx).

Co-authored-by: Cursor <cursoragent@cursor.com>

@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: 4

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/CLIPipeline_test.cpp`:
- Around line 1051-1054: The test uses a hardcoded machine-specific path
(jumpPath) and GTEST_SKIP(), making it non-portable; replace the hardcoded
"/home/fernando/Downloads/Jump.fbx" with the repository test fixture path (use
testDataDir() or equivalent to build the path to "Jump.fbx"), remove the
GTEST_SKIP() branch, and assert presence with
ASSERT_TRUE(QFile::exists(jumpPath)) << "Jump.fbx must be present in test data:
" << jumpPath.toStdString(); ensure jumpPath is constructed via
testDataDir().filePath("Jump.fbx") (or equivalent) so the asset is loaded from
the repo testData directory and the test fails loudly on CI.

In `@src/ModelTurntableRenderer_test.cpp`:
- Around line 189-210: This test calls RTShaderHelper::initialize(sceneMgr) but
never restores global RTShaderHelper state; add a teardown to undo that by
calling the RTShaderHelper cleanup API (e.g. RTShaderHelper::finalize() or the
library's corresponding shutdown/uninitialize function) after the assertions —
either append the call at the end of this test or, better, put it in the test
fixture TearDown/AfterEach so RTShaderHelper::initialize and
RTShaderHelper::finalize/uninitialize are always paired and global listener
state is not left installed for subsequent tests.

In `@src/ModelTurntableRenderer.cpp`:
- Around line 328-341: The deduplication uses mat->getName() which conflates
materials from different resource groups; instead deduplicate by the material
object identity. Change processed from std::unordered_set<std::string> to
something like std::unordered_set<const Ogre::Material*> (or uintptr_t) and use
mat.get() (or the raw pointer) as the key when inserting/checking, leaving calls
to RTShaderHelper::excludeNormalMapFromFfpChain(mat) and
RTShaderHelper::wirePbrSlotsForFFP(mat.get()) unchanged so each distinct
Material instance is processed exactly once.

In `@src/RTShaderHelper.cpp`:
- Around line 335-345: The loop that intends to remove duplicate texture units
is decrementing canonicalIdx whenever it encounters a non-albedo unit before
canonicalIdx, even if no removal occurred; fix by only decrementing canonicalIdx
when you actually remove a texture unit. In the loop in RTShaderHelper.cpp where
you iterate texture units (the block using pass->getTextureUnitState,
isAlbedoSlotName, and pass->removeTextureUnitState), change the logic so you
check if tus->getTextureName() == canonicalTex, then call
pass->removeTextureUnitState(...) and only after that, if i < canonicalIdx,
decrement canonicalIdx; do not decrement canonicalIdx in any other branch.
🪄 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: 4cc192c3-54c1-42dc-8cb1-450e01ad7fb5

📥 Commits

Reviewing files that changed from the base of the PR and between 10c9db6 and 52bca5c.

📒 Files selected for processing (7)
  • src/Assimp/MaterialProcessor.cpp
  • src/CLIPipeline_test.cpp
  • src/MeshImporterExporter.cpp
  • src/ModelTurntableRenderer.cpp
  • src/ModelTurntableRenderer_test.cpp
  • src/RTShaderHelper.cpp
  • src/RTShaderHelper.h

Comment thread src/CLIPipeline_test.cpp Outdated
Comment thread src/ModelTurntableRenderer_test.cpp
Comment thread src/ModelTurntableRenderer.cpp Outdated
Comment thread src/RTShaderHelper.cpp
fernandotonon and others added 4 commits May 20, 2026 08:52
Defer ShaderGenerator wiring until all PBR texture units exist, remove
extra Bump/NormalMap units, dedupe diffuse+albedo FFP modulation, and
rebuild the shader technique on turntable capture (Jump.fbx and similar).

Co-authored-by: Cursor <cursoragent@cursor.com>
Load Ogre resources after the headless render window so turntable matches
the GUI normal-map path. Harden CLI flag parsing, sequence output patterns,
orbit framing per angle, and CI tests (no skipped Jump.fbx fixture).

Co-authored-by: Cursor <cursoragent@cursor.com>
Add turntable to the CLI reference, quick start, Docker examples, and the
landing-page pipeline tab with usage for sprite sheets and frame sequences.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fernandotonon
fernandotonon merged commit 3a588c9 into master May 20, 2026
13 checks passed
@fernandotonon
fernandotonon deleted the feat/issue-294-turntable-png branch May 20, 2026 19:44
@sonarqubecloud

Copy link
Copy Markdown

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.

Allow converting the 3D model to PNG turntable

1 participant