From 3c3c647b5285ca1cbce5c18bee4fc44b57bb01b3 Mon Sep 17 00:00:00 2001 From: DevKage Date: Thu, 29 Oct 2026 22:23:05 +0530 Subject: [PATCH] fix(flame_3d): Invalidate the AABBs of nested components when an ancestor moves When a Component3D's transform changed, only the world transform dirty flag was propagated down to its descendants, while the AABB dirty flag was only propagated up. Descendants kept the AABB cached for their old world position and were frustum culled based on it. The AABB is now marked dirty together with the world transform when propagating down the subtree, and the parent's AABB is marked dirty from the component whose transform changed. --- .../lib/src/components/component_3d.dart | 12 ++- .../test/components/component_3d_test.dart | 84 +++++++++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 packages/flame_3d/test/components/component_3d_test.dart diff --git a/packages/flame_3d/lib/src/components/component_3d.dart b/packages/flame_3d/lib/src/components/component_3d.dart index f08ad8b29ee..ffd72f24dc9 100644 --- a/packages/flame_3d/lib/src/components/component_3d.dart +++ b/packages/flame_3d/lib/src/components/component_3d.dart @@ -182,15 +182,19 @@ abstract class Component3D({ } } - /// Mark this component's world transform as needing recomputation. + /// Mark this component's world transform, and therefore its AABB, as + /// needing recomputation. /// /// This propagates down to child [Component3D]s so the entire subtree /// stays consistent. void _markWorldTransformDirty() { - if (_worldTransformDirty) { + // The world transform and the AABB are recomputed independently, so one + // can be dirty while the other is not. Only stop when both already are. + if (_worldTransformDirty && _aabbDirty) { return; } _worldTransformDirty = true; + _aabbDirty = true; for (final child in _childComponents) { child._markWorldTransformDirty(); } @@ -198,6 +202,8 @@ abstract class Component3D({ void _onTransformChanged() { _markWorldTransformDirty(); - markAabbDirty(); + if (parent case final Component3D parent) { + parent.markAabbDirty(); + } } } diff --git a/packages/flame_3d/test/components/component_3d_test.dart b/packages/flame_3d/test/components/component_3d_test.dart new file mode 100644 index 00000000000..8c7a42fcc19 --- /dev/null +++ b/packages/flame_3d/test/components/component_3d_test.dart @@ -0,0 +1,84 @@ +import 'package:flame_3d/components.dart'; +import 'package:flame_3d/core.dart'; +import 'package:flame_3d/graphics.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Component3D', () { + testWithFlameGame( + 'aabb of a nested component follows its parent when it moves', + (game) async { + final child = _Box(position: Vector3(1, 0, 0)); + final parent = _Group(children: [child]); + await game.world.ensureAdd(parent); + + expect(child.aabb.center, closeToVector3(Vector3(1, 0, 0))); + expect(parent.aabb.center, closeToVector3(Vector3(1, 0, 0))); + + parent.position.setValues(10, 0, 0); + + expect(child.aabb.center, closeToVector3(Vector3(11, 0, 0))); + expect(parent.aabb.center, closeToVector3(Vector3(11, 0, 0))); + }, + ); + + testWithFlameGame( + 'aabb of a deeply nested component follows its ancestors', + (game) async { + final leaf = _Box(); + final root = _Group( + children: [ + _Group( + position: Vector3(0, 1, 0), + children: [ + _Group(position: Vector3(0, 0, 1), children: [leaf]), + ], + ), + ], + ); + await game.world.ensureAdd(root); + + expect(leaf.aabb.center, closeToVector3(Vector3(0, 1, 1))); + + root.position.setValues(1, 0, 0); + + expect(leaf.aabb.center, closeToVector3(Vector3(1, 1, 1))); + expect(root.aabb.center, closeToVector3(Vector3(1, 1, 1))); + }, + ); + + testWithFlameGame( + 'aabb of a nested component follows its ancestors after repeated moves', + (game) async { + final leaf = _Box(); + final middle = _Group(children: [leaf]); + final root = _Group(children: [middle]); + await game.world.ensureAdd(root); + + // Only read the leaf in between the moves, so that the ancestors' + // AABBs stay dirty while the leaf's is recomputed. + root.position.setValues(1, 0, 0); + expect(leaf.aabb.center, closeToVector3(Vector3(1, 0, 0))); + + root.position.setValues(2, 0, 0); + expect(leaf.aabb.center, closeToVector3(Vector3(2, 0, 0))); + + middle.position.setValues(0, 1, 0); + expect(leaf.aabb.center, closeToVector3(Vector3(2, 1, 0))); + expect(root.aabb.center, closeToVector3(Vector3(2, 1, 0))); + }, + ); + }); +} + +class _Group({super.position, super.children}) extends Component3D; + +class _Box({super.position}) extends Object3D { + @override + Aabb3? computeLocalAabb() => + Aabb3.minMax(Vector3.all(-0.5), Vector3.all(0.5)); + + @override + void draw(RenderContext context) {} +}