From 0f417bb7eeb51793d74e8dfe4aee8185a467b842 Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Mon, 26 Feb 2024 15:52:33 +0100 Subject: [PATCH] feat(flame_3d): initial implementation of 3D support (#3012) Co-authored-by: Lukas Klingsbo --- packages/flame_3d/.metadata | 10 + packages/flame_3d/CHANGELOG.md | 3 + packages/flame_3d/CONTRIBUTING.md | 48 ++++ packages/flame_3d/LICENSE | 22 ++ packages/flame_3d/README.md | 139 ++++++++++++ packages/flame_3d/ROADMAP.md | 50 +++++ packages/flame_3d/analysis_options.yaml | 1 + .../shaders/standard_material.shaderbundle | Bin 0 -> 10304 bytes packages/flame_3d/bin/build_shaders.dart | 62 +++++ packages/flame_3d/example/.metadata | 45 ++++ packages/flame_3d/example/README.md | 3 + .../flame_3d/example/analysis_options.yaml | 1 + .../flame_3d/example/assets/images/crate.jpg | Bin 0 -> 37566 bytes packages/flame_3d/example/lib/crate.dart | 29 +++ .../lib/keyboard_controlled_camera.dart | 196 ++++++++++++++++ packages/flame_3d/example/lib/main.dart | 142 ++++++++++++ packages/flame_3d/example/lib/player_box.dart | 29 +++ packages/flame_3d/example/lib/simple_hud.dart | 76 +++++++ packages/flame_3d/example/pubspec.yaml | 22 ++ packages/flame_3d/lib/camera.dart | 4 + packages/flame_3d/lib/components.dart | 2 + packages/flame_3d/lib/extensions.dart | 6 + packages/flame_3d/lib/game.dart | 6 + packages/flame_3d/lib/graphics.dart | 1 + packages/flame_3d/lib/resources.dart | 12 + .../lib/src/camera/camera_component_3d.dart | 207 +++++++++++++++++ .../flame_3d/lib/src/camera/world_3d.dart | 48 ++++ .../lib/src/components/component_3d.dart | 113 ++++++++++ .../lib/src/components/mesh_component.dart | 42 ++++ .../flame_3d/lib/src/extensions/aabb3.dart | 15 ++ .../flame_3d/lib/src/extensions/color.dart | 8 + .../flame_3d/lib/src/extensions/matrix4.dart | 31 +++ .../flame_3d/lib/src/extensions/vector2.dart | 9 + .../flame_3d/lib/src/extensions/vector3.dart | 9 + .../flame_3d/lib/src/extensions/vector4.dart | 9 + .../flame_3d/lib/src/game/flame_game_3d.dart | 18 ++ .../lib/src/game/notifying_quaternion.dart | 174 +++++++++++++++ .../lib/src/game/notifying_vector3.dart | 211 ++++++++++++++++++ .../flame_3d/lib/src/game/transform_3d.dart | 147 ++++++++++++ .../lib/src/graphics/graphics_device.dart | 190 ++++++++++++++++ .../flame_3d/lib/src/resources/material.dart | 2 + .../lib/src/resources/material/material.dart | 68 ++++++ .../resources/material/standard_material.dart | 48 ++++ packages/flame_3d/lib/src/resources/mesh.dart | 6 + .../lib/src/resources/mesh/cuboid_mesh.dart | 64 ++++++ .../flame_3d/lib/src/resources/mesh/mesh.dart | 67 ++++++ .../lib/src/resources/mesh/plane_mesh.dart | 23 ++ .../lib/src/resources/mesh/sphere_mesh.dart | 53 +++++ .../lib/src/resources/mesh/surface.dart | 96 ++++++++ .../lib/src/resources/mesh/vertex.dart | 57 +++++ .../flame_3d/lib/src/resources/resource.dart | 19 ++ .../flame_3d/lib/src/resources/texture.dart | 3 + .../src/resources/texture/color_texture.dart | 20 ++ .../src/resources/texture/image_texture.dart | 17 ++ .../lib/src/resources/texture/texture.dart | 42 ++++ packages/flame_3d/pubspec.yaml | 28 +++ .../flame_3d/shaders/standard_material.frag | 16 ++ .../flame_3d/shaders/standard_material.vert | 16 ++ 58 files changed, 2785 insertions(+) create mode 100644 packages/flame_3d/.metadata create mode 100644 packages/flame_3d/CHANGELOG.md create mode 100644 packages/flame_3d/CONTRIBUTING.md create mode 100644 packages/flame_3d/LICENSE create mode 100644 packages/flame_3d/README.md create mode 100644 packages/flame_3d/ROADMAP.md create mode 100644 packages/flame_3d/analysis_options.yaml create mode 100644 packages/flame_3d/assets/shaders/standard_material.shaderbundle create mode 100644 packages/flame_3d/bin/build_shaders.dart create mode 100644 packages/flame_3d/example/.metadata create mode 100644 packages/flame_3d/example/README.md create mode 100644 packages/flame_3d/example/analysis_options.yaml create mode 100644 packages/flame_3d/example/assets/images/crate.jpg create mode 100644 packages/flame_3d/example/lib/crate.dart create mode 100644 packages/flame_3d/example/lib/keyboard_controlled_camera.dart create mode 100644 packages/flame_3d/example/lib/main.dart create mode 100644 packages/flame_3d/example/lib/player_box.dart create mode 100644 packages/flame_3d/example/lib/simple_hud.dart create mode 100644 packages/flame_3d/example/pubspec.yaml create mode 100644 packages/flame_3d/lib/camera.dart create mode 100644 packages/flame_3d/lib/components.dart create mode 100644 packages/flame_3d/lib/extensions.dart create mode 100644 packages/flame_3d/lib/game.dart create mode 100644 packages/flame_3d/lib/graphics.dart create mode 100644 packages/flame_3d/lib/resources.dart create mode 100644 packages/flame_3d/lib/src/camera/camera_component_3d.dart create mode 100644 packages/flame_3d/lib/src/camera/world_3d.dart create mode 100644 packages/flame_3d/lib/src/components/component_3d.dart create mode 100644 packages/flame_3d/lib/src/components/mesh_component.dart create mode 100644 packages/flame_3d/lib/src/extensions/aabb3.dart create mode 100644 packages/flame_3d/lib/src/extensions/color.dart create mode 100644 packages/flame_3d/lib/src/extensions/matrix4.dart create mode 100644 packages/flame_3d/lib/src/extensions/vector2.dart create mode 100644 packages/flame_3d/lib/src/extensions/vector3.dart create mode 100644 packages/flame_3d/lib/src/extensions/vector4.dart create mode 100644 packages/flame_3d/lib/src/game/flame_game_3d.dart create mode 100644 packages/flame_3d/lib/src/game/notifying_quaternion.dart create mode 100644 packages/flame_3d/lib/src/game/notifying_vector3.dart create mode 100644 packages/flame_3d/lib/src/game/transform_3d.dart create mode 100644 packages/flame_3d/lib/src/graphics/graphics_device.dart create mode 100644 packages/flame_3d/lib/src/resources/material.dart create mode 100644 packages/flame_3d/lib/src/resources/material/material.dart create mode 100644 packages/flame_3d/lib/src/resources/material/standard_material.dart create mode 100644 packages/flame_3d/lib/src/resources/mesh.dart create mode 100644 packages/flame_3d/lib/src/resources/mesh/cuboid_mesh.dart create mode 100644 packages/flame_3d/lib/src/resources/mesh/mesh.dart create mode 100644 packages/flame_3d/lib/src/resources/mesh/plane_mesh.dart create mode 100644 packages/flame_3d/lib/src/resources/mesh/sphere_mesh.dart create mode 100644 packages/flame_3d/lib/src/resources/mesh/surface.dart create mode 100644 packages/flame_3d/lib/src/resources/mesh/vertex.dart create mode 100644 packages/flame_3d/lib/src/resources/resource.dart create mode 100644 packages/flame_3d/lib/src/resources/texture.dart create mode 100644 packages/flame_3d/lib/src/resources/texture/color_texture.dart create mode 100644 packages/flame_3d/lib/src/resources/texture/image_texture.dart create mode 100644 packages/flame_3d/lib/src/resources/texture/texture.dart create mode 100644 packages/flame_3d/pubspec.yaml create mode 100644 packages/flame_3d/shaders/standard_material.frag create mode 100644 packages/flame_3d/shaders/standard_material.vert diff --git a/packages/flame_3d/.metadata b/packages/flame_3d/.metadata new file mode 100644 index 00000000000..18428991e0b --- /dev/null +++ b/packages/flame_3d/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "1b197762c51e993cb77d7fafe9729ef2506e2bf7" + channel: "beta" + +project_type: package diff --git a/packages/flame_3d/CHANGELOG.md b/packages/flame_3d/CHANGELOG.md new file mode 100644 index 00000000000..6ef630e4a42 --- /dev/null +++ b/packages/flame_3d/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0-dev.1 + +- Initial experimental release of `flame_3d`. diff --git a/packages/flame_3d/CONTRIBUTING.md b/packages/flame_3d/CONTRIBUTING.md new file mode 100644 index 00000000000..a16aea724ca --- /dev/null +++ b/packages/flame_3d/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Contribution Guidelines + +Read the main [Flame Contribution Guidelines](https://github.com/flame-engine/flame/blob/main/CONTRIBUTING.md) +first and then come back to this one. + +## How To Contribute + + +### Environment Setup + +First follow the steps described in the main [Flame Contribution Guidelines](https://github.com/flame-engine/flame/blob/main/CONTRIBUTING.md#environment-setup) + +After you have followed those steps you have to setup Flutter to use the specific build that this +package is built against: + +```sh +cd $(dirname $(which flutter)) && git checkout 8a5509ea6a277d48c15e5965163b08bd4ad4816a -q && echo "Engine commit: $(cat internal/engine.version)" && cd - >/dev/null +``` + +This will check out the GIT repo of your Flutter installation to the specific commit that we require +and also gets us t he the commit SHA of the Flutter Engine that you need to use in setting up the +Flutter GPU. For that you can follow the steps described in the +[Flutter Wiki](https://github.com/flutter/flutter/wiki/Flutter-GPU#try-out-flutter-gpu). + +Once you have cloned the Flutter engine you can add the `flutter_gpu` as an override dependency +to the `pubspec_overrides.yaml` file in the `flame_3d` directory and it's example: + +```yaml +dependency_overrides: + ... # Melos related overrides + flutter_gpu: + path: /lib/gpu +``` + +After all of that you should run `flutter pub get` one more time to ensure all dependencies are +set up correctly. + + +### Shader changes + +If you have added/changed/removed any of the shaders in the `shaders` directory make sure to run the +build script for shaders: + +```sh +dart bin/build_shaders.dart +``` + +This is currently a manual process until Flutter provides bundling support. \ No newline at end of file diff --git a/packages/flame_3d/LICENSE b/packages/flame_3d/LICENSE new file mode 100644 index 00000000000..f7831247594 --- /dev/null +++ b/packages/flame_3d/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Blue Fire + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/packages/flame_3d/README.md b/packages/flame_3d/README.md new file mode 100644 index 00000000000..de7c155786d --- /dev/null +++ b/packages/flame_3d/README.md @@ -0,0 +1,139 @@ + +

+ + flame + +

+ +

+Adds 3D support for Flame using the Flutter GPU. +

+ +

+ + + + +

+ +--- + + + +# flame_3d + +This package provides an experimental implementation of 3D support for Flame. The main focus is to +explore the potential capabilities of 3D for Flame while providing a familiar API to existing Flame +developers. + +Supported platforms: + +| Platform | Supported | +| -------- | --------- | +| Android | ❌ | +| iOS | ❌ | +| macOS | ✅ | +| Windows | ❌ | +| Linux | ❌ | +| Web | ❌ | + +## Prologue + +**STOP**, we know you are hyped up and want to start coding some funky 3D stuff but we first have to +set your expectations and clarify some things. So turn down your music, put away the coffee and make +some tea instead because you have to do some reading first! + +This package provides 3D support for Flame but it depends on the still experimental +[Flutter GPU](https://github.com/flutter/flutter/wiki/Flutter-GPU) which in turn depends on +Impeller. The Flutter GPU is currently not shipped with Flutter so this package wont work without +following the prerequisites steps. + +Because we depend on Flutter GPU this package is also highly experimental. Our long term goal is to +eventually deprecate this package and integrate it into the core `flame` package, for more +information on this see the [Roadmap](https://github.com/flame-engine/flame/blob/main/packages/flame_3d/ROADMAP.md). + +This package does not guarantee that it will follow correct [semver](https://semver.org/) versioning +rules nor does it assure that it's APIs wont break. Be ready to constantly have to refactor your +code if you are planning on using this package in a semi-production environment, which we do not +recommend. + +Documentation and tests might be lacking for quite a while because of the potential constant changes +of the API. Where possible we will try to provide in-code documentation and code examples to help +developers but our main goal for now is to enable the usage of 3D rendering within a Flame +ecosystem. + + +## Prerequisites + +Before you can get started with using this package a few steps have to happen first. Step one is +switching to a specific commit on the Flutter tooling. Because this package is still experimental +some of the features it requires are still being worked on from the Flutter side. + +So to make sure you are using the same build that we use while developing you have to manually +checkout a specific Flutter build. Thankfully we were able to simplify that process into a +one-liner: + +```sh +cd $(dirname $(which flutter)) && && git fetch && git checkout bcdd1b2c481bca0647beff690238efaae68ca5ac -q && echo "Engine commit: $(cat internal/engine.version)" && cd - >/dev/null +``` + +This will check out the GIT repo of your Flutter installation to the specific commit that we require +and also return the commit SHA of the Flutter Engine that it was build with. We need for step two. + +Step two is setting up the Flutter GPU. You can follow the steps described in the [Flutter Wiki](https://github.com/flutter/flutter/wiki/Flutter-GPU#try-out-flutter-gpu). +The engine commit that you should use is the one we got in step one. + +Once you have cloned the Flutter engine you can add the `flutter_gpu` as an override dependency +to your `pubspec.yaml` or in a `pubspec_overrides.yaml` file: + +```yaml +dependency_overrides: + flutter_gpu: + path: /lib/gpu +``` + +Step three would be to enable impeller for the macOS platform, add the following to the +`Info.plist` in your `macos/` directory: + +```xml + + ... + FLTEnableImpeller + + +``` + +Now everything is set up you can start doing some 3D magic! You can check out the +[example](https://github.com/flame-engine/flame/tree/main/packages/flame_3d/example) to see how you +can set up a simple 3D environment using Flame. + + +## Building shaders + +You can write your own shaders and use them on Materials. Currently Flutter does not do the bundling +of shaders for us so this package provides a simple dart script. Create your fragment and vertex +shader in a `shaders` directory, make sure the file names are identical. Like so: + +- `my_custom_shader`.frag +- `my_custom_shader`.vert + +You can then run `dart pub run flame_3d:build_shaders` to bundle the shaders. They will +automatically be placed in `assets/shaders`. + +You can check out the +[default shaders](https://github.com/flame-engine/flame/tree/main/packages/flame_3d/shaders) if you +want to have some examples. + + +## Contributing + +Have you found a bug or have a suggestion of how to enhance the 3D APIs? Open an issue and we will +take a look at it as soon as possible. + +Do you want to contribute with a PR? PRs are always welcome, just make sure to create it from the +correct branch (main) and follow the [checklist](.github/pull_request_template.md) which will +appear when you open the PR. + +For bigger changes, or if in doubt, make sure to talk about your contribution to the team. Either +via an issue, GitHub discussion, or reach out to the team using the +[Discord server](https://discord.gg/pxrBmy4). \ No newline at end of file diff --git a/packages/flame_3d/ROADMAP.md b/packages/flame_3d/ROADMAP.md new file mode 100644 index 00000000000..b225bec5ad5 --- /dev/null +++ b/packages/flame_3d/ROADMAP.md @@ -0,0 +1,50 @@ +# Roadmap + +In the interest of transparency, we provide a high-level detail of the roadmap for adding 3D +support to Flame. We hope this roadmap will help others in making plans and priorities based on the +work we are doing and potentially contribute back to the project itself. + +The goal of the package can be split up into two sections, the primary goal is to provide an API for +Flame developers so they can create 3D environments without having to learn new Flame concepts. This +means the package will tie into the existing [FCS](https://docs.flame-engine.org/latest/flame/components.html#component) +and provide the tools needed, like a [`CameraComponent`](https://docs.flame-engine.org/latest/flame/camera_component.html), +`World` and similar components. + +In a perfect world this API does not depend or even know about the Flutter GPU, which brings us +to our secondary goal: to abstract the Flutter GPU into an API that is user-friendly for 3D +development. That includes simplifying things like creating render targets, setting up the color +and depth textures and configuring depth stencils. But it also includes higher level APIs like +geometric shapes, texture/material rendering and creating Meshes that can use those shapes and +materials. + +## Goals + +### Abstracting the Flutter GPU into a user-friendly API for 3D + +- [x] Abstract the GPU setup into a class that represents the graphics device + - [ ] Setup binding logic for meshes, geometry and materials. +- [ ] Provide a `Mesh` API + - [x] Provide `Surface`s that can hold geometric shapes. + - [x] Provide a `Material` API + - [x] Define a `Texture` API to be used with the `Material` API + - [x] Support images as textures + - [x] Support single color textures + - [x] Support generated textures + - [x] Provide a standard `Material` + - [ ] Support custom shaders + - [ ] Add a more dev friendly way to set uniforms + - [x] Support multiple `Material`s by defining surfaces on a mesh. + + +### Providing a familiar API for developers to use 3D with Flame + +- [x] Use the existing `CameraComponent` API for 3D rendering + - [x] Provide a custom `World` + - [x] Support existing and custom viewports + - [ ] Support existing and custom viewfinders +- [x] Create a new core component for 3D rendering (`Component3D`) + - [x] Implement a `Transform3D` for 3D transformations + - [x] Implement a notifying `Vector3` and `Quaternion` for 3D positioning and rotation + - [ ] Add support for gesture event callbacks +- [x] Create a component that can show meshes (`MeshComponent`) + - [x] Ensure materials can be set outside of construction (in the `onLoad` for instance) \ No newline at end of file diff --git a/packages/flame_3d/analysis_options.yaml b/packages/flame_3d/analysis_options.yaml new file mode 100644 index 00000000000..92aae2f2499 --- /dev/null +++ b/packages/flame_3d/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flame_lint/analysis_options_with_dcm.yaml diff --git a/packages/flame_3d/assets/shaders/standard_material.shaderbundle b/packages/flame_3d/assets/shaders/standard_material.shaderbundle new file mode 100644 index 0000000000000000000000000000000000000000..37d2f9e01b7d2cf0d2cf87dfb1f1822561945461 GIT binary patch literal 10304 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 Wppv)ie-RhSAL&pem+cN^;r|5~ViJV_ literal 0 HcmV?d00001 diff --git a/packages/flame_3d/bin/build_shaders.dart b/packages/flame_3d/bin/build_shaders.dart new file mode 100644 index 00000000000..0cefe877f75 --- /dev/null +++ b/packages/flame_3d/bin/build_shaders.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; +import 'dart:io'; + +/// Bundle a shader (.frag & .vert) into a single shader bundle and +/// store it in the assets directory. +/// +/// This script is just a temporary way to bundle shaders. In the long run +/// Flutter might support auto-bundling themselves but until then we have to +/// do it manually. +/// +/// Note: this script should be run from the root of the package: +/// packages/flame_3d +void main() async { + final root = Directory.current; + + final assets = Directory.fromUri(root.uri.resolve('assets/shaders')); + // Delete all the bundled shaders so we can replace them with new ones. + if (assets.existsSync()) { + assets.deleteSync(recursive: true); + } + // 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'); + } + + // Get a list of unique shader names. Each shader should have a .frag and + // .vert with the same basename to be considered a bundle. + final uniqueShaders = shaders + .listSync() + .whereType() + .map((f) => f.path.split('/').last.split('.').first) + .toSet(); + + for (final name in uniqueShaders) { + final bundle = { + 'TextureFragment': { + 'type': 'fragment', + 'file': '${root.path}/shaders/$name.frag', + }, + 'TextureVertex': { + 'type': 'vertex', + 'file': '${root.path}/shaders/$name.vert', + }, + }; + + final result = await Process.run(impellerC, [ + '--sl=${assets.path}/$name.shaderbundle', + '--shader-bundle=${jsonEncode(bundle)}', + ]); + + if (result.exitCode != 0) { + return stderr.writeln(result.stderr); + } + } +} + +final impellerC = + '${Platform.environment['FLUTTER_HOME']}/bin/cache/artifacts/engine/darwin-x64/impellerc'; diff --git a/packages/flame_3d/example/.metadata b/packages/flame_3d/example/.metadata new file mode 100644 index 00000000000..a2eed5f2b7c --- /dev/null +++ b/packages/flame_3d/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "1b197762c51e993cb77d7fafe9729ef2506e2bf7" + channel: "beta" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 + base_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 + - platform: android + create_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 + base_revision: 1b197762c51e993cb77d7fafe9729ef2506e2bf7 + - 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 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/flame_3d/example/README.md b/packages/flame_3d/example/README.md new file mode 100644 index 00000000000..e6a8c4c9085 --- /dev/null +++ b/packages/flame_3d/example/README.md @@ -0,0 +1,3 @@ +# flame_3d example + +An example for using the `flame_3d` package. \ No newline at end of file diff --git a/packages/flame_3d/example/analysis_options.yaml b/packages/flame_3d/example/analysis_options.yaml new file mode 100644 index 00000000000..92aae2f2499 --- /dev/null +++ b/packages/flame_3d/example/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flame_lint/analysis_options_with_dcm.yaml diff --git a/packages/flame_3d/example/assets/images/crate.jpg b/packages/flame_3d/example/assets/images/crate.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eaebeba17865fe93f90e2c4a6858b0009b49210d GIT binary patch literal 37566 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%0D;@YS4s~A`2k#!%?wYO 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 onLoad() async { + final crateTexture = await Flame.images.loadTexture('crate.jpg'); + mesh.addMaterialToSurface(0, StandardMaterial(albedoTexture: crateTexture)); + } + + double direction = 0.1; + + @override + void update(double dt) { + if (scale.x >= 1.19 || scale.x <= 0.99) { + direction *= -1; + } + scale.add(Vector3.all(direction * dt)); + } +} diff --git a/packages/flame_3d/example/lib/keyboard_controlled_camera.dart b/packages/flame_3d/example/lib/keyboard_controlled_camera.dart new file mode 100644 index 00000000000..936e8685631 --- /dev/null +++ b/packages/flame_3d/example/lib/keyboard_controlled_camera.dart @@ -0,0 +1,196 @@ +import 'package:flame/components.dart' show KeyboardHandler; +import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/game.dart'; +import 'package:flutter/gestures.dart' show kMiddleMouseButton; +import 'package:flutter/services.dart' + show KeyEvent, KeyRepeatEvent, LogicalKeyboardKey, PointerEvent; + +class KeyboardControlledCamera extends CameraComponent3D with KeyboardHandler { + KeyboardControlledCamera({ + super.world, + super.viewport, + super.viewfinder, + super.backdrop, + super.hudComponents, + }) : super( + projection: CameraProjection.perspective, + mode: CameraMode.firstPerson, + position: Vector3(0, 2, 4), + target: Vector3(0, 2, 0), + up: Vector3(0, 1, 0), + fovY: 60, + ); + + final double moveSpeed = 0.9; + final double rotationSpeed = 0.3; + final double panSpeed = 2; + final double orbitalSpeed = 0.5; + + Set _keysDown = {}; + PointerEvent? pointerEvent; + double scrollMove = 0; + + final Matrix4 _orbitalMatrix = Matrix4.identity(); + + @override + bool onKeyEvent(KeyEvent event, Set keysPressed) { + _keysDown = keysPressed; + + // Switch camera mode + if (isKeyDown(Key.digit1)) { + mode = CameraMode.free; + up = Vector3(0, 1, 0); // Reset roll + } else if (isKeyDown(Key.digit2)) { + mode = CameraMode.firstPerson; + up = Vector3(0, 1, 0); // Reset roll + } else if (isKeyDown(Key.digit3)) { + mode = CameraMode.thirdPerson; + up = Vector3(0, 1, 0); // Reset roll + } else if (isKeyDown(Key.digit4)) { + mode = CameraMode.orbital; + up = Vector3(0, 1, 0); // Reset roll + } + + if (isKeyDown(Key.keyP) && event is! KeyRepeatEvent) { + if (projection == CameraProjection.perspective) { + // Create an isometric view. + mode = CameraMode.thirdPerson; + projection = CameraProjection.orthographic; + + position = Vector3(0, 2, -100); + target = Vector3(0, 2, 0); + up = Vector3(0, 1, 0); + fovY = 20; + + yaw(-135 * degrees2Radians, rotateAroundTarget: true); + pitch(-45 * degrees2Radians, lockView: true, rotateAroundTarget: true); + } else if (projection == CameraProjection.orthographic) { + // Reset to default view. + mode = CameraMode.thirdPerson; + projection = CameraProjection.perspective; + + position = Vector3(0, 2, 10); + target = Vector3(0, 2, 0); + up = Vector3(0, 1, 0); + fovY = 60; + } + } + + return false; + } + + @override + void update(double dt) { + final moveInWorldPlane = switch (mode) { + CameraMode.firstPerson || CameraMode.thirdPerson => true, + _ => false, + }; + final rotateAroundTarget = switch (mode) { + CameraMode.thirdPerson || CameraMode.orbital => true, + _ => false, + }; + final lockView = switch (mode) { + CameraMode.free || CameraMode.firstPerson || CameraMode.orbital => true, + _ => false, + }; + + if (mode == CameraMode.orbital) { + final rotation = _orbitalMatrix + ..setIdentity() + ..rotate(up, orbitalSpeed * dt); + final view = rotation.transform3(position - target); + position = target + view; + } else { + // Camera rotation + if (isKeyDown(Key.arrowDown)) { + pitch( + -rotationSpeed * dt, + lockView: lockView, + rotateAroundTarget: rotateAroundTarget, + ); + } else if (isKeyDown(Key.arrowUp)) { + pitch( + rotationSpeed * dt, + lockView: lockView, + rotateAroundTarget: rotateAroundTarget, + ); + } + if (isKeyDown(Key.arrowRight)) { + yaw(-rotationSpeed * dt, rotateAroundTarget: rotateAroundTarget); + } else if (isKeyDown(Key.arrowLeft)) { + yaw(rotationSpeed * dt, rotateAroundTarget: rotateAroundTarget); + } + if (isKeyDown(Key.keyQ)) { + roll(-rotationSpeed * dt); + } else if (isKeyDown(Key.keyE)) { + roll(rotationSpeed * dt); + } + + // Camera movement, if mode is free and mouse button is down we pan the + // camera. + if (pointerEvent != null) { + if (mode == CameraMode.free && + pointerEvent?.buttons == kMiddleMouseButton) { + final mouseDelta = pointerEvent!.delta; + if (mouseDelta.dx > 0) { + moveRight(panSpeed * dt, moveInWorldPlane: moveInWorldPlane); + } else if (mouseDelta.dx < 0) { + moveRight(-panSpeed * dt, moveInWorldPlane: moveInWorldPlane); + } + if (mouseDelta.dy > 0) { + moveUp(-panSpeed * dt); + } else if (mouseDelta.dy < 0) { + moveUp(panSpeed * dt); + } + } else { + const mouseMoveSensitivity = 0.003; + yaw( + (pointerEvent?.delta.dx ?? 0) * mouseMoveSensitivity, + rotateAroundTarget: rotateAroundTarget, + ); + pitch( + (pointerEvent?.delta.dy ?? 0) * mouseMoveSensitivity, + lockView: lockView, + rotateAroundTarget: rotateAroundTarget, + ); + } + pointerEvent = null; + } + + // Keyboard movement + if (isKeyDown(Key.keyW)) { + moveForward(moveSpeed * dt); + } else if (isKeyDown(Key.keyS)) { + moveForward(-moveSpeed * dt); + } + if (isKeyDown(Key.keyA)) { + moveRight(-moveSpeed * dt); + } else if (isKeyDown(Key.keyD)) { + moveRight(moveSpeed * dt); + } + + if (mode == CameraMode.free) { + if (isKeyDown(Key.space)) { + moveUp(moveSpeed * dt); + } else if (isKeyDown(Key.controlLeft)) { + moveUp(-moveSpeed * dt); + } + } + } + + // if (mode == CameraMode.thirdPerson || + // mode == CameraMode.orbital || + // mode == CameraMode.free) { + // moveToTarget(-scrollMove); + // if (isKeyDown(Key.numpadSubtract)) { + // moveToTarget(2 * dt); + // } else if (isKeyDown(Key.numpadAdd)) { + // moveToTarget(-2 * dt); + // } + // } + } + + bool isKeyDown(Key key) => _keysDown.contains(key); +} + +typedef Key = LogicalKeyboardKey; diff --git a/packages/flame_3d/example/lib/main.dart b/packages/flame_3d/example/lib/main.dart new file mode 100644 index 00000000000..843d71fcc1a --- /dev/null +++ b/packages/flame_3d/example/lib/main.dart @@ -0,0 +1,142 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:ui'; + +import 'package:example/crate.dart'; +import 'package:example/keyboard_controlled_camera.dart'; +import 'package:example/player_box.dart'; +import 'package:example/simple_hud.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart' as v64 show Vector2; +import 'package:flame/game.dart' show FlameGame, GameWidget; +import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/components.dart'; +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/resources.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' show runApp, Color, Colors, Listener; + +class ExampleGame3D extends FlameGame + with HasKeyboardHandlerComponents { + ExampleGame3D() + : super( + world: World3D(clearColor: const Color(0xFFFFFFFF)), + camera: KeyboardControlledCamera( + viewport: FixedResolutionViewport( + resolution: v64.Vector2(800, 600), + ), + hudComponents: [SimpleHud()], + ), + ); + + @override + KeyboardControlledCamera get camera => + super.camera as KeyboardControlledCamera; + + @override + FutureOr onLoad() async { + world.addAll([ + // Add a player box + PlayerBox(), + + // Floating crate + Crate(size: Vector3.all(1), position: Vector3(0, 5, 0)), + + // Floating sphere + MeshComponent( + position: Vector3(5, 5, 5), + mesh: SphereMesh( + radius: 1, + material: StandardMaterial( + albedoTexture: ColorTexture(Colors.purple), + ), + ), + ), + + // Floor + MeshComponent( + mesh: PlaneMesh( + size: Vector2(32, 32), + material: StandardMaterial(albedoTexture: ColorTexture(Colors.grey)), + ), + ), + + // Front wall + MeshComponent( + position: Vector3(16.5, 2.5, 0), + mesh: CuboidMesh( + size: Vector3(1, 5, 32), + material: + StandardMaterial(albedoTexture: ColorTexture(Colors.yellow)), + ), + ), + + // Left wall + MeshComponent( + position: Vector3(0, 2.5, 16.5), + mesh: CuboidMesh( + size: Vector3(32, 5, 1), + material: StandardMaterial(albedoTexture: ColorTexture(Colors.blue)), + ), + ), + + // Right wall + MeshComponent( + position: Vector3(0, 2.5, -16.5), + mesh: CuboidMesh( + size: Vector3(32, 5, 1), + material: StandardMaterial(albedoTexture: ColorTexture(Colors.lime)), + ), + ), + ]); + + final rnd = Random(); + for (var i = 0; i < 20; i++) { + final height = rnd.range(1, 12); + + world.add( + MeshComponent( + position: Vector3(rnd.range(-15, 15), height / 2, rnd.range(-15, 15)), + mesh: CuboidMesh( + size: Vector3(1, height, 1), + material: StandardMaterial( + albedoTexture: ColorTexture( + Color.fromRGBO(rnd.iRange(20, 255), rnd.iRange(10, 55), 30, 1), + ), + ), + ), + ), + ); + } + } +} + +void main() { + final example = ExampleGame3D(); + + runApp( + Listener( + onPointerMove: (event) { + if (!event.down) { + return; + } + example.camera.pointerEvent = event; + }, + onPointerSignal: (event) { + if (event is! PointerScrollEvent || !event.down) { + return; + } + example.camera.scrollMove = event.delta.dy / 3000; + }, + onPointerUp: (event) => example.camera.pointerEvent = null, + onPointerCancel: (event) => example.camera.pointerEvent = null, + child: GameWidget(game: example), + ), + ); +} + +extension on Random { + double range(num min, num max) => nextDouble() * (max - min) + min; + + int iRange(int min, int max) => range(min, max).toInt(); +} diff --git a/packages/flame_3d/example/lib/player_box.dart b/packages/flame_3d/example/lib/player_box.dart new file mode 100644 index 00000000000..fc42d7f75bb --- /dev/null +++ b/packages/flame_3d/example/lib/player_box.dart @@ -0,0 +1,29 @@ +import 'dart:ui'; + +import 'package:example/main.dart'; +import 'package:flame/components.dart' show HasGameReference; +import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/components.dart'; +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/resources.dart'; +import 'package:flutter/material.dart' show Colors; + +class PlayerBox extends MeshComponent with HasGameReference { + PlayerBox() + : super( + mesh: CuboidMesh( + size: Vector3.all(0.5), + material: + StandardMaterial(albedoTexture: ColorTexture(Colors.purple)), + ), + ); + + @override + void renderTree(Canvas canvas) { + // Only show the box if we are in third person mode. + if (game.camera.mode == CameraMode.thirdPerson) { + position.setFrom(game.camera.target); + super.renderTree(canvas); + } + } +} diff --git a/packages/flame_3d/example/lib/simple_hud.dart b/packages/flame_3d/example/lib/simple_hud.dart new file mode 100644 index 00000000000..757a70a34a1 --- /dev/null +++ b/packages/flame_3d/example/lib/simple_hud.dart @@ -0,0 +1,76 @@ +import 'dart:ui'; + +import 'package:example/main.dart'; +import 'package:flame/components.dart'; +import 'package:flame/text.dart'; +import 'package:flame_3d/camera.dart'; + +const _width = 1.2; +const _color = Color(0xFFFFFFFF); + +final _style = TextStyle( + color: const Color(0xFF000000), + shadows: [ + for (var x = 1; x < _width + 5; x++) + for (var y = 1; y < _width + 5; y++) ...[ + Shadow(offset: Offset(-_width / x, -_width / y), color: _color), + Shadow(offset: Offset(-_width / x, _width / y), color: _color), + Shadow(offset: Offset(_width / x, -_width / y), color: _color), + Shadow(offset: Offset(_width / x, _width / y), color: _color), + ], + ], +); + +class SimpleHud extends Component with HasGameReference { + SimpleHud() : super(children: [FpsComponent()]); + + String get fps => + children.query().firstOrNull?.fps.toStringAsFixed(2) ?? '0'; + + final _textLeft = TextPaint(style: _style); + + final _textCenter = TextPaint(style: _style.copyWith(fontSize: 20)); + + final _textRight = TextPaint(style: _style, textDirection: TextDirection.rtl); + + @override + void render(Canvas canvas) { + final CameraComponent3D(:position, :target, :up) = game.camera; + + _textLeft.render( + canvas, + ''' +Camera controls: +- Move using W, A, S, D, Space, Left-Ctrl +- Look around with arrow keys or mouse +- Change camera mode with 1, 2, 3 or 4 +- Change camera projection with P +- Zoom in and out with scroll +''', + Vector2.all(8), + ); + + _textCenter.render( + canvas, + 'Welcome to the 3D world', + Vector2(game.size.x / 2, game.size.y - 8), + anchor: Anchor.bottomCenter, + ); + + _textRight.render( + canvas, + ''' +FPS: $fps +Mode: ${game.camera.mode.name} +Projection: ${game.camera.projection.name} +Culled: ${game.world.culled} + +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)} +Up: ${up.x.toStringAsFixed(2)}, ${up.y.toStringAsFixed(2)}, ${up.z.toStringAsFixed(2)} +''', + Vector2(game.size.x - 8, 8), + anchor: Anchor.topRight, + ); + } +} diff --git a/packages/flame_3d/example/pubspec.yaml b/packages/flame_3d/example/pubspec.yaml new file mode 100644 index 00000000000..497c039c6fa --- /dev/null +++ b/packages/flame_3d/example/pubspec.yaml @@ -0,0 +1,22 @@ +name: example +description: An example for flame_3d. The example shows how to set up 3D support in a flame game. +version: 0.0.1+1 +publish_to: none + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + flame: ^1.16.0 + flame_3d: ^0.1.0 + flutter: + sdk: flutter + +dev_dependencies: + flame_lint: ^1.1.2 + +flutter: + uses-material-design: true + + assets: + - assets/images/ diff --git a/packages/flame_3d/lib/camera.dart b/packages/flame_3d/lib/camera.dart new file mode 100644 index 00000000000..0459754995e --- /dev/null +++ b/packages/flame_3d/lib/camera.dart @@ -0,0 +1,4 @@ +export 'package:flame/camera.dart'; + +export 'src/camera/camera_component_3d.dart'; +export 'src/camera/world_3d.dart'; diff --git a/packages/flame_3d/lib/components.dart b/packages/flame_3d/lib/components.dart new file mode 100644 index 00000000000..d3c590b1d53 --- /dev/null +++ b/packages/flame_3d/lib/components.dart @@ -0,0 +1,2 @@ +export 'src/components/component_3d.dart'; +export 'src/components/mesh_component.dart'; diff --git a/packages/flame_3d/lib/extensions.dart b/packages/flame_3d/lib/extensions.dart new file mode 100644 index 00000000000..7a262a03afb --- /dev/null +++ b/packages/flame_3d/lib/extensions.dart @@ -0,0 +1,6 @@ +export 'src/extensions/aabb3.dart'; +export 'src/extensions/color.dart'; +export 'src/extensions/matrix4.dart'; +export 'src/extensions/vector2.dart'; +export 'src/extensions/vector3.dart'; +export 'src/extensions/vector4.dart'; diff --git a/packages/flame_3d/lib/game.dart b/packages/flame_3d/lib/game.dart new file mode 100644 index 00000000000..b79118633a3 --- /dev/null +++ b/packages/flame_3d/lib/game.dart @@ -0,0 +1,6 @@ +export 'package:vector_math/vector_math.dart' hide Colors; + +export 'src/game/flame_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/graphics.dart b/packages/flame_3d/lib/graphics.dart new file mode 100644 index 00000000000..ab2eee25192 --- /dev/null +++ b/packages/flame_3d/lib/graphics.dart @@ -0,0 +1 @@ +export 'src/graphics/graphics_device.dart'; diff --git a/packages/flame_3d/lib/resources.dart b/packages/flame_3d/lib/resources.dart new file mode 100644 index 00000000000..b6ba8d86973 --- /dev/null +++ b/packages/flame_3d/lib/resources.dart @@ -0,0 +1,12 @@ +import 'package:flame/cache.dart'; +import 'package:flame_3d/resources.dart'; + +export 'src/resources/material.dart'; +export 'src/resources/mesh.dart'; +export 'src/resources/resource.dart'; +export 'src/resources/texture.dart'; + +extension TextureCache on Images { + Future loadTexture(String path) => + load('crate.jpg').then(ImageTexture.create); +} diff --git a/packages/flame_3d/lib/src/camera/camera_component_3d.dart b/packages/flame_3d/lib/src/camera/camera_component_3d.dart new file mode 100644 index 00000000000..d747e1f7644 --- /dev/null +++ b/packages/flame_3d/lib/src/camera/camera_component_3d.dart @@ -0,0 +1,207 @@ +import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/extensions.dart'; +import 'package:flame_3d/game.dart'; + +enum CameraProjection { perspective, orthographic } + +enum CameraMode { custom, free, orbital, firstPerson, thirdPerson } + +/// {@template camera_component_3d} +/// [CameraComponent3D] is a component through which a [World3D] is observed. +/// {@endtemplate} +class CameraComponent3D extends CameraComponent { + /// {@macro camera_component_3d} + CameraComponent3D({ + this.fovY = 60, + Vector3? position, + Vector3? target, + Vector3? up, + this.projection = CameraProjection.perspective, + this.mode = CameraMode.free, + World3D? super.world, + super.viewport, + super.viewfinder, + super.backdrop, + super.hudComponents, + }) : position = position?.clone() ?? Vector3.zero(), + target = target?.clone() ?? Vector3.zero(), + _up = up?.clone() ?? Vector3(0, 1, 0); + + @override + World3D? get world => super.world as World3D?; + + @override + set world(covariant World3D? world) => super.world = world; + + /// The [fovY] is the field of view in Y (degrees) when the [projection] is + /// [CameraProjection.perspective] otherwise it is used as the near plane when + /// the [projection] is [CameraProjection.orthographic]. + double fovY; + + /// The position of the camera in 3D space. + /// + /// Often also referred to as the "eye". + Vector3 position; + + /// The target in 3D space that the camera is looking at. + Vector3 target; + + /// The forward direction relative to the camera. + Vector3 get forward => target - position; + + /// The right direction relative to the camera. + Vector3 get right => forward.cross(up); + + /// The up direction relative to the camera. + Vector3 get up => _up.normalized(); + set up(Vector3 up) => _up.setFrom(up); + final Vector3 _up; + + /// The current camera projection. + CameraProjection projection; + + /// The current camera mode. + CameraMode mode; + + /// The view matrix of the camera, this is without any projection applied on + /// it. + Matrix4 get viewMatrix => _viewMatrix..setAsViewMatrix(position, target, up); + final Matrix4 _viewMatrix = Matrix4.zero(); + + /// The projection matrix of the camera. + Matrix4 get projectionMatrix { + final aspectRatio = viewport.virtualSize.x / viewport.virtualSize.y; + return switch (projection) { + CameraProjection.perspective => _projectionMatrix + ..setAsPerspective(fovY, aspectRatio, distanceNear, distanceFar), + CameraProjection.orthographic => _projectionMatrix + ..setAsOrthographic(fovY, aspectRatio, distanceNear, distanceFar) + } + ..multiply(viewMatrix); + } + + final Matrix4 _projectionMatrix = Matrix4.zero(); + final Frustum _frustum = Frustum(); + + Frustum get frustum => _frustum..setFromMatrix(_projectionMatrix); + + void moveForward(double distance, {bool moveInWorldPlane = false}) { + final forward = this.forward..scale(distance); + + if (moveInWorldPlane) { + forward.y = 0; + forward.normalize(); + } + + position.add(forward); + target.add(forward); + } + + void moveUp(double distance) { + final up = this.up..scale(distance); + position.add(up); + target.add(up); + } + + void moveRight(double distance, {bool moveInWorldPlane = false}) { + final right = this.right..scale(distance); + + if (moveInWorldPlane) { + right.y = 0; + right.normalize(); + } + + position.add(right); + target.add(right); + } + + void moveToTarget(double delta) { + var distance = position.distanceTo(target); + distance += delta; + + if (distance <= 0) { + distance = 0.001; + } + + final forward = this.forward; + position.setValues( + target.x + (forward.x * -distance), + target.y + (forward.y * -distance), + target.z + (forward.z * -distance), + ); + } + + void yaw(double angle, {bool rotateAroundTarget = false}) { + final targetPosition = (target - position)..applyAxisAngle(up, angle); + + if (rotateAroundTarget) { + position.setValues( + target.x - targetPosition.x, + target.y - targetPosition.y, + target.z - targetPosition.z, + ); + } else { + target.setValues( + position.x + targetPosition.x, + position.y + targetPosition.y, + position.z + targetPosition.z, + ); + } + } + + void pitch( + double angle, { + bool lockView = false, + bool rotateAroundTarget = false, + bool rotateUp = false, + }) { + var localAngle = angle; + final up = this.up; + final targetPosition = target - position; + + if (lockView) { + final maxAngleUp = up.angleTo(targetPosition); + if (localAngle > maxAngleUp) { + localAngle = maxAngleUp; + } + + var maxAngleDown = (-up).angleTo(targetPosition); + maxAngleDown *= -1.0; + + if (localAngle < maxAngleDown) { + localAngle = maxAngleDown; + } + } + + final right = this.right; + targetPosition.applyAxisAngle(right, localAngle); + + if (rotateAroundTarget) { + position.setValues( + target.x - targetPosition.x, + target.y - targetPosition.y, + target.z - targetPosition.z, + ); + } else { + target.setValues( + position.x + targetPosition.x, + position.y + targetPosition.y, + position.z + targetPosition.z, + ); + } + + if (rotateUp) { + _up.applyAxisAngle(right, angle); + } + } + + void roll(double angle) { + _up.applyAxisAngle(forward, angle); + } + + static CameraComponent3D? get currentCamera => + CameraComponent.currentCamera as CameraComponent3D?; + + static const distanceNear = 0.01; + static const distanceFar = 1000.0; +} diff --git a/packages/flame_3d/lib/src/camera/world_3d.dart b/packages/flame_3d/lib/src/camera/world_3d.dart new file mode 100644 index 00000000000..6497a7d1c0a --- /dev/null +++ b/packages/flame_3d/lib/src/camera/world_3d.dart @@ -0,0 +1,48 @@ +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'; + +/// {@template world_3d} +/// The root component for all 3D world elements. +/// +/// 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 { + /// {@macro world_3d} + World3D({ + super.children, + super.priority, + Color clearColor = const Color(0x00000000), + }) : graphics = GraphicsDevice(clearValue: clearColor); + + /// The graphical device attached to this world. + final GraphicsDevice graphics; + + final _paint = Paint(); + + @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, + ); + + culled = 0; + // ignore: invalid_use_of_internal_member + super.renderFromCamera(canvas); + + final image = graphics.end(); + canvas.drawImage(image, (-viewport.virtualSize / 2).toOffset(), _paint); + image.dispose(); + } + + // TODO(wolfen): 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 new file mode 100644 index 00000000000..d381999dab2 --- /dev/null +++ b/packages/flame_3d/lib/src/components/component_3d.dart @@ -0,0 +1,113 @@ +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]. +/// +/// 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 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 { + /// {@macro component_3d} + Component3D({ + Vector3? position, + Quaternion? rotation, + }) : transform = Transform3D() + ..position = position ?? Vector3.zero() + ..rotation = rotation ?? Quaternion.euler(0, 0, 0) + ..scale = Vector3.all(1); + + final Transform3D transform; + + /// The total transformation matrix for the component. This matrix combines + /// translation, rotation and scale transforms into a single entity. The + /// matrix is cached and gets recalculated only as necessary. + Matrix4 get transformMatrix => transform.transformMatrix; + + /// The position of this component's anchor on the screen. + NotifyingVector3 get position => transform.position; + set position(Vector3 position) => transform.position = position; + + /// X position of this component's anchor on the screen. + double get x => transform.x; + set x(double x) => transform.x = x; + + /// Y position of this component's anchor on the screen. + double get y => transform.y; + set y(double y) => transform.y = y; + + /// Z position of this component's anchor on the screen. + double get z => transform.z; + set z(double z) => transform.z = z; + + /// The rotation of this component. + NotifyingQuaternion get rotation => transform.rotation; + set rotation(NotifyingQuaternion rotation) => transform.rotation = rotation; + + /// The scale factor of this component. The scale can be different along + /// the X, Y and Z dimensions. A scale greater than 1 makes the component + /// bigger along that axis, and less than 1 smaller. The scale can also be negative, + /// which results in a mirror reflection along the corresponding axis. + NotifyingVector3 get scale => transform.scale; + set scale(Vector3 scale) => transform.scale = scale; + + /// 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/mesh_component.dart b/packages/flame_3d/lib/src/components/mesh_component.dart new file mode 100644 index 00000000000..4c9e8ae6003 --- /dev/null +++ b/packages/flame_3d/lib/src/components/mesh_component.dart @@ -0,0 +1,42 @@ +import 'package:flame_3d/components.dart'; +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/resources.dart'; +import 'package:flame_3d/src/camera/camera_component_3d.dart'; +import 'package:flame_3d/src/extensions/aabb3.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] +/// and [scale] applied. +/// +/// This is a commonly used subclass of [Component3D]. +/// {@endtemplate} +class MeshComponent extends Component3D { + /// {@macro mesh_component} + MeshComponent({ + required Mesh mesh, + super.position, + super.rotation, + }) : _mesh = mesh; + + /// The mesh resource. + Mesh get mesh => _mesh; + final Mesh _mesh; + + Aabb3 get aabb => _aabb + ..setFrom(mesh.aabb) + ..transform(transformMatrix); + final Aabb3 _aabb = Aabb3(); + + @override + void bind(GraphicsDevice device) { + world.graphics + ..setViewModel(transformMatrix) + ..bindMesh(mesh); + } + + @override + bool shouldCull(CameraComponent3D camera) { + return camera.frustum.intersectsWithAabb3(aabb); + } +} diff --git a/packages/flame_3d/lib/src/extensions/aabb3.dart b/packages/flame_3d/lib/src/extensions/aabb3.dart new file mode 100644 index 00000000000..1fc56a27269 --- /dev/null +++ b/packages/flame_3d/lib/src/extensions/aabb3.dart @@ -0,0 +1,15 @@ +import 'package:flame_3d/game.dart'; + +extension Aabb3Extension on Aabb3 { + /// Set the min and max from the [other]. + void setFrom(Aabb3 other) { + min.setFrom(other.min); + max.setFrom(other.max); + } + + /// Set the min and max to zero. + void setZero() { + min.setZero(); + max.setZero(); + } +} diff --git a/packages/flame_3d/lib/src/extensions/color.dart b/packages/flame_3d/lib/src/extensions/color.dart new file mode 100644 index 00000000000..b8579e1446f --- /dev/null +++ b/packages/flame_3d/lib/src/extensions/color.dart @@ -0,0 +1,8 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +extension ColorExtension on Color { + /// Returns a Float32List that represents the color as a vector. + Float32List get storage => + Float32List.fromList([red / 255, green / 255, blue / 255, opacity]); +} diff --git a/packages/flame_3d/lib/src/extensions/matrix4.dart b/packages/flame_3d/lib/src/extensions/matrix4.dart new file mode 100644 index 00000000000..d7c18ad50b8 --- /dev/null +++ b/packages/flame_3d/lib/src/extensions/matrix4.dart @@ -0,0 +1,31 @@ +import 'package:flame_3d/game.dart'; + +extension Matrix4Extension on Matrix4 { + /// Set the matrix to be a view matrix. + void setAsViewMatrix(Vector3 position, Vector3 target, Vector3 up) { + setViewMatrix(this, position, target, up); + } + + /// Set the matrix to use a projection view. + void setAsPerspective( + double fovy, + double aspectRatio, + double zNear, + double zFar, + ) { + final fovYRadians = fovy * degrees2Radians; + setPerspectiveMatrix(this, fovYRadians, aspectRatio, zNear, zFar); + } + + /// Set the matrix to use a orthographic view. + void setAsOrthographic( + double nearPlaneWidth, + double aspectRatio, + double zNear, + double zFar, + ) { + final top = nearPlaneWidth / 2.0; + final right = top * aspectRatio; + setOrthographicMatrix(this, -right, right, -top, top, zNear, zFar); + } +} diff --git a/packages/flame_3d/lib/src/extensions/vector2.dart b/packages/flame_3d/lib/src/extensions/vector2.dart new file mode 100644 index 00000000000..dc1c46d3cc4 --- /dev/null +++ b/packages/flame_3d/lib/src/extensions/vector2.dart @@ -0,0 +1,9 @@ +import 'package:flame_3d/game.dart'; + +/// Represents an immutable [Vector2]. +typedef ImmutableVector2 = ({double x, double y}); + +extension Vector2Extension on Vector2 { + /// Returns an immutable representation of the vector. + ImmutableVector2 get immutable => (x: x, y: y); +} diff --git a/packages/flame_3d/lib/src/extensions/vector3.dart b/packages/flame_3d/lib/src/extensions/vector3.dart new file mode 100644 index 00000000000..a577ed007d3 --- /dev/null +++ b/packages/flame_3d/lib/src/extensions/vector3.dart @@ -0,0 +1,9 @@ +import 'package:flame_3d/game.dart'; + +/// Represents an immutable [Vector3]. +typedef ImmutableVector3 = ({double x, double y, double z}); + +extension Vector3Extension on Vector3 { + /// Returns an immutable representation of the vector. + ImmutableVector3 get immutable => (x: x, y: y, z: z); +} diff --git a/packages/flame_3d/lib/src/extensions/vector4.dart b/packages/flame_3d/lib/src/extensions/vector4.dart new file mode 100644 index 00000000000..cadee0da270 --- /dev/null +++ b/packages/flame_3d/lib/src/extensions/vector4.dart @@ -0,0 +1,9 @@ +import 'package:flame_3d/game.dart'; + +/// Represents an immutable [Vector3]. +typedef ImmutableVector4 = ({double x, double y, double z, double w}); + +extension Vector4Extension on Vector4 { + /// Returns an immutable representation of the vector. + ImmutableVector4 get immutable => (x: x, y: y, z: z, w: w); +} diff --git a/packages/flame_3d/lib/src/game/flame_game_3d.dart b/packages/flame_3d/lib/src/game/flame_game_3d.dart new file mode 100644 index 00000000000..ec3fa7d8ec9 --- /dev/null +++ b/packages/flame_3d/lib/src/game/flame_game_3d.dart @@ -0,0 +1,18 @@ +import 'dart:ui'; + +import 'package:flame/game.dart'; +import 'package:flame_3d/camera.dart'; + +class FlameGame3D extends FlameGame { + FlameGame3D({ + super.children, + W? world, + CameraComponent3D? camera, + }) : super( + world: world ?? World3D(clearColor: const Color(0xFFFFFFFF)) as W, + camera: camera ?? CameraComponent3D(), + ); + + @override + CameraComponent3D get camera => super.camera as CameraComponent3D; +} diff --git a/packages/flame_3d/lib/src/game/notifying_quaternion.dart b/packages/flame_3d/lib/src/game/notifying_quaternion.dart new file mode 100644 index 00000000000..29385889976 --- /dev/null +++ b/packages/flame_3d/lib/src/game/notifying_quaternion.dart @@ -0,0 +1,174 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:flame_3d/game.dart'; +import 'package:flutter/widgets.dart'; + +/// {@template notifying_quaternion} +/// Extension of the standard [Quaternion] class, implementing the +/// [ChangeNotifier] functionality. This allows any interested party to be +/// notified when the value of this quaternion changes. +/// +/// This class can be used as a regular [Quaternion] class. However, if you do +/// subscribe to notifications, don't forget to eventually unsubscribe in +/// order to avoid resource leaks. +/// +/// Direct modification of this quaternion's [storage] is not allowed. +/// {@endtemplate} +class NotifyingQuaternion extends Quaternion with ChangeNotifier { + /// {@macro notifying_quaternion} + /// + /// Constructs a quaternion using the raw values [x], [y], [z], and [w]. + factory NotifyingQuaternion(double x, double y, double z, double w) => + NotifyingQuaternion._()..setValues(x, y, z, w); + NotifyingQuaternion._() : super.fromFloat32List(Float32List(4)); + + /// {@macro notifying_quaternion} + /// + /// Constructs a quaternion from a rotation matrix [rotationMatrix]. + factory NotifyingQuaternion.fromRotation(Matrix3 rotationMatrix) => + NotifyingQuaternion._()..setFromRotation(rotationMatrix); + + /// {@macro notifying_quaternion} + /// + /// Constructs a quaternion from a rotation of [angle] around [axis]. + factory NotifyingQuaternion.axisAngle(Vector3 axis, double angle) => + NotifyingQuaternion._()..setAxisAngle(axis, angle); + + /// {@macro notifying_quaternion} + /// + /// Constructs a quaternion as a copy of [other]. + factory NotifyingQuaternion.copy(Quaternion other) => + NotifyingQuaternion._()..setFrom(other); + + /// {@macro notifying_quaternion} + /// + /// Constructs a quaternion from time derivative of [q] with angular + /// velocity [omega]. + factory NotifyingQuaternion.dq(Quaternion q, Vector3 omega) => + NotifyingQuaternion._()..setDQ(q, omega); + + /// {@macro notifying_quaternion} + /// + /// Constructs a quaternion from [yaw], [pitch] and [roll]. + factory NotifyingQuaternion.euler(double yaw, double pitch, double roll) => + NotifyingQuaternion._()..setEuler(yaw, pitch, roll); + + @override + void setValues(double x, double y, double z, double w) { + super.setValues(x, y, z, w); + notifyListeners(); + } + + @override + void setAxisAngle(Vector3 axis, double radians) { + super.setAxisAngle(axis, radians); + notifyListeners(); + } + + @override + void setDQ(Quaternion q, Vector3 omega) { + super.setDQ(q, omega); + notifyListeners(); + } + + @override + void setEuler(double yaw, double pitch, double roll) { + super.setEuler(yaw, pitch, roll); + notifyListeners(); + } + + @override + void setFromRotation(Matrix3 rotationMatrix) { + super.setFromRotation(rotationMatrix); + notifyListeners(); + } + + @override + void setFromTwoVectors(Vector3 a, Vector3 b) { + super.setFromTwoVectors(a, b); + notifyListeners(); + } + + @override + void setRandom(Random rn) { + super.setRandom(rn); + notifyListeners(); + } + + @override + void setFrom(Quaternion source) { + super.setFrom(source); + notifyListeners(); + } + + @override + void operator []=(int i, double arg) { + super[i] = arg; + notifyListeners(); + } + + @override + double normalize() { + final l = super.normalize(); + notifyListeners(); + return l; + } + + @override + void add(Quaternion arg) { + super.add(arg); + notifyListeners(); + } + + @override + void sub(Quaternion arg) { + super.sub(arg); + notifyListeners(); + } + + @override + void scale(double scale) { + super.scale(scale); + notifyListeners(); + } + + @override + set x(double x) { + super.x = x; + notifyListeners(); + } + + @override + set y(double y) { + super.y = y; + notifyListeners(); + } + + @override + set z(double z) { + super.z = z; + notifyListeners(); + } + + @override + set w(double w) { + super.w = w; + notifyListeners(); + } + + @override + void conjugate() { + super.conjugate(); + notifyListeners(); + } + + @override + void inverse() { + super.inverse(); + notifyListeners(); + } + + @override + Float32List get storage => UnmodifiableFloat32ListView(super.storage); +} diff --git a/packages/flame_3d/lib/src/game/notifying_vector3.dart b/packages/flame_3d/lib/src/game/notifying_vector3.dart new file mode 100644 index 00000000000..ce62b22db66 --- /dev/null +++ b/packages/flame_3d/lib/src/game/notifying_vector3.dart @@ -0,0 +1,211 @@ +import 'dart:typed_data'; + +import 'package:flame_3d/game.dart'; +import 'package:flutter/widgets.dart'; + +/// {@template notifying_vector_3} +/// Extension of the standard [Vector3] class, implementing the [ChangeNotifier] +/// functionality. This allows any interested party to be notified when the +/// value of this vector changes. +/// +/// This class can be used as a regular [Vector3] class. However, if you do +/// subscribe to notifications, don't forget to eventually unsubscribe in +/// order to avoid resource leaks. +/// +/// Direct modification of this vector's [storage] is not allowed. +/// {@endtemplate} +class NotifyingVector3 extends Vector3 with ChangeNotifier { + /// {@macro notifying_vector_3} + /// + /// Constructs a vector using the raw values [x], [y], and [z]. + factory NotifyingVector3(double x, double y, double z) => + NotifyingVector3.zero()..setValues(x, y, z); + + /// {@macro notifying_vector_3} + /// + /// Create an empty vector. + NotifyingVector3.zero() : super.zero(); + + /// {@macro notifying_vector_3} + /// + /// Create an vector whose values are all [v]. + factory NotifyingVector3.all(double v) => NotifyingVector3.zero()..splat(v); + + /// {@macro notifying_vector_3} + /// + /// Create a copy of the [other] vector. + factory NotifyingVector3.copy(Vector3 other) => + NotifyingVector3.zero()..setFrom(other); + + @override + void setValues(double x, double y, double z) { + super.setValues(x, y, z); + notifyListeners(); + } + + @override + void setFrom(Vector3 other) { + super.setFrom(other); + notifyListeners(); + } + + @override + void setZero() { + super.setZero(); + notifyListeners(); + } + + @override + void splat(double arg) { + super.splat(arg); + notifyListeners(); + } + + @override + void operator []=(int i, double v) { + super[i] = v; + notifyListeners(); + } + + @override + set length(double l) { + super.length = l; + notifyListeners(); + } + + @override + double normalize() { + final l = super.normalize(); + notifyListeners(); + return l; + } + + @override + void postmultiply(Matrix3 arg) { + super.postmultiply(arg); + notifyListeners(); + } + + @override + void add(Vector3 arg) { + super.add(arg); + notifyListeners(); + } + + @override + void addScaled(Vector3 arg, double factor) { + super.addScaled(arg, factor); + notifyListeners(); + } + + @override + void sub(Vector3 arg) { + super.sub(arg); + notifyListeners(); + } + + @override + void multiply(Vector3 arg) { + super.multiply(arg); + notifyListeners(); + } + + @override + void divide(Vector3 arg) { + super.divide(arg); + notifyListeners(); + } + + @override + void scale(double arg) { + super.scale(arg); + notifyListeners(); + } + + @override + void negate() { + super.negate(); + notifyListeners(); + } + + @override + void absolute() { + super.absolute(); + notifyListeners(); + } + + @override + void clamp(Vector3 min, Vector3 max) { + super.clamp(min, max); + notifyListeners(); + } + + @override + void clampScalar(double min, double max) { + super.clampScalar(min, max); + notifyListeners(); + } + + @override + void floor() { + super.floor(); + notifyListeners(); + } + + @override + void ceil() { + super.ceil(); + notifyListeners(); + } + + @override + void round() { + super.round(); + notifyListeners(); + } + + @override + void roundToZero() { + super.roundToZero(); + notifyListeners(); + } + + @override + void copyFromArray(List array, [int offset = 0]) { + super.copyFromArray(array, offset); + notifyListeners(); + } + + @override + set xy(Vector2 arg) { + super.xy = arg; + notifyListeners(); + } + + @override + set yx(Vector2 arg) { + super.yx = arg; + notifyListeners(); + } + + @override + set x(double x) { + super.x = x; + notifyListeners(); + } + + @override + set y(double y) { + super.y = y; + notifyListeners(); + } + + @override + set z(double z) { + super.z = z; + notifyListeners(); + } + + @override + Float32List get storage => UnmodifiableFloat32ListView(super.storage); +} diff --git a/packages/flame_3d/lib/src/game/transform_3d.dart b/packages/flame_3d/lib/src/game/transform_3d.dart new file mode 100644 index 00000000000..4fc09af2642 --- /dev/null +++ b/packages/flame_3d/lib/src/game/transform_3d.dart @@ -0,0 +1,147 @@ +import 'package:flame_3d/game.dart'; +import 'package:flutter/widgets.dart' show ChangeNotifier; + +/// {@template transform_3d} +/// This class describes a generic 3D transform, which is a combination of +/// translations, rotations and scaling. These transforms are combined into a +/// single matrix, that can be either applied to a graphical device like the +/// canvas, composed with another transform, or used directly to convert +/// coordinates. +/// +/// The transform can be visualized as 2 reference frames: a "global" and +/// a "local". At first, these two reference frames coincide. Then, the +/// following sequence of transforms is applied: +/// - translation to point [position]; +/// - rotate using the [rotation]; +/// - scaling in X, Y and Z directions by [scale] factors. +/// +/// The class is optimized for repeated use: the transform matrix is cached +/// and then recalculated only when the underlying properties change. Moreover, +/// recalculation of the transform is postponed until the matrix is actually +/// requested by the user. Thus, modifying multiple properties at once does +/// not incur the penalty of unnecessary recalculations. +/// +/// This class implements the [ChangeNotifier] API, allowing you to subscribe +/// for notifications whenever the transform matrix changes. In addition, you +/// can subscribe to get notified when individual components of the transform +/// change: [position], [scale], and [rotation]. +/// {@endtemplate} +class Transform3D extends ChangeNotifier { + /// {@macro transform_3d} + Transform3D() + : _recalculate = true, + _rotation = NotifyingQuaternion(0, 0, 0, 0), + _position = NotifyingVector3.zero(), + _scale = NotifyingVector3.all(1), + _transformMatrix = Matrix4.zero() { + _position.addListener(_markAsModified); + _scale.addListener(_markAsModified); + _rotation.addListener(_markAsModified); + } + + /// {@macro transform_3d} + /// + /// Create a copy of the [other] transform. + factory Transform3D.copy(Transform3D other) => Transform3D()..setFrom(other); + + /// {@macro transform_3d} + /// + /// Create an instance of [Transform3D] and apply the [matrix] on it. + factory Transform3D.fromMatrix4(Matrix4 matrix) { + final transform = Transform3D(); + matrix.decompose(transform.position, transform.rotation, transform.scale); + return transform; + } + + /// Clone of this. + Transform3D clone() => Transform3D.copy(this); + + /// The translation part of the transform. This translation is applied + /// relative to the global coordinate space. + /// + /// The returned vector can be modified by the user, and the changes + /// will be propagated back to the transform matrix. + NotifyingVector3 get position => _position; + set position(Vector3 position) => _position.setFrom(position); + final NotifyingVector3 _position; + + /// X coordinate of the translation transform. + double get x => _position.x; + set x(double x) => _position.x = x; + + /// Y coordinate of the translation transform. + double get y => _position.y; + set y(double y) => _position.y = y; + + /// Z coordinate of the translation transform. + double get z => _position.z; + set z(double y) => _position.z = z; + + NotifyingQuaternion get rotation => _rotation; + set rotation(Quaternion rotation) => _rotation.setFrom(rotation); + final NotifyingQuaternion _rotation; + + /// The scale part of the transform. The default scale factor is (1, 1, 1), + /// a scale greater than 1 corresponds to expansion, and less than 1 is + /// contraction. A negative scale is also allowed, and it corresponds + /// to a mirror reflection around the corresponding axis. + /// Scale factors can be different for X, Y and Z directions. + /// + /// The returned vector can be modified by the user, and the changes + /// will be propagated back to the transform matrix. + NotifyingVector3 get scale => _scale; + set scale(Vector3 scale) => _scale.setFrom(scale); + final NotifyingVector3 _scale; + + /// The total transformation matrix for the component. This matrix combines + /// translation, rotation and scale transforms into a single entity. The + /// matrix is cached and gets recalculated only when necessary. + /// + /// The returned matrix must not be modified by the user. + Matrix4 get transformMatrix { + if (_recalculate) { + _transformMatrix.setFromTranslationRotationScale( + _position, + _rotation, + _scale, + ); + _recalculate = false; + } + return _transformMatrix; + } + + final Matrix4 _transformMatrix; + bool _recalculate; + + /// Set this to the values of the [other] [Transform3D]. + void setFrom(Transform3D other) { + rotation.setFrom(other.rotation); + position.setFrom(other.position); + scale.setFrom(other.scale); + } + + /// Check whether this transform is equal to [other], up to the given + /// [tolerance]. Setting tolerance to zero will check for exact equality. + /// Transforms are considered equal if their rotation angles are the same + /// or differ by a multiple of 2π, and if all other transform parameters: + /// translation, scale, and offset are the same. + /// + /// The [tolerance] parameter is in absolute units, not relative. + bool closeTo(Transform3D other, {double tolerance = 1e-10}) { + return (position.x - other.position.x).abs() <= tolerance && + (position.y - other.position.y).abs() <= tolerance && + (position.z - other.position.z).abs() <= tolerance && + (rotation.x - other.rotation.x).abs() <= tolerance && + (rotation.y - other.rotation.y).abs() <= tolerance && + (rotation.z - other.rotation.z).abs() <= tolerance && + (rotation.w - other.rotation.w).abs() <= tolerance && + (scale.x - other.scale.x).abs() <= tolerance && + (scale.y - other.scale.y).abs() <= tolerance && + (scale.z - other.scale.z).abs() <= tolerance; + } + + void _markAsModified() { + _recalculate = true; + notifyListeners(); + } +} diff --git a/packages/flame_3d/lib/src/graphics/graphics_device.dart b/packages/flame_3d/lib/src/graphics/graphics_device.dart new file mode 100644 index 00000000000..a609bbd8d65 --- /dev/null +++ b/packages/flame_3d/lib/src/graphics/graphics_device.dart @@ -0,0 +1,190 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/resources.dart'; +import 'package:flutter_gpu/gpu.dart' as gpu; + +enum BlendState { + additive, + alphaBlend, + opaque, +} + +enum DepthStencilState { + standard, + depthRead, + none, +} + +/// {@template graphics_device} +/// 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. Anything that gets binded to the device in between will be +/// uploaded to the GPU and returns as an [Image] in [end]. +/// {@endtemplate} +class GraphicsDevice { + /// {@macro graphics_device} + GraphicsDevice({this.clearValue = const Color(0x00000000)}); + + /// The clear value, used to clear out the screen. + final Color clearValue; + + late gpu.CommandBuffer _commandBuffer; + late gpu.HostBuffer _hostBuffer; + late gpu.RenderPass _renderPass; + late gpu.RenderTarget _renderTarget; + final _transformMatrix = Matrix4.identity(); + final _viewModelMatrix = Matrix4.identity(); + + Size _previousSize = Size.zero; + + /// Begin a new rendering batch. + /// + /// 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(wolfen): unused at the moment + BlendState blendState = BlendState.alphaBlend, + // TODO(wolfen): used incorrectly + DepthStencilState depthStencilState = DepthStencilState.depthRead, + Matrix4? transformMatrix, + }) { + _commandBuffer = gpu.gpuContext.createCommandBuffer(); + _hostBuffer = gpu.gpuContext.createHostBuffer(); + _renderPass = _commandBuffer.createRenderPass(_getRenderTarget(size)) + ..setColorBlendEnable(true) + ..setColorBlendEquation( + gpu.ColorBlendEquation( + sourceAlphaBlendFactor: blendState == BlendState.alphaBlend + ? gpu.BlendFactor.oneMinusSourceAlpha + : gpu.BlendFactor.one, + ), + ) + ..setDepthWriteEnable(depthStencilState == DepthStencilState.depthRead) + ..setDepthCompareOperation( + // TODO(wolfen): 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 + /// the result. + Image end() { + _commandBuffer.submit(); + return _renderTarget.colorAttachments[0].texture.asImage(); + } + + void clearBindings() { + _renderPass.clearBindings(); + } + + void setViewModel(Matrix4 mvp) => _viewModelMatrix.setFrom(mvp); + + /// Bind a [mesh]. + void bindMesh(Mesh mesh) { + _renderPass.clearBindings(); + mesh.bind(this); + _renderPass.draw(); + } + + /// Bind a [surface]. + void bindSurface(Surface surface) { + _renderPass.clearBindings(); + if (surface.material != null) { + bindMaterial(surface.material!); + } + + _renderPass.bindVertexBuffer( + gpu.BufferView( + surface.resource!, + offsetInBytes: 0, + lengthInBytes: surface.verticesBytes, + ), + surface.vertexCount, + ); + + _renderPass.bindIndexBuffer( + gpu.BufferView( + surface.resource!, + offsetInBytes: surface.verticesBytes, + lengthInBytes: surface.indicesBytes, + ), + gpu.IndexType.int16, + surface.indexCount, + ); + + _renderPass.draw(); + } + + /// 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(), + ); + } + + /// 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), + ); + } + + void bindTexture(gpu.Shader shader, String name, Texture texture) { + _renderPass.bindTexture(shader.getUniformSlot(name), texture.resource); + } + + gpu.RenderTarget _getRenderTarget(Size size) { + if (_previousSize != size) { + _previousSize = size; + + final colorTexture = gpu.gpuContext.createTexture( + gpu.StorageMode.devicePrivate, + size.width.toInt(), + size.height.toInt(), + ); + + final depthTexture = gpu.gpuContext.createTexture( + gpu.StorageMode.deviceTransient, + size.width.toInt(), + size.height.toInt(), + format: gpu.gpuContext.defaultDepthStencilFormat, + ); + + _renderTarget = gpu.RenderTarget.singleColor( + gpu.ColorAttachment(texture: colorTexture!, clearValue: clearValue), + depthStencilAttachment: gpu.DepthStencilAttachment( + texture: depthTexture!, + depthClearValue: 1.0, + ), + ); + } + + return _renderTarget; + } +} diff --git a/packages/flame_3d/lib/src/resources/material.dart b/packages/flame_3d/lib/src/resources/material.dart new file mode 100644 index 00000000000..dab1184d5dc --- /dev/null +++ b/packages/flame_3d/lib/src/resources/material.dart @@ -0,0 +1,2 @@ +export 'material/material.dart'; +export 'material/standard_material.dart'; diff --git a/packages/flame_3d/lib/src/resources/material/material.dart b/packages/flame_3d/lib/src/resources/material/material.dart new file mode 100644 index 00000000000..5d9706fef34 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/material/material.dart @@ -0,0 +1,68 @@ +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:flutter_gpu/gpu.dart' as gpu; + +/// {@template material} +/// Base material [Resource], it holds the shader library that should be used +/// for the texture. +/// {@endtemplate} +abstract class Material extends Resource { + /// {@macro material} + Material(gpu.ShaderLibrary library) + : super( + gpu.gpuContext.createRenderPipeline( + library['TextureVertex']!, + library['TextureFragment']!, + ), + ); + + 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); + } +} + +/// {@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); + + /// 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); + + /// Add a [Matrix4] to the buffer. + void addMatrix4(Matrix4 matrix) => _storage.addAll(matrix.storage); + + /// Clear the buffer. + void clear() => _storage.clear(); +} diff --git a/packages/flame_3d/lib/src/resources/material/standard_material.dart b/packages/flame_3d/lib/src/resources/material/standard_material.dart new file mode 100644 index 00000000000..854045c9e5d --- /dev/null +++ b/packages/flame_3d/lib/src/resources/material/standard_material.dart @@ -0,0 +1,48 @@ +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.dart b/packages/flame_3d/lib/src/resources/mesh.dart new file mode 100644 index 00000000000..3ae45920121 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/mesh.dart @@ -0,0 +1,6 @@ +export 'mesh/cuboid_mesh.dart'; +export 'mesh/mesh.dart'; +export 'mesh/plane_mesh.dart'; +export 'mesh/sphere_mesh.dart'; +export 'mesh/surface.dart'; +export 'mesh/vertex.dart'; diff --git a/packages/flame_3d/lib/src/resources/mesh/cuboid_mesh.dart b/packages/flame_3d/lib/src/resources/mesh/cuboid_mesh.dart new file mode 100644 index 00000000000..988730c1910 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/mesh/cuboid_mesh.dart @@ -0,0 +1,64 @@ +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/resources.dart'; + +/// {@template cuboid_mesh} +/// Represents a Cuboid's geometry with a single surface. +/// {@endtemplate} +class CuboidMesh extends Mesh { + /// {@macro cuboid_mesh} + CuboidMesh({ + required Vector3 size, + Material? material, + }) { + final Vector3(:x, :y, :z) = size / 2; + + 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)), + + // 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)), + + // 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)), + + // 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)), + + // 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)), + + // 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)), + ]; + + final indices = [ + 0, 1, 2, 2, 3, 0, // Face 1 + 4, 5, 6, 6, 7, 4, // Face 2 + 8, 9, 10, 10, 11, 8, // Face 3 + 12, 13, 14, 14, 15, 12, // Face 4 + 16, 17, 18, 18, 19, 16, // Face 5 + 20, 21, 22, 22, 23, 20, // Face 6 + ]; + + addSurface(vertices, indices, material: material); + } +} diff --git a/packages/flame_3d/lib/src/resources/mesh/mesh.dart b/packages/flame_3d/lib/src/resources/mesh/mesh.dart new file mode 100644 index 00000000000..c3366bfb2aa --- /dev/null +++ b/packages/flame_3d/lib/src/resources/mesh/mesh.dart @@ -0,0 +1,67 @@ +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/graphics.dart'; +import 'package:flame_3d/resources.dart'; + +/// {@template mesh} +/// A [Resource] that represents a geometric shape that is divided up in one or +/// more [Surface]s. +/// +/// This class isn't a true resource, it does not upload it self to the GPU. +/// Instead it uploads [Surface]s, it acts as a proxy. +/// {@endtemplate} +class Mesh extends Resource { + /// {@macro mesh} + Mesh() + : _surfaces = [], + super(null); + + /// The AABB of the mesh. + /// + /// This is the sum of all the AABB's of the surfaces it contains. + Aabb3 get aabb { + if (_aabb == null) { + var aabb = Aabb3(); + for (var i = 0; i < _surfaces.length; i++) { + if (i == 0) { + aabb = _surfaces[i].aabb; + } else { + aabb.hull(_surfaces[i].aabb); + } + } + _aabb = aabb; + } + return _aabb!; + } + + Aabb3? _aabb; + + final List _surfaces; + + /// The total surface count of the mesh. + int get surfaceCount => _surfaces.length; + + void bind(GraphicsDevice device) { + for (final surface in _surfaces) { + device.bindSurface(surface); + } + } + + /// Add a new surface represented by [vertices], [indices] and a material. + void addSurface( + List vertices, + List indices, { + Material? material, + }) { + _surfaces.add(Surface(vertices, indices, material)); + } + + /// Add a material to the surface at [index]. + void addMaterialToSurface(int index, Material material) { + _surfaces[index].material = material; + } + + /// Get a material from the surface at [index]. + Material? getMaterialToSurface(int index) { + return _surfaces[index].material; + } +} diff --git a/packages/flame_3d/lib/src/resources/mesh/plane_mesh.dart b/packages/flame_3d/lib/src/resources/mesh/plane_mesh.dart new file mode 100644 index 00000000000..f84a2be90ef --- /dev/null +++ b/packages/flame_3d/lib/src/resources/mesh/plane_mesh.dart @@ -0,0 +1,23 @@ +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/resources.dart'; + +/// {@template plane_mesh} +/// Represents a 2D Plane's geometry with a single surface. +/// {@endtemplate} +class PlaneMesh extends Mesh { + /// {@macro plane_mesh} + PlaneMesh({ + required Vector2 size, + Material? material, + }) { + 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)), + ]; + addSurface(vertices, [0, 1, 2, 2, 3, 0], material: material); + } +} diff --git a/packages/flame_3d/lib/src/resources/mesh/sphere_mesh.dart b/packages/flame_3d/lib/src/resources/mesh/sphere_mesh.dart new file mode 100644 index 00000000000..dafca4b8ba7 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/mesh/sphere_mesh.dart @@ -0,0 +1,53 @@ +import 'dart:math' as math; + +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/resources.dart'; + +/// {@template sphere_mesh} +/// Represents a Sphere's geometry with a single surface. +/// {@endtemplate} +class SphereMesh extends Mesh { + /// {@macro sphere_mesh} + SphereMesh({ + required double radius, + int segments = 64, + Material? material, + }) { + final vertices = []; + for (var i = 0; i <= segments; i++) { + final theta = i * (2 * math.pi) / segments; + for (var j = 0; j <= segments; j++) { + final phi = j * math.pi / segments; + + final x = radius * math.sin(phi) * math.cos(theta); + final y = radius * math.cos(phi); + final z = radius * math.sin(phi) * math.sin(theta); + + final u = theta / (2 * math.pi); + final v = phi / math.pi; + + vertices.add( + Vertex(position: Vector3(x, y, z), texCoord: Vector2(u, v)), + ); + } + } + + final indices = []; + for (var i = 0; i < segments; i++) { + for (var j = 0; j < segments; j++) { + final first = i * (segments + 1) + j; + final second = first + segments + 1; + + indices.add(first); + indices.add(second); + indices.add(first + 1); + + indices.add(second); + indices.add(second + 1); + indices.add(first + 1); + } + } + + addSurface(vertices, indices, material: material); + } +} diff --git a/packages/flame_3d/lib/src/resources/mesh/surface.dart b/packages/flame_3d/lib/src/resources/mesh/surface.dart new file mode 100644 index 00000000000..52f591d0872 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/mesh/surface.dart @@ -0,0 +1,96 @@ +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:flame_3d/game.dart'; +import 'package:flame_3d/resources.dart'; +import 'package:flutter_gpu/gpu.dart' as gpu; + +enum PrimitiveType { + triangles, +} + +/// {@template surface} +/// Base surface [Resource], it describes a single surface to be rendered. +/// {@endtemplate} +class Surface extends Resource { + /// {@macro surface} + Surface( + List vertices, + List indices, [ + this.material, + ]) : super(null) { + // `TODO`(bdero): This should have an attribute map instead and be fully SoA + // but vertex attributes in Impeller aren't flexible enough yet. + // See also https://github.com/flutter/flutter/issues/116168. + _vertices = Float32List.fromList( + vertices.fold([], (p, v) => p..addAll(v.storage)), + ).buffer; + _vertexCount = _vertices.lengthInBytes ~/ (vertices.length * 9); + + _indices = Uint16List.fromList(indices).buffer; + _indexCount = _indices.lengthInBytes ~/ 2; + + _calculateAabb(vertices); + } + + Material? material; + + Aabb3 get aabb => _aabb; + late Aabb3 _aabb; + + int get verticesBytes => _vertices.lengthInBytes; + late ByteBuffer _vertices; + + int get vertexCount => _vertexCount; + late int _vertexCount; + + int get indicesBytes => _indices.lengthInBytes; + late ByteBuffer _indices; + + int get indexCount => _indexCount; + late int _indexCount; + + @override + gpu.DeviceBuffer? get resource { + var resource = super.resource; + final sizeInBytes = _vertices.lengthInBytes + _indices.lengthInBytes; + if (resource?.sizeInBytes != sizeInBytes) { + // Store the device buffer in the resource parent. + resource = super.resource = gpu.gpuContext.createDeviceBuffer( + gpu.StorageMode.hostVisible, + sizeInBytes, + ); + + resource + ?..overwrite(_vertices.asByteData()) + ..overwrite( + _indices.asByteData(), + destinationOffsetInBytes: _vertices.lengthInBytes, + ); + } + return resource; + } + + void _calculateAabb(List vertices) { + var minX = double.infinity; + var minY = double.infinity; + var minZ = double.infinity; + var maxX = double.negativeInfinity; + var maxY = double.negativeInfinity; + var maxZ = double.negativeInfinity; + + for (final vertex in vertices) { + minX = math.min(minX, vertex.position.x); + minY = math.min(minY, vertex.position.y); + minZ = math.min(minZ, vertex.position.z); + maxX = math.max(maxX, vertex.position.x); + maxY = math.max(maxY, vertex.position.y); + maxZ = math.max(maxZ, vertex.position.z); + } + + _aabb = Aabb3.minMax( + Vector3(minX, minY, minZ), + Vector3(maxX, maxY, maxZ), + ); + } +} diff --git a/packages/flame_3d/lib/src/resources/mesh/vertex.dart b/packages/flame_3d/lib/src/resources/mesh/vertex.dart new file mode 100644 index 00000000000..b278095df65 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/mesh/vertex.dart @@ -0,0 +1,57 @@ +import 'dart:typed_data'; + +import 'package:flame_3d/extensions.dart'; +import 'package:flame_3d/game.dart'; +import 'package:flutter/widgets.dart'; + +/// {@template vertex} +/// Represents a vertex in 3D space. +/// +/// A vertex consists out of space coordinates, UV/texture coordinates and a +/// color. +/// {@endtemplate} +@immutable +class Vertex { + /// {@macro vertex} + Vertex({ + required Vector3 position, + required Vector2 texCoord, + Vector3? normal, + this.color = const Color(0xFFFFFFFF), + }) : 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, + ]); + + Float32List get storage => _storage; + final Float32List _storage; + + /// The position of the vertex in 3D space. + final ImmutableVector3 position; + + /// The UV coordinates of the texture to map. + final ImmutableVector2 texCoord; + + /// The normal vector of the vertex. + final ImmutableVector3 normal; + + /// The color on the vertex. + final Color color; + + @override + bool operator ==(Object other) => + other is Vertex && + position == other.position && + texCoord == other.texCoord && + normal == other.normal && + color == other.color; + + @override + int get hashCode => Object.hashAll([position, texCoord, normal, color]); +} diff --git a/packages/flame_3d/lib/src/resources/resource.dart b/packages/flame_3d/lib/src/resources/resource.dart new file mode 100644 index 00000000000..068a0227b56 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/resource.dart @@ -0,0 +1,19 @@ +import 'package:flutter/foundation.dart'; + +// TODO(wolfen): 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} +/// A Resource is the base class for any resource typed classes. The primary +/// use case is to be a data container. +/// {@endtemplate} +class Resource { + /// {@macro resource} + Resource(this._resource); + + /// The resource data. + R get resource => _resource; + @protected + set resource(R resource) => _resource = resource; + R _resource; +} diff --git a/packages/flame_3d/lib/src/resources/texture.dart b/packages/flame_3d/lib/src/resources/texture.dart new file mode 100644 index 00000000000..aad730afa8c --- /dev/null +++ b/packages/flame_3d/lib/src/resources/texture.dart @@ -0,0 +1,3 @@ +export 'texture/color_texture.dart'; +export 'texture/image_texture.dart'; +export 'texture/texture.dart'; diff --git a/packages/flame_3d/lib/src/resources/texture/color_texture.dart b/packages/flame_3d/lib/src/resources/texture/color_texture.dart new file mode 100644 index 00000000000..2ef6a490e7e --- /dev/null +++ b/packages/flame_3d/lib/src/resources/texture/color_texture.dart @@ -0,0 +1,20 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flame_3d/resources.dart'; + +/// {@template color_texture} +/// A texture that holds a single color. By default it creates a 1x1 texture. +/// {@endtemplate} +class ColorTexture extends Texture { + /// {@macro color_texture} + ColorTexture(Color color, {int width = 1, int height = 1}) + : super( + Uint32List.fromList( + List.filled(width * height, color.value), + ).buffer.asByteData(), + width: width, + height: height, + format: PixelFormat.bgra8888, + ); +} diff --git a/packages/flame_3d/lib/src/resources/texture/image_texture.dart b/packages/flame_3d/lib/src/resources/texture/image_texture.dart new file mode 100644 index 00000000000..4f395577ff5 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/texture/image_texture.dart @@ -0,0 +1,17 @@ +import 'dart:ui'; + +import 'package:flame_3d/resources.dart'; + +/// {@template image_texture} +/// A texture that holds an image as it's render-able texture. +/// {@endtemplate} +class ImageTexture extends Texture { + /// {@macro image_texture} + ImageTexture(super.source, {required super.width, required super.height}); + + /// Create a [ImageTexture] from the given [image]. + static Future create(Image image) async { + final Image(:toByteData, :width, :height) = image; + return ImageTexture((await toByteData())!, width: width, height: height); + } +} diff --git a/packages/flame_3d/lib/src/resources/texture/texture.dart b/packages/flame_3d/lib/src/resources/texture/texture.dart new file mode 100644 index 00000000000..8f4c840fea6 --- /dev/null +++ b/packages/flame_3d/lib/src/resources/texture/texture.dart @@ -0,0 +1,42 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flame_3d/resources.dart'; +import 'package:flutter_gpu/gpu.dart' as gpu; + +/// {@template texture} +/// Base texture [Resource], represents an image/texture on the GPU. +/// {@endtemplate} +class Texture extends Resource { + /// {@macro texture} + Texture( + ByteData sourceData, { + required int width, + required int height, + PixelFormat format = PixelFormat.rgba8888, + }) : super( + gpu.gpuContext.createTexture( + gpu.StorageMode.hostVisible, + width, + height, + format: switch (format) { + PixelFormat.rgba8888 => gpu.PixelFormat.r8g8b8a8UNormInt, + PixelFormat.bgra8888 => gpu.PixelFormat.b8g8r8a8UNormInt, + PixelFormat.rgbaFloat32 => gpu.PixelFormat.r32g32b32a32Float, + }, + )! + ..overwrite(sourceData), + ); + + int get width => resource.width; + + int get height => resource.height; + + Image toImage() => resource.asImage(); + + /// A transparent single pixel texture. + static final empty = ColorTexture(const Color(0x00000000)); + + /// A white single pixel texture. + static final standard = ColorTexture(const Color(0xFFFFFFFF)); +} diff --git a/packages/flame_3d/pubspec.yaml b/packages/flame_3d/pubspec.yaml new file mode 100644 index 00000000000..44e71ff7131 --- /dev/null +++ b/packages/flame_3d/pubspec.yaml @@ -0,0 +1,28 @@ +name: flame_3d +description: Experimental 3D support for the Flame Engine +version: 0.1.0-dev.1 +homepage: https://github.com/flame-engine/flame/tree/main/packages/flame_3d +funding: + - https://opencollective.com/blue-fire + - https://github.com/sponsors/bluefireteam + - https://patreon.com/bluefireoss + +environment: + sdk: '>=3.3.0-279.2.beta <4.0.0' + flutter: ">=3.13.0" + +dependencies: + flame: ^1.16.0 + flutter: + sdk: flutter + flutter_gpu: + +dev_dependencies: + flame_lint: ^1.1.2 + flame_test: ^1.15.3 + flutter_test: + sdk: flutter + +flutter: + assets: + - assets/shaders/ \ No newline at end of file diff --git a/packages/flame_3d/shaders/standard_material.frag b/packages/flame_3d/shaders/standard_material.frag new file mode 100644 index 00000000000..8253fe6945c --- /dev/null +++ b/packages/flame_3d/shaders/standard_material.frag @@ -0,0 +1,16 @@ + +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 new file mode 100644 index 00000000000..d93ce85ecc6 --- /dev/null +++ b/packages/flame_3d/shaders/standard_material.vert @@ -0,0 +1,16 @@ +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); +}