Skip to content

updateMatrixWorld() --> ensureMatrices()#32962

Open
PoseidonEnergy wants to merge 5 commits intomrdoob:devfrom
PoseidonEnergy:refactor/ensure-matrices-2
Open

updateMatrixWorld() --> ensureMatrices()#32962
PoseidonEnergy wants to merge 5 commits intomrdoob:devfrom
PoseidonEnergy:refactor/ensure-matrices-2

Conversation

@PoseidonEnergy
Copy link
Contributor

@PoseidonEnergy PoseidonEnergy commented Feb 5, 2026

Related: #32894, #21387, #14138, #18344, #20220, #25115, #27261, #14543

This PR is an attempt to fix issues surrounding updateMatrixWorld() and how it is used internally. It also gives more control to users over the world matrix update process that is very much needed. This PR does not address performance issues related to updateMatrixWorld(). It only fixes single-responsibility problems and does not change internal logic.

👍 updateMatrix() is all good

  1. Currently, and true to its name, updateMatrix() updates the local matrix only. It works even if matrixAutoUpdate is false, which is what I would expect -- if the user has opted out of "automatic updates" by setting matrixAutoUpdate to false, the user can still manually update the local matrix when they feel it is necessary.

  2. Because updateMatrix() has a single responsibility (to calculate the local matrix), this means methods that override updateMatrix() only need to worry about doing just that, and nothing else.

updateMatrixWorld() is a different story

Unfortunately, updateMatrixWorld() does not follow the same pattern as updateMatrix():

  1. If matrixWorldAutoUpdate is false, it is impossible for a user to update the world matrix by just calling the method alone. They must first set matrixWorldAutoUpdate to true, call the method, then set matrixWorldAutoUpdate back to false. This deviates from the behavior of updateMatrix() as described above. This is surprising.

Here's an example of where this behavioral inconsistency between updateMatrix() and updateMatrixWorld() is an issue. In the following snippet, the call to object.updateMatrixWorld() doesn't do anything, because matrixAutoUpdate was set to false for all of the objects in the scene. This means that matrixWorldNeedsUpdate never gets set to true, which means the world matrix is never actually calculated. The author of this example probably assumed it would though:

object.updateMatrix();
object.updateMatrixWorld();

  1. It is impossible to only update the world matrix if matrixAutoUpdate is true without adding code to unset matrixAutoUpdate before calling, and resetting matrixAutoUpdate after calling. This is inconvenient.

You can see the local matrix get double-calculated in several places in three.js:

lwLight.updateMatrix();
lwLight.updateMatrixWorld( true );

  1. Lastly, not only does updateMatrixWorld() calculate the local and world matrices, it also traverses through all descendants to do the same to them. Because updateMatrixWorld() tries to do three things at once, it is more complicated to override the method, because each override must also implement the local matrix update and descendant update logic.

😔 Override Sadness

Because of the issues with overriding updateMatrixWorld() mentioned above, you have things like this in three.js. In the following excerpt from Gyroscope.js, notice that all this method wants to do is override the world matrix calculation, but because Object3D.updateWorldMatrix() currently does three different things, Gyroscope.js has to re-implement the local matrix calculation and child traversal code in addition to the world matrix calculation:

updateMatrixWorld( force ) {
this.matrixAutoUpdate && this.updateMatrix();
// update matrixWorld
if ( this.matrixWorldNeedsUpdate || force ) {
if ( this.parent !== null ) {
this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
this.matrixWorld.decompose( _translationWorld, _quaternionWorld, _scaleWorld );
this.matrix.decompose( _translationObject, _quaternionObject, _scaleObject );
this.matrixWorld.compose( _translationWorld, _quaternionObject, _scaleWorld );
} else {
this.matrixWorld.copy( this.matrix );
}
this.matrixWorldNeedsUpdate = false;
force = true;
}
// update children
for ( let i = 0, l = this.children.length; i < l; i ++ ) {
this.children[ i ].updateMatrixWorld( force );
}
}

The following excerpt is another issue caused by updateMatrixWorld() doing more than it should. In Camera.js, notice that both updateMatrixWorld() and updateWorldMatrix() have to be overridden (with the same identical code). This is because the actual world matrix calculation is not isolated into its own method:

updateMatrixWorld( force ) {
super.updateMatrixWorld( force );
// exclude scale from view matrix to be glTF conform
this.matrixWorld.decompose( _position, _quaternion, _scale );
if ( _scale.x === 1 && _scale.y === 1 && _scale.z === 1 ) {
this.matrixWorldInverse.copy( this.matrixWorld ).invert();
} else {
this.matrixWorldInverse.compose( _position, _quaternion, _scale.set( 1, 1, 1 ) ).invert();
}
}
updateWorldMatrix( updateParents, updateChildren ) {
super.updateWorldMatrix( updateParents, updateChildren );
// exclude scale from view matrix to be glTF conform
this.matrixWorld.decompose( _position, _quaternion, _scale );
if ( _scale.x === 1 && _scale.y === 1 && _scale.z === 1 ) {
this.matrixWorldInverse.copy( this.matrixWorld ).invert();
} else {
this.matrixWorldInverse.compose( _position, _quaternion, _scale.set( 1, 1, 1 ) ).invert();
}
}

♻️ updateWorldMatrix() is just updateMatrixWorld(true) with extra arguments

Right now, updateWorldMatrix() is identical to updateMatrixWorld(), except that it ignores matrixWorldNeedsUpdate, and has the ability to control whether parents or descendants are traversed. If we simply added the arguments updateParents, updateChildren, and respectAutoUpdateFlags to updateMatrixWorld(), then we can make updateWorldMatrix() internally call updateMatrixWorld(true, ...args).

💎 What this PR does

  1. Made updateMatrixWorld() ONLY calculate the world matrix and ignore matrixWorldAutoUpdate, to be consistent with updateMatrix().
  2. Moved the code from updateWorldMatrix(updateParents, updateChildren) into a method named ensureMatrices(force=false, updateParents=false, updateChildren=true, respectAutoUpdateFlags=true) so we recycle existing code, and also have control over whether we want to respect the "automatic update" flags.
  3. Replaces all calls to updateMatrixWorld(force) with calls to ensureMatrices(force).

🔨 Just 1 tiny breaking change

  1. Any code that calls updateMatrixWorld(...args) should instead call ensureMatrices(...args). The default arguments to ensureMatrices() are set to match the old behavior of updateMatrixWorld(), so the only thing to update would be the function name. Best of all, there is no change required for users calling updateWorldMatrix().

🎯 Old versus new ways of calculating matrices

Scenario Current After Refactor
I want to trigger an "automatic update" of all the matrices in the scene. scene.updateMatrixWorld(); scene.ensureMatrices();
I want to update a single object's local and world matrix and have it respect that object's matrixAutoUpdate and matrixWorldAutoUpdate flags, but ignore the matrixWorldNeedsUpdate flag. o.updateWorldMatrix(); o.updateWorldMatrix();
I want to update a single object's world matrix when matrixWorldAutoUpdate is false. o.matrixWorldAutoUpdate = true;
o.updateWorldMatrix();
o.matrixWorldAutoUpdate = false;
o.updateMatrixWorld();
I want to update a single object's world matrix only, without also updating the local matrix. o.matrixAutoUpdate = false;
o.updateWorldMatrix();
o.matrixAutoUpdate = true;
o.updateMatrixWorld();
I want to force an update of ONLY the world matrix of an object and all its descendants, regardless if the object's and its descendants' matrixWorldAutoUpdate flags are false. o.traverse(c => {
c.matrixAutoUpdate = false;
c.matrixWorldAutoUpdate = true;
});
o.updateWorldMatrix(false, true);
o.traverse(c => {
c.matrixAutoUpdate = true;
c.matrixWorldAutoUpdate = false;
});
o.updateWorldMatrix(false, true, false, true, false);

💲 API Surface & Thoughts

Here is the API surface in this PR:

// updateMatrixWorld: No parameters. Updates the world matrix of the current object only.
updateMatrixWorld(
)

// ensureMatrices: Full control over the scene graph matrix update process.
ensureMatrices(
    force: boolean, // default is false
    updateParents: boolean, // default is false
    updateChildren: boolean, // default is true
    updateLocal: boolean, // default is true
    updateWorld: boolean, // default is true
    respectAutoUpdateFlags: boolean // default is true
)

// updateWorldMatrix: No change, except for added arguments for more control.
updateWorldMatrix(
    updateParents: boolean, // default is false
    updateChildren: boolean, // default is false
    updateLocal: boolean, // default is true
    updateWorld: boolean, // default is true
    respectAutoUpdateFlags: boolean // default is true
)

As you can see, this refactor trivializes previously cumbersome matrix operations, and fixes the confusing overridability issues. Let me know of any questions you may have.

@github-actions
Copy link

github-actions bot commented Feb 5, 2026

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 359.19
85.29
359.18
85.3
-12 B
+9 B
WebGPU 619.36
172.53
619.34
172.54
-15 B
+7 B
WebGPU Nodes 617.94
172.28
617.92
172.29
-15 B
+8 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 490.85
119.65
490.69
119.72
-160 B
+70 B
WebGPU 692.96
187.55
692.8
187.62
-163 B
+64 B
WebGPU Nodes 642.17
174.93
642
174.99
-163 B
+60 B

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