Conversation
The 3D Model Mesh Parts Control feature in GDevelop allows developers to manipulate individual parts of a 3D model. This feature enhances the flexibility and control over 3D assets used in GDevelop projects.
…BForgeLab/GDevelop into feature/mesh-parts-control-dev
📝 WalkthroughWalkthroughThis PR introduces normalization scale tracking to the 3D rendering system. The Model3DRuntimeObject3DRenderer computes a normalization scale after stretching models into a unitary cube and stores it on MeshParts. Model3DRuntimeObjectMeshParts now tracks original per-mesh transforms and applies normalization conversions when setting or getting mesh positions, rotations, and scales. Changes
Sequence DiagramsequenceDiagram
participant Renderer as 3DRenderer
participant MeshParts as MeshParts
participant Mesh as Mesh/Group
Renderer->>Renderer: Compute scale for unit cube
Renderer->>MeshParts: setNormalizationScale(scaleX, scaleY, scaleZ)
MeshParts->>MeshParts: Store normalization scale
Note over Renderer,Mesh: Later mesh operations
Renderer->>MeshParts: setMeshPosition(x, y, z)
MeshParts->>MeshParts: Convert to normalized-space<br/>using normalization scale
MeshParts->>MeshParts: Add to original position
MeshParts->>Mesh: Update position
Renderer->>MeshParts: getMeshPositionX/Y/Z()
MeshParts->>MeshParts: Compute offset from original<br/>Convert back to object-space<br/>Apply normalization scale
MeshParts->>Renderer: Return normalized position
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the 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. Comment |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Caution Docstrings generation - FAILED No docstrings were generated. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@Extensions/3D/Model3DRuntimeObject3DRenderer.ts`:
- Around line 208-210: The function stretchModelIntoUnitaryCube currently
mutates external state by calling
this._model3DRuntimeObject._meshParts.setNormalizationScale, violating its
JSDoc; change stretchModelIntoUnitaryCube to return the computed
scaleX/scaleY/scaleZ (e.g., along with the bounding box) and remove the
setNormalizationScale call from it, then update the caller
(_updateDefaultTransformation or _updateModel) to receive those scale values and
perform this._model3DRuntimeObject._meshParts.setNormalizationScale(scaleX,
scaleY, scaleZ) there; alternatively, if you prefer not to change callers,
update the JSDoc for stretchModelIntoUnitaryCube to explicitly state it mutates
_model3DRuntimeObject._meshParts via setNormalizationScale so callers know about
the side effect.
🧹 Nitpick comments (3)
Extensions/3D/Model3DRuntimeObjectMeshParts.ts (3)
11-14:_originalMeshRotationsis stored but never used in any computation.The field is populated in
buildMeshesMap(line 62), cleaned up inremoveMesh/clear, but neithersetMeshRotationnorgetMeshRotationX/Y/Zreference it. If it's reserved for a future relative-rotation API, consider adding a comment to that effect. Otherwise, remove it to avoid confusion.Also applies to: 20-23
263-279: Rotation uses absolute values while position and scale are relative to originals — document the asymmetry.
setMeshPositionandsetMeshScaleoperate relative to the original transform, butsetMeshRotationsets absolute rotation, discarding the mesh's original rotation from the model. This meanssetMeshRotation(name, 0, 0, 0)resets any authored rotation. The JSDoc notes this, but it's a potential foot-gun for users who expect uniform behavior.If relative rotation is eventually desired, the stored (but currently unused)
_originalMeshRotationscould be leveraged — composing relative Euler angles is non-trivial though, so the absolute approach is understandable for now.
325-346:setMeshScalewith zero original scale silently returns — consider a warning.If
originalScaleis missing (line 336), the method exits silently. This is a defensive check, but an original scale of(0, 0, 0)on a component won't be caught — the multiplication will produce zero regardless of user input. Unlike the getters, the setter has no zero-check on individual components. This is likely fine in practice (a zero-scale mesh is degenerate) but worth being aware of.
| // Store the normalization scale for mesh parts positioning | ||
| // Note: Y scale is negated in the matrix but we store the absolute value | ||
| this._model3DRuntimeObject._meshParts.setNormalizationScale(scaleX, scaleY, scaleZ); |
There was a problem hiding this comment.
stretchModelIntoUnitaryCube now mutates state outside threeObject, violating its JSDoc contract.
Line 146 documents: "This function doesn't mutate anything outside of threeObject." The new setNormalizationScale call on line 210 mutates _meshParts, which is outside threeObject. Update the JSDoc to reflect this side effect, or move the setNormalizationScale call to the caller (_updateDefaultTransformation or _updateModel) to preserve the contract.
Option A: Move the call to the caller
In stretchModelIntoUnitaryCube, return the scale values along with the bounding box, then call setNormalizationScale in _updateDefaultTransformation:
// in stretchModelIntoUnitaryCube, remove:
- // Store the normalization scale for mesh parts positioning
- // Note: Y scale is negated in the matrix but we store the absolute value
- this._model3DRuntimeObject._meshParts.setNormalizationScale(scaleX, scaleY, scaleZ); // in _updateDefaultTransformation, after stretchModelIntoUnitaryCube returns:
+ const scaleX = modelWidth < epsilon ? 1 : 1 / modelWidth;
+ const scaleY = modelHeight < epsilon ? 1 : 1 / modelHeight;
+ const scaleZ = modelDepth < epsilon ? 1 : 1 / modelDepth;
+ this._model3DRuntimeObject._meshParts.setNormalizationScale(scaleX, scaleY, scaleZ);Option B: Update the JSDoc
- * This function doesn't mutate anything outside of `threeObject`.
+ * This function doesn't mutate anything outside of `threeObject`,
+ * except for storing the normalization scale on the runtime object's mesh parts.🤖 Prompt for AI Agents
In `@Extensions/3D/Model3DRuntimeObject3DRenderer.ts` around lines 208 - 210, The
function stretchModelIntoUnitaryCube currently mutates external state by calling
this._model3DRuntimeObject._meshParts.setNormalizationScale, violating its
JSDoc; change stretchModelIntoUnitaryCube to return the computed
scaleX/scaleY/scaleZ (e.g., along with the bounding box) and remove the
setNormalizationScale call from it, then update the caller
(_updateDefaultTransformation or _updateModel) to receive those scale values and
perform this._model3DRuntimeObject._meshParts.setNormalizationScale(scaleX,
scaleY, scaleZ) there; alternatively, if you prefer not to change callers,
update the JSDoc for stretchModelIntoUnitaryCube to explicitly state it mutates
_model3DRuntimeObject._meshParts via setNormalizationScale so callers know about
the side effect.
|
|
||
| const normalizedX = objectWidth !== 0 ? (x * this._normalizationScale.x) / objectWidth : 0; | ||
| // Y axis is flipped in the renderer, so we need to negate it | ||
| const normalizedY = objectHeight !== 0 ? (-y * this._normalizationScale.y) / objectHeight : 0; | ||
| const normalizedZ = objectDepth !== 0 ? (z * this._normalizationScale.z) / objectDepth : 0; |
There was a problem hiding this comment.
🔴 Mesh position conversion formula multiplies by normalization scale instead of dividing
The setMeshPosition and getMeshPosition* methods use an inverted normalization scale factor, causing mesh positions to be visually incorrect by a factor of normalizationScale^2.
Root Cause and Detailed Explanation
The transform chain from mesh local space to world space is:
worldOffset = dp * normalizationScale * objectDimension
where normalizationScale = 1/modelDimension (set at Model3DRuntimeObject3DRenderer.ts:197-199).
To convert a user-specified object-space offset x to mesh local space:
dp.x = x / (normalizationScale.x * objectWidth)
But the code at line 162 computes:
normalizedX = (x * this._normalizationScale.x) / objectWidth
which equals x * normalizationScale.x / objectWidth — multiplying by normalizationScale instead of dividing.
Concrete example: For a model 10 units wide (normalizationScale.x = 0.1), displayed at 200px (objectWidth = 200), setting x = 40 should move the mesh by 40 world units. The correct local offset is dp.x = 40 / (0.1 * 200) = 2. But the code computes dp.x = (40 * 0.1) / 200 = 0.02, resulting in a world displacement of only 0.02 * 0.1 * 200 = 0.4 instead of 40.
The getter has the symmetric inverse error (line 196: divides by normalizationScale.x instead of multiplying), so get/set roundtrips are consistent, but the actual visual displacement is wrong by a factor of modelWidth^2.
The same bug affects Y (line 164) and Z (line 165) axes, and their corresponding getters (lines 223, 249).
Impact: All mesh position manipulations via setMeshPosition will produce drastically incorrect visual results. The magnitude of the error depends on the model's original dimensions.
| const normalizedX = objectWidth !== 0 ? (x * this._normalizationScale.x) / objectWidth : 0; | |
| // Y axis is flipped in the renderer, so we need to negate it | |
| const normalizedY = objectHeight !== 0 ? (-y * this._normalizationScale.y) / objectHeight : 0; | |
| const normalizedZ = objectDepth !== 0 ? (z * this._normalizationScale.z) / objectDepth : 0; | |
| const normalizedX = objectWidth !== 0 && this._normalizationScale.x !== 0 ? x / (this._normalizationScale.x * objectWidth) : 0; | |
| // Y axis is flipped in the renderer, so we need to negate it | |
| const normalizedY = objectHeight !== 0 && this._normalizationScale.y !== 0 ? -y / (this._normalizationScale.y * objectHeight) : 0; | |
| const normalizedZ = objectDepth !== 0 && this._normalizationScale.z !== 0 ? z / (this._normalizationScale.z * objectDepth) : 0; |
Was this helpful? React with 👍 or 👎 to provide feedback.
| const normalizedOffset = currentPos.x - originalPos.x; | ||
| return this._normalizationScale.x !== 0 | ||
| ? (normalizedOffset * objectWidth) / this._normalizationScale.x | ||
| : 0; |
There was a problem hiding this comment.
🔴 getMeshPosition getters use inverted normalization scale (symmetric to setter bug)*
The getter methods for mesh position X, Y, and Z divide by normalizationScale instead of multiplying, producing incorrect object-space values.
Root Cause
The correct conversion from mesh local offset back to object space is:
x = normalizedOffset * normalizationScale.x * objectWidth
But getMeshPositionX at line 195-197 computes:
return (normalizedOffset * objectWidth) / this._normalizationScale.x
This divides by normalizationScale.x instead of multiplying. The same error exists in getMeshPositionY (line 222-224, also with Y-flip) and getMeshPositionZ (line 248-250).
While the getters are consistent with the buggy setters (roundtrip works), both are wrong relative to the actual visual position of the mesh.
Impact: Reading mesh positions returns values that don't correspond to actual world-space positions.
Prompt for agents
Fix the three getMeshPosition* methods to multiply by normalizationScale instead of dividing:
In getMeshPositionX (around line 195-198), change:
return this._normalizationScale.x !== 0 ? (normalizedOffset * objectWidth) / this._normalizationScale.x : 0;
to:
return normalizedOffset * this._normalizationScale.x * objectWidth;
In getMeshPositionY (around line 222-224), change:
return this._normalizationScale.y !== 0 ? (-normalizedOffset * objectHeight) / this._normalizationScale.y : 0;
to:
return -normalizedOffset * this._normalizationScale.y * objectHeight;
In getMeshPositionZ (around line 248-250), change:
return this._normalizationScale.z !== 0 ? (normalizedOffset * objectDepth) / this._normalizationScale.z : 0;
to:
return normalizedOffset * this._normalizationScale.z * objectDepth;
Was this helpful? React with 👍 or 👎 to provide feedback.
| // Store the normalization scale for mesh parts positioning | ||
| // Note: Y scale is negated in the matrix but we store the absolute value | ||
| this._model3DRuntimeObject._meshParts.setNormalizationScale(scaleX, scaleY, scaleZ); |
There was a problem hiding this comment.
🔴 stretchModelIntoUnitaryCube side effect corrupts mesh normalization scale when called from Physics3D
stretchModelIntoUnitaryCube now mutates _meshParts._normalizationScale, violating its documented contract and causing incorrect mesh positioning when called from Physics3D with a different mesh shape resource.
Root Cause
The new line 210 in Model3DRuntimeObject3DRenderer.ts adds a side effect to stretchModelIntoUnitaryCube:
this._model3DRuntimeObject._meshParts.setNormalizationScale(scaleX, scaleY, scaleZ);
The function's doc comment at line 146 states: "This function doesn't mutate anything outside of threeObject." This contract is now violated.
The function is also called from Physics3DRuntimeBehavior.ts:913:
model3DRuntimeObject._renderer.stretchModelIntoUnitaryCube(modelInCube, ...);
The Physics3D behavior may use a different model resource (meshShapeResourceName at Physics3DRuntimeBehavior.ts:902), which would have different bounding box dimensions. When this happens, stretchModelIntoUnitaryCube computes normalization scales based on the physics mesh (not the visual model) and overwrites _meshParts._normalizationScale with incorrect values.
Impact: After getMeshShapeSettings is called with a custom physics mesh resource, all subsequent setMeshPosition/getMeshPosition* calls will use wrong scale factors, producing incorrect mesh positions.
Prompt for agents
Move the setNormalizationScale call out of stretchModelIntoUnitaryCube and into _updateDefaultTransformation or _updateModel instead, so that it is only called when the visual model is being set up, not when Physics3D calls stretchModelIntoUnitaryCube for physics shape computation.
For example, in _updateDefaultTransformation after the call to stretchModelIntoUnitaryCube, compute the normalization scale from the bounding box and call setNormalizationScale there. Or pass the normalization scale as a return value from stretchModelIntoUnitaryCube and let the caller decide what to do with it.
Also update the doc comment on stretchModelIntoUnitaryCube to accurately reflect any remaining side effects.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary by CodeRabbit
Release Notes