Skip to content

fix: make GLTF meshes load with embedded materials / correct orientation - #1192

Open
cadkin wants to merge 2 commits into
lichtblick-suite:developfrom
cadkin:feature/gltf-fix
Open

fix: make GLTF meshes load with embedded materials / correct orientation#1192
cadkin wants to merge 2 commits into
lichtblick-suite:developfrom
cadkin:feature/gltf-fix

Conversation

@cadkin

@cadkin cadkin commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

User-Facing Changes

GLB/glTF files now load with embedded materials and respect the Scene > Mesh up axis option.

Description

Previously when loading a .glb file, the orientation option would not be respected and any embedded materials would be lost:

ksnip_20260704-180404

With these changes:

ksnip_20260704-180324

Checklist

  • The web version was tested and it is running ok
  • The desktop version was tested and it is running ok
  • This change is covered by unit tests
    • How should this change be tested?
  • Files constants.ts, types.ts and *.style.ts have been checked and relevant code snippets have been relocated

Summary by CodeRabbit

  • Bug Fixes
    • Corrected 3D model orientation so glTF/GLB files are rotated only when needed, based on the configured up-axis.
    • Preserved embedded materials for both Collada (DAE) and glTF (GLTF/GLB) meshes.
  • Tests
    • Added automated Jest coverage for up-axis conversion and embedded-material preservation, including reusable mock GLB test assets.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: be66e2b0-ce99-42df-b2a9-617fd549995b

📥 Commits

Reviewing files that changed from the base of the PR and between 7400c89 and abcc771.

📒 Files selected for processing (5)
  • packages/suite-base/src/panels/ThreeDeeRender/ModelCache.test.ts
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/MockAssets.ts
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.test.tsx
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.ts
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/markers/RenderableMeshResource.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/MockAssets.ts
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.test.tsx
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.ts
  • packages/suite-base/src/panels/ThreeDeeRender/ModelCache.test.ts
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/markers/RenderableMeshResource.ts

Walkthrough

ModelCache now applies glTF Y-up to Z-up rotation conditionally, Urdfs supports embedded materials for glTF meshes, and GLB-based tests validate orientation and material preservation. RenderableMeshResource stores its mesh state in TypeScript private properties.

Changes

3D Mesh Loading and Rendering

Layer / File(s) Summary
Conditional glTF axis conversion
packages/suite-base/src/panels/ThreeDeeRender/ModelCache.ts, packages/suite-base/src/panels/ThreeDeeRender/ModelCache.test.ts, packages/suite-base/src/panels/ThreeDeeRender/renderables/MockAssets.ts
meshUpAxis is passed into glTF loading, rotation is applied only for "y_up", and a mocked GLB verifies the resulting rotation.
Embedded materials for GLB meshes
packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.ts, packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.test.tsx
createRenderable is exported, glTF files are eligible for embedded materials, and a GLB test verifies material color preservation.
Renderable mesh state access
packages/suite-base/src/panels/ThreeDeeRender/renderables/markers/RenderableMeshResource.ts
Mesh, material, and reference URL state use TypeScript private properties throughout disposal, updates, model loading, and material replacement.

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

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 clearly summarizes the main change: GLTF meshes now preserve embedded materials and respect mesh orientation.
Description check ✅ Passed The description follows the template with user-facing changes, a description, and a checklist, though the unit-test item is left unchecked.
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

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.

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

🤖 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 `@packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.ts`:
- Around line 1097-1103: In Urdfs.ts, fix the missing semicolon after the isGLTF
assignment to match the semicolon style required for TS files, and update the
stale comment above the embedded variable so it reflects both Collada and glTF
handling instead of only Collada. Keep the logic in the visual.geometry.filename
checks and the embedded material selection unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c5ede05-2a5b-4ad0-82f7-7e9e00d289cc

📥 Commits

Reviewing files that changed from the base of the PR and between a5a6951 and c516645.

📒 Files selected for processing (2)
  • packages/suite-base/src/panels/ThreeDeeRender/ModelCache.ts
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.ts

Comment on lines 1097 to +1103
const isCollada = visual.geometry.filename.toLowerCase().endsWith(".dae");
const isGLTF = (
visual.geometry.filename.toLowerCase().endsWith(".gltf") ||
visual.geometry.filename.toLowerCase().endsWith(".glb")
)
// Use embedded materials if the mesh is a Collada file
const embedded = isCollada ? EmbeddedMaterialUsage.Use : EmbeddedMaterialUsage.Ignore;
const embedded = (isCollada || isGLTF) ? EmbeddedMaterialUsage.Use : EmbeddedMaterialUsage.Ignore;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Missing semicolon and stale comment.

Line 1101 closing paren lacks a semicolon, and the comment on line 1102 still only mentions Collada despite now covering glTF too.

As per coding guidelines, **/*.{ts,tsx,js,jsx,mjs,cjs} files must "Use semicolons".

🔧 Proposed fix
     case "mesh": {
-      const isCollada = visual.geometry.filename.toLowerCase().endsWith(".dae");
-      const isGLTF = (
-        visual.geometry.filename.toLowerCase().endsWith(".gltf") ||
-        visual.geometry.filename.toLowerCase().endsWith(".glb")
-      )
-      // Use embedded materials if the mesh is a Collada file
-      const embedded = (isCollada || isGLTF) ? EmbeddedMaterialUsage.Use : EmbeddedMaterialUsage.Ignore;
+      const filename = visual.geometry.filename.toLowerCase();
+      const isCollada = filename.endsWith(".dae");
+      const isGLTF = filename.endsWith(".gltf") || filename.endsWith(".glb");
+      // Use embedded materials for Collada or glTF files
+      const embedded =
+        isCollada || isGLTF ? EmbeddedMaterialUsage.Use : EmbeddedMaterialUsage.Ignore;
📝 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
const isCollada = visual.geometry.filename.toLowerCase().endsWith(".dae");
const isGLTF = (
visual.geometry.filename.toLowerCase().endsWith(".gltf") ||
visual.geometry.filename.toLowerCase().endsWith(".glb")
)
// Use embedded materials if the mesh is a Collada file
const embedded = isCollada ? EmbeddedMaterialUsage.Use : EmbeddedMaterialUsage.Ignore;
const embedded = (isCollada || isGLTF) ? EmbeddedMaterialUsage.Use : EmbeddedMaterialUsage.Ignore;
const filename = visual.geometry.filename.toLowerCase();
const isCollada = filename.endsWith(".dae");
const isGLTF = filename.endsWith(".gltf") || filename.endsWith(".glb");
// Use embedded materials for Collada or glTF files
const embedded =
isCollada || isGLTF ? EmbeddedMaterialUsage.Use : EmbeddedMaterialUsage.Ignore;
🤖 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 `@packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.ts` around
lines 1097 - 1103, In Urdfs.ts, fix the missing semicolon after the isGLTF
assignment to match the semicolon style required for TS files, and update the
stale comment above the embedded variable so it reflects both Collada and glTF
handling instead of only Collada. Keep the logic in the visual.geometry.filename
checks and the embedded material selection unchanged.

Source: Coding guidelines

@rodrigo-rodrigues-ctw

Copy link
Copy Markdown
Contributor

@cadkin please address sonar issues

@cadkin
cadkin force-pushed the feature/gltf-fix branch from c516645 to 7400c89 Compare July 11, 2026 23:44

@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

🤖 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 `@packages/suite-base/src/panels/ThreeDeeRender/ModelCache.test.ts`:
- Around line 37-58: Update the “should respect meshUpAxis option” test to
return or await the promise from modelCache.load, making the test callback
asynchronous so Jest waits for the rotation assertions to execute. Preserve the
existing assertions and console warning cleanup inside the awaited success path.

In
`@packages/suite-base/src/panels/ThreeDeeRender/renderables/markers/RenderableMeshResource.ts`:
- Line 157: Update the replaceMaterials call in `#loadModel` to pass this.material
instead of this.mesh.material, ensuring non-embedded-material meshes do not
dereference the undefined mesh while preserving the existing embedded-material
conditional.

In `@packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.test.tsx`:
- Around line 86-103: Move the createRenderable call out of the describe-level
initialization and into an async beforeAll or beforeEach for the test suite,
then await the renderable’s exposed mesh-loading promise or ready state before
assigning/asserting renderable. Keep the existing preserve embedded materials
assertion unchanged and avoid timing-based setTimeout flushing when a loading
API is available.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ac27d892-5cba-461c-b510-e96432bb86e5

📥 Commits

Reviewing files that changed from the base of the PR and between c516645 and 7400c89.

📒 Files selected for processing (6)
  • packages/suite-base/src/panels/ThreeDeeRender/ModelCache.test.ts
  • packages/suite-base/src/panels/ThreeDeeRender/ModelCache.ts
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/MockAssets.ts
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.test.tsx
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.ts
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/markers/RenderableMeshResource.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.ts
  • packages/suite-base/src/panels/ThreeDeeRender/ModelCache.ts

Comment on lines +37 to +58
it("should respect meshUpAxis option", () => {
const modelCache = new ModelCache({
edgeMaterial: new THREE.LineBasicMaterial({ dithering: true }),
ignoreColladaUpAxis: true,
meshUpAxis: "y_up",
fetchAsset: mockFetch
});

modelCache.load(
"file:///mock/cube.glb",
{},
mockError
).then(
(model) => {
expect(model.rotation.x).toEqual(1.5707963267948963);
expect(model.rotation.y).toEqual(0);
// FP inaccurary means this is 'neg' zero after the rotation.
expect(model.rotation.z).toEqual(-0);

console.warn.mockClear();
}
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Test promise is not returned — assertions are never checked by Jest.

The it callback is synchronous, but modelCache.load(...).then(...) is async. Since the promise is not returned from the test, Jest marks the test as passed before the .then() assertions execute. The test will always pass regardless of whether the rotation values are correct.

🔧 Proposed fix: make the test async and await the load
-    it("should respect meshUpAxis option", () => {
+    it("should respect meshUpAxis option", async () => {
       const modelCache = new ModelCache({
         edgeMaterial: new THREE.LineBasicMaterial({ dithering: true }),
         ignoreColladaUpAxis: true,
         meshUpAxis: "y_up",
         fetchAsset: mockFetch
       });

-      modelCache.load(
-        "file:///mock/cube.glb",
-        {},
-        mockError
-      ).then(
-        (model) => {
-          expect(model.rotation.x).toEqual(1.5707963267948963);
-          expect(model.rotation.y).toEqual(0);
-          // FP inaccuray means this is 'neg' zero after the rotation.
-          expect(model.rotation.z).toEqual(-0);
-
-          console.warn.mockClear();
-        }
-      );
+      const model = await modelCache.load("file:///mock/cube.glb", {}, mockError);
+      expect(model.rotation.x).toEqual(1.5707963267948963);
+      expect(model.rotation.y).toEqual(0);
+      // FP inaccuracy means this is 'neg' zero after the rotation.
+      expect(model.rotation.z).toEqual(-0);
     });
📝 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
it("should respect meshUpAxis option", () => {
const modelCache = new ModelCache({
edgeMaterial: new THREE.LineBasicMaterial({ dithering: true }),
ignoreColladaUpAxis: true,
meshUpAxis: "y_up",
fetchAsset: mockFetch
});
modelCache.load(
"file:///mock/cube.glb",
{},
mockError
).then(
(model) => {
expect(model.rotation.x).toEqual(1.5707963267948963);
expect(model.rotation.y).toEqual(0);
// FP inaccurary means this is 'neg' zero after the rotation.
expect(model.rotation.z).toEqual(-0);
console.warn.mockClear();
}
);
it("should respect meshUpAxis option", async () => {
const modelCache = new ModelCache({
edgeMaterial: new THREE.LineBasicMaterial({ dithering: true }),
ignoreColladaUpAxis: true,
meshUpAxis: "y_up",
fetchAsset: mockFetch
});
const model = await modelCache.load("file:///mock/cube.glb", {}, mockError);
expect(model.rotation.x).toEqual(1.5707963267948963);
expect(model.rotation.y).toEqual(0);
// FP inaccuracy means this is 'neg' zero after the rotation.
expect(model.rotation.z).toEqual(-0);
});
🤖 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 `@packages/suite-base/src/panels/ThreeDeeRender/ModelCache.test.ts` around
lines 37 - 58, Update the “should respect meshUpAxis option” test to return or
await the promise from modelCache.load, making the test callback asynchronous so
Jest waits for the rotation assertions to execute. Preserve the existing
assertions and console warning cleanup inside the awaited success path.

Comment on lines +86 to +103
let renderable = createRenderable({
visual: visual,
robot: robot,
id: 0,
frameId: "test-frame",
renderer: mockRenderer
});

it("should preserve embedded materials", () => {
const expected = {
isColor: true,
r: 0.8000074625015259,
g: 0.058865584433078766,
b: 0.03260880336165428
};

expect(renderable.mesh.children[0].material.color).toEqual(expected);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Mesh loading is async but the test doesn't await it — assertions may run before the mesh is loaded.

createRenderable is called at the describe level, which triggers async mesh loading via modelCache.load(). The it block then accesses renderable.mesh.children[0].material.color synchronously. If the microtask queue hasn't drained by the time the it callback runs, children will be empty and the test will throw. Move createRenderable into a beforeAll/beforeEach and add an explicit await for the mesh loading to complete.

🔧 Proposed fix: await mesh loading in beforeAll
   describe("loading glTF files", () => {
-    let robot: UrdfRobot = { name: "mock" };
-    let visual: UrdfVisual = {
+    const robot: UrdfRobot = { name: "mock" };
+    const visual: UrdfVisual = {
       geometry: {
         geometryType: "mesh",
         filename: "file:///mock/cube.glb",
       },
       origin: {
         xyz: {x: 0, y: 0, z: 0},
         rpy: {x: 0, y: 0, z: 0}
       }
     };
 
-    let renderable = createRenderable({
+    let renderable: ReturnType<typeof createRenderable>;
+
+    beforeAll(async () => {
+      renderable = createRenderable({
         visual: visual,
         robot: robot,
         id: 0,
         frameId: "test-frame",
         renderer: mockRenderer
       });
+      // Allow async mesh loading to complete
+      await new Promise((resolve) => setTimeout(resolve, 0));
+    });
 
     it("should preserve embedded materials", () => {

If RenderableMeshResource exposes a loading promise or ready state, prefer awaiting that directly instead of using a setTimeout flush.

📝 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
let renderable = createRenderable({
visual: visual,
robot: robot,
id: 0,
frameId: "test-frame",
renderer: mockRenderer
});
it("should preserve embedded materials", () => {
const expected = {
isColor: true,
r: 0.8000074625015259,
g: 0.058865584433078766,
b: 0.03260880336165428
};
expect(renderable.mesh.children[0].material.color).toEqual(expected);
});
describe("loading glTF files", () => {
const robot: UrdfRobot = { name: "mock" };
const visual: UrdfVisual = {
geometry: {
geometryType: "mesh",
filename: "file:///mock/cube.glb",
},
origin: {
xyz: {x: 0, y: 0, z: 0},
rpy: {x: 0, y: 0, z: 0}
}
};
let renderable: ReturnType<typeof createRenderable>;
beforeAll(async () => {
renderable = createRenderable({
visual: visual,
robot: robot,
id: 0,
frameId: "test-frame",
renderer: mockRenderer
});
// Allow async mesh loading to complete
await new Promise((resolve) => setTimeout(resolve, 0));
});
it("should preserve embedded materials", () => {
const expected = {
isColor: true,
r: 0.8000074625015259,
g: 0.058865584433078766,
b: 0.03260880336165428
};
expect(renderable.mesh.children[0].material.color).toEqual(expected);
});
🤖 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 `@packages/suite-base/src/panels/ThreeDeeRender/renderables/Urdfs.test.tsx`
around lines 86 - 103, Move the createRenderable call out of the describe-level
initialization and into an async beforeAll or beforeEach for the test suite,
then await the renderable’s exposed mesh-loading promise or ready state before
assigning/asserting renderable. Keep the existing preserve embedded materials
assertion unchanged and avoid timing-based setTimeout flushing when a loading
API is available.

@cadkin
cadkin force-pushed the feature/gltf-fix branch from 7400c89 to abcc771 Compare July 11, 2026 23:58
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
59.5% Coverage on New Code (required ≥ 80%)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

@cadkin

cadkin commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@rodrigo-rodrigues-ctw

I had a chance to circle back around to this. These render classes had no previous tests to base things off of, so I did what seemed correct to me. Let me know if this isn't how you want to do this.

Notably, the RenderableMeshResource did not have any way to access the underlying mesh, so I tweaked it to use the private keyword for access during testing. Let me know if this is not desirable or if you have any suggestions. It does mark these changes as new code in Sonar, but there is nothing new about them.

I'll take care of the outstanding lint issues soon (tm).

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.

3 participants