Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

[flame_3d] Children added to an Object3D after it was rendered can be culled while on screen

未关闭
#4,164 0 条评论 0 个 reaction 已指派 1 人 在 GitHub 查看

维护者通常 1 天内回复

@ufrshubham 已经在做这个了。

开始于 2026年10月29日。

评估

这个 Issue 还没有评估数据。

描述

bug flame_3d
What happened?

When a child is added to a Component3D whose AABB has already been computed, the parent's cached AABB is not invalidated. The parent's AABB is the hull of its own geometry and the AABBs of its children, so it no longer covers the new child.

This matters when the parent is an Object3D: Object3D.renderTree frustum culls the parent's AABB first and skips the whole subtree when it is outside of the view. A child added inside the view, but outside of the parent's stale AABB, is therefore never rendered.

Component3D.onMount only marks the new child's own world transform as dirty, nothing marks the parent's AABB as dirty. A typical case is a spawner or pool that is an Object3D and adds children to itself at runtime.

What do you expect?

Adding a child to a component should invalidate the cached AABB of that component (and its ancestors), so that the new child is taken into account when culling.

How can we reproduce this?
  1. Run the main.dart below with flutter run --enable-flutter-gpu (on Windows also add --enable-impeller).
  2. The Spawner is an Object3D that starts with a single (blue) child behind the camera, so its AABB is behind the camera too. After one second it adds a green cube to itself, in front of the camera.
  3. Only the red control cube is ever rendered. The green cube is skipped because the Spawner is culled using its stale AABB.
What steps should take to fix this?

Mark the parent's AABB dirty when a component is mounted:

@override
void onMount() {
  super.onMount();
  _markWorldTransformDirty();
  if (parent case final Component3D parent) {
    parent.markAabbDirty();
  }
}

Doing the same in onRemove is optional. Without it the parent's AABB stays bigger than needed after a child is removed. That isn't visible and only makes culling less effective, but it keeps the bounds tight.

Marking is cheap since it stops at the first ancestor that is already dirty, and the parent's AABB is only recomputed lazily on the next read. This is the same cost as a child moving, which already marks its parents dirty.

Do have an example of where the bug occurs?
import 'package:flame/game.dart';
import 'package:flame_3d/camera.dart';
import 'package:flame_3d/components.dart';
import 'package:flame_3d/game.dart';
import 'package:flame_3d/graphics.dart';
import 'package:flame_3d/resources.dart';
import 'package:flutter/widgets.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await GpuBackend.initialize();
  runApp(GameWidget(game: ReproGame()));
}

class ReproGame extends FlameGame3D {
  ReproGame()
    : super(
        camera: CameraComponent3D(
          position: Vector3(0, 2, -6),
          target: Vector3(0, 1, 0),
        ),
      );

  @override
  Future<void> onLoad() async {
    world.addAll([
      // Red: not nested, world position (1.5, 1, 0). Rendered.
      MeshComponent(
        mesh: _cubeMesh(const Color(0xFFFF0000)),
        position: Vector3(1.5, 1, 0),
      ),
      // A group with a single child behind the camera. After one second a
      // green cube is added to it at world position (-1.5, 1, 0). The green
      // cube is never rendered.
      Spawner(
        initial: MeshComponent(
          mesh: _cubeMesh(const Color(0xFF0000FF)),
          position: Vector3(0, 1, -20),
        ),
        spawned: MeshComponent(
          mesh: _cubeMesh(const Color(0xFF00FF00)),
          position: Vector3(-1.5, 1, 0),
        ),
      ),
    ]);
  }

  Mesh _cubeMesh(Color color) => CuboidMesh(
    size: Vector3.all(1),
    material: UnlitMaterial(albedoColor: color),
  );
}

/// An [Object3D] group that adds [spawned] to itself after one second.
class Spawner extends Object3D {
  Spawner({required Component3D initial, required this.spawned})
    : super(children: [initial]);

  final Component3D spawned;
  double _elapsed = 0;

  @override
  void update(double dt) {
    super.update(dt);
    _elapsed += dt;
    if (_elapsed > 1 && spawned.parent == null) {
      add(spawned);
    }
  }

  @override
  void draw(RenderContext context) {}
}
Relevant log output
No errors or warnings are logged.
Execute in a terminal and put output into the code block below
Flutter 3.47.8 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 737ba8f885 (6 days ago) • 2026-10-22 21:37:31 -0700
Engine • hash dfd2e6cb7d319db05e3bf1503f3e2ac60f6288a2 (revision cf636b1dfb) (6 days ago) • 2026-10-23 02:13:38.000Z
Tools • Dart 3.13.7 • DevTools 2.60.0
Affected platforms

All

Other information

Reproduced on Windows (Impeller with the OpenGL ES backend), but the culling logic is platform independent.

Related to #4163, which is the other case of cached AABBs not being invalidated (when an ancestor moves). Both touch the same code in Component3D, but they can be reproduced and fixed independently.

If the parent is a plain Component3D the bug isn't visible, since only Object3D culls its subtree.

Are you interested in working on a PR for this?
  • I want to work on this
主要语言
Dart
星标
10.8k
派生
1.1k
平均合并
1 天 21 小时
30 天内合并 PR
52

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

flame-engine/flame 的其他 Issue

查看 flame-engine/flame 的全部 Issue

相似的 Issue

更多 Dart Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。