From dd7e424d05d24672a8ea00d3d62fbfa09527eb8e Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Mon, 23 Mar 2026 10:49:16 +0100 Subject: [PATCH 1/5] feat(flame_3d): Introduce `RenderContext` --- .../lib/scenarios/culling_scenario.dart | 2 +- packages/flame_3d/lib/graphics.dart | 2 + .../flame_3d/lib/src/camera/world_3d.dart | 24 +++-- .../lib/src/components/mesh_component.dart | 8 +- .../lib/src/components/object_3d.dart | 18 +--- .../lib/src/graphics/graphics_device.dart | 44 ++------- .../lib/src/graphics/render_context.dart | 7 ++ .../lib/src/graphics/render_context_3d.dart | 99 +++++++++++++++++++ .../lib/src/model/model_component.dart | 6 +- .../lib/src/resources/material/material.dart | 2 +- .../resources/material/spatial_material.dart | 37 +++---- .../flame_3d/lib/src/resources/mesh/mesh.dart | 6 +- 12 files changed, 157 insertions(+), 98 deletions(-) create mode 100644 packages/flame_3d/lib/src/graphics/render_context.dart create mode 100644 packages/flame_3d/lib/src/graphics/render_context_3d.dart diff --git a/packages/flame_3d/example/lib/scenarios/culling_scenario.dart b/packages/flame_3d/example/lib/scenarios/culling_scenario.dart index 585fece1398..b5a2823ee47 100644 --- a/packages/flame_3d/example/lib/scenarios/culling_scenario.dart +++ b/packages/flame_3d/example/lib/scenarios/culling_scenario.dart @@ -80,5 +80,5 @@ class _ObjectGroup extends Object3D { }) : super(children: children); @override - void bind(GraphicsDevice device) {} + void draw(RenderContext context) {} } diff --git a/packages/flame_3d/lib/graphics.dart b/packages/flame_3d/lib/graphics.dart index ab2eee25192..b7eb4070ee5 100644 --- a/packages/flame_3d/lib/graphics.dart +++ b/packages/flame_3d/lib/graphics.dart @@ -1 +1,3 @@ export 'src/graphics/graphics_device.dart'; +export 'src/graphics/render_context.dart'; +export 'src/graphics/render_context_3d.dart'; diff --git a/packages/flame_3d/lib/src/camera/world_3d.dart b/packages/flame_3d/lib/src/camera/world_3d.dart index d7ee43e12d7..570f780ef28 100644 --- a/packages/flame_3d/lib/src/camera/world_3d.dart +++ b/packages/flame_3d/lib/src/camera/world_3d.dart @@ -20,13 +20,13 @@ class World3D extends flame.World with flame.HasGameReference { super.children, super.priority, Color clearColor = const Color(0x00000000), - }) : device = GraphicsDevice(clearValue: clearColor) { + }) : context = RenderContext3D(GraphicsDevice(clearValue: clearColor)) { children.register(); } - /// The graphical device attached to this world. + /// The 3D render context attached to this world. @internal - final GraphicsDevice device; + final RenderContext3D context; Iterable get lights => children.query().map((component) => component.light); @@ -45,20 +45,18 @@ class World3D extends flame.World with flame.HasGameReference { viewport.virtualSize.y * devicePixelRatio, ); - device - // Set the view matrix - ..view.setFrom(camera.viewMatrix) - // Set the projection matrix - ..projection.setFrom(camera.projectionMatrix) - ..begin(size); + context + ..setCamera(camera.viewMatrix, camera.projectionMatrix) + ..device.begin(size); culled = 0; - _prepareDevice(); + _prepareContext(); // ignore: invalid_use_of_internal_member super.renderFromCamera(canvas); + context.flush(); - final image = device.end(); + final image = context.device.end(); canvas.drawImageRect( image, Offset.zero & size, @@ -70,8 +68,8 @@ class World3D extends flame.World with flame.HasGameReference { } // TODO(luan): consider making this a fixed-size array later - void _prepareDevice() { - device.lightingInfo.lights = lights; + void _prepareContext() { + context.lightingInfo.lights = lights; } // TODO(wolfenrain): this is only here for testing purposes diff --git a/packages/flame_3d/lib/src/components/mesh_component.dart b/packages/flame_3d/lib/src/components/mesh_component.dart index 8e831da0373..f763d1a0c5d 100644 --- a/packages/flame_3d/lib/src/components/mesh_component.dart +++ b/packages/flame_3d/lib/src/components/mesh_component.dart @@ -1,8 +1,8 @@ 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:flame_3d/src/camera/camera_component_3d.dart'; -import 'package:flame_3d/src/graphics/graphics_device.dart'; /// {@template mesh_component} /// An [Object3D] that renders a [Mesh] at the [position] with the [rotation] @@ -27,10 +27,10 @@ class MeshComponent extends Object3D { Aabb3? computeLocalAabb() => mesh.aabb; @override - void bind(GraphicsDevice device) { - device + void draw(covariant RenderContext3D context) { + context ..model.setFrom(worldTransformMatrix) - ..bindMesh(mesh); + ..drawMesh(mesh); } @override diff --git a/packages/flame_3d/lib/src/components/object_3d.dart b/packages/flame_3d/lib/src/components/object_3d.dart index 96c4ac03dc7..94c7270b899 100644 --- a/packages/flame_3d/lib/src/components/object_3d.dart +++ b/packages/flame_3d/lib/src/components/object_3d.dart @@ -10,8 +10,8 @@ import 'package:flame_3d/resources.dart'; /// {@template object_3d} /// [Object3D]s are the basic building blocks for a 3D [FlameGame]. /// -/// It is an object that is positioned in 3D space and can be bind to be -/// rendered by a [GraphicsDevice]. +/// It is an object that is positioned in 3D space and can be drawn by a +/// [RenderContext]. /// /// However, it has no visual representation of its own (except in /// debug mode). It is common, therefore, to derive from this class @@ -61,23 +61,13 @@ abstract class Object3D extends Component3D { _ancestorFullyInside = wasAncestorFullyInside; if (cullResult == CullResult.inside || shouldCull(camera!)) { - // We set the priority to the distance between the camera and the object. - // This ensures that our rendering is done in a specific order allowing - // for alpha blending. - // - // Note(wolfenrain): we should optimize this in the long run it currently - // sucks. - priority = -(CameraComponent3D.currentCamera!.position - position).length - .abs() - .toInt(); - - bind(world.device); + world.context.submitDraw(this, worldTransformMatrix); } else { world.culled++; } } - void bind(GraphicsDevice device); + void draw(RenderContext context); bool shouldCull(CameraComponent3D camera) { return camera.frustum.containsVector3(position); diff --git a/packages/flame_3d/lib/src/graphics/graphics_device.dart b/packages/flame_3d/lib/src/graphics/graphics_device.dart index caad366b071..b39c7996067 100644 --- a/packages/flame_3d/lib/src/graphics/graphics_device.dart +++ b/packages/flame_3d/lib/src/graphics/graphics_device.dart @@ -4,7 +4,6 @@ import 'dart:ui'; import 'package:flame_3d/game.dart'; import 'package:flame_3d/resources.dart'; import 'package:flame_3d/src/graphics/gpu_context_wrapper.dart'; -import 'package:flame_3d/src/graphics/joints_info.dart'; import 'package:flutter_gpu/gpu.dart' as gpu; enum BlendState { @@ -59,25 +58,8 @@ class GraphicsDevice { late gpu.RenderPass _renderPass; late gpu.RenderTarget _renderTarget; - Matrix4 get model => _modelMatrix; - final Matrix4 _modelMatrix = Matrix4.zero(); - - Matrix4 get view => _viewMatrix; - final Matrix4 _viewMatrix = Matrix4.zero(); - - Matrix4 get projection => _projectionMatrix; - final Matrix4 _projectionMatrix = Matrix4.zero(); - Size _previousSize = Size.zero; - /// Must be set by the rendering pipeline before elements are bound. - /// Can be accessed by elements in their bind method. - final JointsInfo jointsInfo = JointsInfo(); - - /// Must be set by the rendering pipeline before elements are bound. - /// Can be accessed by elements in their bind method. - final LightingInfo lightingInfo = LightingInfo(); - /// Begin a new rendering batch. /// /// After [begin] is called the graphics device can be used to bind resources @@ -126,16 +108,15 @@ class GraphicsDevice { _renderPass.clearBindings(); } - /// Bind a [mesh]. - void bindMesh(Mesh mesh) { - mesh.bind(this); + /// Bind a render [pipeline] with the given [cullMode]. + void bindPipeline(gpu.RenderPipeline pipeline, CullMode cullMode) { + _renderPass + ..bindPipeline(pipeline) + ..setCullMode(gpu.CullMode.values[cullMode.index]); } - /// Bind a [surface]. - void bindSurface(Surface surface) { - _renderPass.clearBindings(); - bindMaterial(surface.material); - + /// Bind a [surface]'s geometry (vertex/index buffers) and draw. + void bindGeometry(Surface surface) { _renderPass.bindVertexBuffer( gpu.BufferView( surface.resource!, @@ -158,17 +139,6 @@ class GraphicsDevice { _renderPass.draw(); } - /// Bind a [material] and set up the buffer correctly. - void bindMaterial(Material material) { - _renderPass - ..bindPipeline(material.resource) - ..setCullMode(gpu.CullMode.values[material.cullMode.index]); - - material.bind(this); - material.vertexShader.bind(this); - material.fragmentShader.bind(this); - } - /// Bind a uniform [slot] to the [buffer]. void bindUniform(gpu.UniformSlot slot, ByteBuffer buffer) { _renderPass.bindUniform(slot, _hostBuffer.emplace(buffer.asByteData())); diff --git a/packages/flame_3d/lib/src/graphics/render_context.dart b/packages/flame_3d/lib/src/graphics/render_context.dart new file mode 100644 index 00000000000..96e93bc52a3 --- /dev/null +++ b/packages/flame_3d/lib/src/graphics/render_context.dart @@ -0,0 +1,7 @@ +import 'package:flame_3d/graphics.dart'; + +abstract class RenderContext { + const RenderContext(this.device); + + final GraphicsDevice device; +} diff --git a/packages/flame_3d/lib/src/graphics/render_context_3d.dart b/packages/flame_3d/lib/src/graphics/render_context_3d.dart new file mode 100644 index 00000000000..4e41124567e --- /dev/null +++ b/packages/flame_3d/lib/src/graphics/render_context_3d.dart @@ -0,0 +1,99 @@ +import 'package:flame_3d/components.dart'; +import 'package:flame_3d/core.dart'; +import 'package:flame_3d/graphics.dart'; +import 'package:flame_3d/resources.dart'; +import 'package:flame_3d/src/graphics/joints_info.dart'; + +class RenderContext3D extends RenderContext { + RenderContext3D(super.device); + + Matrix4 get model => _modelMatrix; + final Matrix4 _modelMatrix = Matrix4.zero(); + + Matrix4 get view => _viewMatrix; + final Matrix4 _viewMatrix = Matrix4.zero(); + + Matrix4 get projection => _projectionMatrix; + final Matrix4 _projectionMatrix = Matrix4.zero(); + + /// Camera world-space position, precomputed from the view matrix. + /// Set once per frame via [setCamera], avoids repeated matrix inversions. + Vector3 get cameraPosition => _cameraPosition; + final Vector3 _cameraPosition = Vector3.zero(); + + final JointsInfo jointsInfo = JointsInfo(); + + final LightingInfo lightingInfo = LightingInfo(); + + final _drawPool = <_DrawEntry>[]; + int _drawCount = 0; + + /// Set camera matrices for this frame. + void setCamera(Matrix4 viewMatrix, Matrix4 projectionMatrix) { + _viewMatrix.setFrom(viewMatrix); + _projectionMatrix.setFrom(projectionMatrix); + _cameraPosition.setFrom( + Matrix4.inverted(viewMatrix).transform3(Vector3.zero()), + ); + } + + /// Submit a deferred draw. + void submitDraw(Object3D object, Matrix4 worldTransform) { + // Distance for each object in world space. We dont use + // Vector3 here as that would be unnecessarily allocating + // values. + final dx = _cameraPosition.x - worldTransform.storage[12]; + final dy = _cameraPosition.y - worldTransform.storage[13]; + final dz = _cameraPosition.z - worldTransform.storage[14]; + + if (_drawCount >= _drawPool.length) { + _drawPool.add(_DrawEntry()); + } + _drawPool[_drawCount] + ..distance = dx * dx + dy * dy + dz * dz + ..object = object; + _drawCount++; + } + + /// Sort and execute all deferred draws (back-to-front for alpha blending). + void flush() { + for (var i = 1; i < _drawCount; i++) { + final entry = _drawPool[i]; + var j = i - 1; + while (j >= 0 && _drawPool[j].distance < entry.distance) { + _drawPool[j + 1] = _drawPool[j]; + j--; + } + _drawPool[j + 1] = entry; + } + + // Draw and then release the objects in the draw pool. + for (var i = 0; i < _drawCount; i++) { + _drawPool[i].object!.draw(this); + _drawPool[i].object = null; + } + _drawCount = 0; + } + + void drawMesh(Mesh mesh) { + mesh.draw(this); + } + + void drawSurface(Surface surface) { + device.clearBindings(); + applyMaterial(surface.material); + device.bindGeometry(surface); + } + + void applyMaterial(Material material) { + device.bindPipeline(material.resource, material.cullMode); + material.apply(this); + material.vertexShader.bind(device); + material.fragmentShader.bind(device); + } +} + +class _DrawEntry { + double distance = 0; + Object3D? object; +} diff --git a/packages/flame_3d/lib/src/model/model_component.dart b/packages/flame_3d/lib/src/model/model_component.dart index c38dc770cc5..31d5cfca9c7 100644 --- a/packages/flame_3d/lib/src/model/model_component.dart +++ b/packages/flame_3d/lib/src/model/model_component.dart @@ -24,7 +24,7 @@ class ModelComponent extends Object3D { Aabb3? computeLocalAabb() => model.aabb; @override - void bind(GraphicsDevice device) { + void draw(covariant RenderContext3D context) { final nodes = model.processNodes(_animation); for (final MapEntry(key: index, value: node) in nodes.entries) { if (_hiddenNodes.contains(index)) { @@ -33,12 +33,12 @@ class ModelComponent extends Object3D { final mesh = node.node.mesh; if (mesh != null) { - device + context ..jointsInfo.jointTransformsPerSurface = node.jointTransforms ..model.setFrom( worldTransformMatrix.multiplied(node.combinedTransform), ) - ..bindMesh(mesh); + ..drawMesh(mesh); } } } diff --git a/packages/flame_3d/lib/src/resources/material/material.dart b/packages/flame_3d/lib/src/resources/material/material.dart index 8b73d34c2ef..10e65dae9a1 100644 --- a/packages/flame_3d/lib/src/resources/material/material.dart +++ b/packages/flame_3d/lib/src/resources/material/material.dart @@ -44,5 +44,5 @@ abstract class Material extends Resource { /// Face culling mode for this material. Defaults to [CullMode.none]. CullMode cullMode = CullMode.none; - void bind(GraphicsDevice device) {} + void apply(RenderContext context) {} } diff --git a/packages/flame_3d/lib/src/resources/material/spatial_material.dart b/packages/flame_3d/lib/src/resources/material/spatial_material.dart index 6953ba2e6fa..ff7cbdec61f 100644 --- a/packages/flame_3d/lib/src/resources/material/spatial_material.dart +++ b/packages/flame_3d/lib/src/resources/material/spatial_material.dart @@ -1,6 +1,5 @@ import 'dart:ui'; -import 'package:flame_3d/game.dart'; import 'package:flame_3d/graphics.dart'; import 'package:flame_3d/resources.dart'; @@ -54,22 +53,22 @@ class SpatialMaterial extends Material { double roughness; @override - void bind(GraphicsDevice device) { - _bindVertexInfo(device); - _bindJointMatrices(device); - _bindMaterial(device); - _bindCamera(device); + void apply(covariant RenderContext3D context) { + _bindVertexInfo(context); + _bindJointMatrices(context); + _bindMaterial(context); + _bindCamera(context); } - void _bindVertexInfo(GraphicsDevice device) { + void _bindVertexInfo(RenderContext3D context) { vertexShader - ..setMatrix4('VertexInfo.model', device.model) - ..setMatrix4('VertexInfo.view', device.view) - ..setMatrix4('VertexInfo.projection', device.projection); + ..setMatrix4('VertexInfo.model', context.model) + ..setMatrix4('VertexInfo.view', context.view) + ..setMatrix4('VertexInfo.projection', context.projection); } - void _bindJointMatrices(GraphicsDevice device) { - final jointTransforms = device.jointsInfo.jointTransforms; + void _bindJointMatrices(RenderContext3D context) { + final jointTransforms = context.jointsInfo.jointTransforms; if (jointTransforms.length > _maxJoints) { throw Exception( 'At most $_maxJoints joints per surface are supported;' @@ -81,8 +80,8 @@ class SpatialMaterial extends Material { } } - void _bindMaterial(GraphicsDevice device) { - _applyLights(device); + void _bindMaterial(RenderContext3D context) { + context.lightingInfo.apply(fragmentShader); fragmentShader ..setTexture('albedoTexture', albedoTexture) ..setColor('Material.albedoColor', albedoColor) @@ -90,14 +89,8 @@ class SpatialMaterial extends Material { ..setFloat('Material.roughness', roughness); } - void _bindCamera(GraphicsDevice device) { - final invertedView = Matrix4.inverted(device.view); - final cameraPosition = invertedView.transform3(Vector3.zero()); - fragmentShader.setVector3('Camera.position', cameraPosition); - } - - void _applyLights(GraphicsDevice device) { - device.lightingInfo.apply(fragmentShader); + void _bindCamera(RenderContext3D context) { + fragmentShader.setVector3('Camera.position', context.cameraPosition); } static const _maxJoints = 16; diff --git a/packages/flame_3d/lib/src/resources/mesh/mesh.dart b/packages/flame_3d/lib/src/resources/mesh/mesh.dart index f5b06f23732..6ab2a753820 100644 --- a/packages/flame_3d/lib/src/resources/mesh/mesh.dart +++ b/packages/flame_3d/lib/src/resources/mesh/mesh.dart @@ -23,10 +23,10 @@ class Mesh extends Resource { int get vertexCount => _surfaces.fold(0, (p, e) => p + e.vertexCount); - void bind(GraphicsDevice device) { + void draw(RenderContext3D context) { for (final (index, surface) in _surfaces.indexed) { - device.jointsInfo.setSurface(index); - device.bindSurface(surface); + context.jointsInfo.setSurface(index); + context.drawSurface(surface); } } From c93cb8e3457a9d47edc1ff5105eac766c2b7079c Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Thu, 26 Mar 2026 09:47:00 +0100 Subject: [PATCH 2/5] Merge branch 'main' of github.com:flame-engine/flame into feat(flame_3d)/introduce-render-context --- packages/flame_3d/lib/src/graphics/render_context_3d.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/flame_3d/lib/src/graphics/render_context_3d.dart b/packages/flame_3d/lib/src/graphics/render_context_3d.dart index 4e41124567e..69d3a037815 100644 --- a/packages/flame_3d/lib/src/graphics/render_context_3d.dart +++ b/packages/flame_3d/lib/src/graphics/render_context_3d.dart @@ -23,7 +23,9 @@ class RenderContext3D extends RenderContext { final JointsInfo jointsInfo = JointsInfo(); - final LightingInfo lightingInfo = LightingInfo(); + /// Must be set by the rendering pipeline before elements are bound. + /// Can be accessed by elements in their bind method. + Iterable lights = []; final _drawPool = <_DrawEntry>[]; int _drawCount = 0; From f0dd04cd29eafb542c964cc571cb86fa628c5c61 Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Thu, 26 Mar 2026 21:34:12 +0100 Subject: [PATCH 3/5] feat(flame_3d): Introduce `RenderContext` --- packages/flame_3d/lib/game.dart | 1 + .../flame_3d/lib/src/camera/world_3d.dart | 41 ++-- .../flame_3d/lib/src/game/flame_game_3d.dart | 8 +- packages/flame_3d/lib/src/game/game_3d.dart | 20 ++ .../lib/src/graphics/graphics_device.dart | 195 ++++++++++++------ 5 files changed, 179 insertions(+), 86 deletions(-) create mode 100644 packages/flame_3d/lib/src/game/game_3d.dart diff --git a/packages/flame_3d/lib/game.dart b/packages/flame_3d/lib/game.dart index 71ecd61de4b..769cbb25e7f 100644 --- a/packages/flame_3d/lib/game.dart +++ b/packages/flame_3d/lib/game.dart @@ -1,5 +1,6 @@ export 'core.dart'; export 'src/game/flame_game_3d.dart'; +export 'src/game/game_3d.dart'; export 'src/game/notifying_quaternion.dart'; export 'src/game/notifying_vector3.dart'; export 'src/game/transform_3d.dart'; diff --git a/packages/flame_3d/lib/src/camera/world_3d.dart b/packages/flame_3d/lib/src/camera/world_3d.dart index 6cb6a999217..36c3077ed4c 100644 --- a/packages/flame_3d/lib/src/camera/world_3d.dart +++ b/packages/flame_3d/lib/src/camera/world_3d.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flame/components.dart' as flame; 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' show MediaQuery; @@ -14,20 +15,17 @@ import 'package:meta/meta.dart'; /// The primary feature of this component is that it allows [Component3D]s to /// render directly to a [GraphicsDevice] instead of the regular rendering. /// {@endtemplate} -class World3D extends flame.World with flame.HasGameReference { +class World3D extends flame.World with flame.HasGameReference { /// {@macro world_3d} World3D({ super.children, super.priority, - Color clearColor = const Color(0x00000000), - }) : context = RenderContext3D(GraphicsDevice(clearValue: clearColor)); - - /// The 3D render context attached to this world. - @internal - final RenderContext3D context; + }); final List _lights = []; + RenderContext3D get context => game.context; + /// Register a [light] with this world. @internal void addLight(Light light) => _lights.add(light); @@ -36,37 +34,30 @@ class World3D extends flame.World with flame.HasGameReference { @internal void removeLight(Light light) => _lights.remove(light); - final _paint = Paint(); - @internal @override void renderFromCamera(Canvas canvas) { + culled = 0; + final camera = CameraComponent3D.currentCamera!; - final viewport = camera.viewport; + final Viewport(virtualSize: size) = camera.viewport; - final devicePixelRatio = MediaQuery.of(game.buildContext!).devicePixelRatio; - final size = Size( - viewport.virtualSize.x * devicePixelRatio, - viewport.virtualSize.y * devicePixelRatio, - ); + final pixelRatio = MediaQuery.devicePixelRatioOf(game.buildContext!); + final renderSize = Size(size.x * pixelRatio, size.y * pixelRatio); context ..lights = _lights - ..setCamera(camera.viewMatrix, camera.projectionMatrix) - ..device.begin(size); + ..setCamera(camera.viewMatrix, camera.projectionMatrix); - culled = 0; - - // ignore: invalid_use_of_internal_member + game.device.beginPass(renderSize); super.renderFromCamera(canvas); context.flush(); - final image = context.device.end(); + final image = game.device.endPass(); canvas.drawImageRect( image, - Offset.zero & size, - (-viewport.virtualSize / 2).toOffset() & - Size(viewport.virtualSize.x, viewport.virtualSize.y), + Offset.zero & renderSize, + Offset(-size.x / 2, -size.y / 2) & Size(size.x, size.y), _paint, ); image.dispose(); @@ -74,4 +65,6 @@ class World3D extends flame.World with flame.HasGameReference { // TODO(wolfenrain): this is only here for testing purposes int culled = 0; + + static final _paint = Paint(); } diff --git a/packages/flame_3d/lib/src/game/flame_game_3d.dart b/packages/flame_3d/lib/src/game/flame_game_3d.dart index db66b3c8b9d..724364f7e17 100644 --- a/packages/flame_3d/lib/src/game/flame_game_3d.dart +++ b/packages/flame_3d/lib/src/game/flame_game_3d.dart @@ -1,16 +1,16 @@ -import 'dart:ui'; - import 'package:flame/game.dart'; import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/game.dart'; class FlameGame3D - extends FlameGame { + extends FlameGame + with Game3D { FlameGame3D({ super.children, W? world, C? camera, }) : super( - world: world ?? World3D(clearColor: const Color(0xFFFFFFFF)) as W, + world: world ?? World3D() as W, camera: camera ?? CameraComponent3D() as C, ); diff --git a/packages/flame_3d/lib/src/game/game_3d.dart b/packages/flame_3d/lib/src/game/game_3d.dart new file mode 100644 index 00000000000..8f1a2e19622 --- /dev/null +++ b/packages/flame_3d/lib/src/game/game_3d.dart @@ -0,0 +1,20 @@ +import 'dart:ui'; + +import 'package:flame/game.dart'; +import 'package:flame_3d/graphics.dart'; +import 'package:meta/meta.dart'; + +mixin Game3D on Game { + @internal + final GraphicsDevice device = GraphicsDevice(); + + @internal + late final RenderContext3D context = RenderContext3D(device); + + @override + void render(Canvas canvas) { + device.begin(); + super.render(canvas); + device.end(); + } +} diff --git a/packages/flame_3d/lib/src/graphics/graphics_device.dart b/packages/flame_3d/lib/src/graphics/graphics_device.dart index be9cb422a2b..6874e3edb05 100644 --- a/packages/flame_3d/lib/src/graphics/graphics_device.dart +++ b/packages/flame_3d/lib/src/graphics/graphics_device.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'dart:typed_data'; import 'dart:ui'; @@ -6,15 +8,30 @@ import 'package:flame_3d/resources.dart'; import 'package:flame_3d/src/graphics/gpu_context_wrapper.dart'; import 'package:flutter_gpu/gpu.dart' as gpu; +/// Color blending mode for the render pass output. enum BlendState { - additive, - alphaBlend, + /// No blending, source overwrites destination. opaque, + + /// Standard premultiplied-alpha blending: + /// `color = src * 1 + dst * (1 - srcAlpha)` + alphaBlend, + + /// Additive blending: `color = src * 1 + dst * 1`. + additive, } +/// Depth buffer behavior for the render pass. enum DepthStencilState { + /// Depth test and write enabled (normal opaque 3D rendering). + /// Fragments closer than the stored depth pass and update the buffer. standard, + + /// Depth test enabled, write disabled (transparent objects). + /// Fragments are tested against existing depth but don't update it. depthRead, + + /// No depth testing, all fragments pass regardless of depth. none, } @@ -37,9 +54,9 @@ enum CullMode { /// The Graphical Device provides a way for developers to interact with the GPU /// by binding different resources to it. /// -/// A single render call starts with a call to [begin] and only ends when [end] -/// is called. Any resource that gets bound to the device in between these two -/// method calls will be uploaded to the GPU and returns as an [Image] in [end]. +/// A frame starts with [begin] and ends with [end]. Within a frame, one or more +/// render passes can be created with [beginPass]/[endPass], each producing an +/// [Image]. /// {@endtemplate} class GraphicsDevice { /// {@macro graphics_device} @@ -53,53 +70,103 @@ class GraphicsDevice { /// The clear value, used to clear out the screen. final Color clearValue; + /// Blend state applied to each render pass. + BlendState blendState = BlendState.alphaBlend; + + /// Depth/stencil state applied to each render pass. + DepthStencilState depthStencilState = DepthStencilState.depthRead; + + late final gpu.HostBuffer _hostBuffer = _gpuContext.createHostBuffer(); late gpu.CommandBuffer _commandBuffer; - late gpu.HostBuffer _hostBuffer; + late gpu.RenderPass _renderPass; late gpu.RenderTarget _renderTarget; - Size _previousSize = Size.zero; + // Render target pool: keyed by (width, height), each entry is a list of + // targets. _poolIndex tracks how many have been handed out this frame. + final _pool = <(int, int), List>{}; + final _poolIndex = <(int, int), int>{}; + + /// Begin a new frame. + /// + /// Creates the host buffer shared across all render passes this frame. + /// Call [end] when the frame is complete. + void begin() { + // Reset the pool indices, each existing target becomes available again. + _poolIndex.updateAll((_, __) => 0); + } + + /// End the frame. + void end() { + _hostBuffer.reset(); + + // Ensure stale render targets are removed from the pool by checking + // how many targets were actually used in the last render. + _pool.removeWhere((key, targets) { + final used = _poolIndex[key] ?? 0; + if (used == 0) { + _poolIndex.remove(key); + return true; + } + if (used < targets.length) { + targets.removeRange(used, targets.length); + } + return false; + }); + } - /// Begin a new rendering batch. + /// Begin a render pass at the given [size]. + /// + /// Uses the current [blendState] and [depthStencilState]. After [beginPass] + /// is called, resources can be bound. Call [endPass] to finalize the pass + /// and obtain the rendered [Image]. + /// /// /// After [begin] is called the graphics device can be used to bind resources /// like [Mesh]s, [Material]s and [Texture]s. /// /// Once you have executed all your bindings you can submit the batch to the /// GPU with [end]. - void begin( - Size size, { - // TODO(wolfenrain): unused at the moment - BlendState blendState = BlendState.alphaBlend, - // TODO(wolfenrain): used incorrectly - DepthStencilState depthStencilState = DepthStencilState.depthRead, - }) { + void beginPass(Size size) { _commandBuffer = _gpuContext.createCommandBuffer(); - _hostBuffer = _gpuContext.createHostBuffer(); - - _renderPass = _commandBuffer.createRenderPass(_getRenderTarget(size)) + _renderTarget = _acquireRenderTarget(size); + _renderPass = _commandBuffer.createRenderPass(_renderTarget) ..setWindingOrder(gpu.WindingOrder.counterClockwise) - ..setColorBlendEnable(true) + ..setColorBlendEnable(blendState != BlendState.opaque) ..setColorBlendEquation( - gpu.ColorBlendEquation( - destinationAlphaBlendFactor: blendState == BlendState.alphaBlend - ? gpu.BlendFactor.oneMinusSourceAlpha - : gpu.BlendFactor.zero, - ), + switch (blendState) { + BlendState.opaque => gpu.ColorBlendEquation( + sourceColorBlendFactor: gpu.BlendFactor.one, + destinationColorBlendFactor: gpu.BlendFactor.zero, + sourceAlphaBlendFactor: gpu.BlendFactor.one, + destinationAlphaBlendFactor: gpu.BlendFactor.zero, + ), + BlendState.alphaBlend => gpu.ColorBlendEquation( + sourceColorBlendFactor: gpu.BlendFactor.one, + destinationColorBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha, + sourceAlphaBlendFactor: gpu.BlendFactor.one, + destinationAlphaBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha, + ), + BlendState.additive => gpu.ColorBlendEquation( + sourceColorBlendFactor: gpu.BlendFactor.one, + destinationColorBlendFactor: gpu.BlendFactor.one, + sourceAlphaBlendFactor: gpu.BlendFactor.one, + destinationAlphaBlendFactor: gpu.BlendFactor.one, + ), + }, ) - ..setDepthWriteEnable(depthStencilState == DepthStencilState.depthRead) + ..setDepthWriteEnable(depthStencilState == DepthStencilState.standard) ..setDepthCompareOperation( switch (depthStencilState) { - DepthStencilState.none => gpu.CompareFunction.never, - DepthStencilState.standard => gpu.CompareFunction.lessEqual, - DepthStencilState.depthRead => gpu.CompareFunction.less, + DepthStencilState.standard => gpu.CompareFunction.less, + DepthStencilState.depthRead => gpu.CompareFunction.lessEqual, + DepthStencilState.none => gpu.CompareFunction.always, }, ); } - /// Submit the rendering batch and it's the commands to the GPU and return - /// the result. - Image end() { + /// Submit the render pass to the GPU and return the rendered [Image]. + Image endPass() { _commandBuffer.submit(); return _renderTarget.colorAttachments[0].texture.asImage(); } @@ -149,36 +216,48 @@ class GraphicsDevice { _renderPass.bindTexture(slot, texture.resource); } - gpu.RenderTarget _getRenderTarget(Size size) { - if (_previousSize != size) { - _previousSize = size; + /// Acquire a render target from the pool, creating one if needed. + /// + /// Multiple passes at the same size within a frame get distinct targets. + /// Targets are reused across frames. + gpu.RenderTarget _acquireRenderTarget(Size size) { + final key = (size.width.toInt(), size.height.toInt()); + final targets = _pool.putIfAbsent(key, () => []); + final index = _poolIndex.putIfAbsent(key, () => 0); - final gpuContext = GpuContextWrapper(_gpuContext); - final colorTexture = gpuContext.createTexture( - gpu.StorageMode.devicePrivate, - size.width.toInt(), - size.height.toInt(), - ); + if (index < targets.length) { + _poolIndex[key] = index + 1; + return targets[index]; + } - final depthTexture = gpuContext.createTexture( - gpu.StorageMode.deviceTransient, - size.width.toInt(), - size.height.toInt(), - format: _gpuContext.defaultDepthStencilFormat, - ); + // Pool exhausted for this size — create a new target. + final gpuContext = GpuContextWrapper(_gpuContext); + final colorTexture = gpuContext.createTexture( + gpu.StorageMode.devicePrivate, + key.$1, + key.$2, + ); - _renderTarget = gpu.RenderTarget.singleColor( - gpu.ColorAttachment( - texture: colorTexture, - clearValue: Vector4Utils.fromColor(clearValue), - ), - depthStencilAttachment: gpu.DepthStencilAttachment( - texture: depthTexture, - depthClearValue: 1.0, - ), - ); - } + final depthTexture = gpuContext.createTexture( + gpu.StorageMode.deviceTransient, + key.$1, + key.$2, + format: _gpuContext.defaultDepthStencilFormat, + ); + + final target = gpu.RenderTarget.singleColor( + gpu.ColorAttachment( + texture: colorTexture, + clearValue: Vector4Utils.fromColor(clearValue), + ), + depthStencilAttachment: gpu.DepthStencilAttachment( + texture: depthTexture, + depthClearValue: 1.0, + ), + ); - return _renderTarget; + targets.add(target); + _poolIndex[key] = index + 1; + return target; } } From 6b65757ce4639985155b8eaa90da784ed854390e Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Thu, 26 Mar 2026 21:35:06 +0100 Subject: [PATCH 4/5] feat(flame_3d): Introduce `RenderContext` --- packages/flame_3d/example/lib/example_game_3d.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flame_3d/example/lib/example_game_3d.dart b/packages/flame_3d/example/lib/example_game_3d.dart index 1183e4cee69..432e69b5e6c 100644 --- a/packages/flame_3d/example/lib/example_game_3d.dart +++ b/packages/flame_3d/example/lib/example_game_3d.dart @@ -17,7 +17,7 @@ class ExampleGame3D extends FlameGame3D ExampleGame3D() : super( - world: World3D(clearColor: const Color(0xFFFFFFFF)), + world: World3D(), camera: ExampleCamera3D(), ); From fd3d22c294b72b6e414ecf832f3d1a6c39866fba Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Thu, 26 Mar 2026 23:44:18 +0100 Subject: [PATCH 5/5] feat(flame_3d): Introduce `RenderContext` --- packages/flame_3d/example/lib/components/simple_hud.dart | 2 +- packages/flame_3d/lib/src/camera/world_3d.dart | 5 ----- packages/flame_3d/lib/src/components/mesh_component.dart | 2 +- packages/flame_3d/lib/src/components/object_3d.dart | 7 ++----- packages/flame_3d/lib/src/graphics/render_context_3d.dart | 4 ++++ packages/flame_3d/lib/src/model/model_component.dart | 2 +- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/flame_3d/example/lib/components/simple_hud.dart b/packages/flame_3d/example/lib/components/simple_hud.dart index 354b13a7b0b..ad0bb77177e 100644 --- a/packages/flame_3d/example/lib/components/simple_hud.dart +++ b/packages/flame_3d/example/lib/components/simple_hud.dart @@ -62,7 +62,7 @@ Camera controls: ''' FPS: $fps Projection: ${game.camera.projection.name} -Culled: ${game.world.culled} +Draw: ${game.world.context.drawCount} Position: ${position.x.toStringAsFixed(2)}, ${position.y.toStringAsFixed(2)}, ${position.z.toStringAsFixed(2)} Target: ${target.x.toStringAsFixed(2)}, ${target.y.toStringAsFixed(2)}, ${target.z.toStringAsFixed(2)} diff --git a/packages/flame_3d/lib/src/camera/world_3d.dart b/packages/flame_3d/lib/src/camera/world_3d.dart index 36c3077ed4c..2a0f4e5252f 100644 --- a/packages/flame_3d/lib/src/camera/world_3d.dart +++ b/packages/flame_3d/lib/src/camera/world_3d.dart @@ -37,8 +37,6 @@ class World3D extends flame.World with flame.HasGameReference { @internal @override void renderFromCamera(Canvas canvas) { - culled = 0; - final camera = CameraComponent3D.currentCamera!; final Viewport(virtualSize: size) = camera.viewport; @@ -63,8 +61,5 @@ class World3D extends flame.World with flame.HasGameReference { image.dispose(); } - // TODO(wolfenrain): this is only here for testing purposes - int culled = 0; - static final _paint = Paint(); } diff --git a/packages/flame_3d/lib/src/components/mesh_component.dart b/packages/flame_3d/lib/src/components/mesh_component.dart index fcdc766a76c..83d5397b084 100644 --- a/packages/flame_3d/lib/src/components/mesh_component.dart +++ b/packages/flame_3d/lib/src/components/mesh_component.dart @@ -35,7 +35,7 @@ class MeshComponent extends Object3D { } @override - bool shouldCull(CameraComponent3D camera) { + bool isVisible(CameraComponent3D camera) { return camera.frustum.intersectsWithAabb3(aabb); } } diff --git a/packages/flame_3d/lib/src/components/object_3d.dart b/packages/flame_3d/lib/src/components/object_3d.dart index 94c7270b899..aaf48d4075f 100644 --- a/packages/flame_3d/lib/src/components/object_3d.dart +++ b/packages/flame_3d/lib/src/components/object_3d.dart @@ -48,7 +48,6 @@ abstract class Object3D extends Component3D { // Result is fully outside, skip children and self. if (cullResult == CullResult.outside) { - world.culled++; return; } @@ -60,16 +59,14 @@ abstract class Object3D extends Component3D { super.renderTree(canvas); _ancestorFullyInside = wasAncestorFullyInside; - if (cullResult == CullResult.inside || shouldCull(camera!)) { + if (cullResult == CullResult.inside || isVisible(camera!)) { world.context.submitDraw(this, worldTransformMatrix); - } else { - world.culled++; } } void draw(RenderContext context); - bool shouldCull(CameraComponent3D camera) { + bool isVisible(CameraComponent3D camera) { return camera.frustum.containsVector3(position); } } diff --git a/packages/flame_3d/lib/src/graphics/render_context_3d.dart b/packages/flame_3d/lib/src/graphics/render_context_3d.dart index 69d3a037815..344cfa34b70 100644 --- a/packages/flame_3d/lib/src/graphics/render_context_3d.dart +++ b/packages/flame_3d/lib/src/graphics/render_context_3d.dart @@ -30,6 +30,9 @@ class RenderContext3D extends RenderContext { final _drawPool = <_DrawEntry>[]; int _drawCount = 0; + int get drawCount => _lastDrawCount; + int _lastDrawCount = 0; + /// Set camera matrices for this frame. void setCamera(Matrix4 viewMatrix, Matrix4 projectionMatrix) { _viewMatrix.setFrom(viewMatrix); @@ -74,6 +77,7 @@ class RenderContext3D extends RenderContext { _drawPool[i].object!.draw(this); _drawPool[i].object = null; } + _lastDrawCount = _drawCount; _drawCount = 0; } diff --git a/packages/flame_3d/lib/src/model/model_component.dart b/packages/flame_3d/lib/src/model/model_component.dart index 01d41976d60..3acd15e2c45 100644 --- a/packages/flame_3d/lib/src/model/model_component.dart +++ b/packages/flame_3d/lib/src/model/model_component.dart @@ -82,7 +82,7 @@ class ModelComponent extends Object3D { } @override - bool shouldCull(CameraComponent3D camera) { + bool isVisible(CameraComponent3D camera) { // TODO(luan): this actually does not work because of animations // it might end up culling something that is actually visible return camera.frustum.intersectsWithAabb3(aabb);