Skip to content

fix: PBR generation renders the surface too dark (roughness/metallic FFP wiring) - #795

Merged
fernandotonon merged 4 commits into
masterfrom
fix/pbr-rtss-dark
Jul 4, 2026
Merged

fix: PBR generation renders the surface too dark (roughness/metallic FFP wiring)#795
fernandotonon merged 4 commits into
masterfrom
fix/pbr-rtss-dark

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Generating PBR maps (Material Editor "Generate PBR maps" or the image-to-3D PBR step) made the model render dark — "like the lights are off on the model," while other meshes lit normally. Export→reload of the same mesh looked correct, which localized both bugs to the live in-session material wiring.

Two independent root causes, both fixed:

1. Roughness/metallic darkened the diffuse (FFP wiring)

The FFP fallback wired roughness as LBX_MODULATE_X2 and metallic as LBX_ADD_SIGNED against the running colour — but those are BRDF specular-lobe inputs, not colour channels. A typical generated roughness map (mean ≈ 0.44) multiplied the visible colour by roughness × 2 ≈ 0.87, tinting the surface darker. Fix: make roughness/metallic inert in the FFP path (LBX_SOURCE1, non-FFP), like normal maps; the real contribution comes from the Cook-Torrance SRS when PBR-tagged with IBL. (RTShaderHelper::wirePbrSlotsForFFP + MaterialPresetLibrary::configurePbrSlots.)

2. RTSS normal map without tangents → unlit surface (the main "lights off" symptom)

The live paths wired the RTSS SRS_NORMALMAP sub-render-state on a mesh with no per-vertex tangents. Ogre's normal-map shader needs tangents to build the tangent-space basis; without them the basis is degenerate, the perturbed normal collapses, N·L → ~0, and the surface renders as if unlit. The mesh importer already builds tangents before wiring normal maps (applyNormalMapsToEntity, "Build tangent vectors … required by RTSS normal mapping") — which is exactly why export→reload looked fine but live generation didn't. Fix: route both live paths through that same tested importer routine:

  • image-to-3D (MeshGenBuilder): defer normal-map wiring until the entity exists, then call applyNormalMapsToEntity (builds tangents when UVs exist, then wires SRS_NORMALMAP).
  • Material Editor (generatePbrFromDiffuse): after binding the maps, run applyNormalMapsToEntity on every scene entity using the material.

Verification

  • Roughness tint: lit-face brightness 126 → 150 on the live bind path; regression test asserts roughness/metallic resolve to LBX_SOURCE1.
  • Normal-map/tangent darkening: reproduced on a primitive sphere (brightness 104 → 81 after the old raw applyNormalMap); after the fix the image-to-3D generated mesh lights the same as an export→reload (verified in the viewport).

Paths covered

Both wirePbrSlotsForFFP (Material Editor / image-to-3D / CLI / MCP) and applyNormalMapsToEntity routing (the two live viewport paths). Export/CLI/MCP paths were already correct.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed PBR metallic and roughness inputs so they no longer darken the material in fixed-function fallback mode.
    • Improved normal map handling for generated and imported materials so lighting appears correctly on live meshes.
  • Tests

    • Added regression coverage for the updated PBR fallback behavior.

Generating PBR maps in the Material Editor, or the image-to-3D PBR step,
made the surface render "darkish, like in shadow". The exported+reloaded
material looked fine, which localized the bug to the LIVE bind path.

Root cause: the FFP fallback wired the roughness map as LBX_MODULATE_X2 and
the metallic map as LBX_ADD_SIGNED against the running colour. Roughness and
metallic are BRDF specular-lobe inputs, NOT colour channels — multiplying a
map straight into the diffuse tints the whole surface. A typical generated
roughness map has mean ~0.44, and MODULATE_X2 multiplies the visible colour
by roughness*2 ≈ 0.87 (darker still in rough regions), i.e. the surface is
literally multiplied by the roughness map. Measured on the live bind path:
lit-face brightness 126 (before) → 150 (after), ~19% recovered.

Fix: in the FFP fallback make the roughness and metallic units inert — mark
them non-FFP and pass the current colour through unchanged (LBX_SOURCE1),
the same treatment normal maps already get. The real metal-roughness BRDF
still comes from applyPbrIfTagged's Cook-Torrance SRS when the material is
PBR-tagged and IBL is present. Applied in both copies of the wiring
(RTShaderHelper::wirePbrSlotsForFFP and MaterialPresetLibrary's
configurePbrSlots) plus the stale doc comment in MaterialEditorQML.

Adds a regression test asserting roughness/metallic resolve to LBX_SOURCE1
(inert) and albedo stays LBX_MODULATE after wirePbrSlotsForFFP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fernandotonon, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af3eb626-80da-4a53-accb-226803dd4b6c

📥 Commits

Reviewing files that changed from the base of the PR and between bd904ac and 6e6b65f.

📒 Files selected for processing (1)
  • src/MaterialPresetLibrary.cpp
📝 Walkthrough

Walkthrough

Metallic and roughness PBR texture-unit handling changes from FFP color modification to passthrough behavior, with updated comments and regression coverage. Normal-map application is moved to entity-based wiring for generated and edited materials.

Changes

PBR FFP passthrough fix

Layer / File(s) Summary
FFP passthrough for metallic and roughness
src/MaterialPresetLibrary.cpp, src/RTShaderHelper.cpp
Both PBR slot handlers mark metallic and roughness as non-FFP and switch their texture-unit color ops to current-color passthrough instead of the prior ADD_SIGNED/MODULATE_X2 approximations.
Comment and regression coverage
src/MaterialEditorQML.cpp, src/RTShaderHelper_syncMaterial_test.cpp
The PBR slot comments now describe metallic and roughness as inert inputs, and the sync-material regression test expects both slots to use LBX_SOURCE1 instead of the former darkening operations.
Deferred normal-map wiring on live entities
src/ImageTo3D/MeshGenBuilder.cpp, src/MaterialEditorQML.cpp
Normal maps are applied after an entity exists by calling MeshImporterExporter::applyNormalMapsToEntity, with a fallback to RTShaderHelper::applyNormalMap when no matching entity is found.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the PR's main fix: PBR surfaces rendering too dark due to roughness/metallic FFP wiring.
Description check ✅ Passed The description covers the bug, root causes, fixes, verification, and affected paths, though it doesn't follow the template headings exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pbr-rtss-dark

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.

@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: 660a92700d

ℹ️ 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/MaterialPresetLibrary.cpp Outdated
Ogre::LBS_TEXTURE,
Ogre::LBS_CURRENT);
} else if (n == "roughness") {
} else if (n == "metallic" || n == "roughness") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve specular-glossiness slot wiring

When the user applies the Specular-Glossiness preset, these same canonical slots are reused as specular/glossiness inputs, and applyPbrIfTagged explicitly leaves that workflow on the slice-E FFP path rather than attaching Cook-Torrance. This unconditional metallic/roughness branch therefore makes specular/glossiness maps inert too, so adding those maps to a spec-gloss material no longer affects the render. Gate the inert behavior to metallic-roughness materials or use distinct handling for the specular-glossiness workflow.

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.

🧹 Nitpick comments (3)
src/RTShaderHelper.cpp (1)

810-810: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Helper name no longer matches its usage.

markNormalUnitNonFfp is now also called for metallic/roughness, not just normal maps. Consider a more generic name (e.g. markUnitInertNonFfp) so future readers aren't misled about its scope.

Also applies to: 845-845

🤖 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/RTShaderHelper.cpp` at line 810, The helper name is too specific for its
expanded use, since markNormalUnitNonFfp is now applied to metallic/roughness
units as well as normal maps. Rename the helper to a more generic symbol such as
markUnitInertNonFfp, and update all call sites in RTShaderHelper.cpp so the name
matches its broader behavior and future readers are not misled.
src/MaterialPresetLibrary.cpp (1)

90-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider centralizing this duplicated wiring logic.

The exact same non-FFP/passthrough sequence is now duplicated between here and RTShaderHelper.cpp's markNormalUnitNonFfp. The comment even calls out that this mirrors the other file. This duplication is exactly why the same darkening bug had to be fixed in two places for this PR — extracting a shared helper (exposed via RTShaderHelper's header) would prevent future drift.

🤖 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/MaterialPresetLibrary.cpp` around lines 90 - 101, The non-FFP passthrough
setup for metallic/roughness is duplicated here and in
RTShaderHelper::markNormalUnitNonFfp, so factor the shared wiring into one
helper instead of keeping two copies. Move the _markNonFFP and
setColourOperationEx sequence into a reusable RTShaderHelper function exposed
through its header, then call that helper from MaterialPresetLibrary::... and
the existing RTShaderHelper path. Keep the behavior identical, just centralize
the logic to avoid future drift.
src/RTShaderHelper_syncMaterial_test.cpp (1)

74-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No equivalent regression test for MaterialPresetLibrary::configurePbrSlots.

This test only covers RTShaderHelper::wirePbrSlotsForFFP. Since MaterialPresetLibrary.cpp duplicates the identical fix, an analogous assertion there would guard against the two implementations drifting apart again.

🤖 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/RTShaderHelper_syncMaterial_test.cpp` around lines 74 - 98, The
regression coverage only exercises RTShaderHelper::wirePbrSlotsForFFP, but
MaterialPresetLibrary::configurePbrSlots contains the same PBR slot logic and
could drift independently. Add an equivalent test alongside
PbrFfpRoughnessMetallicDoNotDarkenDiffuse that creates a material, configures
albedo/roughness/metallic texture units, calls
MaterialPresetLibrary::configurePbrSlots, and asserts the same blend modes so
both implementations stay aligned.
🤖 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.

Nitpick comments:
In `@src/MaterialPresetLibrary.cpp`:
- Around line 90-101: The non-FFP passthrough setup for metallic/roughness is
duplicated here and in RTShaderHelper::markNormalUnitNonFfp, so factor the
shared wiring into one helper instead of keeping two copies. Move the
_markNonFFP and setColourOperationEx sequence into a reusable RTShaderHelper
function exposed through its header, then call that helper from
MaterialPresetLibrary::... and the existing RTShaderHelper path. Keep the
behavior identical, just centralize the logic to avoid future drift.

In `@src/RTShaderHelper_syncMaterial_test.cpp`:
- Around line 74-98: The regression coverage only exercises
RTShaderHelper::wirePbrSlotsForFFP, but MaterialPresetLibrary::configurePbrSlots
contains the same PBR slot logic and could drift independently. Add an
equivalent test alongside PbrFfpRoughnessMetallicDoNotDarkenDiffuse that creates
a material, configures albedo/roughness/metallic texture units, calls
MaterialPresetLibrary::configurePbrSlots, and asserts the same blend modes so
both implementations stay aligned.

In `@src/RTShaderHelper.cpp`:
- Line 810: The helper name is too specific for its expanded use, since
markNormalUnitNonFfp is now applied to metallic/roughness units as well as
normal maps. Rename the helper to a more generic symbol such as
markUnitInertNonFfp, and update all call sites in RTShaderHelper.cpp so the name
matches its broader behavior and future readers are not misled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 791fa3e2-415f-49fe-a5bb-41cfc7597515

📥 Commits

Reviewing files that changed from the base of the PR and between 9bf8a98 and 660a927.

📒 Files selected for processing (4)
  • src/MaterialEditorQML.cpp
  • src/MaterialPresetLibrary.cpp
  • src/RTShaderHelper.cpp
  • src/RTShaderHelper_syncMaterial_test.cpp

fernandotonon and others added 3 commits July 3, 2026 01:17
…ormal map

Root cause (distinct from the roughness-tint fix in the previous commit):
generating PBR in the Material Editor or the image-to-3D flow wired the RTSS
SRS_NORMALMAP sub-render-state onto a mesh that has NO per-vertex tangents.
Ogre's normal-map shader needs tangents to build the tangent-space basis;
without them the basis is degenerate, the perturbed normal collapses, N·L
goes to ~0, and the surface renders as if the lights were off — while every
other (imported) mesh lit normally. This is exactly why an export→reload
looked correct: the mesh importer already builds tangents before wiring
normal maps (MeshImporterExporter::applyNormalMapsToEntity, "Build tangent
vectors … required by RTSS normal mapping"); the live generation paths
skipped that step.

Fix: route both live paths through the same tested importer routine:
- image-to-3D (MeshGenBuilder): defer normal-map wiring until after the
  entity exists, then call applyNormalMapsToEntity (builds tangents when UVs
  are present, then wires SRS_NORMALMAP).
- Material Editor (generatePbrFromDiffuse): after binding the maps, run
  applyNormalMapsToEntity on every scene entity using the material; fall back
  to the direct applyNormalMap only when no entity uses it.

Result: live PBR generation now lights identically to an export→reload of
the same mesh (verified in the viewport).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PbrSlotColourOpsApproximatePbrSemantics still asserted the old FFP ops
(metallic=ADD_SIGNED, roughness=MODULATE_X2) that the darkness fix replaced
with the inert LBX_SOURCE1 pass-through. Update the expectations (and the
comment) to the corrected semantics — roughness/metallic are BRDF inputs and
must not modulate the diffuse in the FFP fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es inert

Codex P2: the metallic/roughness canonical slots are reused by the
Specular-Glossiness preset as specular + glossiness maps, and that workflow
stays on the FFP path (applyPbrIfTagged skips it) — so making them
unconditionally inert removed the only thing that made spec-gloss maps
visible. Thread the workflow into configurePbrSlots and gate the inert
pass-through to metallic-roughness; spec-gloss keeps the legacy ADD_SIGNED
(specular) / MODULATE_X2 (glossiness) approximations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@fernandotonon

Copy link
Copy Markdown
Owner Author

@chatgpt-codex-connector re: specular-glossiness slot wiring — fixed in 6e6b65f. configurePbrSlots now takes the workflow: metallic-roughness keeps the inert LBX_SOURCE1 pass-through (real response via Cook-Torrance when IBL is present), but specular-glossiness — which stays on the FFP path since applyPbrIfTagged skips it — retains the legacy ADD_SIGNED (specular) / MODULATE_X2 (glossiness) ops so those maps still affect the render.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@sonarqubecloud

sonarqubecloud Bot commented Jul 4, 2026

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit cafded3 into master Jul 4, 2026
21 checks passed
@fernandotonon
fernandotonon deleted the fix/pbr-rtss-dark branch July 4, 2026 00:57
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