diff --git a/.github/.cspell/gamedev_dictionary.txt b/.github/.cspell/gamedev_dictionary.txt index df93f810d8d..3471e8d69c3 100644 --- a/.github/.cspell/gamedev_dictionary.txt +++ b/.github/.cspell/gamedev_dictionary.txt @@ -24,6 +24,7 @@ goldens # test files used as reference for Golden Tests hitbox # the collision box around objects for the purposes of collision detection hitboxes # plural of hitbox ints # short for integers +impellerc # Flutter's impeller compiler jank # stutter or inconsistent gap or timing lerp # short for linear interpolation LTRB # left top right bottom @@ -49,8 +50,10 @@ respawn # when the player character dies and is brought back after some time and retarget # to direct (something) toward a different target RGBA # red green blue alpha RGBO # red green blue opacity +BGRA # blue green red alpha rrect # rounded rect scos # cosine of a rotation multiplied by the scale factor +shaderbundle # a file extension used to bundle shaders for GLSL spritesheet # a single image packing multiple sprites, normally in a grid ssin # sine of a rotation multiplied by the scale factor stylesheet # name of a CSS style file @@ -58,6 +61,7 @@ subfolders # plural of subfolders sublist # any sub-set of elements of a given list sublists # plural of sublist subrange # a range entirely contained on a given range +struct # a type of data model in programming that aggregates fields SVGs # plural of SVG tileset # image with a collection of tiles. in games, tiles are small square sprites laid out in a grid to form the game map tilesets # plural of tileset diff --git a/.github/.cspell/people_usernames.txt b/.github/.cspell/people_usernames.txt index a8155a58550..aac65df45da 100644 --- a/.github/.cspell/people_usernames.txt +++ b/.github/.cspell/people_usernames.txt @@ -1,5 +1,6 @@ # specific people's names and/or usernames akida # github.com/akida +bdero # github.com/bdero bluefireteam # github.com/bluefireteam erayzesen # erayzesen.itch.io erickzanardo # github.com/erickzanardo diff --git a/packages/flame_3d/assets/shaders/spatial_material.shaderbundle b/packages/flame_3d/assets/shaders/spatial_material.shaderbundle new file mode 100644 index 00000000000..fec0fef819e Binary files /dev/null and b/packages/flame_3d/assets/shaders/spatial_material.shaderbundle differ diff --git a/packages/flame_3d/assets/shaders/standard_material.shaderbundle b/packages/flame_3d/assets/shaders/standard_material.shaderbundle index 37d2f9e01b7..22c8f76a0f0 100644 Binary files a/packages/flame_3d/assets/shaders/standard_material.shaderbundle and b/packages/flame_3d/assets/shaders/standard_material.shaderbundle differ diff --git a/packages/flame_3d/bin/build_shaders.dart b/packages/flame_3d/bin/build_shaders.dart index 0cefe877f75..bf5aac83902 100644 --- a/packages/flame_3d/bin/build_shaders.dart +++ b/packages/flame_3d/bin/build_shaders.dart @@ -10,10 +10,21 @@ import 'dart:io'; /// /// Note: this script should be run from the root of the package: /// packages/flame_3d -void main() async { +void main(List arguments) async { final root = Directory.current; - final assets = Directory.fromUri(root.uri.resolve('assets/shaders')); + final shaders = Directory.fromUri(root.uri.resolve('shaders')); + + await compute(assets, shaders); + if (arguments.contains('watch')) { + stdout.writeln('Running in watch mode'); + shaders.watch(recursive: true).listen((event) { + compute(assets, shaders); + }); + } +} + +Future compute(Directory assets, Directory shaders) async { // Delete all the bundled shaders so we can replace them with new ones. if (assets.existsSync()) { assets.deleteSync(recursive: true); @@ -21,8 +32,6 @@ void main() async { // Create if not exists. assets.createSync(recursive: true); - // Directory where our unbundled shaders are stored. - final shaders = Directory.fromUri(root.uri.resolve('shaders')); if (!shaders.existsSync()) { return stderr.writeln('Missing shader directory'); } @@ -39,14 +48,15 @@ void main() async { final bundle = { 'TextureFragment': { 'type': 'fragment', - 'file': '${root.path}/shaders/$name.frag', + 'file': '${shaders.path}/$name.frag', }, 'TextureVertex': { 'type': 'vertex', - 'file': '${root.path}/shaders/$name.vert', + 'file': '${shaders.path}/$name.vert', }, }; + stdout.writeln('Computing shader "$name"'); final result = await Process.run(impellerC, [ '--sl=${assets.path}/$name.shaderbundle', '--shader-bundle=${jsonEncode(bundle)}', diff --git a/packages/flame_3d/example/.metadata b/packages/flame_3d/example/.metadata index a2eed5f2b7c..f4bb071e916 100644 --- a/packages/flame_3d/example/.metadata +++ b/packages/flame_3d/example/.metadata @@ -4,8 +4,8 @@ # This file should be version controlled and should not be manually edited. version: - revision: "1b197762c51e993cb77d7fafe9729ef2506e2bf7" - channel: "beta" + revision: "bcdd1b2c481bca0647beff690238efaae68ca5ac" + channel: "[user-branch]" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - base_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - - platform: android - create_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - base_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 + create_revision: bcdd1b2c481bca0647beff690238efaae68ca5ac + base_revision: bcdd1b2c481bca0647beff690238efaae68ca5ac - platform: ios - create_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - base_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - - platform: linux - create_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - base_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - - platform: macos - create_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - base_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - - platform: web - create_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - base_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - - platform: windows - create_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 - base_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 + create_revision: bcdd1b2c481bca0647beff690238efaae68ca5ac + base_revision: bcdd1b2c481bca0647beff690238efaae68ca5ac # User provided section diff --git a/packages/flame_3d/example/lib/crate.dart b/packages/flame_3d/example/lib/crate.dart index c0f66cef3ea..2721acab3d8 100644 --- a/packages/flame_3d/example/lib/crate.dart +++ b/packages/flame_3d/example/lib/crate.dart @@ -15,7 +15,7 @@ class Crate extends MeshComponent { FutureOr onLoad() async { final crateTexture = await Flame.images.loadTexture('crate.jpg'); mesh.updateSurfaces((surfaces) { - surfaces[0].material = StandardMaterial( + surfaces[0].material = SpatialMaterial( albedoTexture: crateTexture, ); }); diff --git a/packages/flame_3d/example/lib/main.dart b/packages/flame_3d/example/lib/main.dart index b7a247a4a06..4a0a8658bc0 100644 --- a/packages/flame_3d/example/lib/main.dart +++ b/packages/flame_3d/example/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:example/crate.dart'; import 'package:example/keyboard_controlled_camera.dart'; import 'package:example/player_box.dart'; +import 'package:example/rotating_light.dart'; import 'package:example/simple_hud.dart'; import 'package:flame/events.dart'; import 'package:flame/extensions.dart' as v64 show Vector2; @@ -35,6 +36,8 @@ class ExampleGame3D extends FlameGame @override FutureOr onLoad() async { world.addAll([ + RotatingLight(), + // Add a player box PlayerBox(), @@ -46,7 +49,7 @@ class ExampleGame3D extends FlameGame position: Vector3(5, 5, 5), mesh: SphereMesh( radius: 1, - material: StandardMaterial( + material: SpatialMaterial( albedoTexture: ColorTexture(Colors.purple), ), ), @@ -56,7 +59,7 @@ class ExampleGame3D extends FlameGame MeshComponent( mesh: PlaneMesh( size: Vector2(32, 32), - material: StandardMaterial(albedoTexture: ColorTexture(Colors.grey)), + material: SpatialMaterial(albedoTexture: ColorTexture(Colors.grey)), ), ), @@ -65,8 +68,7 @@ class ExampleGame3D extends FlameGame position: Vector3(16.5, 2.5, 0), mesh: CuboidMesh( size: Vector3(1, 5, 32), - material: - StandardMaterial(albedoTexture: ColorTexture(Colors.yellow)), + material: SpatialMaterial(albedoTexture: ColorTexture(Colors.yellow)), ), ), @@ -75,7 +77,7 @@ class ExampleGame3D extends FlameGame position: Vector3(0, 2.5, 16.5), mesh: CuboidMesh( size: Vector3(32, 5, 1), - material: StandardMaterial(albedoTexture: ColorTexture(Colors.blue)), + material: SpatialMaterial(albedoTexture: ColorTexture(Colors.blue)), ), ), @@ -84,7 +86,7 @@ class ExampleGame3D extends FlameGame position: Vector3(0, 2.5, -16.5), mesh: CuboidMesh( size: Vector3(32, 5, 1), - material: StandardMaterial(albedoTexture: ColorTexture(Colors.lime)), + material: SpatialMaterial(albedoTexture: ColorTexture(Colors.lime)), ), ), ]); @@ -98,7 +100,7 @@ class ExampleGame3D extends FlameGame position: Vector3(rnd.range(-15, 15), height / 2, rnd.range(-15, 15)), mesh: CuboidMesh( size: Vector3(1, height, 1), - material: StandardMaterial( + material: SpatialMaterial( albedoTexture: ColorTexture( Color.fromRGBO(rnd.iRange(20, 255), rnd.iRange(10, 55), 30, 1), ), diff --git a/packages/flame_3d/example/lib/player_box.dart b/packages/flame_3d/example/lib/player_box.dart index fc42d7f75bb..39bc0c34e70 100644 --- a/packages/flame_3d/example/lib/player_box.dart +++ b/packages/flame_3d/example/lib/player_box.dart @@ -14,7 +14,7 @@ class PlayerBox extends MeshComponent with HasGameReference { mesh: CuboidMesh( size: Vector3.all(0.5), material: - StandardMaterial(albedoTexture: ColorTexture(Colors.purple)), + SpatialMaterial(albedoTexture: ColorTexture(Colors.purple)), ), ); diff --git a/packages/flame_3d/example/lib/rotating_light.dart b/packages/flame_3d/example/lib/rotating_light.dart new file mode 100644 index 00000000000..efc749164f2 --- /dev/null +++ b/packages/flame_3d/example/lib/rotating_light.dart @@ -0,0 +1,20 @@ +import 'dart:math'; + +import 'package:flame_3d/components.dart'; +import 'package:flame_3d/game.dart'; + +class RotatingLight extends LightComponent { + RotatingLight() + : super.spot( + position: Vector3.zero(), + ); + + @override + void update(double dt) { + const radius = 15; + final angle = DateTime.now().millisecondsSinceEpoch / 4000; + final x = cos(angle) * radius; + final z = sin(angle) * radius; + position.setValues(x, 10, z); + } +} diff --git a/packages/flame_3d/lib/components.dart b/packages/flame_3d/lib/components.dart index d3c590b1d53..98af07237d0 100644 --- a/packages/flame_3d/lib/components.dart +++ b/packages/flame_3d/lib/components.dart @@ -1,2 +1,4 @@ export 'src/components/component_3d.dart'; +export 'src/components/light_component.dart'; export 'src/components/mesh_component.dart'; +export 'src/components/object_3d.dart'; diff --git a/packages/flame_3d/lib/core.dart b/packages/flame_3d/lib/core.dart index 7d12648dc96..97d9212061c 100644 --- a/packages/flame_3d/lib/core.dart +++ b/packages/flame_3d/lib/core.dart @@ -1,4 +1,12 @@ export 'package:vector_math/vector_math.dart' - show Vector2, Vector3, Vector4, Matrix3, Matrix4, Quaternion, Aabb3; + show + Vector2, + Vector3, + Vector4, + Matrix2, + Matrix3, + Matrix4, + Quaternion, + Aabb3; export 'extensions.dart'; diff --git a/packages/flame_3d/lib/resources.dart b/packages/flame_3d/lib/resources.dart index b6ba8d86973..37f5374f3f2 100644 --- a/packages/flame_3d/lib/resources.dart +++ b/packages/flame_3d/lib/resources.dart @@ -1,9 +1,11 @@ import 'package:flame/cache.dart'; import 'package:flame_3d/resources.dart'; +export 'src/resources/light.dart'; export 'src/resources/material.dart'; export 'src/resources/mesh.dart'; export 'src/resources/resource.dart'; +export 'src/resources/shader.dart'; export 'src/resources/texture.dart'; extension TextureCache on Images { diff --git a/packages/flame_3d/lib/src/camera/camera_component_3d.dart b/packages/flame_3d/lib/src/camera/camera_component_3d.dart index a2442309c85..bbbfb3196bf 100644 --- a/packages/flame_3d/lib/src/camera/camera_component_3d.dart +++ b/packages/flame_3d/lib/src/camera/camera_component_3d.dart @@ -75,14 +75,19 @@ class CameraComponent3D extends CameraComponent { ..setAsPerspective(fovY, aspectRatio, distanceNear, distanceFar), CameraProjection.orthographic => _projectionMatrix ..setAsOrthographic(fovY, aspectRatio, distanceNear, distanceFar) - } - ..multiply(viewMatrix); + }; } final Matrix4 _projectionMatrix = Matrix4.zero(); + + Matrix4 get viewProjectionMatrix => _viewProjectionMatrix + ..setFrom(projectionMatrix) + ..multiply(viewMatrix); + final Matrix4 _viewProjectionMatrix = Matrix4.zero(); + final Frustum _frustum = Frustum(); - Frustum get frustum => _frustum..setFromMatrix(_projectionMatrix); + Frustum get frustum => _frustum..setFromMatrix(viewProjectionMatrix); void moveForward(double distance, {bool moveInWorldPlane = false}) { final forward = this.forward..scale(distance); diff --git a/packages/flame_3d/lib/src/camera/world_3d.dart b/packages/flame_3d/lib/src/camera/world_3d.dart index 6497a7d1c0a..83bf4917f5c 100644 --- a/packages/flame_3d/lib/src/camera/world_3d.dart +++ b/packages/flame_3d/lib/src/camera/world_3d.dart @@ -1,9 +1,10 @@ -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/graphics.dart'; +import 'package:flame_3d/resources.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; /// {@template world_3d} /// The root component for all 3D world elements. @@ -11,38 +12,65 @@ import 'package:flame_3d/graphics.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 { +class World3D extends flame.World with flame.HasGameReference { /// {@macro world_3d} World3D({ super.children, super.priority, Color clearColor = const Color(0x00000000), - }) : graphics = GraphicsDevice(clearValue: clearColor); + }) : device = GraphicsDevice(clearValue: clearColor) { + children.register(); + } /// The graphical device attached to this world. - final GraphicsDevice graphics; + @internal + final GraphicsDevice device; + + Iterable get lights => + children.query().map((component) => component.light); final _paint = Paint(); + @internal @override void renderFromCamera(Canvas canvas) { final camera = CameraComponent3D.currentCamera!; - final viewport = camera.viewport; - graphics.begin( - Size(viewport.virtualSize.x, viewport.virtualSize.y), - transformMatrix: camera.projectionMatrix, + + final devicePixelRatio = MediaQuery.of(game.buildContext!).devicePixelRatio; + final size = Size( + viewport.virtualSize.x * devicePixelRatio, + viewport.virtualSize.y * devicePixelRatio, ); + device + // Set the view matrix + ..view.setFrom(camera.viewMatrix) + // Set the projection matrix + ..projection.setFrom(camera.projectionMatrix) + ..begin(size); + culled = 0; + + _prepareDevice(); // ignore: invalid_use_of_internal_member super.renderFromCamera(canvas); - final image = graphics.end(); - canvas.drawImage(image, (-viewport.virtualSize / 2).toOffset(), _paint); + final image = device.end(); + canvas.drawImageRect( + image, + Offset.zero & size, + (-viewport.virtualSize / 2).toOffset() & + Size(viewport.virtualSize.x, viewport.virtualSize.y), + _paint, + ); image.dispose(); } - // TODO(wolfen): this is only here for testing purposes + void _prepareDevice() { + device.lights = lights; + } + + // TODO(wolfenrain): this is only here for testing purposes int culled = 0; } diff --git a/packages/flame_3d/lib/src/components/component_3d.dart b/packages/flame_3d/lib/src/components/component_3d.dart index 5d961b5fb0c..3ada2bdb9c4 100644 --- a/packages/flame_3d/lib/src/components/component_3d.dart +++ b/packages/flame_3d/lib/src/components/component_3d.dart @@ -1,47 +1,48 @@ -import 'dart:ui'; - import 'package:flame/components.dart' show Component, HasWorldReference; -import 'package:flame/game.dart' show FlameGame; 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'; /// {@template component_3d} -/// [Component3D]s are the basic building blocks for a 3D [FlameGame]. +/// [Component3D] is a base class for any concept that lives in 3D space. /// /// It is a [Component] implementation that represents a 3D object that can be /// freely moved around in 3D space, rotated, and scaled. /// -/// The [Component3D] class has no visual representation of its own (except in -/// debug mode). It is common, therefore, to derive from this class -/// and implement a specific rendering logic. +/// The main property of this class is the [transform] (which combines +/// the [position], [rotation], and [scale]). Thus, the [Component3D] can be +/// seen as an object in 3D space. +/// +/// It is typically not used directly, but rather use one of the following +/// implementations: +/// - [Object3D] for a 3D object that can be bound and rendered by the GPU +/// - [LightComponent] for a light source that affects how objects are rendered +/// +/// If you want to have a pure group for several components, you have two +/// options: +/// - Use an [Object3D], the group itself will have some superfluous render +/// logic but should not affect your children. +/// - Extend the abstract class [Component3D] yourself. /// /// The base [Component3D] class can also be used as a container /// for several other components. In this case, changing the position, /// rotating or scaling the [Component3D] will affect the whole /// group as if it was a single entity. -/// -/// The main property of this class is the [transform] (which combines -/// the [position], [rotation], and [scale]). Thus, the [Component3D] can be -/// seen as an object in 3D space where you can change its perceived -/// visualization. -/// -/// See the [MeshComponent] for a [Component3D] that has a visual representation -/// by using [Mesh]es /// {@endtemplate} -class Component3D extends Component with HasWorldReference { +abstract class Component3D extends Component with HasWorldReference { + final Transform3D transform; + /// {@macro component_3d} Component3D({ Vector3? position, + Vector3? scale, Quaternion? rotation, - }) : transform = Transform3D() + List children = const [], + }) : transform = Transform3D() ..position = position ?? Vector3.zero() ..rotation = rotation ?? Quaternion.euler(0, 0, 0) - ..scale = Vector3.all(1); - - final Transform3D transform; + ..scale = scale ?? Vector3.all(1), + super(children: children); /// The total transformation matrix for the component. This matrix combines /// translation, rotation and scale transforms into a single entity. The @@ -79,36 +80,4 @@ class Component3D extends Component with HasWorldReference { /// Measure the distance (in parent's coordinate space) between this /// component's anchor and the [other] component's anchor. double distance(Component3D other) => position.distanceTo(other.position); - - @override - void renderTree(Canvas canvas) { - super.renderTree(canvas); - final camera = CameraComponent3D.currentCamera; - assert( - camera != null, - '''Component is either not part of a World3D or the render is being called outside of the camera rendering''', - ); - if (!shouldCull(camera!)) { - world.culled++; - return; - } - - // 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(wolfen): we should optimize this in the long run it currently sucks. - priority = -(CameraComponent3D.currentCamera!.position - position) - .length - .abs() - .toInt(); - - bind(world.graphics); - } - - void bind(GraphicsDevice device) {} - - bool shouldCull(CameraComponent3D camera) { - return camera.frustum.containsVector3(position); - } } diff --git a/packages/flame_3d/lib/src/components/light_component.dart b/packages/flame_3d/lib/src/components/light_component.dart new file mode 100644 index 00000000000..ea73c664e26 --- /dev/null +++ b/packages/flame_3d/lib/src/components/light_component.dart @@ -0,0 +1,36 @@ +import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/components.dart'; +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/resources.dart'; + +/// A [Component3D] that represents a light source in the 3D world. +class LightComponent extends Component3D { + LightComponent({ + required this.source, + super.position, + }); + + LightComponent.spot({ + Vector3? position, + }) : this( + source: SpotLight(), + position: position, + ); + + final LightSource source; + + late final Light _light = Light( + transform: transform, + source: source, + ); + + Light get light => _light; + + @override + void onMount() { + assert( + parent is World3D, + 'Lights must be added to the root of the World3D', + ); + } +} diff --git a/packages/flame_3d/lib/src/components/mesh_component.dart b/packages/flame_3d/lib/src/components/mesh_component.dart index 40f3c6d09a2..bf46dbd8f6e 100644 --- a/packages/flame_3d/lib/src/components/mesh_component.dart +++ b/packages/flame_3d/lib/src/components/mesh_component.dart @@ -5,16 +5,17 @@ import 'package:flame_3d/src/camera/camera_component_3d.dart'; import 'package:flame_3d/src/graphics/graphics_device.dart'; /// {@template mesh_component} -/// A [Component3D] that renders a [Mesh] at the [position] with the [rotation] +/// An [Object3D] that renders a [Mesh] at the [position] with the [rotation] /// and [scale] applied. /// -/// This is a commonly used subclass of [Component3D]. +/// This is a commonly used subclass of [Object3D]. /// {@endtemplate} -class MeshComponent extends Component3D { +class MeshComponent extends Object3D { /// {@macro mesh_component} MeshComponent({ required Mesh mesh, super.position, + super.scale, super.rotation, }) : _mesh = mesh; @@ -29,8 +30,8 @@ class MeshComponent extends Component3D { @override void bind(GraphicsDevice device) { - world.graphics - ..setViewModel(transformMatrix) + world.device + ..model.setFrom(transformMatrix) ..bindMesh(mesh); } diff --git a/packages/flame_3d/lib/src/components/object_3d.dart b/packages/flame_3d/lib/src/components/object_3d.dart new file mode 100644 index 00000000000..eff41e3231a --- /dev/null +++ b/packages/flame_3d/lib/src/components/object_3d.dart @@ -0,0 +1,62 @@ +import 'dart:ui'; + +import 'package:flame/game.dart' show FlameGame; +import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/components.dart'; +import 'package:flame_3d/graphics.dart'; +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]. +/// +/// However, it has no visual representation of its own (except in +/// debug mode). It is common, therefore, to derive from this class +/// and implement a specific rendering logic. +/// +/// See the [MeshComponent] for an [Object3D] that has a visual representation +/// using a [Mesh]. +/// {@endtemplate} +abstract class Object3D extends Component3D { + /// {@macro object_3d} + Object3D({ + super.position, + super.scale, + super.rotation, + }); + + @override + void renderTree(Canvas canvas) { + super.renderTree(canvas); + final camera = CameraComponent3D.currentCamera; + assert( + camera != null, + '''Component is either not part of a World3D or the render is being called outside of the camera rendering''', + ); + if (!shouldCull(camera!)) { + world.culled++; + return; + } + + // 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); + } + + void bind(GraphicsDevice device); + + bool shouldCull(CameraComponent3D camera) { + return camera.frustum.containsVector3(position); + } +} diff --git a/packages/flame_3d/lib/src/extensions/matrix4.dart b/packages/flame_3d/lib/src/extensions/matrix4.dart index 393f4fd5de7..7060adfe66a 100644 --- a/packages/flame_3d/lib/src/extensions/matrix4.dart +++ b/packages/flame_3d/lib/src/extensions/matrix4.dart @@ -15,12 +15,12 @@ extension Matrix4Extension on Matrix4 { /// Set the matrix to use a projection view. void setAsPerspective( - double fovy, + double fovY, double aspectRatio, double zNear, double zFar, ) { - final fovYRadians = fovy * degrees2Radians; + final fovYRadians = fovY * degrees2Radians; setPerspectiveMatrix(this, fovYRadians, aspectRatio, zNear, zFar); } diff --git a/packages/flame_3d/lib/src/game/notifying_quaternion.dart b/packages/flame_3d/lib/src/game/notifying_quaternion.dart index 29385889976..8380c774343 100644 --- a/packages/flame_3d/lib/src/game/notifying_quaternion.dart +++ b/packages/flame_3d/lib/src/game/notifying_quaternion.dart @@ -170,5 +170,5 @@ class NotifyingQuaternion extends Quaternion with ChangeNotifier { } @override - Float32List get storage => UnmodifiableFloat32ListView(super.storage); + Float32List get storage => super.storage.asUnmodifiableView(); } diff --git a/packages/flame_3d/lib/src/game/notifying_vector3.dart b/packages/flame_3d/lib/src/game/notifying_vector3.dart index ce62b22db66..c8b279c505e 100644 --- a/packages/flame_3d/lib/src/game/notifying_vector3.dart +++ b/packages/flame_3d/lib/src/game/notifying_vector3.dart @@ -207,5 +207,5 @@ class NotifyingVector3 extends Vector3 with ChangeNotifier { } @override - Float32List get storage => UnmodifiableFloat32ListView(super.storage); + Float32List get storage => super.storage.asUnmodifiableView(); } diff --git a/packages/flame_3d/lib/src/graphics/graphics_device.dart b/packages/flame_3d/lib/src/graphics/graphics_device.dart index a609bbd8d65..22d8178e91d 100644 --- a/packages/flame_3d/lib/src/graphics/graphics_device.dart +++ b/packages/flame_3d/lib/src/graphics/graphics_device.dart @@ -22,7 +22,7 @@ enum DepthStencilState { /// by binding different resources to it. /// /// A single render call starts with a call to [begin] and only ends when [end] -/// is called. Anything that gets binded to the device in between will be +/// is called. Anything that gets bound to the device in between will be /// uploaded to the GPU and returns as an [Image] in [end]. /// {@endtemplate} class GraphicsDevice { @@ -36,11 +36,22 @@ class GraphicsDevice { late gpu.HostBuffer _hostBuffer; late gpu.RenderPass _renderPass; late gpu.RenderTarget _renderTarget; - final _transformMatrix = Matrix4.identity(); - final _viewModelMatrix = Matrix4.identity(); + + 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. + Iterable lights = []; + /// Begin a new rendering batch. /// /// After [begin] is called the graphics device can be used to bind resources @@ -50,11 +61,10 @@ class GraphicsDevice { /// GPU with [end]. void begin( Size size, { - // TODO(wolfen): unused at the moment + // TODO(wolfenrain): unused at the moment BlendState blendState = BlendState.alphaBlend, - // TODO(wolfen): used incorrectly + // TODO(wolfenrain): used incorrectly DepthStencilState depthStencilState = DepthStencilState.depthRead, - Matrix4? transformMatrix, }) { _commandBuffer = gpu.gpuContext.createCommandBuffer(); _hostBuffer = gpu.gpuContext.createHostBuffer(); @@ -69,14 +79,13 @@ class GraphicsDevice { ) ..setDepthWriteEnable(depthStencilState == DepthStencilState.depthRead) ..setDepthCompareOperation( - // TODO(wolfen): this is not correctly implemented AT all. + // TODO(wolfenrain): this is not correctly implemented AT all. switch (depthStencilState) { DepthStencilState.none => gpu.CompareFunction.never, DepthStencilState.standard => gpu.CompareFunction.always, DepthStencilState.depthRead => gpu.CompareFunction.less, }, ); - _transformMatrix.setFrom(transformMatrix ?? Matrix4.identity()); } /// Submit the rendering batch and it's the commands to the GPU and return @@ -90,8 +99,6 @@ class GraphicsDevice { _renderPass.clearBindings(); } - void setViewModel(Matrix4 mvp) => _viewModelMatrix.setFrom(mvp); - /// Bind a [mesh]. void bindMesh(Mesh mesh) { _renderPass.clearBindings(); @@ -131,32 +138,20 @@ class GraphicsDevice { /// Bind a [material] and set up the buffer correctly. void bindMaterial(Material material) { _renderPass.bindPipeline(material.resource); - material.vertexBuffer - ..clear() - ..addMatrix4(_transformMatrix.multiplied(_viewModelMatrix)); - material.fragmentBuffer.clear(); - material.bind(this); - } - /// Bind a [shader] with the given [buffer]. - void bindShader(gpu.Shader shader, ShaderBuffer buffer) { - bindUniform( - shader, - buffer.slot, - buffer.bytes.asByteData(), - ); + material.bind(this); + material.vertexShader.bind(this); + material.fragmentShader.bind(this); } - /// Bind a uniform slot of [name] with the [data] on the [shader]. - void bindUniform(gpu.Shader shader, String name, ByteData data) { - _renderPass.bindUniform( - shader.getUniformSlot(name), - _hostBuffer.emplace(data), - ); + /// Bind a uniform [slot] to the [buffer]. + void bindUniform(gpu.UniformSlot slot, ByteBuffer buffer) { + _renderPass.bindUniform(slot, _hostBuffer.emplace(buffer.asByteData())); } - void bindTexture(gpu.Shader shader, String name, Texture texture) { - _renderPass.bindTexture(shader.getUniformSlot(name), texture.resource); + /// Bind a uniform [slot] to the [texture]. + void bindTexture(gpu.UniformSlot slot, Texture texture) { + _renderPass.bindTexture(slot, texture.resource); } gpu.RenderTarget _getRenderTarget(Size size) { diff --git a/packages/flame_3d/lib/src/resources/light.dart b/packages/flame_3d/lib/src/resources/light.dart new file mode 100644 index 00000000000..646ac51a7da --- /dev/null +++ b/packages/flame_3d/lib/src/resources/light.dart @@ -0,0 +1,3 @@ +export 'light/light.dart'; +export 'light/light_source.dart'; +export 'light/spot_light.dart'; diff --git a/packages/flame_3d/lib/src/resources/light/light.dart b/packages/flame_3d/lib/src/resources/light/light.dart new file mode 100644 index 00000000000..64748417b41 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/light/light.dart @@ -0,0 +1,29 @@ +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/resources.dart'; + +/// {@template light} +/// A [Resource] that represents a light source that is positioned in the scene +/// and changes how other objects are rendered. +/// +/// This class isn't a true resource, it does not upload it self to the GPU. +/// Instead, it is used to modify how other resources are uploaded. +/// +/// {@endtemplate} +class Light extends Resource { + final Transform3D transform; + final LightSource source; + + /// {@macro light} + Light({ + required this.transform, + required this.source, + }) : super(null); + + void apply(Shader shader) { + shader.setVector3('Light.position', transform.position); + // apply additional parameters + source.apply(shader); + } + + static UniformSlot shaderSlot = UniformSlot.value('Light', {'position'}); +} diff --git a/packages/flame_3d/lib/src/resources/light/light_source.dart b/packages/flame_3d/lib/src/resources/light/light_source.dart new file mode 100644 index 00000000000..3b96a99a84d --- /dev/null +++ b/packages/flame_3d/lib/src/resources/light/light_source.dart @@ -0,0 +1,8 @@ +import 'package:flame_3d/resources.dart'; + +/// Describes the properties of a light source. +/// There are three types of light sources: directional, point, and spot. +/// Currently only [SpotLight] is implemented. +abstract class LightSource { + void apply(Shader shader); +} diff --git a/packages/flame_3d/lib/src/resources/light/spot_light.dart b/packages/flame_3d/lib/src/resources/light/spot_light.dart new file mode 100644 index 00000000000..30376a864c3 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/light/spot_light.dart @@ -0,0 +1,11 @@ +import 'package:flame_3d/resources.dart'; + +/// A point light that emits light in all directions equally. +class SpotLight extends LightSource { + // TODO(luan): add color, intensity, etc + + @override + void apply(Shader shader) { + // + } +} diff --git a/packages/flame_3d/lib/src/resources/material.dart b/packages/flame_3d/lib/src/resources/material.dart index dab1184d5dc..eefe422c3ef 100644 --- a/packages/flame_3d/lib/src/resources/material.dart +++ b/packages/flame_3d/lib/src/resources/material.dart @@ -1,2 +1,2 @@ export 'material/material.dart'; -export 'material/standard_material.dart'; +export 'material/spatial_material.dart'; diff --git a/packages/flame_3d/lib/src/resources/material/material.dart b/packages/flame_3d/lib/src/resources/material/material.dart index 5d9706fef34..2fa7e8f3f4b 100644 --- a/packages/flame_3d/lib/src/resources/material/material.dart +++ b/packages/flame_3d/lib/src/resources/material/material.dart @@ -1,9 +1,5 @@ -import 'dart:typed_data'; - -import 'package:flame_3d/game.dart'; import 'package:flame_3d/graphics.dart'; -import 'package:flame_3d/src/resources/resource.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flame_3d/resources.dart'; import 'package:flutter_gpu/gpu.dart' as gpu; /// {@template material} @@ -12,57 +8,46 @@ import 'package:flutter_gpu/gpu.dart' as gpu; /// {@endtemplate} abstract class Material extends Resource { /// {@macro material} - Material(gpu.ShaderLibrary library) - : super( + Material({ + required Shader vertexShader, + required Shader fragmentShader, + }) : _vertexShader = vertexShader, + _fragmentShader = fragmentShader, + super( gpu.gpuContext.createRenderPipeline( - library['TextureVertex']!, - library['TextureFragment']!, + vertexShader.resource, + fragmentShader.resource, ), ); - final _vertexBuffer = ShaderBuffer('VertexInfo'); - final _fragmentBuffer = ShaderBuffer('FragmentInfo'); - - /// The vertex shader being used. - gpu.Shader get vertexShader => resource.vertexShader; - ShaderBuffer get vertexBuffer => _vertexBuffer; - - /// The fragment shader being used. - gpu.Shader get fragmentShader => resource.fragmentShader; - ShaderBuffer get fragmentBuffer => _fragmentBuffer; - - @mustCallSuper - void bind(GraphicsDevice device) { - device.bindShader(vertexShader, _vertexBuffer); - device.bindShader(fragmentShader, fragmentBuffer); + @override + gpu.RenderPipeline get resource { + var resource = super.resource; + if (_recreateResource) { + resource = super.resource = gpu.gpuContext.createRenderPipeline( + _vertexShader.resource, + _fragmentShader.resource, + ); + _recreateResource = false; + } + return resource; } -} - -/// {@template shader_buffer} -/// Class that buffers all the float uniforms that have to be uploaded to a -/// shader. -/// {@endtemplate} -class ShaderBuffer { - /// {@macro shader_buffer} - ShaderBuffer(this.slot); - - final String slot; - - final List _storage = []; - ByteBuffer get bytes => Float32List.fromList(_storage).buffer; - /// Add a [Vector2] to the buffer. - void addVector2(Vector2 vector) => _storage.addAll(vector.storage); + bool _recreateResource = false; - /// Add a [Vector3] to the buffer. - void addVector3(Vector3 vector) => _storage.addAll(vector.storage); - - /// Add a [Vector4] to the buffer. - void addVector4(Vector4 vector) => _storage.addAll(vector.storage); + Shader get vertexShader => _vertexShader; + Shader _vertexShader; + set vertexShader(Shader shader) { + _vertexShader = shader; + _recreateResource = true; + } - /// Add a [Matrix4] to the buffer. - void addMatrix4(Matrix4 matrix) => _storage.addAll(matrix.storage); + Shader get fragmentShader => _fragmentShader; + Shader _fragmentShader; + set fragmentShader(Shader shader) { + _fragmentShader = shader; + _recreateResource = true; + } - /// Clear the buffer. - void clear() => _storage.clear(); + void bind(GraphicsDevice device) {} } diff --git a/packages/flame_3d/lib/src/resources/material/spatial_material.dart b/packages/flame_3d/lib/src/resources/material/spatial_material.dart new file mode 100644 index 00000000000..2944f4187ea --- /dev/null +++ b/packages/flame_3d/lib/src/resources/material/spatial_material.dart @@ -0,0 +1,101 @@ +import 'dart:ui'; + +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/graphics.dart'; +import 'package:flame_3d/resources.dart'; +import 'package:flutter_gpu/gpu.dart' as gpu; + +class SpatialMaterial extends Material { + SpatialMaterial({ + Texture? albedoTexture, + Color albedoColor = const Color(0xFFFFFFFF), + this.metallic = 0, + this.metallicSpecular = 0.5, + this.roughness = 1.0, + }) : albedoTexture = albedoTexture ?? Texture.standard, + super( + vertexShader: Shader( + _library['TextureVertex']!, + slots: [ + UniformSlot.value('VertexInfo', {'model', 'view', 'projection'}), + ], + ), + fragmentShader: Shader( + _library['TextureFragment']!, + slots: [ + UniformSlot.sampler('albedoTexture'), + UniformSlot.value('Material', { + 'albedoColor', + 'metallic', + 'metallicSpecular', + 'roughness', + }), + Light.shaderSlot, + UniformSlot.value('Camera', {'position'}), + ], + ), + ) { + this.albedoColor = albedoColor; + } + + /// The material's base color. + Color get albedoColor => _albedoColor; + set albedoColor(Color color) { + _albedoColor = color; + _albedoCache.copyFromArray(color.storage); + } + + late Color _albedoColor; + final Vector3 _albedoCache = Vector3.zero(); + + /// The texture that will be multiplied by [albedoColor]. + Texture albedoTexture; + + double metallic; + + double metallicSpecular; + + double roughness; + + @override + void bind(GraphicsDevice device) { + _bindVertexInfo(device); + _bindMaterial(device); + _bindCamera(device); + } + + void _bindVertexInfo(GraphicsDevice device) { + vertexShader + ..setMatrix4('VertexInfo.model', device.model) + ..setMatrix4('VertexInfo.view', device.view) + ..setMatrix4('VertexInfo.projection', device.projection); + } + + void _bindMaterial(GraphicsDevice device) { + _applyLights(device); + fragmentShader + ..setTexture('albedoTexture', albedoTexture) + ..setVector3('Material.albedoColor', _albedoCache) + ..setFloat('Material.metallic', metallic) + ..setFloat('Material.metallicSpecular', metallicSpecular) + ..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) { + final light = device.lights.firstOrNull; + if (light == null) { + return; + } + light.apply(fragmentShader); + } + + static final _library = gpu.ShaderLibrary.fromAsset( + 'packages/flame_3d/assets/shaders/spatial_material.shaderbundle', + )!; +} diff --git a/packages/flame_3d/lib/src/resources/material/standard_material.dart b/packages/flame_3d/lib/src/resources/material/standard_material.dart deleted file mode 100644 index 854045c9e5d..00000000000 --- a/packages/flame_3d/lib/src/resources/material/standard_material.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:ui'; - -import 'package:flame_3d/game.dart'; -import 'package:flame_3d/graphics.dart'; -import 'package:flame_3d/resources.dart'; -import 'package:flutter_gpu/gpu.dart' as gpu; - -/// {@template standard_material} -/// The standard material, it applies the [albedoColor] to the [albedoTexture]. -/// {@endtemplate} -class StandardMaterial extends Material { - /// {@macro standard_material} - StandardMaterial({ - Texture? albedoTexture, - Color? albedoColor, - }) : albedoTexture = albedoTexture ?? Texture.standard, - _albedoColorCache = Vector4.zero(), - super(_library) { - this.albedoColor = albedoColor ?? const Color(0xFFFFFFFF); - } - - Texture albedoTexture; - - Color get albedoColor => _albedoColor; - set albedoColor(Color color) { - _albedoColor = color; - _albedoColorCache.setValues( - color.red / 255, - color.green / 255, - color.blue / 255, - color.alpha / 255, - ); - } - - late Color _albedoColor; - final Vector4 _albedoColorCache; - - @override - void bind(GraphicsDevice device) { - fragmentBuffer.addVector4(_albedoColorCache); - device.bindTexture(fragmentShader, 'albedoTexture', albedoTexture); - return super.bind(device); - } - - static final _library = gpu.ShaderLibrary.fromAsset( - 'packages/flame_3d/assets/shaders/standard_material.shaderbundle', - )!; -} diff --git a/packages/flame_3d/lib/src/resources/mesh/cuboid_mesh.dart b/packages/flame_3d/lib/src/resources/mesh/cuboid_mesh.dart index dbd4187184d..c3a6eaf358c 100644 --- a/packages/flame_3d/lib/src/resources/mesh/cuboid_mesh.dart +++ b/packages/flame_3d/lib/src/resources/mesh/cuboid_mesh.dart @@ -14,40 +14,136 @@ class CuboidMesh extends Mesh { final vertices = [ // Face 1 (front) - Vertex(position: Vector3(-x, -y, -z), texCoord: Vector2(0, 0)), - Vertex(position: Vector3(x, -y, -z), texCoord: Vector2(1, 0)), - Vertex(position: Vector3(x, y, -z), texCoord: Vector2(1, 1)), - Vertex(position: Vector3(-x, y, -z), texCoord: Vector2(0, 1)), + Vertex( + position: Vector3(-x, -y, -z), + texCoord: Vector2(0, 0), + normal: Vector3(0, 0, -1), + ), + Vertex( + position: Vector3(x, -y, -z), + texCoord: Vector2(1, 0), + normal: Vector3(0, 0, -1), + ), + Vertex( + position: Vector3(x, y, -z), + texCoord: Vector2(1, 1), + normal: Vector3(0, 0, -1), + ), + Vertex( + position: Vector3(-x, y, -z), + texCoord: Vector2(0, 1), + normal: Vector3(0, 0, -1), + ), // Face 2 (back) - Vertex(position: Vector3(-x, -y, z), texCoord: Vector2(0, 0)), - Vertex(position: Vector3(x, -y, z), texCoord: Vector2(1, 0)), - Vertex(position: Vector3(x, y, z), texCoord: Vector2(1, 1)), - Vertex(position: Vector3(-x, y, z), texCoord: Vector2(0, 1)), + Vertex( + position: Vector3(-x, -y, z), + texCoord: Vector2(0, 0), + normal: Vector3(0, 0, 1), + ), + Vertex( + position: Vector3(x, -y, z), + texCoord: Vector2(1, 0), + normal: Vector3(0, 0, 1), + ), + Vertex( + position: Vector3(x, y, z), + texCoord: Vector2(1, 1), + normal: Vector3(0, 0, 1), + ), + Vertex( + position: Vector3(-x, y, z), + texCoord: Vector2(0, 1), + normal: Vector3(0, 0, 1), + ), // Face 3 (left) - Vertex(position: Vector3(-x, -y, z), texCoord: Vector2(0, 0)), - Vertex(position: Vector3(-x, -y, -z), texCoord: Vector2(1, 0)), - Vertex(position: Vector3(-x, y, -z), texCoord: Vector2(1, 1)), - Vertex(position: Vector3(-x, y, z), texCoord: Vector2(0, 1)), + Vertex( + position: Vector3(-x, -y, z), + texCoord: Vector2(0, 0), + normal: Vector3(-1, 0, 0), + ), + Vertex( + position: Vector3(-x, -y, -z), + texCoord: Vector2(1, 0), + normal: Vector3(-1, 0, 0), + ), + Vertex( + position: Vector3(-x, y, -z), + texCoord: Vector2(1, 1), + normal: Vector3(-1, 0, 0), + ), + Vertex( + position: Vector3(-x, y, z), + texCoord: Vector2(0, 1), + normal: Vector3(-1, 0, 0), + ), // Face 4 (right) - Vertex(position: Vector3(x, -y, -z), texCoord: Vector2(0, 0)), - Vertex(position: Vector3(x, -y, z), texCoord: Vector2(1, 0)), - Vertex(position: Vector3(x, y, z), texCoord: Vector2(1, 1)), - Vertex(position: Vector3(x, y, -z), texCoord: Vector2(0, 1)), + Vertex( + position: Vector3(x, -y, -z), + texCoord: Vector2(0, 0), + normal: Vector3(1, 0, 0), + ), + Vertex( + position: Vector3(x, -y, z), + texCoord: Vector2(1, 0), + normal: Vector3(1, 0, 0), + ), + Vertex( + position: Vector3(x, y, z), + texCoord: Vector2(1, 1), + normal: Vector3(1, 0, 0), + ), + Vertex( + position: Vector3(x, y, -z), + texCoord: Vector2(0, 1), + normal: Vector3(1, 0, 0), + ), // Face 5 (top) - Vertex(position: Vector3(-x, y, -z), texCoord: Vector2(0, 0)), - Vertex(position: Vector3(x, y, -z), texCoord: Vector2(1, 0)), - Vertex(position: Vector3(x, y, z), texCoord: Vector2(1, 1)), - Vertex(position: Vector3(-x, y, z), texCoord: Vector2(0, 1)), + Vertex( + position: Vector3(-x, y, -z), + texCoord: Vector2(0, 0), + normal: Vector3(0, 1, 0), + ), + Vertex( + position: Vector3(x, y, -z), + texCoord: Vector2(1, 0), + normal: Vector3(0, 1, 0), + ), + Vertex( + position: Vector3(x, y, z), + texCoord: Vector2(1, 1), + normal: Vector3(0, 1, 0), + ), + Vertex( + position: Vector3(-x, y, z), + texCoord: Vector2(0, 1), + normal: Vector3(0, 1, 0), + ), // Face 6 (bottom) - Vertex(position: Vector3(-x, -y, z), texCoord: Vector2(0, 0)), - Vertex(position: Vector3(x, -y, z), texCoord: Vector2(1, 0)), - Vertex(position: Vector3(x, -y, -z), texCoord: Vector2(1, 1)), - Vertex(position: Vector3(-x, -y, -z), texCoord: Vector2(0, 1)), + Vertex( + position: Vector3(-x, -y, z), + texCoord: Vector2(0, 0), + normal: Vector3(0, -1, 0), + ), + Vertex( + position: Vector3(x, -y, z), + texCoord: Vector2(1, 0), + normal: Vector3(0, -1, 0), + ), + Vertex( + position: Vector3(x, -y, -z), + texCoord: Vector2(1, 1), + normal: Vector3(0, -1, 0), + ), + Vertex( + position: Vector3(-x, -y, -z), + texCoord: Vector2(0, 1), + normal: Vector3(0, -1, 0), + ), ]; final indices = [ diff --git a/packages/flame_3d/lib/src/resources/mesh/mesh.dart b/packages/flame_3d/lib/src/resources/mesh/mesh.dart index a8ebd099f80..765b03f5acb 100644 --- a/packages/flame_3d/lib/src/resources/mesh/mesh.dart +++ b/packages/flame_3d/lib/src/resources/mesh/mesh.dart @@ -23,6 +23,8 @@ class Mesh extends Resource { /// This is the sum of all the AABB's of the surfaces it contains. Aabb3 get aabb => _aabb ??= _recomputeAabb3(); + int get vertexCount => _surfaces.fold(0, (p, e) => p + e.vertexCount); + void bind(GraphicsDevice device) { for (final surface in _surfaces) { device.bindSurface(surface); @@ -33,7 +35,7 @@ class Mesh extends Resource { int get surfaceCount => _surfaces.length; /// An unmodifiable iterable over the list of the surfaces. - /// + /// /// Note: if you modify the geometry of any [Surface] within this list, /// you will need to call [updateBounds] to update the mesh's bounds. Iterable get surfaces => _surfaces; @@ -59,7 +61,8 @@ class Mesh extends Resource { updateBounds(); } - /// Update the surfaces of the mesh, making sure to recompute the bounds after. + /// Update the surfaces of the mesh, making sure to recompute the bounds + /// after. void updateSurfaces(void Function(List surfaces) update) { update(_surfaces); updateBounds(); diff --git a/packages/flame_3d/lib/src/resources/mesh/plane_mesh.dart b/packages/flame_3d/lib/src/resources/mesh/plane_mesh.dart index 82924ef3b55..faea9893c1c 100644 --- a/packages/flame_3d/lib/src/resources/mesh/plane_mesh.dart +++ b/packages/flame_3d/lib/src/resources/mesh/plane_mesh.dart @@ -13,10 +13,26 @@ class PlaneMesh extends Mesh { final Vector2(:x, :y) = size / 2; final vertices = [ - Vertex(position: Vector3(-x, 0, -y), texCoord: Vector2(0, 0)), - Vertex(position: Vector3(x, 0, -y), texCoord: Vector2(1, 0)), - Vertex(position: Vector3(x, 0, y), texCoord: Vector2(1, 1)), - Vertex(position: Vector3(-x, 0, y), texCoord: Vector2(0, 1)), + Vertex( + position: Vector3(-x, 0, -y), + texCoord: Vector2(0, 0), + normal: Vector3(0, 1, 0), + ), + Vertex( + position: Vector3(x, 0, -y), + texCoord: Vector2(1, 0), + normal: Vector3(0, 1, 0), + ), + Vertex( + position: Vector3(x, 0, y), + texCoord: Vector2(1, 1), + normal: Vector3(0, 1, 0), + ), + Vertex( + position: Vector3(-x, 0, y), + texCoord: Vector2(0, 1), + normal: Vector3(0, 1, 0), + ), ]; addSurface( Surface( diff --git a/packages/flame_3d/lib/src/resources/mesh/vertex.dart b/packages/flame_3d/lib/src/resources/mesh/vertex.dart index bca7b21cd27..027cfaa997c 100644 --- a/packages/flame_3d/lib/src/resources/mesh/vertex.dart +++ b/packages/flame_3d/lib/src/resources/mesh/vertex.dart @@ -15,17 +15,17 @@ class Vertex { Vertex({ required Vector3 position, required Vector2 texCoord, - Vector3? normal, this.color = const Color(0xFFFFFFFF), + Vector3? normal, }) : position = position.immutable, texCoord = texCoord.immutable, normal = (normal ?? Vector3.zero()).immutable, _storage = Float32List.fromList([ - ...position.storage, - ...texCoord.storage, - // `TODO`(wolfen): uhh normals fuck shit up, I should read up on it - // ...(normal ?? Vector3.zero()).storage, - ...color.storage, + ...position.storage, // 1, 2, 3 + ...texCoord.storage, // 4, 5 + ...color.storage, // 6,7,8 + // TODO(wolfenrain): fix normals not working properly + ...(normal ?? Vector3.zero()).storage, // 9, 10, 11 ]); Float32List get storage => _storage; @@ -53,4 +53,19 @@ class Vertex { @override int get hashCode => Object.hashAll([position, texCoord, normal, color]); + + Vertex copyWith({ + Vector3? position, + Vector2? texCoord, + Vector3? normal, + Color? color, + }) { + // TODO(wolfenrain): optimize this. + return Vertex( + position: position ?? this.position.mutable, + texCoord: texCoord ?? this.texCoord.mutable, + normal: normal ?? this.normal.mutable, + color: color ?? this.color, + ); + } } diff --git a/packages/flame_3d/lib/src/resources/resource.dart b/packages/flame_3d/lib/src/resources/resource.dart index 068a0227b56..497a03f311b 100644 --- a/packages/flame_3d/lib/src/resources/resource.dart +++ b/packages/flame_3d/lib/src/resources/resource.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; -// TODO(wolfen): in the long run it would be nice of we can make it +// TODO(wolfenrain): in the long run it would be nice of we can make it // automatically refer to same type of objects to prevent memory leaks /// {@template resource} diff --git a/packages/flame_3d/lib/src/resources/shader.dart b/packages/flame_3d/lib/src/resources/shader.dart new file mode 100644 index 00000000000..eb68f8a8dec --- /dev/null +++ b/packages/flame_3d/lib/src/resources/shader.dart @@ -0,0 +1,5 @@ +export 'shader/shader.dart'; +export 'shader/uniform_instance.dart'; +export 'shader/uniform_sampler.dart'; +export 'shader/uniform_slot.dart'; +export 'shader/uniform_value.dart'; diff --git a/packages/flame_3d/lib/src/resources/shader/shader.dart b/packages/flame_3d/lib/src/resources/shader/shader.dart new file mode 100644 index 00000000000..70272352153 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/shader/shader.dart @@ -0,0 +1,94 @@ +import 'dart:collection'; + +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/graphics.dart'; +import 'package:flame_3d/resources.dart'; +import 'package:flutter_gpu/gpu.dart' as gpu; + +/// {@template shader} +/// +/// {@endtemplate} +class Shader extends Resource { + /// {@macro shader} + Shader( + super.resource, { + List slots = const [], + }) : _slots = slots, + _instances = {} { + for (final slot in slots) { + slot.resource = resource.getUniformSlot(slot.name); + } + } + + final List _slots; + + final Map _instances; + + /// Set a [Texture] at the given [key] on the buffer. + void setTexture(String key, Texture texture) => _setSampler(key, texture); + + /// Set a [Vector2] at the given [key] on the buffer. + void setVector2(String key, Vector2 vector) => _setValue(key, vector.storage); + + /// Set a [Vector3] at the given [key] on the buffer. + void setVector3(String key, Vector3 vector) => _setValue(key, vector.storage); + + /// Set a [Vector4] at the given [key] on the buffer. + void setVector4(String key, Vector4 vector) => _setValue(key, vector.storage); + + /// Set a [double] at the given [key] on the buffer. + void setFloat(String key, double value) => _setValue(key, [value]); + + /// Set a [Matrix2] at the given [key] on the buffer. + void setMatrix2(String key, Matrix2 matrix) => _setValue(key, matrix.storage); + + /// Set a [Matrix3] at the given [key] on the buffer. + void setMatrix3(String key, Matrix3 matrix) => _setValue(key, matrix.storage); + + /// Set a [Matrix4] at the given [key] on the buffer. + void setMatrix4(String key, Matrix4 matrix) => _setValue(key, matrix.storage); + + void bind(GraphicsDevice device) { + for (final slot in _slots) { + _instances[slot.name]?.bind(device); + } + } + + /// Set the [data] to the [UniformSlot] identified by [key]. + void _setValue(String key, List data) { + final (uniform, field) = _getInstance(key); + uniform[field!] = data; + } + + void _setSampler(String key, Texture data) { + final (uniform, _) = _getInstance(key); + uniform.resource = data; + } + + /// Get the slot for the [key], it only calculates it once for every unique + /// [key]. + (T, String?) _getInstance(String key) { + final keys = key.split('.'); + + // Check if we already have a uniform instance created. + if (!_instances.containsKey(keys.first)) { + // If the slot or it's property isn't mapped in the uniform it will be + // enforced. + final slot = _slots.firstWhere( + (e) => e.name == keys.first, + orElse: () => throw StateError('Uniform "$key" is unmapped'), + ); + + final instance = slot.create(); + if (instance is UniformValue && + keys.length > 1 && + !slot.fields.contains(keys[1])) { + throw StateError('Field "${keys[1]}" is unmapped for "${keys.first}"'); + } + + _instances[slot.name] = instance; + } + + return (_instances[keys.first], keys.elementAtOrNull(1)) as (T, String?); + } +} diff --git a/packages/flame_3d/lib/src/resources/shader/uniform_instance.dart b/packages/flame_3d/lib/src/resources/shader/uniform_instance.dart new file mode 100644 index 00000000000..94e4bc6dc54 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/shader/uniform_instance.dart @@ -0,0 +1,16 @@ +import 'package:flame_3d/graphics.dart'; +import 'package:flame_3d/resources.dart'; + +/// {@template uniform_instance} +/// An instance of a [UniformSlot] that can cache the [resource] that will be +/// bound to a [Shader]. +/// {@endtemplate} +abstract class UniformInstance extends Resource { + /// {@macro uniform_instance} + UniformInstance(this.slot) : super(null); + + /// The slot this instance belongs too. + final UniformSlot slot; + + void bind(GraphicsDevice device); +} diff --git a/packages/flame_3d/lib/src/resources/shader/uniform_sampler.dart b/packages/flame_3d/lib/src/resources/shader/uniform_sampler.dart new file mode 100644 index 00000000000..2a8b1524c5d --- /dev/null +++ b/packages/flame_3d/lib/src/resources/shader/uniform_sampler.dart @@ -0,0 +1,15 @@ +import 'package:flame_3d/graphics.dart'; +import 'package:flame_3d/resources.dart'; + +/// {@template uniform_sampler} +/// Instance of a uniform sampler. Represented by a [Texture]. +/// {@endtemplate} +class UniformSampler extends UniformInstance { + /// {@macro uniform_sampler} + UniformSampler(super.slot); + + @override + void bind(GraphicsDevice device) { + device.bindTexture(slot.resource!, resource!); + } +} diff --git a/packages/flame_3d/lib/src/resources/shader/uniform_slot.dart b/packages/flame_3d/lib/src/resources/shader/uniform_slot.dart new file mode 100644 index 00000000000..5355c39597e --- /dev/null +++ b/packages/flame_3d/lib/src/resources/shader/uniform_slot.dart @@ -0,0 +1,46 @@ +import 'package:flame_3d/resources.dart'; +import 'package:flutter_gpu/gpu.dart' as gpu; + +/// {@template uniform_slot} +/// Class that maps a uniform slot in such a way that it is easier to do memory +/// allocation. +/// +/// This allows the [Shader] to create [UniformInstance]s that bind themselves +/// to the shader without the shader needing to the inner workings. +/// {@endtemplate} +class UniformSlot extends Resource { + UniformSlot._(this.name, this.fields, this._instanceCreator) + : _fieldIndices = {for (var (index, key) in fields.indexed) key: index}, + super(null); + + /// {@macro uniform_slot} + /// + /// Used for struct uniforms in shaders. + /// + /// The [fields] should be defined in order as they appear in the struct. + UniformSlot.value(String name, Set fields) + : this._(name, fields, UniformValue.new); + + /// {@macro uniform_slot} + /// + /// Used for sampler uniforms in shaders. + UniformSlot.sampler(String name) : this._(name, {}, UniformSampler.new); + + /// The uniform slot's name. + final String name; + + /// The fields in the uniform and the order in which the memory should be + /// allocated. + /// + /// This is empty if the slot is a sampler. + final Set fields; + + /// Cache of the fields mapped to their index. + final Map _fieldIndices; + + final UniformInstance Function(UniformSlot) _instanceCreator; + + int indexOf(String field) => _fieldIndices[field]!; + + UniformInstance create() => _instanceCreator.call(this); +} diff --git a/packages/flame_3d/lib/src/resources/shader/uniform_value.dart b/packages/flame_3d/lib/src/resources/shader/uniform_value.dart new file mode 100644 index 00000000000..a138dfdf7f9 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/shader/uniform_value.dart @@ -0,0 +1,62 @@ +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:flame_3d/graphics.dart'; +import 'package:flame_3d/resources.dart'; + +/// {@template uniform_value} +/// Instance of a uniform value. Represented by a [ByteBuffer]. +/// +/// The `[]` operator can be used to set the raw data of a field. If the data is +/// different from the last set it will recalculated the [resource]. +/// {@endtemplate} +class UniformValue extends UniformInstance { + /// {@macro uniform_value} + UniformValue(super.slot); + + final Map data})> _storage = HashMap(); + + @override + ByteBuffer? get resource { + if (super.resource == null) { + var previousIndex = -1; + + final data = _storage.entries.fold>([], (p, e) { + if (previousIndex + 1 != e.key) { + final field = + slot.fields.indexed.firstWhere((e) => e.$1 == previousIndex + 1); + throw StateError('Uniform ${slot.name}.${field.$2} was not set'); + } + previousIndex = e.key; + return p..addAll(e.value.data); + }); + + super.resource = Float32List.fromList(data).buffer; + } + + return super.resource; + } + + List? operator [](String key) => _storage[slot.indexOf(key)]?.data; + + void operator []=(String key, List data) { + final index = slot.indexOf(key); + + // Ensure that we are only setting new data if the hash has changed. + final hash = Object.hashAll(data); + if (_storage[index]?.hash == hash) { + return; + } + + // Store the storage at the given slot index. + _storage[index] = (data: data, hash: hash); + + // Clear the cache. + super.resource = null; + } + + @override + void bind(GraphicsDevice device) { + device.bindUniform(slot.resource!, resource!); + } +} diff --git a/packages/flame_3d/pubspec.yaml b/packages/flame_3d/pubspec.yaml index 4114caabda9..41b64f5c490 100644 --- a/packages/flame_3d/pubspec.yaml +++ b/packages/flame_3d/pubspec.yaml @@ -8,7 +8,7 @@ funding: - https://patreon.com/bluefireoss environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.3.0 <4.0.0" flutter: ">=3.19.0" dependencies: @@ -16,14 +16,15 @@ dependencies: flutter: sdk: flutter flutter_gpu: + meta: ^1.9.1 vector_math: ^2.1.4 dev_dependencies: flame_lint: ^1.1.2 - flame_test: ^1.15.3 + flame_test: ^1.15.3 flutter_test: sdk: flutter flutter: assets: - - assets/shaders/ \ No newline at end of file + - assets/shaders/ diff --git a/packages/flame_3d/shaders/spatial_material.frag b/packages/flame_3d/shaders/spatial_material.frag new file mode 100644 index 00000000000..fe3fa27c524 --- /dev/null +++ b/packages/flame_3d/shaders/spatial_material.frag @@ -0,0 +1,56 @@ +#version 460 core + +in vec2 fragTexCoord; +in vec4 fragColor; +in vec3 fragPosition; +smooth in vec3 fragNormal; + +out vec4 outColor; + +uniform sampler2D albedoTexture; // Albedo texture + +uniform Material { + vec3 albedoColor; + float metallic; + float metallicSpecular; + float roughness; +} material; + +uniform Light { + vec3 position; +} light; + +uniform Camera { + vec3 position; +} camera; + +// Schlick GGX function +float SchlickGGX(float NdotV, float roughness) +{ + float k = (roughness * roughness) / 2.0; + float nom = NdotV; + float denom = NdotV * (1.0 - k) + k; + return nom / denom; +} + +void main() { + vec3 viewDir = normalize(camera.position - fragPosition); + vec3 lightDir = normalize(light.position - fragPosition); + vec3 halfwayDir = normalize(viewDir + lightDir); + + vec3 normal = normalize(fragNormal); + float NdotV = max(dot(normal, viewDir), 0.0); + float fresnel = SchlickGGX(NdotV, material.roughness); + + float NdotL = max(dot(normal, lightDir), 0.0); + float NdotH = max(dot(normal, halfwayDir), 0.0); + float specular = SchlickGGX(NdotL, material.roughness) * SchlickGGX(NdotH, material.roughness); + + vec3 baseColor = material.albedoColor; + baseColor *= texture(albedoTexture, fragTexCoord).rgb; + + vec3 diffuse = mix(baseColor, vec3(0.04, 0.04, 0.04), material.metallic); + vec3 finalColor = (diffuse + specular * material.metallicSpecular) * NdotL * fresnel; + + outColor = vec4(finalColor, 1.0); +} \ No newline at end of file diff --git a/packages/flame_3d/shaders/spatial_material.vert b/packages/flame_3d/shaders/spatial_material.vert new file mode 100644 index 00000000000..535c1579a13 --- /dev/null +++ b/packages/flame_3d/shaders/spatial_material.vert @@ -0,0 +1,33 @@ +#version 460 core + +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec4 vertexColor; +in vec3 vertexNormal; + +out vec2 fragTexCoord; +out vec4 fragColor; +out vec3 fragPosition; +out vec3 fragNormal; + +uniform VertexInfo { + mat4 model; + mat4 view; + mat4 projection; +} vertex_info; + +void main() { + // Calculate the modelview projection matrix + mat4 modelViewProjection = vertex_info.projection * vertex_info.view * vertex_info.model; + + // Transform the vertex position + gl_Position = modelViewProjection * vec4(vertexPosition, 1.0); + + // Pass the interpolated values to the fragment shader + fragTexCoord = vertexTexCoord; + fragColor = vertexColor; + + // Calculate the world-space position and normal + fragPosition = vec3(vertex_info.model * vec4(vertexPosition, 1.0)); + fragNormal = mat3(transpose(inverse(vertex_info.model))) * vertexNormal; +} diff --git a/packages/flame_3d/shaders/standard_material.frag b/packages/flame_3d/shaders/standard_material.frag deleted file mode 100644 index 8253fe6945c..00000000000 --- a/packages/flame_3d/shaders/standard_material.frag +++ /dev/null @@ -1,16 +0,0 @@ - -in vec2 fragTexCoord; -in vec4 fragColor; - -out vec4 outColor; - -uniform sampler2D albedoTexture; - -uniform FragmentInfo { - vec4 albedoColor; -} fragment_info; - -void main() { - vec4 texelColor = texture(albedoTexture, fragTexCoord); - outColor = texelColor * fragment_info.albedoColor * fragColor; -} diff --git a/packages/flame_3d/shaders/standard_material.vert b/packages/flame_3d/shaders/standard_material.vert deleted file mode 100644 index d93ce85ecc6..00000000000 --- a/packages/flame_3d/shaders/standard_material.vert +++ /dev/null @@ -1,16 +0,0 @@ -in vec3 vertexPosition; -in vec2 vertexTexCoord; -in vec4 vertexColor; - -out vec2 fragTexCoord; -out vec4 fragColor; - -uniform VertexInfo { - mat4 mvp; -} vertex_info; - -void main() { - fragTexCoord = vertexTexCoord; - fragColor = vertexColor; - gl_Position = vertex_info.mvp * vec4(vertexPosition, 1.0); -} diff --git a/packages/flame_3d/test/vector2_extensions_test.dart b/packages/flame_3d/test/vector2_extensions_test.dart index b8a773608c2..e8ff4582d9a 100644 --- a/packages/flame_3d/test/vector2_extensions_test.dart +++ b/packages/flame_3d/test/vector2_extensions_test.dart @@ -16,4 +16,4 @@ void main() { expect(mutable.y, 2); }); }); -} \ No newline at end of file +} diff --git a/packages/flame_3d/test/vector3_extensions_test.dart b/packages/flame_3d/test/vector3_extensions_test.dart index ffbf937cfe9..51128a778b2 100644 --- a/packages/flame_3d/test/vector3_extensions_test.dart +++ b/packages/flame_3d/test/vector3_extensions_test.dart @@ -26,4 +26,4 @@ void main() { expect((a - b.immutable).storage, [-1, 8, -3]); }); }); -} \ No newline at end of file +} diff --git a/packages/flame_3d/test/vector4_extensions_test.dart b/packages/flame_3d/test/vector4_extensions_test.dart index 560d1caefa2..c0ba75ce7a7 100644 --- a/packages/flame_3d/test/vector4_extensions_test.dart +++ b/packages/flame_3d/test/vector4_extensions_test.dart @@ -20,4 +20,4 @@ void main() { expect(mutable.w, 4); }); }); -} \ No newline at end of file +}