├── .github ├── dependabot.yml └── workflows │ └── flutter.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── assets ├── base.shaderbundle ├── ibl_brdf_lut.png ├── royal_esplanade.png └── royal_esplanade_irradiance.png ├── build.sh ├── build_utils.sh ├── examples ├── README.md ├── assets_src │ ├── dash.glb │ ├── fcar.glb │ ├── flutter_logo_baked.glb │ └── two_triangles.glb ├── build.sh └── flutter_app │ ├── .gitignore │ ├── README.md │ ├── assets │ ├── little_paris_eiffel_tower.png │ └── little_paris_eiffel_tower_irradiance.png │ ├── hook │ └── build.dart │ ├── lib │ ├── example_animation.dart │ ├── example_car.dart │ ├── example_cuboid.dart │ ├── example_logo.dart │ └── main.dart │ └── pubspec.yaml ├── flutter_scene_pubignore ├── hook └── build.dart ├── importer ├── .clang-format ├── .gitignore ├── .metadata ├── .vscode │ └── launch.json ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bin │ └── import.dart ├── conversions.cc ├── conversions.h ├── hook │ └── build.dart ├── importer.h ├── importer.sh ├── importer_gltf.cc ├── lib │ ├── build_hooks.dart │ ├── constants.dart │ ├── flatbuffer.dart │ ├── generated │ │ └── scene_impeller.fb_flatbuffers.dart │ ├── importer.dart │ ├── offline_import.dart │ └── third_party │ │ └── flat_buffers.dart ├── pubspec.lock ├── pubspec.yaml ├── scene.fbs ├── scenec_main.cc ├── test │ └── importer_test.dart ├── types.h ├── vertices_builder.cc └── vertices_builder.h ├── lib ├── scene.dart └── src │ ├── animation.dart │ ├── animation │ ├── animation.dart │ ├── animation_clip.dart │ ├── animation_player.dart │ ├── animation_transform.dart │ └── property_resolver.dart │ ├── asset_helpers.dart │ ├── camera.dart │ ├── geometry │ └── geometry.dart │ ├── material │ ├── environment.dart │ ├── material.dart │ ├── physically_based_material.dart │ └── unlit_material.dart │ ├── math_extensions.dart │ ├── mesh.dart │ ├── node.dart │ ├── scene.dart │ ├── scene_encoder.dart │ ├── shaders.dart │ ├── skin.dart │ └── surface.dart ├── macos └── Flutter │ └── GeneratedPluginRegistrant.swift ├── publish.sh ├── pubspec.yaml ├── screenshots └── flutter_scene_logo.png ├── shaders ├── base.shaderbundle.json ├── flutter_scene_skinned.vert ├── flutter_scene_standard.frag ├── flutter_scene_unlit.frag ├── flutter_scene_unskinned.vert ├── normals.glsl ├── pbr.glsl ├── texture.glsl └── tone_mapping.glsl └── test └── node_test.dart /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "pub" 9 | versioning-strategy: "increase-if-necessary" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | - package-ecosystem: "pub" 14 | versioning-strategy: "increase-if-necessary" 15 | directory: "/importer/" 16 | schedule: 17 | interval: "daily" 18 | - package-ecosystem: "pub" 19 | versioning-strategy: "increase-if-necessary" 20 | directory: "/examples/flutter_app/" 21 | schedule: 22 | interval: "daily" 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/flutter.yml: -------------------------------------------------------------------------------- 1 | name: Flutter CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | lint-and-test: 11 | name: Lint and Test 12 | runs-on: macos-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Flutter 16 | uses: subosito/flutter-action@v2 17 | with: 18 | channel: master 19 | - run: dart --version 20 | - run: flutter --version 21 | - run: flutter pub get 22 | - name: enable asset building 23 | run: flutter config --enable-native-assets 24 | - name: Lint analysis 25 | run: dart analyze 26 | - name: Dart format 27 | run: dart format --output none --set-exit-if-changed $(find . -name '**.dart' -not -name '*_flatbuffers.dart' -not -path '*/build/*') 28 | - name: dart fix 29 | run: dart fix --dry-run 30 | - name: Run tests 31 | run: flutter test --enable-impeller 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | **/ephemeral/ 32 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 1220245b330c94ec573d9f4801e93c5c72908f4f 8 | channel: unknown 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1-dev.1 2 | 3 | * Initial render box. 4 | 5 | ## 0.1.0 6 | 7 | * Rewrite for Flutter GPU. 8 | * Physically based rendering. 9 | * More conventional interface for scene construction. 10 | 11 | ## 0.1.1 12 | 13 | * Rename PhysicallyBasedMaterial and UnlitMaterial 14 | * Fix environment lighting problems in PhysicallyBasedMaterial. 15 | * Add default environment map. 16 | 17 | ## 0.2.0 18 | 19 | * Skinned mesh import. 20 | * Fix readme for pub.dev. 21 | 22 | ## 0.2.1-0 23 | 24 | * Switch to pre-release versioning. 25 | * Bump version of flutter_scene_importer. 26 | 27 | ## 0.2.1-1 28 | 29 | * Bump flutter_scene_importer version. 30 | 31 | ## 0.3.0-0 32 | 33 | * Add Animation/playback support (Animation, AnimationPlayer, and AnimationClip). 34 | * Import animations from scene models. 35 | * Add support for cloning nodes. 36 | 37 | ## 0.4.0-0 38 | 39 | * Support node cloning for skins. 40 | * Fix default/animation-less pose. 41 | 42 | ## 0.5.0-0 43 | 44 | * Support non-embedded/URI-only image embeds. 45 | 46 | ## 0.6.0-0 47 | 48 | * Fix memory leak in transients buffer. 49 | * Optional MSAA support on iOS and Android (enabled by default). 50 | * Cull backfaces by default. 51 | * Fix animation blending bugs. 52 | * Pin native_assets_cli to <0.9.0 53 | (https://github.com/bdero/flutter_gpu_shaders/issues/3) 54 | * Add car model and animation blending examples. 55 | * Fancy readme and FAQ. 56 | 57 | ## 0.7.0-0 58 | 59 | * Update to native_assets_cli 0.9.0. 60 | * Update to flutter_gpu_shaders 0.2.0. 61 | 62 | ## 0.8.0-0 63 | 64 | * Update to Flutter 3.29.0-1.0.pre.242. 65 | 66 | ## 0.9.0-0 67 | 68 | * Update to native_assets_cli 0.13.0. 69 | * Update to flutter_gpu_shaders 0.3.0. 70 | 71 | ## 0.9.1-0 72 | 73 | * Fix invalid usage of textureLod on desktop platforms. 74 | 75 | ## 0.9.2-0 76 | 77 | * Fix globalTransform calculation. 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Brandon DeRosier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Flutter Scene 4 | 5 |

6 | 7 |

8 |

9 | Scene: 3D library for Flutter 10 |

11 |

12 | 13 |

14 | 15 | 16 |

17 | 18 | Scene is a general purpose realtime 3D rendering library for Flutter. It started life as a C++ component of the Impeller rendering backend in Flutter Engine, and is currently being actively developed as a pure Dart package powered by the Flutter GPU API. 19 | 20 | The primary goal of this project is to make performant cross platform 3D easy in Flutter. 21 | 22 |

Examples AppExample GameDocsFAQ

23 | 24 | --- 25 | 26 |

27 | Flutter Scene 28 |

29 | 30 |

31 | Flutter Scene 32 |

33 | 34 |

35 | Flutter Scene 36 |

37 | 38 |

39 | Flutter Scene 40 |

41 | 42 |

43 | Flutter Scene 44 |

45 | 46 | ## Early preview! ⚠️ 47 | 48 | - This package is in an early preview state. Things may break! 49 | - Relies on [Flutter GPU](https://github.com/flutter/flutter/blob/main/engine/src/flutter/docs/impeller/Flutter-GPU.md) for rendering, which is also in preview state. 50 | - This package currently only works when [Impeller is enabled](https://docs.flutter.dev/perf/impeller#availability). 51 | - This package uses the experimental [Dart "Native Assets"](https://github.com/dart-lang/sdk/issues/50565) feature to automate some build tasks. 52 | - Given the reliance on non-production features, switching to the [master channel](https://docs.flutter.dev/release/upgrade#other-channels) is recommended when using Flutter Scene. 53 | 54 | ## Features 55 | 56 | * glTF (.glb) asset import. 57 | * PBR materials. 58 | * Environment maps/image-based lighting. 59 | * Blended animation system. 60 | 61 | ## FAQ 62 | 63 | ### **Q:** What platforms does this package support? 64 | 65 | `flutter_scene` supports all platforms that [Impeller](https://docs.flutter.dev/perf/impeller#availability) currently supports. 66 | 67 | On iOS and Android, Impeller is Flutter's default production renderer. So on these platforms, `flutter_scene` works without any additional project configuration. 68 | 69 | On MacOS, Windows, and Linux, Impeller is able to run, but is not on by default and must be enabled. When invoking `flutter run`, Impeller can be enabled by passing the `--enable-impeller` flag. 70 | 71 | | Platform | Status | 72 | | ---------------: | :-------------- | 73 | | iOS | 🟢 Supported | 74 | | Android | 🟢 Supported | 75 | | MacOS | 🟡 Preview | 76 | | Windows | 🟡 Preview | 77 | | Linux | 🟡 Preview | 78 | | Web | 🔴 Not Supported | 79 | | Custom embedders | 🟢 Supported | 80 | 81 | ### **Q:** When will web be supported? 82 | 83 | Although there has been some very promising experimentation with porting Impeller to web, there is currently no ETA on web platform support. 84 | 85 | Web is an important platform, and both `flutter_gpu` and `flutter_scene` will eventually support Flutter web. 86 | 87 | ### **Q:** I'm seeing errors when running the importer: `ProcessException: No such file or directory`. How do I fix it? 88 | 89 | Install [CMake](https://cmake.org/download/). 90 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: [ 5 | build/**, 6 | lib/**.g.dart, 7 | importer/build/**, 8 | importer/lib/generated/**, 9 | examples/**, 10 | ] 11 | errors: 12 | asset_does_not_exist: ignore 13 | invalid_dependency: ignore 14 | -------------------------------------------------------------------------------- /assets/base.shaderbundle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/assets/base.shaderbundle -------------------------------------------------------------------------------- /assets/ibl_brdf_lut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/assets/ibl_brdf_lut.png -------------------------------------------------------------------------------- /assets/royal_esplanade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/assets/royal_esplanade.png -------------------------------------------------------------------------------- /assets/royal_esplanade_irradiance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/assets/royal_esplanade_irradiance.png -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ################################################################################ 3 | ## 4 | ## ______ _ _ _ _____ 5 | ## | ___| | | | | | / ___| 6 | ## | |_ | |_ _| |_| |_ ___ _ __ \ `--. ___ ___ _ __ ___ 7 | ## | _| | | | | | __| __/ _ \ '__| `--. \/ __/ _ \ '_ \ / _ \ 8 | ## | | | | |_| | |_| || __/ | /\__/ / (_| __/ | | | __/ 9 | ## \_| |_|\__,_|\__|\__\___|_| \____/ \___\___|_| |_|\___| 10 | ## -----------------[ Universal build script ]----------------- 11 | ## 12 | ## 13 | ## 14 | ## Optional environment variables 15 | ## ============================== 16 | ## 17 | ## IMPELLERC: Path to the impellerc executable. 18 | ## If not set, the script will use the impellerc executable 19 | ## in the Flutter SDK. 20 | ## 21 | ## ENGINE_SRC_DIR: Path to the Flutter engine source directory. Only needed 22 | ## if using a custom engine build. 23 | ## If not set,the script will attempt to find and copy the 24 | ## 'flutter_gpu' package from the Flutter SDK engine 25 | ## artifacts. 26 | ## 27 | ################################################################################ 28 | 29 | set -e 30 | 31 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 32 | cd $SCRIPT_DIR 33 | 34 | source build_utils.sh 35 | 36 | ################################################################################ 37 | ## 38 | ## 0. Warn the user if they're not using the latest version of Flutter Scene. 39 | ## 40 | CURRENT_COMMIT=$(git rev-parse HEAD) 41 | 42 | # Fetch the JSON line containing the latest commit SHA from the 'flutter-gpu' branch. 43 | LATEST_COMMIT=$(curl https://api.github.com/repos/bdero/flutter_scene/commits/flutter-gpu 2>/dev/null | grep sha | head -n 1) 44 | # Remove the JSON key. 45 | LATEST_COMMIT="${LATEST_COMMIT#*:}" 46 | # Remove any remaining non-alphanumeric junk (quotes, commas, whitespace). 47 | LATEST_COMMIT=$(echo $LATEST_COMMIT | sed "s/[^[:alnum:]-]//g") 48 | 49 | if [ -z "$LATEST_COMMIT" ]; then 50 | PrintWarning "Failed to fetch the latest commit of the 'flutter_scene' repository." 51 | LATEST_COMMIT="unknown" 52 | fi 53 | if [ "$LATEST_COMMIT" == "$CURRENT_COMMIT" ]; then 54 | PrintInfo "${GREEN}You are using the latest commit of ${BGREEN}Flutter Scene${GREEN}!" 55 | else 56 | PrintWarning "${BYELLOW}You are not using the latest commit of Flutter Scene!" 57 | PrintWarningSub "Current commit:" "$CURRENT_COMMIT" 58 | PrintWarningSub "Latest commit:" "$LATEST_COMMIT" 59 | fi 60 | 61 | ################################################################################ 62 | ## 63 | ## 3. Build the example app along with its assets. 64 | ## 65 | pushd examples >/dev/null 66 | bash build.sh 67 | popd >/dev/null 68 | 69 | ################################################################################ 70 | ## 71 | ## 4. \o/ 72 | ## 73 | PrintInfo "${GREEN}Successfully built ${BGREEN}Flutter Scene${GREEN}!${COLOR_RESET}" 74 | -------------------------------------------------------------------------------- /build_utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ################################################################################ 3 | ## 4 | ## This script is intended to be sourced by other scripts, not executed 5 | ## directly. 6 | ## 7 | ################################################################################ 8 | 9 | FLUTTER_SCENE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 10 | IMPORTER_DIR="${FLUTTER_SCENE_DIR}/importer" 11 | 12 | # Reset ANSI color code 13 | COLOR_RESET='\033[0m' 14 | # Normal ANSI color codes 15 | BLACK='\033[0;30m' 16 | RED='\033[0;31m' 17 | GREEN='\033[0;32m' 18 | YELLOW='\033[0;33m' 19 | BLUE='\033[0;34m' 20 | PURPLE='\033[0;35m' 21 | CYAN='\033[0;36m' 22 | WHITE='\033[0;37m' 23 | # Bold ANSI color codes 24 | BBLACK='\033[1;30m' 25 | BRED='\033[1;31m' 26 | BGREEN='\033[1;32m' 27 | BYELLOW='\033[1;33m' 28 | BBLUE='\033[1;34m' 29 | BPURPLE='\033[1;35m' 30 | BCYAN='\033[1;36m' 31 | BWHITE='\033[1;37m' 32 | 33 | function PrintInfo { 34 | >&2 echo 35 | >&2 echo -e "${BCYAN}[INFO] ${CYAN}$1${COLOR_RESET}" 36 | } 37 | 38 | function PrintInfoSub { 39 | >&2 echo -e "${CYAN} $1${COLOR_RESET} $2" 40 | } 41 | 42 | function PrintWarning { 43 | >&2 echo 44 | >&2 echo -e "${BYELLOW}[WARNING] ${YELLOW}$1${COLOR_RESET}" 45 | } 46 | 47 | function PrintWarningSub { 48 | >&2 echo -e "${YELLOW} $1${COLOR_RESET} $2" 49 | } 50 | 51 | function PrintFatal { 52 | >&2 echo 53 | >&2 echo -e "${BRED}[FATAL] ${RED}$1${COLOR_RESET}" 54 | exit 1 55 | } 56 | 57 | FLUTTER_CMD="$(which flutter 2>/dev/null)" 58 | if [ -z "$FLUTTER_CMD" ]; then 59 | PrintFatal "Flutter command not found in the path! Make sure to add the 'flutter/bin' directory to your PATH." 60 | fi 61 | FLUTTER_SDK_DIR="$(dirname ${FLUTTER_CMD})/.." 62 | ENGINE_ARTIFACTS_DIR="${FLUTTER_SDK_DIR}/bin/cache/artifacts/engine" 63 | 64 | 65 | # TODO(bdero): Refactor the routines below to be less repetitive... but don't 66 | # carried away. It's a shell script. 67 | 68 | function GetFlutterGpuArtifactsDirectory { 69 | LOCATIONS=( 70 | darwin-x64/flutter_gpu 71 | linux-x64/flutter_gpu 72 | windows-x64/flutter_gpu 73 | ) 74 | FOUND="" 75 | for LOCATION in ${LOCATIONS[@]}; do 76 | FULL_PATH="${ENGINE_ARTIFACTS_DIR}/${LOCATION}" 77 | # >&2 echo " Checking ${FULL_PATH}..." 78 | if test -d "$FULL_PATH"; then 79 | FOUND="$FULL_PATH" 80 | break 81 | fi 82 | done 83 | if [ -z "$FOUND" ]; then 84 | PrintFatal "Failed to find the Flutter GPU artifacts directory." 85 | fi 86 | PrintInfoSub "Flutter GPU artifacts directory found:" "$FOUND" 87 | echo "$FOUND" 88 | } 89 | 90 | function GetImpellerShaderLibDirectory { 91 | LOCATIONS=( 92 | darwin-x64/shader_lib 93 | linux-x64/shader_lib 94 | windows-x64/shader_lib 95 | ) 96 | FOUND="" 97 | for LOCATION in ${LOCATIONS[@]}; do 98 | FULL_PATH="${ENGINE_ARTIFACTS_DIR}/${LOCATION}" 99 | # >&2 echo " Checking ${FULL_PATH}..." 100 | if test -d "$FULL_PATH"; then 101 | FOUND="$FULL_PATH" 102 | break 103 | fi 104 | done 105 | if [ -z "$FOUND" ]; then 106 | PrintFatal "Failed to find the Impeller shader_lib directory." 107 | fi 108 | PrintInfoSub "Impeller shader_lib directory found:" "$FOUND" 109 | echo "$FOUND" 110 | } 111 | 112 | function GetImpellercExecutable { 113 | if [ ! -z "$IMPELLERC" ]; then 114 | PrintInfo "Using impellerc environment variable: $IMPELLERC" 115 | echo "$IMPELLERC" 116 | return 117 | fi 118 | LOCATIONS=( 119 | darwin-x64/impellerc 120 | linux-x64/impellerc 121 | windows-x64/impellerc.exe 122 | ) 123 | FOUND="" 124 | for LOCATION in ${LOCATIONS[@]}; do 125 | FULL_PATH="${ENGINE_ARTIFACTS_DIR}/${LOCATION}" 126 | # >&2 echo " Checking ${FULL_PATH}..." 127 | if test -f "$FULL_PATH"; then 128 | FOUND="$FULL_PATH" 129 | break 130 | fi 131 | done 132 | if [ -z "$FOUND" ]; then 133 | PrintFatal "Failed to find impellerc in the engine artifacts." 134 | fi 135 | PrintInfoSub "impellerc executable found:" "$FOUND" 136 | echo "$FOUND" 137 | } 138 | 139 | function GetImporterExecutable { 140 | LOCATIONS=( 141 | Release/importer 142 | Release/importer.exe 143 | Debug/importer 144 | Debug/importer.exe 145 | importer 146 | importer.exe 147 | ) 148 | FOUND="" 149 | for LOCATION in ${LOCATIONS[@]}; do 150 | FULL_PATH="${IMPORTER_DIR}/build/${LOCATION}" 151 | # >&2 echo " Checking ${FULL_PATH}..." 152 | if test -f "$FULL_PATH"; then 153 | FOUND="$FULL_PATH" 154 | break 155 | fi 156 | done 157 | if [ -z "$FOUND" ]; then 158 | PrintFatal "Failed to find importer! Has the importer been built?" 159 | fi 160 | PrintInfoSub "importer executable found:" "$FOUND" 161 | echo "$FOUND" 162 | } 163 | 164 | function GetFlatcExecutable { 165 | LOCATIONS=( 166 | Release/flatc 167 | Release/flatc.exe 168 | Debug/flatc 169 | Debug/flatc.exe 170 | flatc 171 | flatc.exe 172 | ) 173 | FOUND="" 174 | for LOCATION in ${LOCATIONS[@]}; do 175 | FULL_PATH="${IMPORTER_DIR}/build/_deps/flatbuffers-build/${LOCATION}" 176 | # >&2 echo " Checking ${FULL_PATH}..." 177 | if test -f "$FULL_PATH"; then 178 | FOUND="$FULL_PATH" 179 | break 180 | fi 181 | done 182 | if [ -z "$FOUND" ]; then 183 | PrintFatal "Failed to find flatc! Has the importer been built?" 184 | fi 185 | PrintInfoSub "flatc executable found:" "$FOUND" 186 | echo "$FOUND" 187 | } 188 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Flutter Scene examples 2 | 3 | To build the example assets and prepare the example projects, first run `build.sh`. 4 | 5 | Then, navigate to one of the example directories and run the Flutter app with impeller enabled. For example: 6 | ```bash 7 | cd flutter_app 8 | flutter run -d macos --enable-impeller 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/assets_src/dash.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/examples/assets_src/dash.glb -------------------------------------------------------------------------------- /examples/assets_src/fcar.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/examples/assets_src/fcar.glb -------------------------------------------------------------------------------- /examples/assets_src/flutter_logo_baked.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/examples/assets_src/flutter_logo_baked.glb -------------------------------------------------------------------------------- /examples/assets_src/two_triangles.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/examples/assets_src/two_triangles.glb -------------------------------------------------------------------------------- /examples/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 5 | cd $SCRIPT_DIR 6 | 7 | source ../build_utils.sh 8 | 9 | PrintInfo "Building examples..." 10 | 11 | # Prepare example projects 12 | 13 | function prepare_example { 14 | PrintInfo "Preparing example app $1..." 15 | pushd $1 > /dev/null 16 | set +e 17 | flutter create . --platforms macos,ios,android 18 | flutter pub get 19 | set -e 20 | popd > /dev/null 21 | } 22 | prepare_example flutter_app 23 | -------------------------------------------------------------------------------- /examples/flutter_app/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | ios/ 48 | android/ 49 | linux/ 50 | windows/ 51 | macos/ 52 | web/ 53 | test/ 54 | pubspec.lock 55 | .metadata 56 | -------------------------------------------------------------------------------- /examples/flutter_app/README.md: -------------------------------------------------------------------------------- 1 | # Scene Examples App 2 | 3 | This is a Flutter App that contains several Flutter Scene usage examples. 4 | 5 | The app is just a simple harness with a dropdown that selects an example widget. 6 | -------------------------------------------------------------------------------- /examples/flutter_app/assets/little_paris_eiffel_tower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/examples/flutter_app/assets/little_paris_eiffel_tower.png -------------------------------------------------------------------------------- /examples/flutter_app/assets/little_paris_eiffel_tower_irradiance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/examples/flutter_app/assets/little_paris_eiffel_tower_irradiance.png -------------------------------------------------------------------------------- /examples/flutter_app/hook/build.dart: -------------------------------------------------------------------------------- 1 | import 'package:native_assets_cli/native_assets_cli.dart'; 2 | import 'package:flutter_scene_importer/build_hooks.dart'; 3 | 4 | void main(List args) { 5 | build(args, (config, output) async { 6 | buildModels( 7 | buildInput: config, 8 | inputFilePaths: [ 9 | '../assets_src/two_triangles.glb', 10 | '../assets_src/flutter_logo_baked.glb', 11 | '../assets_src/dash.glb', 12 | '../assets_src/fcar.glb', 13 | ], 14 | ); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /examples/flutter_app/lib/example_animation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_scene/scene.dart'; 5 | import 'package:vector_math/vector_math.dart' as vm; 6 | 7 | class ExampleAnimation extends StatefulWidget { 8 | const ExampleAnimation({super.key, this.elapsedSeconds = 0}); 9 | final double elapsedSeconds; 10 | 11 | @override 12 | ExampleAnimationState createState() => ExampleAnimationState(); 13 | } 14 | 15 | class ExampleAnimationState extends State { 16 | Scene scene = Scene(); 17 | bool loaded = false; 18 | AnimationClip? idleClip; 19 | AnimationClip? runClip; 20 | AnimationClip? walkClip; 21 | 22 | @override 23 | void initState() { 24 | final dashModel = Node.fromAsset('build/models/dash.model').then(( 25 | modelNode, 26 | ) { 27 | for (final animation in modelNode.parsedAnimations) { 28 | debugPrint('Animation: ${animation.name}'); 29 | } 30 | 31 | scene.add(modelNode); 32 | 33 | idleClip = 34 | modelNode.createAnimationClip(modelNode.findAnimationByName('Idle')!) 35 | ..loop = true 36 | ..play(); 37 | walkClip = 38 | modelNode.createAnimationClip(modelNode.findAnimationByName('Walk')!) 39 | ..loop = true 40 | ..weight = 0 41 | ..play(); 42 | runClip = 43 | modelNode.createAnimationClip(modelNode.findAnimationByName('Run')!) 44 | ..loop = true 45 | ..weight = 0 46 | ..play(); 47 | }); 48 | 49 | Future.wait([dashModel]).then((_) { 50 | debugPrint('Scene loaded.'); 51 | setState(() { 52 | loaded = true; 53 | }); 54 | }); 55 | 56 | super.initState(); 57 | } 58 | 59 | @override 60 | void dispose() { 61 | // Technically this isn't necessary, since `Node.fromAsset` doesn't perform 62 | // a caching import. 63 | scene.removeAll(); 64 | super.dispose(); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | if (!loaded) { 70 | return const Center(child: CircularProgressIndicator()); 71 | } 72 | return Stack( 73 | children: [ 74 | SizedBox.expand( 75 | child: CustomPaint( 76 | painter: _ScenePainter(scene, widget.elapsedSeconds), 77 | ), 78 | ), 79 | // Door open slider 80 | if (idleClip != null) 81 | Column( 82 | children: [ 83 | const Spacer(), 84 | Row( 85 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 86 | children: [ 87 | for (final clip in [idleClip, walkClip, runClip]) 88 | Slider( 89 | value: clip!.weight, 90 | onChanged: (value) { 91 | clip.weight = value; 92 | }, 93 | ), 94 | ], 95 | ), 96 | Row( 97 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 98 | children: [ 99 | Slider( 100 | min: -2, 101 | max: 2, 102 | value: walkClip!.playbackTimeScale, 103 | onChanged: (value) { 104 | idleClip!.playbackTimeScale = value; 105 | walkClip!.playbackTimeScale = value; 106 | runClip!.playbackTimeScale = value; 107 | }, 108 | ), 109 | ], 110 | ), 111 | ], 112 | ), 113 | ], 114 | ); 115 | } 116 | } 117 | 118 | class _ScenePainter extends CustomPainter { 119 | _ScenePainter(this.scene, this.elapsedTime); 120 | Scene scene; 121 | double elapsedTime; 122 | 123 | @override 124 | void paint(Canvas canvas, Size size) { 125 | double rotationAmount = elapsedTime * 0.5; 126 | const double distance = 6; 127 | final camera = PerspectiveCamera( 128 | position: vm.Vector3( 129 | sin(rotationAmount) * distance, 130 | 2, 131 | cos(rotationAmount) * distance, 132 | ), 133 | target: vm.Vector3(0, 1.5, 0), 134 | ); 135 | 136 | scene.render(camera, canvas, viewport: Offset.zero & size); 137 | } 138 | 139 | @override 140 | bool shouldRepaint(covariant CustomPainter oldDelegate) => true; 141 | } 142 | -------------------------------------------------------------------------------- /examples/flutter_app/lib/example_car.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_scene/scene.dart'; 5 | import 'package:vector_math/vector_math.dart' as vm; 6 | 7 | class ExampleCar extends StatefulWidget { 8 | const ExampleCar({super.key, this.elapsedSeconds = 0}); 9 | final double elapsedSeconds; 10 | 11 | @override 12 | ExampleCarState createState() => ExampleCarState(); 13 | } 14 | 15 | class NodeState { 16 | NodeState(this.node, this.startTransform); 17 | 18 | Node node; 19 | vm.Matrix4 startTransform; 20 | double amount = 0; 21 | } 22 | 23 | class ExampleCarState extends State { 24 | Scene scene = Scene(); 25 | bool loaded = false; 26 | 27 | double wheelRotation = 0; 28 | 29 | Map nodes = {}; 30 | 31 | @override 32 | void initState() { 33 | final loadModel = Node.fromAsset('build/models/fcar.model').then((value) { 34 | value.name = 'Car'; 35 | scene.add(value); 36 | debugPrint('Model loaded: ${value.name}'); 37 | 38 | for (final doorName in [ 39 | 'DoorFront.L', 40 | 'DoorFront.R', 41 | 'DoorBack.L', 42 | 'DoorBack.R', 43 | 'Frunk', 44 | 'Trunk', 45 | 'WheelFront.L', 46 | 'WheelFront.R', 47 | 'WheelBack.L', 48 | 'WheelBack.R', 49 | ]) { 50 | final door = value.getChildByNamePath([doorName])!; 51 | nodes[doorName] = NodeState(door, door.localTransform.clone()); 52 | } 53 | }); 54 | 55 | EnvironmentMap.fromAssets( 56 | radianceImagePath: 'assets/little_paris_eiffel_tower.png', 57 | irradianceImagePath: 'assets/little_paris_eiffel_tower_irradiance.png', 58 | ).then((environment) { 59 | scene.environment.environmentMap = environment; 60 | scene.environment.exposure = 2.0; 61 | scene.environment.intensity = 2.0; 62 | }); 63 | 64 | Future.wait([loadModel]).then((_) { 65 | debugPrint('Scene loaded.'); 66 | setState(() { 67 | loaded = true; 68 | }); 69 | }); 70 | 71 | super.initState(); 72 | } 73 | 74 | @override 75 | void dispose() { 76 | // Technically this isn't necessary, since `Node.fromAsset` doesn't perform 77 | // a caching import. 78 | scene.removeAll(); 79 | super.dispose(); 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | if (!loaded) { 85 | return const Center(child: CircularProgressIndicator()); 86 | } 87 | 88 | // Rotate the wheels at a given speed. 89 | final wheelSpeed = nodes['WheelBack.L']!.amount; 90 | wheelRotation += wheelSpeed / 10; 91 | 92 | for (final wheelName in ['WheelBack.L', 'WheelBack.R']) { 93 | final wheel = nodes[wheelName]!; 94 | wheel.node.localTransform = 95 | wheel.startTransform.clone() 96 | ..rotate(vm.Vector3(0, 0, -1), wheelRotation); 97 | } 98 | 99 | final wheelTurn = nodes['WheelFront.L']!.amount; 100 | 101 | for (final wheelName in ['WheelFront.L', 'WheelFront.R']) { 102 | final wheel = nodes[wheelName]!; 103 | wheel.node.localTransform = 104 | wheel.startTransform.clone() * 105 | vm.Matrix4.rotationY(-wheelTurn / 2) * 106 | vm.Matrix4.rotationZ(-wheelRotation); 107 | } 108 | 109 | return Stack( 110 | children: [ 111 | SizedBox.expand( 112 | child: CustomPaint( 113 | painter: _ScenePainter(scene, widget.elapsedSeconds), 114 | ), 115 | ), 116 | // Door open slider 117 | Column( 118 | children: [ 119 | const Spacer(), 120 | Row( 121 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 122 | children: [ 123 | for (final doorName in [ 124 | 'DoorFront.L', 125 | 'DoorFront.R', 126 | 'DoorBack.L', 127 | 'DoorBack.R', 128 | ]) 129 | Slider( 130 | value: nodes[doorName]!.amount, 131 | onChanged: (value) { 132 | final door = nodes[doorName]!; 133 | door.node.localTransform = 134 | door.startTransform.clone() 135 | ..rotate(vm.Vector3(0, -1, 0), value * pi / 2); 136 | door.amount = value; 137 | }, 138 | ), 139 | ], 140 | ), 141 | Row( 142 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 143 | children: [ 144 | Slider( 145 | value: nodes['Frunk']!.amount, 146 | onChanged: (value) { 147 | final door = nodes['Frunk']!; 148 | door.node.localTransform = 149 | door.startTransform.clone() 150 | ..rotate(vm.Vector3(0, 0, 1), value * pi / 2); 151 | door.amount = value; 152 | }, 153 | ), 154 | Slider( 155 | value: nodes['Trunk']!.amount, 156 | onChanged: (value) { 157 | final door = nodes['Trunk']!; 158 | door.node.localTransform = 159 | door.startTransform.clone() 160 | ..rotate(vm.Vector3(0, 0, -1), value * pi / 2); 161 | door.amount = value; 162 | }, 163 | ), 164 | Slider( 165 | value: nodes['WheelBack.L']!.amount, 166 | onChanged: (value) { 167 | nodes['WheelBack.L']!.amount = value; 168 | }, 169 | ), 170 | Slider( 171 | min: -1, 172 | max: 1, 173 | value: nodes['WheelFront.L']!.amount, 174 | onChanged: (value) { 175 | nodes['WheelFront.L']!.amount = value; 176 | }, 177 | ), 178 | ], 179 | ), 180 | ], 181 | ), 182 | ], 183 | ); 184 | } 185 | } 186 | 187 | class _ScenePainter extends CustomPainter { 188 | _ScenePainter(this.scene, this.elapsedTime); 189 | Scene scene; 190 | double elapsedTime; 191 | 192 | @override 193 | void paint(Canvas canvas, Size size) { 194 | double rotationAmount = elapsedTime * 0.2; 195 | final camera = PerspectiveCamera( 196 | position: 197 | vm.Vector3(sin(rotationAmount) * 5, 2, cos(rotationAmount) * 5) * 2, 198 | target: vm.Vector3(0, 0, 0), 199 | ); 200 | 201 | scene.render(camera, canvas, viewport: Offset.zero & size); 202 | } 203 | 204 | @override 205 | bool shouldRepaint(covariant CustomPainter oldDelegate) => true; 206 | } 207 | -------------------------------------------------------------------------------- /examples/flutter_app/lib/example_cuboid.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_scene/scene.dart'; 5 | import 'package:vector_math/vector_math.dart' as vm; 6 | 7 | class ExampleCuboid extends StatefulWidget { 8 | const ExampleCuboid({super.key, this.elapsedSeconds = 0}); 9 | final double elapsedSeconds; 10 | 11 | @override 12 | ExampleCuboidState createState() => ExampleCuboidState(); 13 | } 14 | 15 | class ExampleCuboidState extends State { 16 | Scene scene = Scene(); 17 | 18 | @override 19 | void initState() { 20 | final mesh = Mesh(CuboidGeometry(vm.Vector3(1, 1, 1)), UnlitMaterial()); 21 | scene.addMesh(mesh); 22 | 23 | super.initState(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return CustomPaint(painter: _ScenePainter(scene, widget.elapsedSeconds)); 29 | } 30 | } 31 | 32 | class _ScenePainter extends CustomPainter { 33 | _ScenePainter(this.scene, this.elapsedTime); 34 | Scene scene; 35 | double elapsedTime; 36 | 37 | @override 38 | void paint(Canvas canvas, Size size) { 39 | final camera = PerspectiveCamera( 40 | position: vm.Vector3(sin(elapsedTime) * 5, 2, cos(elapsedTime) * 5), 41 | target: vm.Vector3(0, 0, 0), 42 | ); 43 | 44 | scene.render(camera, canvas, viewport: Offset.zero & size); 45 | } 46 | 47 | @override 48 | bool shouldRepaint(covariant CustomPainter oldDelegate) => true; 49 | } 50 | -------------------------------------------------------------------------------- /examples/flutter_app/lib/example_logo.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_scene/scene.dart'; 5 | import 'package:vector_math/vector_math.dart' as vm; 6 | 7 | class ExampleLogo extends StatefulWidget { 8 | const ExampleLogo({super.key, this.elapsedSeconds = 0}); 9 | final double elapsedSeconds; 10 | 11 | @override 12 | ExampleLogoState createState() => ExampleLogoState(); 13 | } 14 | 15 | class ExampleLogoState extends State { 16 | Scene scene = Scene(); 17 | bool loaded = false; 18 | 19 | @override 20 | void initState() { 21 | final loadModel = Node.fromAsset( 22 | 'build/models/flutter_logo_baked.model', 23 | ).then((value) { 24 | value.name = 'FlutterLogo'; 25 | scene.add(value); 26 | debugPrint('Model loaded: ${value.name}'); 27 | }); 28 | 29 | Future.wait([loadModel]).then((_) { 30 | debugPrint('Scene loaded.'); 31 | setState(() { 32 | loaded = true; 33 | }); 34 | }); 35 | 36 | super.initState(); 37 | } 38 | 39 | @override 40 | void dispose() { 41 | // Technically this isn't necessary, since `Node.fromAsset` doesn't perform 42 | // a caching import. 43 | scene.removeAll(); 44 | super.dispose(); 45 | } 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | if (!loaded) { 50 | return const Center(child: CircularProgressIndicator()); 51 | } 52 | 53 | return CustomPaint(painter: _ScenePainter(scene, widget.elapsedSeconds)); 54 | } 55 | } 56 | 57 | class _ScenePainter extends CustomPainter { 58 | _ScenePainter(this.scene, this.elapsedTime); 59 | Scene scene; 60 | double elapsedTime; 61 | 62 | @override 63 | void paint(Canvas canvas, Size size) { 64 | final camera = PerspectiveCamera( 65 | position: vm.Vector3(sin(elapsedTime) * 5, 2, cos(elapsedTime) * 5), 66 | target: vm.Vector3(0, 0, 0), 67 | ); 68 | 69 | scene.render(camera, canvas, viewport: Offset.zero & size); 70 | } 71 | 72 | @override 73 | bool shouldRepaint(covariant CustomPainter oldDelegate) => true; 74 | } 75 | -------------------------------------------------------------------------------- /examples/flutter_app/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:example_app/example_car.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/scheduler.dart'; 4 | import 'package:example_app/example_animation.dart'; 5 | 6 | import 'example_cuboid.dart'; 7 | import 'example_logo.dart'; 8 | 9 | void main() { 10 | runApp(const MyApp()); 11 | } 12 | 13 | class MyApp extends StatefulWidget { 14 | const MyApp({super.key}); 15 | 16 | @override 17 | State createState() => _MyAppState(); 18 | } 19 | 20 | class _MyAppState extends State { 21 | late Ticker ticker; 22 | double elapsedSeconds = 0; 23 | String selectedExample = ''; 24 | Map examples = {}; 25 | 26 | @override 27 | void initState() { 28 | ticker = Ticker((elapsed) { 29 | setState(() { 30 | elapsedSeconds = elapsed.inMilliseconds.toDouble() / 1000; 31 | }); 32 | }); 33 | ticker.start(); 34 | 35 | examples = { 36 | 'Car': (context) => ExampleCar(elapsedSeconds: elapsedSeconds), 37 | 'Animation': 38 | (context) => ExampleAnimation(elapsedSeconds: elapsedSeconds), 39 | 'Imported Model': 40 | (context) => ExampleLogo(elapsedSeconds: elapsedSeconds), 41 | 'Cuboid': (context) => ExampleCuboid(elapsedSeconds: elapsedSeconds), 42 | }; 43 | selectedExample = examples.keys.first; 44 | 45 | super.initState(); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return MaterialApp( 51 | title: 'Flutter Scene Examples', 52 | theme: ThemeData( 53 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 54 | useMaterial3: true, 55 | ), 56 | home: Scaffold( 57 | appBar: AppBar( 58 | backgroundColor: Theme.of(context).colorScheme.inversePrimary, 59 | title: Text('Example: $selectedExample'), 60 | ), 61 | body: Stack( 62 | children: [ 63 | SizedBox.expand(child: examples[selectedExample]!(context)), 64 | // Dropdown menu 65 | Align( 66 | alignment: Alignment.topLeft, 67 | child: Padding( 68 | padding: const EdgeInsets.all(8.0), 69 | child: DropdownButton( 70 | value: selectedExample, 71 | items: 72 | examples.keys.map>(( 73 | String value, 74 | ) { 75 | return DropdownMenuItem( 76 | value: value, 77 | child: Text(value), 78 | ); 79 | }).toList(), 80 | onChanged: (String? newValue) { 81 | setState(() { 82 | ticker.stop(); 83 | ticker.start(); 84 | selectedExample = newValue!; 85 | }); 86 | }, 87 | ), 88 | ), 89 | ), 90 | ], 91 | ), 92 | ), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /examples/flutter_app/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example_app 2 | description: "flutter_scene example project." 3 | publish_to: 'none' 4 | 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: '>=3.4.0-82.0.dev <4.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_scene: 14 | path: ../../ 15 | #flutter_scene_importer: ^0.9.2-0 16 | flutter_scene_importer: 17 | path: ../../importer 18 | native_assets_cli: '>=0.13.0 <0.14.0' 19 | vector_math: ^2.1.4 20 | 21 | dev_dependencies: 22 | flutter_lints: ^3.0.0 23 | 24 | flutter: 25 | uses-material-design: true 26 | 27 | assets: 28 | - assets/little_paris_eiffel_tower.png 29 | - assets/little_paris_eiffel_tower_irradiance.png 30 | # Imported models. 31 | - build/models/ 32 | -------------------------------------------------------------------------------- /flutter_scene_pubignore: -------------------------------------------------------------------------------- 1 | build/ 2 | **/ephemeral/ 3 | 4 | examples/ 5 | importer/ 6 | -------------------------------------------------------------------------------- /hook/build.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_scene_importer/offline_import.dart'; 2 | import 'package:native_assets_cli/native_assets_cli.dart'; 3 | 4 | import 'package:flutter_gpu_shaders/build.dart'; 5 | 6 | void main(List args) async { 7 | await build(args, (config, output) async { 8 | generateImporterFlatbufferDart(); 9 | 10 | await buildShaderBundleJson( 11 | buildInput: config, 12 | buildOutput: output, 13 | manifestFileName: 'shaders/base.shaderbundle.json', 14 | ); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /importer/.clang-format: -------------------------------------------------------------------------------- 1 | # Defines the Chromium style for automatic reformatting. 2 | # http://clang.llvm.org/docs/ClangFormatStyleOptions.html 3 | BasedOnStyle: Chromium 4 | Standard: Latest 5 | SortIncludes: true 6 | -------------------------------------------------------------------------------- /importer/.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/settings.json 2 | 3 | build/ 4 | _deps/ 5 | CMakeFiles/ 6 | CMakeCache.txt 7 | MakeFile 8 | cmake_install.cmake 9 | compile_commands.json 10 | /generated/ 11 | .cache/ -------------------------------------------------------------------------------- /importer/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "a8345d539996b4cd6605c0f7848349340d0d206c" 8 | channel: "[user-branch]" 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /importer/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug", 11 | "program": "${workspaceFolder}/build/importer", 12 | "args": [ 13 | "${workspaceFolder}/../examples/assets_src/car.glb", 14 | "${workspaceFolder}/../examples/flutter_app/assets_imported/car.model" 15 | ], 16 | "cwd": "${workspaceFolder}/build" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /importer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | 3 | * Implementation for the offline model importer. 4 | * Importer Flatbuffer & Dart codegen. 5 | * bin/import.dart command for invoking the model importer. 6 | * Native assets build hook for compiling the model importer. 7 | 8 | ## 0.1.1 9 | 10 | * Check in generated importer flatbuffer. 11 | 12 | ## 0.1.2-0 13 | 14 | * Mark as pre-release. 15 | 16 | ## 0.1.2-1 17 | 18 | * Use Platform.resolvedExecutable in `buildModels` for resolving the Dart executable. 19 | 20 | ## 0.1.2-2 21 | 22 | * Add more flatbuffer import helpers. 23 | 24 | ## 0.1.2-3 25 | 26 | * Fix erroneous inverse in the flatbuffer->Dart Matrix4 conversion. 27 | 28 | ## 0.2.0-0 29 | 30 | * Remove constexpr qualifiers from matrix for better portability. 31 | * Support non-embedded/URI-only image embeds. 32 | * Fix path interpretation issues on Windows. 33 | 34 | ## 0.6.0-0 35 | 36 | * Pin native_assets_cli to <0.9.0 37 | (https://github.com/bdero/flutter_gpu_shaders/issues/3) 38 | * Place package version in lockstep with flutter_scene. 39 | 40 | ## 0.7.0-0 41 | 42 | * Update to native_assets_cli 0.9.0. 43 | Breaking: `BuildOutput` is now `BuildOutputBuilder` 44 | 45 | ## 0.8.0-0 46 | 47 | * Update to Flutter 3.29.0-1.0.pre.242. 48 | 49 | ## 0.9.0-0 50 | 51 | * Update to native_assets_cli 0.13.0. 52 | Breaking: `BuildConfig` is now `BuildInput` 53 | -------------------------------------------------------------------------------- /importer/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | project(importer LANGUAGES C CXX) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "") 8 | 9 | set(PROJECT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) 10 | set(GENERATED_DIR ${PROJECT_DIR}/generated) 11 | 12 | include(FetchContent) 13 | 14 | FetchContent_Declare( 15 | flatbuffers 16 | GIT_REPOSITORY https://github.com/google/flatbuffers.git 17 | GIT_TAG 129ef422e8a4e89d87a7216a865602673a6d0bf3 18 | ) 19 | 20 | FetchContent_Declare( 21 | tinygltf 22 | GIT_REPOSITORY https://github.com/syoyo/tinygltf.git 23 | GIT_TAG 4fea26f6c8652f545560807bccc934cf0cdd86dd 24 | ) 25 | FetchContent_MakeAvailable(flatbuffers tinygltf) 26 | 27 | 28 | # flatbuffers_schema( 29 | # TARGET dependent 30 | # INPUT filename 31 | # OUTPUT_DIR path 32 | # ) 33 | function(flatbuffers_schema) 34 | cmake_parse_arguments(ARG "" "TARGET;INPUT;OUTPUT_DIR" "" ${ARGN}) 35 | 36 | get_filename_component(INPUT_FILENAME ${ARG_INPUT} NAME_WE) 37 | 38 | set(OUTPUT_HEADER "${ARG_OUTPUT_DIR}/${INPUT_FILENAME}_flatbuffers.h") 39 | add_custom_command( 40 | COMMAND ${CMAKE_COMMAND} -E make_directory "${ARG_OUTPUT_DIR}" 41 | COMMAND "$" 42 | --warnings-as-errors 43 | --cpp 44 | --cpp-std c++17 45 | --cpp-static-reflection 46 | --gen-object-api 47 | --filename-suffix _flatbuffers 48 | -o "${ARG_OUTPUT_DIR}" 49 | "${ARG_INPUT}" 50 | MAIN_DEPENDENCY ${ARG_INPUT} 51 | OUTPUT "${OUTPUT_HEADER}" 52 | COMMENT "Generating flatbuffer schema ${ARG_INPUT}" 53 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") 54 | 55 | target_sources(${ARG_TARGET} PUBLIC "${OUTPUT_HEADER}") 56 | target_include_directories(${ARG_TARGET} 57 | PUBLIC 58 | $) # For includes starting with "flatbuffers/" 59 | endfunction() 60 | 61 | add_executable(importer 62 | "conversions.cc" 63 | "importer_gltf.cc" 64 | "scenec_main.cc" 65 | "vertices_builder.cc" 66 | ) 67 | 68 | target_link_libraries(importer PUBLIC tinygltf) 69 | target_include_directories(importer PUBLIC 70 | "${PROJECT_DIR}" 71 | "${flatbuffers_SOURCE_DIR}/include" 72 | "${tinygltf_SOURCE_DIR}/include") 73 | 74 | flatbuffers_schema( 75 | TARGET importer 76 | INPUT ${PROJECT_DIR}/scene.fbs 77 | OUTPUT_DIR ${GENERATED_DIR} 78 | ) 79 | 80 | install(TARGETS importer flatc 81 | CONFIGURATIONS Debug 82 | RUNTIME 83 | DESTINATION Debug/bin 84 | ) 85 | install(TARGETS importer flatc 86 | CONFIGURATIONS Release 87 | RUNTIME 88 | DESTINATION Release/bin 89 | ) 90 | -------------------------------------------------------------------------------- /importer/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Brandon DeRosier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /importer/README.md: -------------------------------------------------------------------------------- 1 | A 3D model importer for Flutter Scene. 2 | 3 | ## Features 4 | 5 | This packages provides an offline 3D model importer for the `flutter_scene` package, consisting of: 6 | * An offline importer binary that converts GLB files (the glTF binary format) into `model` files. 7 | * A Dart runtime that contains tools for deserializing the imported `model` files. 8 | -------------------------------------------------------------------------------- /importer/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - lib/generated/ 6 | - lib/third_party/ 7 | -------------------------------------------------------------------------------- /importer/bin/import.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | import 'package:flutter_scene_importer/offline_import.dart'; 5 | 6 | void main(List args) { 7 | final parser = ArgParser() 8 | ..addOption('input', abbr: 'i', help: 'Input glTF file path') 9 | ..addOption('output', abbr: 'o', help: 'Output model file path') 10 | ..addOption('working-directory', abbr: 'w', help: 'Working directory'); 11 | 12 | final results = parser.parse(args); 13 | 14 | final input = results['input'] as String?; 15 | final output = results['output'] as String?; 16 | final workingDirectory = results['working-directory'] as String?; 17 | 18 | if (input == null || output == null) { 19 | // ignore: avoid_print 20 | print( 21 | 'Usage: importer --input --output [--working-directory ]'); 22 | exit(1); 23 | } 24 | 25 | importGltf(input, output, workingDirectory: workingDirectory); 26 | } 27 | -------------------------------------------------------------------------------- /importer/conversions.cc: -------------------------------------------------------------------------------- 1 | #include "conversions.h" 2 | 3 | #include 4 | 5 | #include "generated/scene_flatbuffers.h" 6 | 7 | namespace impeller { 8 | namespace scene { 9 | namespace importer { 10 | 11 | Matrix ToMatrix(const std::vector& m) { 12 | return Matrix{ 13 | static_cast(m[0]), static_cast(m[1]), 14 | static_cast(m[2]), static_cast(m[3]), 15 | static_cast(m[4]), static_cast(m[5]), 16 | static_cast(m[6]), static_cast(m[7]), 17 | static_cast(m[8]), static_cast(m[9]), 18 | static_cast(m[10]), static_cast(m[11]), 19 | static_cast(m[12]), static_cast(m[13]), 20 | static_cast(m[14]), static_cast(m[15]), 21 | }; 22 | } 23 | 24 | //----------------------------------------------------------------------------- 25 | /// Flatbuffers -> Impeller 26 | /// 27 | 28 | Matrix ToMatrix(const fb::Matrix& m) { 29 | return Matrix{m.m0(), m.m1(), m.m2(), m.m3(), // 30 | m.m4(), m.m5(), m.m6(), m.m7(), // 31 | m.m8(), m.m9(), m.m10(), m.m11(), // 32 | m.m12(), m.m13(), m.m14(), m.m15()}; 33 | } 34 | 35 | Vector2 ToVector2(const fb::Vec2& v) { 36 | return Vector2{v.x(), v.y()}; 37 | } 38 | 39 | Vector3 ToVector3(const fb::Vec3& v) { 40 | return Vector3{v.x(), v.y(), v.z()}; 41 | } 42 | 43 | Vector4 ToVector4(const fb::Vec4& v) { 44 | return Vector4({v.x(), v.y(), v.z(), v.w()}); 45 | } 46 | 47 | Color ToColor(const fb::Color& c) { 48 | return Color({c.r(), c.g(), c.b(), c.a()}); 49 | } 50 | 51 | //----------------------------------------------------------------------------- 52 | /// Impeller -> Flatbuffers 53 | /// 54 | 55 | fb::Matrix ToFBMatrix(const Matrix& m) { 56 | return fb::Matrix(m.m[0], m.m[1], m.m[2], m.m[3], // 57 | m.m[4], m.m[5], m.m[6], m.m[7], // 58 | m.m[8], m.m[9], m.m[10], m.m[11], // 59 | m.m[12], m.m[13], m.m[14], m.m[15]); 60 | } 61 | 62 | std::unique_ptr ToFBMatrixUniquePtr(const Matrix& m) { 63 | return std::make_unique(m.m[0], m.m[1], m.m[2], m.m[3], // 64 | m.m[4], m.m[5], m.m[6], m.m[7], // 65 | m.m[8], m.m[9], m.m[10], m.m[11], // 66 | m.m[12], m.m[13], m.m[14], m.m[15]); 67 | } 68 | 69 | fb::Vec2 ToFBVec2(const Vector2 v) { 70 | return fb::Vec2(v.x, v.y); 71 | } 72 | 73 | fb::Vec3 ToFBVec3(const Vector3 v) { 74 | return fb::Vec3(v.x, v.y, v.z); 75 | } 76 | 77 | fb::Vec4 ToFBVec4(const Vector4 v) { 78 | return fb::Vec4(v.x, v.y, v.z, v.w); 79 | } 80 | 81 | fb::Color ToFBColor(const Color c) { 82 | return fb::Color(c.red, c.green, c.blue, c.alpha); 83 | } 84 | 85 | std::unique_ptr ToFBColor(const std::vector& c) { 86 | auto* color = new fb::Color(c.size() > 0 ? c[0] : 1, // 87 | c.size() > 1 ? c[1] : 1, // 88 | c.size() > 2 ? c[2] : 1, // 89 | c.size() > 3 ? c[3] : 1); 90 | return std::unique_ptr(color); 91 | } 92 | 93 | std::unique_ptr ToFBColor3(const std::vector& c) { 94 | auto* color = new fb::Vec3(c.size() > 0 ? c[0] : 1, // 95 | c.size() > 1 ? c[1] : 1, // 96 | c.size() > 2 ? c[2] : 1); 97 | return std::unique_ptr(color); 98 | } 99 | 100 | } // namespace importer 101 | } // namespace scene 102 | } // namespace impeller 103 | -------------------------------------------------------------------------------- /importer/conversions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "generated/scene_flatbuffers.h" 8 | #include "types.h" 9 | 10 | namespace impeller { 11 | namespace scene { 12 | namespace importer { 13 | 14 | Matrix ToMatrix(const std::vector& m); 15 | 16 | //----------------------------------------------------------------------------- 17 | /// Flatbuffers -> Impeller 18 | /// 19 | 20 | Matrix ToMatrix(const fb::Matrix& m); 21 | 22 | Vector2 ToVector2(const fb::Vec2& c); 23 | 24 | Vector3 ToVector3(const fb::Vec3& c); 25 | 26 | Vector4 ToVector4(const fb::Vec4& c); 27 | 28 | Color ToColor(const fb::Color& c); 29 | 30 | //----------------------------------------------------------------------------- 31 | /// Impeller -> Flatbuffers 32 | /// 33 | 34 | fb::Matrix ToFBMatrix(const Matrix& m); 35 | 36 | std::unique_ptr ToFBMatrixUniquePtr(const Matrix& m); 37 | 38 | fb::Vec2 ToFBVec2(const Vector2 v); 39 | 40 | fb::Vec3 ToFBVec3(const Vector3 v); 41 | 42 | fb::Vec4 ToFBVec4(const Vector4 v); 43 | 44 | fb::Color ToFBColor(const Color c); 45 | 46 | std::unique_ptr ToFBColor(const std::vector& c); 47 | 48 | std::unique_ptr ToFBColor3(const std::vector& c); 49 | 50 | } // namespace importer 51 | } // namespace scene 52 | } // namespace impeller 53 | -------------------------------------------------------------------------------- /importer/hook/build.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:logging/logging.dart'; 4 | import 'package:native_assets_cli/native_assets_cli.dart'; 5 | 6 | void main(List args) async { 7 | await build(args, (config, output) async { 8 | final logger = Logger('') 9 | ..level = Level.ALL 10 | // ignore: avoid_print 11 | ..onRecord.listen((record) => print(record.message)); 12 | 13 | //------------------------------------------------------------------------- 14 | /// Ensure the "build/" directory exists. 15 | /// `mkdir -p build` 16 | /// 17 | final buildUri = config.packageRoot.resolve('build'); 18 | final outDir = Directory.fromUri(buildUri); 19 | outDir.createSync(recursive: true); 20 | 21 | //------------------------------------------------------------------------- 22 | /// Run the cmake gen step. 23 | /// `cmake -Bbuild -DCMAKE_BUILD_TYPE=Debug` 24 | /// 25 | logger.info('Running cmake gen step...'); 26 | final cmakeGenResult = Process.runSync( 27 | 'cmake', 28 | [ 29 | '-Bbuild', 30 | '-DCMAKE_BUILD_TYPE=Debug', 31 | ], 32 | workingDirectory: config.packageRoot.toFilePath()); 33 | if (cmakeGenResult.exitCode != 0) { 34 | String error = 35 | 'CMake generate step failed (exit code ${cmakeGenResult.exitCode}):\nSTDERR: ${cmakeGenResult.stderr}\nSTDOUT: ${cmakeGenResult.stdout}'; 36 | logger.severe(error); 37 | throw Exception(error); 38 | } 39 | 40 | //------------------------------------------------------------------------- 41 | /// Run the cmake gen step. 42 | /// `cmake --build build --target=importer -j 4` 43 | /// 44 | logger.info('Running cmake build step...'); 45 | final cmakeBuildResult = Process.runSync( 46 | 'cmake', 47 | [ 48 | '--build', 49 | 'build', 50 | '--target=importer', 51 | '-j', 52 | '4', 53 | ], 54 | workingDirectory: config.packageRoot.toFilePath()); 55 | if (cmakeBuildResult.exitCode != 0) { 56 | String error = 57 | 'CMake build step failed (exit code ${cmakeBuildResult.exitCode}):\nSTDERR: ${cmakeBuildResult.stderr}\nSTDOUT: ${cmakeBuildResult.stdout}'; 58 | logger.severe(error); 59 | throw Exception(error); 60 | } 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /importer/importer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "generated/scene_flatbuffers.h" 7 | 8 | namespace impeller { 9 | namespace scene { 10 | namespace importer { 11 | 12 | bool ParseGLTF(const std::vector& input_bytes, fb::SceneT& out_scene); 13 | 14 | } 15 | } // namespace scene 16 | } // namespace impeller 17 | -------------------------------------------------------------------------------- /importer/importer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ## Invoke the importer. 3 | ## usage: importer.sh 4 | set -e 5 | 6 | WORKING_DIR="$(pwd)" 7 | 8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 9 | 10 | pushd $SCRIPT_DIR >/dev/null 11 | source ../build_utils.sh 12 | popd >/dev/null 13 | 14 | IMPORTER_EXE="$(GetImporterExecutable)" 15 | if [ ! -f "$IMPORTER_EXE" ]; then 16 | PrintFatal "Importer not found. Can't build example assets!" 17 | fi 18 | 19 | PrintInfo "Invoking importer..." 20 | 21 | PrintInfoSub " input:" "$1" 22 | PrintInfoSub "output:" "$2" 23 | $IMPORTER_EXE $1 $2 24 | -------------------------------------------------------------------------------- /importer/lib/build_hooks.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:native_assets_cli/native_assets_cli.dart'; 4 | 5 | void buildModels({ 6 | required BuildInput buildInput, 7 | required List inputFilePaths, 8 | String outputDirectory = 'build/models/', 9 | }) { 10 | final outDir = 11 | Directory.fromUri(buildInput.packageRoot.resolve(outputDirectory)); 12 | outDir.createSync(recursive: true); 13 | 14 | final Uri dartExec = Uri.file(Platform.resolvedExecutable); 15 | 16 | for (final inputFilePath in inputFilePaths) { 17 | String outputFileName = Uri(path: inputFilePath).pathSegments.last; 18 | 19 | // Verify that the input file is a glTF file 20 | if (!outputFileName.endsWith('.glb')) { 21 | throw Exception( 22 | 'Input file must be a .glb file. Given file path: $inputFilePath'); 23 | } 24 | 25 | // Replace output extension with .model 26 | outputFileName = 27 | '${outputFileName.substring(0, outputFileName.lastIndexOf('.'))}.model'; 28 | 29 | /// dart --enable-experiment=native-assets run flutter_scene_importer:import \ 30 | /// --input --output --working-directory 31 | final importerResult = Process.runSync( 32 | dartExec.toFilePath(), 33 | [ 34 | '--enable-experiment=native-assets', 35 | 'run', 36 | 'flutter_scene_importer:import', 37 | '--input', 38 | inputFilePath, 39 | '--output', 40 | outDir.uri.resolve(outputFileName).toFilePath(), 41 | '--working-directory', 42 | buildInput.packageRoot.toFilePath(), 43 | ], 44 | ); 45 | if (importerResult.exitCode != 0) { 46 | throw Exception( 47 | 'Failed to run flutter_scene_importer:import command in build hook (exit code ${importerResult.exitCode}):\nSTDERR: ${importerResult.stderr}\nSTDOUT: ${importerResult.stdout}'); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /importer/lib/constants.dart: -------------------------------------------------------------------------------- 1 | // position: 3, normal: 3, textureCoords: 2, color: 4 :: 12 floats :: 48 bytes 2 | const int kUnskinnedPerVertexSize = 48; 3 | 4 | // vertex: 12, joints: 4, weights: 4 :: 20 floats :: 80 bytes 5 | const int kSkinnedPerVertexSize = 80; 6 | -------------------------------------------------------------------------------- /importer/lib/flatbuffer.dart: -------------------------------------------------------------------------------- 1 | // ignore: depend_on_referenced_packages 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter_gpu/gpu.dart' as gpu; 5 | import 'package:vector_math/vector_math.dart'; 6 | 7 | import 'package:flutter_scene_importer/generated/scene_impeller.fb_flatbuffers.dart' 8 | as fb; 9 | export 'package:flutter_scene_importer/generated/scene_impeller.fb_flatbuffers.dart'; 10 | 11 | extension MatrixHelpers on fb.Matrix { 12 | Matrix4 toMatrix4() { 13 | return Matrix4.fromList([ 14 | m0, m1, m2, m3, // 15 | m4, m5, m6, m7, // 16 | m8, m9, m10, m11, // 17 | m12, m13, m14, m15 // 18 | ]); 19 | } 20 | } 21 | 22 | extension Vector3Helpers on fb.Vec3 { 23 | Vector3 toVector3() { 24 | return Vector3(x, y, z); 25 | } 26 | } 27 | 28 | extension QuaternionHelpers on fb.Vec4 { 29 | Quaternion toQuaternion() { 30 | return Quaternion(x, y, z, w); 31 | } 32 | } 33 | 34 | extension IndexTypeHelpers on fb.IndexType { 35 | gpu.IndexType toIndexType() { 36 | switch (this) { 37 | case fb.IndexType.k16Bit: 38 | return gpu.IndexType.int16; 39 | case fb.IndexType.k32Bit: 40 | return gpu.IndexType.int32; 41 | } 42 | throw Exception('Unknown index type'); 43 | } 44 | } 45 | 46 | extension SceneHelpers on fb.Scene { 47 | Matrix4 transformAsMatrix4() { 48 | return transform?.toMatrix4() ?? Matrix4.identity(); 49 | } 50 | 51 | /// Find a root child node in the scene. 52 | fb.Node? getChild(int index) { 53 | int? childIndex = children?[index]; 54 | if (childIndex == null) { 55 | return null; 56 | } 57 | return nodes?[childIndex]; 58 | } 59 | } 60 | 61 | extension NodeHelpers on fb.Node { 62 | fb.Node? getChild(fb.Scene scene, int index) { 63 | int? childIndex = children?[index]; 64 | if (childIndex == null) { 65 | return null; 66 | } 67 | return scene.nodes?[childIndex]; 68 | } 69 | } 70 | 71 | extension TextureHelpers on fb.Texture { 72 | gpu.Texture toTexture() { 73 | if (embeddedImage == null || embeddedImage!.bytes == null) { 74 | throw Exception('Texture has no embedded image'); 75 | } 76 | gpu.Texture texture = gpu.gpuContext.createTexture( 77 | gpu.StorageMode.hostVisible, 78 | embeddedImage!.width, 79 | embeddedImage!.height); 80 | Uint8List textureData = embeddedImage!.bytes! as Uint8List; 81 | texture.overwrite(ByteData.sublistView(textureData)); 82 | 83 | return texture; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /importer/lib/importer.dart: -------------------------------------------------------------------------------- 1 | // ignore: depend_on_referenced_packages 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/services.dart' show rootBundle; 5 | 6 | import 'package:flutter_scene_importer/flatbuffer.dart' as fb; 7 | 8 | class ImportedScene { 9 | static Future fromAsset(String asset) { 10 | return rootBundle.loadStructuredBinaryData(asset, (data) { 11 | return fromFlatbuffer(data); 12 | }); 13 | } 14 | 15 | static ImportedScene fromFlatbuffer(ByteData data) { 16 | final fb.Scene scene = fb.Scene( 17 | data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes)); 18 | 19 | return ImportedScene._(scene); 20 | } 21 | 22 | ImportedScene._(this._scene); 23 | 24 | final fb.Scene _scene; 25 | 26 | get flatbuffer => _scene; 27 | } 28 | -------------------------------------------------------------------------------- /importer/lib/offline_import.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:isolate'; 3 | 4 | Uri findBuiltExecutable(String executableName, Uri packageRoot, 5 | {String dir = 'build/'}) { 6 | List locations = [ 7 | 'Release/$executableName', 8 | 'Release/$executableName.exe', 9 | 'Debug/$executableName', 10 | 'Debug/$executableName.exe', 11 | executableName, 12 | '$executableName.exe', 13 | ]; 14 | 15 | final Uri buildDirectory = packageRoot.resolve(dir); 16 | Uri? found; 17 | List tried = []; 18 | for (final location in locations) { 19 | final uri = buildDirectory.resolve(location); 20 | tried.add(uri); 21 | if (File.fromUri(uri).existsSync()) { 22 | found = uri; 23 | break; 24 | } 25 | } 26 | if (found == null) { 27 | throw Exception( 28 | 'Unable to find build executable $executableName! Tried the following locations: $tried'); 29 | } 30 | return found; 31 | } 32 | 33 | Uri findImporterPackageRoot() { 34 | Uri importerPackageUri = Isolate.resolvePackageUriSync( 35 | Uri.parse('package:flutter_scene_importer/'))!; 36 | return importerPackageUri.resolve('../'); 37 | } 38 | 39 | void generateImporterFlatbufferDart( 40 | {String generatedOutputDirectory = "lib/generated/"}) { 41 | final packageRoot = findImporterPackageRoot(); 42 | final flatc = findBuiltExecutable('flatc', packageRoot, 43 | dir: 'build/_deps/flatbuffers-build/'); 44 | 45 | final flatcResult = Process.runSync( 46 | flatc.toFilePath(), 47 | [ 48 | '-o', 49 | generatedOutputDirectory, 50 | '--warnings-as-errors', 51 | '--gen-object-api', 52 | '--filename-suffix', 53 | '_flatbuffers', 54 | '--dart', 55 | 'scene.fbs', 56 | ], 57 | workingDirectory: packageRoot.toFilePath()); 58 | if (flatcResult.exitCode != 0) { 59 | throw Exception( 60 | 'Failed to generate importer flatbuffer: ${flatcResult.stderr}\n${flatcResult.stdout}'); 61 | } 62 | 63 | /// Update the generated file's flatbuffer include to use a patched version 64 | /// that allows for flatbuffer arrays to be accessed without copies. 65 | /// TODO(bdero): Remove after https://github.com/google/flatbuffers/pull/8289 66 | /// makes it into the Dart package. 67 | final generatedFile = File.fromUri(packageRoot 68 | .resolve(generatedOutputDirectory) 69 | .resolve('scene_impeller.fb_flatbuffers.dart')); 70 | final lines = generatedFile.readAsLinesSync(); 71 | final importLineIndex = lines.indexWhere((element) => element 72 | .contains("import 'package:flat_buffers/flat_buffers.dart' as fb;")); 73 | if (importLineIndex == -1) { 74 | throw Exception('Failed to find flat_buffer import line in generated file'); 75 | } 76 | lines[importLineIndex] = 77 | "import 'package:flutter_scene_importer/third_party/flat_buffers.dart' as fb;"; 78 | generatedFile.writeAsStringSync(lines.join('\n')); 79 | } 80 | 81 | /// Takes an input model (glTF file) and 82 | void importGltf(String inputGltfFilePath, String outputModelFilePath, 83 | {String? workingDirectory}) { 84 | final packageRoot = findImporterPackageRoot(); 85 | final importer = findBuiltExecutable('importer', packageRoot); 86 | 87 | // Parse the paths via Uri.file/Uri.directory and use resolveUri to resolve 88 | // the paths relative to the working directory. Using raw strings doesn't 89 | // bode well with Windows paths. 90 | final inputGltfFilePathUri = Uri.file(inputGltfFilePath); 91 | final outputModelFilePathUri = Uri.file(outputModelFilePath); 92 | final workingDirectoryUri = 93 | Uri.directory(workingDirectory ?? packageRoot.toFilePath()); 94 | inputGltfFilePath = 95 | workingDirectoryUri.resolveUri(inputGltfFilePathUri).toFilePath(); 96 | outputModelFilePath = 97 | workingDirectoryUri.resolveUri(outputModelFilePathUri).toFilePath(); 98 | //throw Exception('root $packageRoot input $inputGltfFilePath output $outputModelFilePath'); 99 | 100 | final importerResult = Process.runSync( 101 | importer.toFilePath(), 102 | [ 103 | inputGltfFilePath, 104 | outputModelFilePath, 105 | ], 106 | workingDirectory: workingDirectory); 107 | if (importerResult.exitCode != 0) { 108 | throw Exception( 109 | 'Failed to run importer: ${importerResult.stderr}\n${importerResult.stdout}'); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /importer/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | args: 5 | dependency: "direct main" 6 | description: 7 | name: args 8 | sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.6.0" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.13.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.2" 28 | characters: 29 | dependency: transitive 30 | description: 31 | name: characters 32 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.4.0" 36 | clock: 37 | dependency: transitive 38 | description: 39 | name: clock 40 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.1.2" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.19.1" 52 | crypto: 53 | dependency: transitive 54 | description: 55 | name: crypto 56 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "3.0.3" 60 | fake_async: 61 | dependency: transitive 62 | description: 63 | name: fake_async 64 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.3.3" 68 | flutter: 69 | dependency: "direct main" 70 | description: flutter 71 | source: sdk 72 | version: "0.0.0" 73 | flutter_gpu: 74 | dependency: "direct main" 75 | description: flutter 76 | source: sdk 77 | version: "0.0.0" 78 | flutter_lints: 79 | dependency: "direct dev" 80 | description: 81 | name: flutter_lints 82 | sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "5.0.0" 86 | flutter_test: 87 | dependency: "direct dev" 88 | description: flutter 89 | source: sdk 90 | version: "0.0.0" 91 | leak_tracker: 92 | dependency: transitive 93 | description: 94 | name: leak_tracker 95 | sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" 96 | url: "https://pub.dev" 97 | source: hosted 98 | version: "10.0.9" 99 | leak_tracker_flutter_testing: 100 | dependency: transitive 101 | description: 102 | name: leak_tracker_flutter_testing 103 | sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 104 | url: "https://pub.dev" 105 | source: hosted 106 | version: "3.0.9" 107 | leak_tracker_testing: 108 | dependency: transitive 109 | description: 110 | name: leak_tracker_testing 111 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 112 | url: "https://pub.dev" 113 | source: hosted 114 | version: "3.0.1" 115 | lints: 116 | dependency: transitive 117 | description: 118 | name: lints 119 | sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" 120 | url: "https://pub.dev" 121 | source: hosted 122 | version: "5.1.0" 123 | logging: 124 | dependency: "direct main" 125 | description: 126 | name: logging 127 | sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 128 | url: "https://pub.dev" 129 | source: hosted 130 | version: "1.3.0" 131 | matcher: 132 | dependency: transitive 133 | description: 134 | name: matcher 135 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "0.12.17" 139 | material_color_utilities: 140 | dependency: transitive 141 | description: 142 | name: material_color_utilities 143 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "0.11.1" 147 | meta: 148 | dependency: transitive 149 | description: 150 | name: meta 151 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "1.16.0" 155 | native_assets_cli: 156 | dependency: "direct main" 157 | description: 158 | name: native_assets_cli 159 | sha256: "0907c5b85a21ae08dcdd0d2b75061cc614939911c2cab5ac903f35a9fbb2a50b" 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "0.13.0" 163 | path: 164 | dependency: transitive 165 | description: 166 | name: path 167 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "1.9.1" 171 | pub_semver: 172 | dependency: transitive 173 | description: 174 | name: pub_semver 175 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "2.1.4" 179 | sky_engine: 180 | dependency: transitive 181 | description: flutter 182 | source: sdk 183 | version: "0.0.0" 184 | source_span: 185 | dependency: transitive 186 | description: 187 | name: source_span 188 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 189 | url: "https://pub.dev" 190 | source: hosted 191 | version: "1.10.1" 192 | stack_trace: 193 | dependency: transitive 194 | description: 195 | name: stack_trace 196 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 197 | url: "https://pub.dev" 198 | source: hosted 199 | version: "1.12.1" 200 | stream_channel: 201 | dependency: transitive 202 | description: 203 | name: stream_channel 204 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 205 | url: "https://pub.dev" 206 | source: hosted 207 | version: "2.1.4" 208 | string_scanner: 209 | dependency: transitive 210 | description: 211 | name: string_scanner 212 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 213 | url: "https://pub.dev" 214 | source: hosted 215 | version: "1.4.1" 216 | term_glyph: 217 | dependency: transitive 218 | description: 219 | name: term_glyph 220 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 221 | url: "https://pub.dev" 222 | source: hosted 223 | version: "1.2.2" 224 | test_api: 225 | dependency: transitive 226 | description: 227 | name: test_api 228 | sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd 229 | url: "https://pub.dev" 230 | source: hosted 231 | version: "0.7.4" 232 | typed_data: 233 | dependency: transitive 234 | description: 235 | name: typed_data 236 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 237 | url: "https://pub.dev" 238 | source: hosted 239 | version: "1.3.2" 240 | vector_math: 241 | dependency: "direct main" 242 | description: 243 | name: vector_math 244 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 245 | url: "https://pub.dev" 246 | source: hosted 247 | version: "2.1.4" 248 | vm_service: 249 | dependency: transitive 250 | description: 251 | name: vm_service 252 | sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 253 | url: "https://pub.dev" 254 | source: hosted 255 | version: "15.0.0" 256 | yaml: 257 | dependency: transitive 258 | description: 259 | name: yaml 260 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 261 | url: "https://pub.dev" 262 | source: hosted 263 | version: "3.1.2" 264 | sdks: 265 | dart: ">=3.7.0 <4.0.0" 266 | flutter: ">=3.29.0-1.0.pre.242" 267 | -------------------------------------------------------------------------------- /importer/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_scene_importer 2 | description: "An offline 3D model importer for Flutter. Converts glTF files into the Flutter Scene model format." 3 | version: 0.9.0-0 4 | homepage: https://github.com/bdero/flutter_scene 5 | platforms: 6 | linux: 7 | macos: 8 | windows: 9 | ios: 10 | android: 11 | 12 | environment: 13 | sdk: '>=3.6.0-0 <4.0.0' 14 | flutter: ">=3.29.0-1.0.pre.242" 15 | 16 | dependencies: 17 | args: ^2.5.0 18 | flutter: 19 | sdk: flutter 20 | flutter_gpu: 21 | sdk: flutter 22 | logging: ^1.2.0 23 | native_assets_cli: '>=0.13.0 <0.14.0' 24 | vector_math: ^2.1.4 25 | 26 | dev_dependencies: 27 | flutter_test: 28 | sdk: flutter 29 | flutter_lints: ^5.0.0 30 | 31 | flutter: 32 | -------------------------------------------------------------------------------- /importer/scene.fbs: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | namespace impeller.fb; 6 | 7 | //----------------------------------------------------------------------------- 8 | /// Materials. 9 | /// 10 | 11 | struct Color { 12 | r: float; 13 | g: float; 14 | b: float; 15 | a: float; 16 | } 17 | 18 | enum ComponentType:byte { 19 | k8Bit, 20 | k16Bit, 21 | } 22 | 23 | table EmbeddedImage { 24 | bytes: [ubyte]; 25 | component_count: ubyte = 0; 26 | component_type: ComponentType; 27 | width: uint; 28 | height: uint; 29 | } 30 | 31 | /// The `bytes` field takes precedent over the `uri` field. 32 | /// If both the `uri` and `bytes` fields are empty, a fully opaque white 33 | /// placeholder will be used. 34 | table Texture { 35 | /// A Flutter asset URI for a compressed image file to import and decode. 36 | uri: string; 37 | /// Decompressed image bytes for uploading to the GPU. If this field is not 38 | /// empty, it takes precedent over the `uri` field for sourcing the texture. 39 | embedded_image: EmbeddedImage; 40 | } 41 | 42 | enum MaterialType:byte { 43 | kUnlit, 44 | kPhysicallyBased, 45 | } 46 | 47 | /// The final color of each material component is the texture color multiplied 48 | /// by the factor of the component. 49 | /// Texture fields are indices into the `Scene`->`textures` array. All textures 50 | /// are optional -- a texture index value of -1 indicates no texture. 51 | table Material { 52 | // When the `MaterialType` is `kUnlit`, only the `base_color` fields are used. 53 | type: MaterialType; 54 | 55 | base_color_factor: Color; 56 | base_color_texture: int = -1; 57 | 58 | metallic_factor: float = 0; 59 | roughness_factor: float = 0.5; 60 | metallic_roughness_texture: int = -1; // Red=Metallic, Green=Roughness. 61 | 62 | normal_scale: float = 1.0; 63 | normal_texture: int = -1; // Tangent space normal map. 64 | 65 | emissive_factor: Vec3; 66 | emissive_texture: int = -1; 67 | 68 | occlusion_strength: float = 1.0; 69 | occlusion_texture: int = -1; 70 | } 71 | 72 | //----------------------------------------------------------------------------- 73 | /// Geometry. 74 | /// 75 | 76 | struct Vec2 { 77 | x: float; 78 | y: float; 79 | } 80 | 81 | struct Vec3 { 82 | x: float; 83 | y: float; 84 | z: float; 85 | } 86 | 87 | struct Vec4 { 88 | x: float; 89 | y: float; 90 | z: float; 91 | w: float; 92 | } 93 | 94 | // This attribute layout is expected to be identical to that within 95 | // `shaders/flutter_scene_unskinned.vert`. 96 | // 97 | // Note: This struct is currently only used for conveniently packing buffers in the importer. 98 | struct Vertex { 99 | position: Vec3; 100 | normal: Vec3; 101 | texture_coords: Vec2; 102 | color: Color; 103 | } 104 | 105 | table UnskinnedVertexBuffer { 106 | //vertices: [Vertex]; 107 | // Hack to make Dart flatbuffers easier to work with. 108 | vertices: [ubyte]; 109 | vertex_count: uint32; 110 | } 111 | 112 | // This attribute layout is expected to be identical to that within 113 | // `shaders/flutter_scene_skinned.vert`. 114 | // 115 | // Note: This struct is currently only used for conveniently packing buffers in the importer. 116 | struct SkinnedVertex { 117 | vertex: Vertex; 118 | /// Four joint indices corresponding to this mesh's skin transforms. These 119 | /// are floats instead of ints because this vertex data is uploaded directly 120 | /// to the GPU, and float attributes work for all Impeller backends. 121 | joints: Vec4; 122 | /// Four weight values that specify the influence of the corresponding 123 | /// joints. 124 | weights: Vec4; 125 | } 126 | 127 | table SkinnedVertexBuffer { 128 | //vertices: [SkinnedVertex]; 129 | // Hack to make Dart flatbuffers easier to work with. 130 | vertices: [ubyte]; 131 | vertex_count: uint32; 132 | } 133 | 134 | union VertexBuffer { UnskinnedVertexBuffer, SkinnedVertexBuffer } 135 | 136 | enum IndexType:byte { 137 | k16Bit, 138 | k32Bit, 139 | } 140 | 141 | table Indices { 142 | data: [ubyte]; 143 | count: uint32; 144 | type: IndexType; 145 | } 146 | 147 | table MeshPrimitive { 148 | vertices: VertexBuffer; 149 | indices: Indices; 150 | material: Material; 151 | } 152 | 153 | //----------------------------------------------------------------------------- 154 | /// Animations. 155 | /// 156 | 157 | table TranslationKeyframes { 158 | values: [Vec3]; 159 | } 160 | 161 | table RotationKeyframes { 162 | values: [Vec4]; 163 | } 164 | 165 | table ScaleKeyframes { 166 | values: [Vec3]; 167 | } 168 | 169 | union Keyframes { TranslationKeyframes, RotationKeyframes, ScaleKeyframes } 170 | 171 | table Channel { 172 | node: int; // Index into `Scene`->`nodes`. 173 | timeline: [float]; 174 | keyframes: Keyframes; 175 | } 176 | 177 | table Animation { 178 | name: string; 179 | channels: [Channel]; 180 | } 181 | 182 | table Skin { 183 | joints: [int]; // Indices into `Scene`->`nodes`. 184 | inverse_bind_matrices: [Matrix]; 185 | /// The root joint of the skeleton. 186 | skeleton: int; // Index into `Scene`->`nodes`. 187 | } 188 | 189 | //----------------------------------------------------------------------------- 190 | /// Scene graph. 191 | /// 192 | 193 | struct Matrix { 194 | // Unfortunately, arrays aren't supported when targetting Dart, so [float:16] can't be used here. 195 | m0: float; 196 | m1: float; 197 | m2: float; 198 | m3: float; 199 | m4: float; 200 | m5: float; 201 | m6: float; 202 | m7: float; 203 | m8: float; 204 | m9: float; 205 | m10: float; 206 | m11: float; 207 | m12: float; 208 | m13: float; 209 | m14: float; 210 | m15: float; 211 | } 212 | 213 | table Node { 214 | name: string; 215 | children: [int]; // Indices into `Scene`->`nodes`. 216 | transform: Matrix; 217 | mesh_primitives: [MeshPrimitive]; 218 | skin: Skin; 219 | } 220 | 221 | table Scene { 222 | children: [int]; // Indices into `Scene`->`nodes`. 223 | transform: Matrix; 224 | nodes: [Node]; 225 | textures: [Texture]; // Textures may be reused across different materials. 226 | animations: [Animation]; 227 | } 228 | 229 | root_type Scene; 230 | file_identifier "IPSC"; 231 | -------------------------------------------------------------------------------- /importer/scenec_main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "generated/scene_flatbuffers.h" 8 | #include "importer.h" 9 | #include "types.h" 10 | 11 | #include "flatbuffers/flatbuffer_builder.h" 12 | 13 | namespace impeller { 14 | namespace scene { 15 | namespace importer { 16 | 17 | [[nodiscard]] std::optional> ReadFileToBuffer( 18 | const std::string file_path) { 19 | std::ifstream in(file_path, std::ios_base::binary | std::ios::ate); 20 | if (!in.is_open()) { 21 | std::cerr << "Failed to open input file: " << file_path << std::endl; 22 | return std::nullopt; 23 | } 24 | size_t length = in.tellg(); 25 | in.seekg(0, std::ios::beg); 26 | 27 | std::vector bytes(length); 28 | in.read(bytes.data(), bytes.size()); 29 | return bytes; 30 | } 31 | 32 | [[nodiscard]] bool WriteBufferToFile(const std::string file_path, 33 | const char* buffer, 34 | size_t size) { 35 | std::ofstream out(file_path, std::ios_base::binary); 36 | if (!out.is_open()) { 37 | std::cerr << "Failed to open output file: " << file_path << std::endl; 38 | return false; 39 | } 40 | out.write(buffer, size); 41 | return true; 42 | } 43 | 44 | bool Main(const std::string& input_file, const std::string& output_file) { 45 | auto input_buffer = impeller::scene::importer::ReadFileToBuffer(input_file); 46 | if (!input_buffer.has_value()) { 47 | return false; 48 | } 49 | 50 | fb::SceneT scene; 51 | if (!ParseGLTF(input_buffer.value(), scene)) { 52 | std::cerr << "Failed to parse input GLB file." << std::endl; 53 | return false; 54 | } 55 | 56 | flatbuffers::FlatBufferBuilder builder; 57 | builder.Finish(fb::Scene::Pack(builder, &scene), fb::SceneIdentifier()); 58 | 59 | if (!WriteBufferToFile( 60 | output_file, 61 | reinterpret_cast(builder.GetBufferPointer()), 62 | builder.GetSize())) { 63 | return false; 64 | } 65 | 66 | return true; 67 | } 68 | 69 | } // namespace importer 70 | } // namespace scene 71 | } // namespace impeller 72 | 73 | void PrintHelp(std::ostream& stream) { 74 | stream << std::endl; 75 | stream << "SceneC is an offline 3D geometry importer." << std::endl; 76 | stream << "---------------------------------------------------------------" 77 | << std::endl; 78 | stream << "Valid usage: importer [input_file] [output_file]" << std::endl; 79 | stream << "Note: Only GLB (glTF binary) input files are currently supported." 80 | << std::endl; 81 | } 82 | 83 | int main(int argc, char const* argv[]) { 84 | if (argc != 3) { 85 | PrintHelp(std::cerr); 86 | return EXIT_FAILURE; 87 | } 88 | 89 | std::string input_file = argv[1]; 90 | std::string output_file = argv[2]; 91 | 92 | return impeller::scene::importer::Main(input_file, output_file) 93 | ? EXIT_SUCCESS 94 | : EXIT_FAILURE; 95 | } 96 | -------------------------------------------------------------------------------- /importer/test/importer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | void main() { 4 | test('placeholder', () { 5 | expect(true, true); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /importer/types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace impeller { 6 | namespace scene { 7 | namespace importer { 8 | 9 | using Scalar = float; 10 | 11 | struct Vector4 { 12 | float x, y, z, w; 13 | 14 | Vector4 Normalize() const { 15 | const Scalar inverse = 1.0f / sqrt(x * x + y * y + z * z + w * w); 16 | return Vector4{x * inverse, y * inverse, z * inverse, w * inverse}; 17 | } 18 | }; 19 | 20 | struct Vector3 { 21 | float x = 0, y = 0, z = 0; 22 | }; 23 | 24 | struct Vector2 { 25 | float x = 0, y = 0; 26 | }; 27 | 28 | struct Quaternion { 29 | float x = 0, y = 0, z = 0, w = 1; 30 | }; 31 | 32 | struct Matrix { 33 | union { 34 | float m[16]; 35 | float e[4][4]; 36 | Vector4 vec[4]; 37 | }; 38 | 39 | Matrix() { 40 | vec[0] = {1.0, 0.0, 0.0, 0.0}; 41 | vec[1] = {0.0, 1.0, 0.0, 0.0}; 42 | vec[2] = {0.0, 0.0, 1.0, 0.0}; 43 | vec[3] = {0.0, 0.0, 0.0, 1.0}; 44 | }; 45 | 46 | Matrix(Scalar m0, 47 | Scalar m1, 48 | Scalar m2, 49 | Scalar m3, 50 | Scalar m4, 51 | Scalar m5, 52 | Scalar m6, 53 | Scalar m7, 54 | Scalar m8, 55 | Scalar m9, 56 | Scalar m10, 57 | Scalar m11, 58 | Scalar m12, 59 | Scalar m13, 60 | Scalar m14, 61 | Scalar m15) { 62 | vec[0] = {m0, m1, m2, m3}; 63 | vec[1] = {m4, m5, m6, m7}; 64 | vec[2] = {m8, m9, m10, m11}; 65 | vec[3] = {m12, m13, m14, m15}; 66 | } 67 | 68 | static Matrix MakeTranslation(const Vector3& t) { 69 | // clang-format off 70 | return Matrix{1.0f, 0.0f, 0.0f, 0.0f, 71 | 0.0f, 1.0f, 0.0f, 0.0f, 72 | 0.0f, 0.0f, 1.0f, 0.0f, 73 | t.x, t.y, t.z, 1.0f}; 74 | // clang-format on 75 | } 76 | 77 | static Matrix MakeScale(const Vector3& s) { 78 | // clang-format off 79 | return Matrix{s.x, 0.0f, 0.0f, 0.0f, 80 | 0.0f, s.y, 0.0f, 0.0f, 81 | 0.0f, 0.0f, s.z, 0.0f, 82 | 0.0f, 0.0f, 0.0f, 1.0f}; 83 | // clang-format on 84 | } 85 | 86 | static Matrix MakeRotation(Quaternion q) { 87 | // clang-format off 88 | return Matrix{ 89 | 1.0f - 2.0f * q.y * q.y - 2.0f * q.z * q.z, 90 | 2.0f * q.x * q.y + 2.0f * q.z * q.w, 91 | 2.0f * q.x * q.z - 2.0f * q.y * q.w, 92 | 0.0f, 93 | 94 | 2.0f * q.x * q.y - 2.0f * q.z * q.w, 95 | 1.0f - 2.0f * q.x * q.x - 2.0f * q.z * q.z, 96 | 2.0f * q.y * q.z + 2.0f * q.x * q.w, 97 | 0.0f, 98 | 99 | 2.0f * q.x * q.z + 2.0f * q.y * q.w, 100 | 2.0f * q.y * q.z - 2.0f * q.x * q.w, 101 | 1.0f - 2.0f * q.x * q.x - 2.0f * q.y * q.y, 102 | 0.0f, 103 | 104 | 0.0f, 105 | 0.0f, 106 | 0.0f, 107 | 1.0f}; 108 | // clang-format on 109 | } 110 | 111 | static Matrix MakeRotation(Scalar radians, const Vector4& r) { 112 | const Vector4 v = r.Normalize(); 113 | 114 | const Scalar cosine = cos(radians); 115 | const Scalar cosp = 1.0f - cosine; 116 | const Scalar sine = sin(radians); 117 | 118 | // clang-format off 119 | return Matrix{ 120 | cosine + cosp * v.x * v.x, 121 | cosp * v.x * v.y + v.z * sine, 122 | cosp * v.x * v.z - v.y * sine, 123 | 0.0f, 124 | 125 | cosp * v.x * v.y - v.z * sine, 126 | cosine + cosp * v.y * v.y, 127 | cosp * v.y * v.z + v.x * sine, 128 | 0.0f, 129 | 130 | cosp * v.x * v.z + v.y * sine, 131 | cosp * v.y * v.z - v.x * sine, 132 | cosine + cosp * v.z * v.z, 133 | 0.0f, 134 | 135 | 0.0f, 136 | 0.0f, 137 | 0.0f, 138 | 1.0f}; 139 | // clang-format on 140 | } 141 | 142 | bool IsIdentity() const { 143 | return ( 144 | // clang-format off 145 | m[0] == 1.0f && m[1] == 0.0f && m[2] == 0.0f && m[3] == 0.0f && 146 | m[4] == 0.0f && m[5] == 1.0f && m[6] == 0.0f && m[7] == 0.0f && 147 | m[8] == 0.0f && m[9] == 0.0f && m[10] == 1.0f && m[11] == 0.0f && 148 | m[12] == 0.0f && m[13] == 0.0f && m[14] == 0.0f && m[15] == 1.0f 149 | // clang-format on 150 | ); 151 | } 152 | 153 | Matrix Multiply(const Matrix& o) const { 154 | // clang-format off 155 | return Matrix{ 156 | m[0] * o.m[0] + m[4] * o.m[1] + m[8] * o.m[2] + m[12] * o.m[3], 157 | m[1] * o.m[0] + m[5] * o.m[1] + m[9] * o.m[2] + m[13] * o.m[3], 158 | m[2] * o.m[0] + m[6] * o.m[1] + m[10] * o.m[2] + m[14] * o.m[3], 159 | m[3] * o.m[0] + m[7] * o.m[1] + m[11] * o.m[2] + m[15] * o.m[3], 160 | m[0] * o.m[4] + m[4] * o.m[5] + m[8] * o.m[6] + m[12] * o.m[7], 161 | m[1] * o.m[4] + m[5] * o.m[5] + m[9] * o.m[6] + m[13] * o.m[7], 162 | m[2] * o.m[4] + m[6] * o.m[5] + m[10] * o.m[6] + m[14] * o.m[7], 163 | m[3] * o.m[4] + m[7] * o.m[5] + m[11] * o.m[6] + m[15] * o.m[7], 164 | m[0] * o.m[8] + m[4] * o.m[9] + m[8] * o.m[10] + m[12] * o.m[11], 165 | m[1] * o.m[8] + m[5] * o.m[9] + m[9] * o.m[10] + m[13] * o.m[11], 166 | m[2] * o.m[8] + m[6] * o.m[9] + m[10] * o.m[10] + m[14] * o.m[11], 167 | m[3] * o.m[8] + m[7] * o.m[9] + m[11] * o.m[10] + m[15] * o.m[11], 168 | m[0] * o.m[12] + m[4] * o.m[13] + m[8] * o.m[14] + m[12] * o.m[15], 169 | m[1] * o.m[12] + m[5] * o.m[13] + m[9] * o.m[14] + m[13] * o.m[15], 170 | m[2] * o.m[12] + m[6] * o.m[13] + m[10] * o.m[14] + m[14] * o.m[15], 171 | m[3] * o.m[12] + m[7] * o.m[13] + m[11] * o.m[14] + m[15] * o.m[15]}; 172 | // clang-format on 173 | } 174 | 175 | Matrix operator*(const Matrix& m) const { return Multiply(m); } 176 | }; 177 | 178 | struct Color { 179 | float red = 0, green = 0, blue = 0, alpha = 0; 180 | }; 181 | 182 | enum class SourceType { 183 | kUnknown, 184 | kGLTF, 185 | }; 186 | 187 | } // namespace importer 188 | } // namespace scene 189 | } // namespace impeller 190 | -------------------------------------------------------------------------------- /importer/vertices_builder.cc: -------------------------------------------------------------------------------- 1 | #include "vertices_builder.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "conversions.h" 11 | #include "generated/scene_flatbuffers.h" 12 | 13 | namespace impeller { 14 | namespace scene { 15 | namespace importer { 16 | 17 | //------------------------------------------------------------------------------ 18 | /// VerticesBuilder 19 | /// 20 | 21 | std::unique_ptr VerticesBuilder::MakeUnskinned() { 22 | return std::make_unique(); 23 | } 24 | 25 | std::unique_ptr VerticesBuilder::MakeSkinned() { 26 | return std::make_unique(); 27 | } 28 | 29 | VerticesBuilder::VerticesBuilder() = default; 30 | 31 | VerticesBuilder::~VerticesBuilder() = default; 32 | 33 | /// @brief Reads a numeric component from `source` and returns a 32bit float. 34 | /// If `normalized` is `true`, signed SourceTypes convert to a range of 35 | /// -1 to 1, and unsigned SourceTypes convert to a range of 0 to 1. 36 | template 37 | static Scalar ToScalar(const void* source, size_t index, bool normalized) { 38 | const SourceType* s = reinterpret_cast(source) + index; 39 | Scalar result = static_cast(*s); 40 | if (normalized) { 41 | SourceType divisor = std::is_integral_v 42 | ? std::numeric_limits::max() 43 | : 1; 44 | result = static_cast(*s) / static_cast(divisor); 45 | } 46 | return result; 47 | } 48 | 49 | /// @brief A ComponentWriter which simply converts all of an attribute's 50 | /// components to normalized scalar form. 51 | static void PassthroughAttributeWriter( 52 | Scalar* destination, 53 | const void* source, 54 | const VerticesBuilder::ComponentProperties& component, 55 | const VerticesBuilder::AttributeProperties& attribute) { 56 | assert(attribute.size_bytes == attribute.component_count * sizeof(Scalar)); 57 | for (size_t component_i = 0; component_i < attribute.component_count; 58 | component_i++) { 59 | *(destination + component_i) = 60 | component.convert_proc(source, component_i, true); 61 | } 62 | } 63 | 64 | /// @brief A ComponentWriter which converts four vertex indices to scalars. 65 | static void JointsAttributeWriter( 66 | Scalar* destination, 67 | const void* source, 68 | const VerticesBuilder::ComponentProperties& component, 69 | const VerticesBuilder::AttributeProperties& attribute) { 70 | assert(attribute.component_count == 4); 71 | for (int i = 0; i < 4; i++) { 72 | *(destination + i) = component.convert_proc(source, i, false); 73 | } 74 | } 75 | 76 | std::map 77 | VerticesBuilder::kAttributeTypes = { 78 | {VerticesBuilder::AttributeType::kPosition, 79 | {.offset_bytes = offsetof(UnskinnedVerticesBuilder::Vertex, position), 80 | .size_bytes = sizeof(UnskinnedVerticesBuilder::Vertex::position), 81 | .component_count = 3, 82 | .write_proc = PassthroughAttributeWriter}}, 83 | {VerticesBuilder::AttributeType::kNormal, 84 | {.offset_bytes = offsetof(UnskinnedVerticesBuilder::Vertex, normal), 85 | .size_bytes = sizeof(UnskinnedVerticesBuilder::Vertex::normal), 86 | .component_count = 3, 87 | .write_proc = PassthroughAttributeWriter}}, 88 | {VerticesBuilder::AttributeType::kTextureCoords, 89 | {.offset_bytes = 90 | offsetof(UnskinnedVerticesBuilder::Vertex, texture_coords), 91 | .size_bytes = 92 | sizeof(UnskinnedVerticesBuilder::Vertex::texture_coords), 93 | .component_count = 2, 94 | .write_proc = PassthroughAttributeWriter}}, 95 | {VerticesBuilder::AttributeType::kColor, 96 | {.offset_bytes = offsetof(UnskinnedVerticesBuilder::Vertex, color), 97 | .size_bytes = sizeof(UnskinnedVerticesBuilder::Vertex::color), 98 | .component_count = 4, 99 | .write_proc = PassthroughAttributeWriter}}, 100 | {VerticesBuilder::AttributeType::kJoints, 101 | {.offset_bytes = offsetof(SkinnedVerticesBuilder::Vertex, joints), 102 | .size_bytes = sizeof(SkinnedVerticesBuilder::Vertex::joints), 103 | .component_count = 4, 104 | .write_proc = JointsAttributeWriter}}, 105 | {VerticesBuilder::AttributeType::kWeights, 106 | {.offset_bytes = offsetof(SkinnedVerticesBuilder::Vertex, weights), 107 | .size_bytes = sizeof(SkinnedVerticesBuilder::Vertex::weights), 108 | .component_count = 4, 109 | .write_proc = JointsAttributeWriter}}}; 110 | 111 | static std::map 113 | kComponentTypes = { 114 | {VerticesBuilder::ComponentType::kSignedByte, 115 | {.size_bytes = sizeof(int8_t), .convert_proc = ToScalar}}, 116 | {VerticesBuilder::ComponentType::kUnsignedByte, 117 | {.size_bytes = sizeof(int8_t), .convert_proc = ToScalar}}, 118 | {VerticesBuilder::ComponentType::kSignedShort, 119 | {.size_bytes = sizeof(int16_t), .convert_proc = ToScalar}}, 120 | {VerticesBuilder::ComponentType::kUnsignedShort, 121 | {.size_bytes = sizeof(int16_t), .convert_proc = ToScalar}}, 122 | {VerticesBuilder::ComponentType::kSignedInt, 123 | {.size_bytes = sizeof(int32_t), .convert_proc = ToScalar}}, 124 | {VerticesBuilder::ComponentType::kUnsignedInt, 125 | {.size_bytes = sizeof(int32_t), .convert_proc = ToScalar}}, 126 | {VerticesBuilder::ComponentType::kFloat, 127 | {.size_bytes = sizeof(float), .convert_proc = ToScalar}}, 128 | }; 129 | 130 | void VerticesBuilder::WriteAttribute(void* destination, 131 | size_t destination_stride_bytes, 132 | AttributeType attribute, 133 | ComponentType component_type, 134 | const void* source, 135 | size_t attribute_stride_bytes, 136 | size_t attribute_count) { 137 | const ComponentProperties& component_props = kComponentTypes[component_type]; 138 | const AttributeProperties& attribute_props = kAttributeTypes[attribute]; 139 | for (size_t i = 0; i < attribute_count; i++) { 140 | const uint8_t* src = 141 | reinterpret_cast(source) + attribute_stride_bytes * i; 142 | uint8_t* dst = reinterpret_cast(destination) + 143 | i * destination_stride_bytes + attribute_props.offset_bytes; 144 | 145 | attribute_props.write_proc(reinterpret_cast(dst), src, 146 | component_props, attribute_props); 147 | } 148 | } 149 | 150 | //------------------------------------------------------------------------------ 151 | /// UnskinnedVerticesBuilder 152 | /// 153 | 154 | UnskinnedVerticesBuilder::UnskinnedVerticesBuilder() = default; 155 | 156 | UnskinnedVerticesBuilder::~UnskinnedVerticesBuilder() = default; 157 | 158 | void UnskinnedVerticesBuilder::WriteFBVertices( 159 | fb::MeshPrimitiveT& primitive) const { 160 | constexpr size_t kPerVertexBytes = 48; 161 | static_assert(sizeof(fb::Vertex) == kPerVertexBytes, 162 | "Unexpected Vertex size! If the flatbuffer schama was " 163 | "intentionally updated, be sure to also update the size " 164 | "constants in `constants.dart`."); 165 | const size_t expected_bytes = vertices_.size() * kPerVertexBytes; 166 | 167 | auto vertex_buffer = fb::UnskinnedVertexBufferT(); 168 | vertex_buffer.vertex_count = vertices_.size(); 169 | vertex_buffer.vertices.resize(expected_bytes); 170 | for (size_t i = 0; i < vertices_.size(); i++) { 171 | const auto& v = vertices_[i]; 172 | auto vertex = fb::Vertex(ToFBVec3(v.position), ToFBVec3(v.normal), 173 | ToFBVec2(v.texture_coords), ToFBColor(v.color)); 174 | std::memcpy(vertex_buffer.vertices.data() + (i * kPerVertexBytes), &vertex, 175 | kPerVertexBytes); 176 | } 177 | primitive.vertices.Set(std::move(vertex_buffer)); 178 | } 179 | 180 | void UnskinnedVerticesBuilder::SetAttributeFromBuffer( 181 | AttributeType attribute, 182 | ComponentType component_type, 183 | const void* buffer_start, 184 | size_t attribute_stride_bytes, 185 | size_t attribute_count) { 186 | if (attribute_count > vertices_.size()) { 187 | vertices_.resize(attribute_count, Vertex()); 188 | } 189 | WriteAttribute(vertices_.data(), // destination 190 | sizeof(Vertex), // destination_stride_bytes 191 | attribute, // attribute 192 | component_type, // component_type 193 | buffer_start, // source 194 | attribute_stride_bytes, // attribute_stride_bytes 195 | attribute_count); // attribute_count 196 | } 197 | 198 | //------------------------------------------------------------------------------ 199 | /// SkinnedVerticesBuilder 200 | /// 201 | 202 | SkinnedVerticesBuilder::SkinnedVerticesBuilder() = default; 203 | 204 | SkinnedVerticesBuilder::~SkinnedVerticesBuilder() = default; 205 | 206 | void SkinnedVerticesBuilder::WriteFBVertices( 207 | fb::MeshPrimitiveT& primitive) const { 208 | constexpr size_t kPerVertexBytes = 80; 209 | static_assert(sizeof(fb::SkinnedVertex) == kPerVertexBytes, 210 | "Unexpected SkinnedVertex size! If the flatbuffer schama was " 211 | "intentionally updated, be sure to also update the size " 212 | "constants in `constants.dart`."); 213 | const size_t expected_bytes = vertices_.size() * kPerVertexBytes; 214 | 215 | auto vertex_buffer = fb::SkinnedVertexBufferT(); 216 | vertex_buffer.vertex_count = vertices_.size(); 217 | vertex_buffer.vertices.resize(expected_bytes); 218 | for (size_t i = 0; i < vertices_.size(); i++) { 219 | const auto& v = vertices_[i]; 220 | auto unskinned_attributes = fb::Vertex( 221 | ToFBVec3(v.vertex.position), ToFBVec3(v.vertex.normal), 222 | ToFBVec2(v.vertex.texture_coords), ToFBColor(v.vertex.color)); 223 | auto vertex = fb::SkinnedVertex(unskinned_attributes, ToFBVec4(v.joints), 224 | ToFBVec4(v.weights)); 225 | std::memcpy(vertex_buffer.vertices.data() + (i * kPerVertexBytes), &vertex, 226 | kPerVertexBytes); 227 | } 228 | 229 | primitive.vertices.Set(std::move(vertex_buffer)); 230 | } 231 | 232 | void SkinnedVerticesBuilder::SetAttributeFromBuffer( 233 | AttributeType attribute, 234 | ComponentType component_type, 235 | const void* buffer_start, 236 | size_t attribute_stride_bytes, 237 | size_t attribute_count) { 238 | if (attribute_count > vertices_.size()) { 239 | vertices_.resize(attribute_count, Vertex()); 240 | } 241 | WriteAttribute(vertices_.data(), // destination 242 | sizeof(Vertex), // destination_stride_bytes 243 | attribute, // attribute 244 | component_type, // component_type 245 | buffer_start, // source 246 | attribute_stride_bytes, // attribute_stride_bytes 247 | attribute_count); // attribute_count 248 | } 249 | 250 | } // namespace importer 251 | } // namespace scene 252 | } // namespace impeller 253 | -------------------------------------------------------------------------------- /importer/vertices_builder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "generated/scene_flatbuffers.h" 7 | #include "types.h" 8 | 9 | namespace impeller { 10 | namespace scene { 11 | namespace importer { 12 | 13 | //------------------------------------------------------------------------------ 14 | /// VerticesBuilder 15 | /// 16 | 17 | class VerticesBuilder { 18 | public: 19 | static std::unique_ptr MakeUnskinned(); 20 | 21 | static std::unique_ptr MakeSkinned(); 22 | 23 | enum class ComponentType { 24 | kSignedByte = 5120, 25 | kUnsignedByte, 26 | kSignedShort, 27 | kUnsignedShort, 28 | kSignedInt, 29 | kUnsignedInt, 30 | kFloat, 31 | }; 32 | 33 | enum class AttributeType { 34 | kPosition, 35 | kNormal, 36 | kTextureCoords, 37 | kColor, 38 | kJoints, 39 | kWeights, 40 | }; 41 | 42 | using ComponentConverter = std::function< 43 | Scalar(const void* source, size_t byte_offset, bool normalized)>; 44 | struct ComponentProperties { 45 | size_t size_bytes = 0; 46 | ComponentConverter convert_proc; 47 | }; 48 | 49 | struct AttributeProperties; 50 | using AttributeWriter = 51 | std::function; 55 | struct AttributeProperties { 56 | size_t offset_bytes = 0; 57 | size_t size_bytes = 0; 58 | size_t component_count = 0; 59 | AttributeWriter write_proc; 60 | }; 61 | 62 | VerticesBuilder(); 63 | 64 | virtual ~VerticesBuilder(); 65 | 66 | virtual void WriteFBVertices(fb::MeshPrimitiveT& primitive) const = 0; 67 | 68 | virtual void SetAttributeFromBuffer(AttributeType attribute, 69 | ComponentType component_type, 70 | const void* buffer_start, 71 | size_t attribute_stride_bytes, 72 | size_t attribute_count) = 0; 73 | 74 | protected: 75 | static void WriteAttribute(void* destination, 76 | size_t destination_stride_bytes, 77 | AttributeType attribute, 78 | ComponentType component_type, 79 | const void* source, 80 | size_t attribute_stride_bytes, 81 | size_t attribute_count); 82 | 83 | private: 84 | static std::map 86 | kAttributeTypes; 87 | 88 | VerticesBuilder(const VerticesBuilder&) = delete; 89 | 90 | VerticesBuilder& operator=(const VerticesBuilder&) = delete; 91 | }; 92 | 93 | //------------------------------------------------------------------------------ 94 | /// UnskinnedVerticesBuilder 95 | /// 96 | 97 | class UnskinnedVerticesBuilder final : public VerticesBuilder { 98 | public: 99 | struct Vertex { 100 | Vector3 position; 101 | Vector3 normal; 102 | Vector2 texture_coords; 103 | Color color = Color{1, 1, 1, 1}; 104 | }; 105 | 106 | UnskinnedVerticesBuilder(); 107 | 108 | virtual ~UnskinnedVerticesBuilder() override; 109 | 110 | // |VerticesBuilder| 111 | void WriteFBVertices(fb::MeshPrimitiveT& primitive) const override; 112 | 113 | // |VerticesBuilder| 114 | void SetAttributeFromBuffer(AttributeType attribute, 115 | ComponentType component_type, 116 | const void* buffer_start, 117 | size_t attribute_stride_bytes, 118 | size_t attribute_count) override; 119 | 120 | private: 121 | std::vector vertices_; 122 | 123 | UnskinnedVerticesBuilder(const UnskinnedVerticesBuilder&) = delete; 124 | 125 | UnskinnedVerticesBuilder& operator=(const UnskinnedVerticesBuilder&) = delete; 126 | }; 127 | 128 | //------------------------------------------------------------------------------ 129 | /// SkinnedVerticesBuilder 130 | /// 131 | 132 | class SkinnedVerticesBuilder final : public VerticesBuilder { 133 | public: 134 | struct Vertex { 135 | UnskinnedVerticesBuilder::Vertex vertex; 136 | Vector4 joints; 137 | Vector4 weights; 138 | }; 139 | 140 | SkinnedVerticesBuilder(); 141 | 142 | virtual ~SkinnedVerticesBuilder() override; 143 | 144 | // |VerticesBuilder| 145 | void WriteFBVertices(fb::MeshPrimitiveT& primitive) const override; 146 | 147 | // |VerticesBuilder| 148 | void SetAttributeFromBuffer(AttributeType attribute, 149 | ComponentType component_type, 150 | const void* buffer_start, 151 | size_t attribute_stride_bytes, 152 | size_t attribute_count) override; 153 | 154 | private: 155 | std::vector vertices_; 156 | 157 | SkinnedVerticesBuilder(const SkinnedVerticesBuilder&) = delete; 158 | 159 | SkinnedVerticesBuilder& operator=(const SkinnedVerticesBuilder&) = delete; 160 | }; 161 | 162 | } // namespace importer 163 | } // namespace scene 164 | } // namespace impeller 165 | -------------------------------------------------------------------------------- /lib/scene.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'src/animation.dart'; 4 | 5 | export 'src/geometry/geometry.dart'; 6 | 7 | export 'src/material/environment.dart'; 8 | export 'src/material/material.dart'; 9 | export 'src/material/physically_based_material.dart'; 10 | export 'src/material/unlit_material.dart'; 11 | 12 | export 'src/asset_helpers.dart'; 13 | export 'src/camera.dart'; 14 | export 'src/math_extensions.dart'; 15 | export 'src/mesh.dart'; 16 | export 'src/node.dart'; 17 | export 'src/scene_encoder.dart'; 18 | export 'src/scene.dart'; 19 | export 'src/shaders.dart'; 20 | export 'src/skin.dart'; 21 | export 'src/surface.dart'; 22 | -------------------------------------------------------------------------------- /lib/src/animation.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | import 'dart:math'; 4 | import 'dart:ui'; 5 | 6 | import 'package:flutter_scene/src/node.dart'; 7 | import 'package:flutter_scene/src/math_extensions.dart'; 8 | import 'package:flutter_scene_importer/flatbuffer.dart' as fb; 9 | import 'package:vector_math/vector_math.dart'; 10 | 11 | part 'animation/animation.dart'; 12 | part 'animation/animation_clip.dart'; 13 | part 'animation/animation_player.dart'; 14 | part 'animation/animation_transform.dart'; 15 | part 'animation/property_resolver.dart'; 16 | -------------------------------------------------------------------------------- /lib/src/animation/animation.dart: -------------------------------------------------------------------------------- 1 | part of '../animation.dart'; 2 | 3 | enum AnimationProperty { translation, rotation, scale } 4 | 5 | class BindKey implements Comparable { 6 | final String nodeName; 7 | final AnimationProperty property; 8 | 9 | BindKey({ 10 | required this.nodeName, 11 | this.property = AnimationProperty.translation, 12 | }); 13 | 14 | @override 15 | int compareTo(BindKey other) { 16 | if (nodeName == other.nodeName && property == other.property) { 17 | return 0; 18 | } 19 | return -1; 20 | } 21 | } 22 | 23 | class AnimationChannel { 24 | final BindKey bindTarget; 25 | final PropertyResolver resolver; 26 | 27 | AnimationChannel({required this.bindTarget, required this.resolver}); 28 | } 29 | 30 | class Animation { 31 | final String name; 32 | final List channels; 33 | final double _endTime; 34 | 35 | Animation({this.name = '', List? channels}) 36 | : channels = channels ?? [], 37 | _endTime = 38 | channels?.fold(0.0, ( 39 | double previousValue, 40 | AnimationChannel element, 41 | ) { 42 | return max(element.resolver.getEndTime(), previousValue); 43 | }) ?? 44 | 0.0; 45 | 46 | factory Animation.fromFlatbuffer( 47 | fb.Animation animation, 48 | List sceneNodes, 49 | ) { 50 | List channels = []; 51 | for (fb.Channel fbChannel in animation.channels!) { 52 | if (fbChannel.node < 0 || 53 | fbChannel.node >= sceneNodes.length || 54 | fbChannel.timeline == null) { 55 | continue; 56 | } 57 | 58 | final outTimes = fbChannel.timeline!; 59 | AnimationProperty outProperty; 60 | PropertyResolver resolver; 61 | 62 | // TODO(bdero): Why are the entries in the keyframe value arrays not 63 | // contiguous in the flatbuffer? We should be able to get rid 64 | // of the subloops below and just memcpy instead. 65 | switch (fbChannel.keyframesType) { 66 | case fb.KeyframesTypeId.TranslationKeyframes: 67 | outProperty = AnimationProperty.translation; 68 | fb.TranslationKeyframes? keyframes = 69 | fbChannel.keyframes as fb.TranslationKeyframes?; 70 | if (keyframes?.values == null) { 71 | continue; 72 | } 73 | List outValues = []; 74 | for (int i = 0; i < keyframes!.values!.length; i++) { 75 | outValues.add(keyframes.values![i].toVector3()); 76 | } 77 | resolver = PropertyResolver.makeTranslationTimeline( 78 | outTimes, 79 | outValues, 80 | ); 81 | break; 82 | case fb.KeyframesTypeId.RotationKeyframes: 83 | outProperty = AnimationProperty.rotation; 84 | fb.RotationKeyframes? keyframes = 85 | fbChannel.keyframes as fb.RotationKeyframes?; 86 | if (keyframes?.values == null) { 87 | continue; 88 | } 89 | List outValues = []; 90 | for (int i = 0; i < keyframes!.values!.length; i++) { 91 | outValues.add(keyframes.values![i].toQuaternion()); 92 | } 93 | resolver = PropertyResolver.makeRotationTimeline(outTimes, outValues); 94 | break; 95 | case fb.KeyframesTypeId.ScaleKeyframes: 96 | outProperty = AnimationProperty.scale; 97 | fb.ScaleKeyframes? keyframes = 98 | fbChannel.keyframes as fb.ScaleKeyframes?; 99 | if (keyframes?.values == null) { 100 | continue; 101 | } 102 | List outValues = []; 103 | for (int i = 0; i < keyframes!.values!.length; i++) { 104 | outValues.add(keyframes.values![i].toVector3()); 105 | } 106 | resolver = PropertyResolver.makeScaleTimeline(outTimes, outValues); 107 | break; 108 | default: 109 | continue; 110 | } 111 | 112 | final bindKey = BindKey( 113 | nodeName: sceneNodes[fbChannel.node].name, 114 | property: outProperty, 115 | ); 116 | channels.add(AnimationChannel(bindTarget: bindKey, resolver: resolver)); 117 | } 118 | 119 | return Animation(name: animation.name!.toString(), channels: channels); 120 | } 121 | 122 | double get endTime => _endTime; 123 | } 124 | -------------------------------------------------------------------------------- /lib/src/animation/animation_clip.dart: -------------------------------------------------------------------------------- 1 | part of '../animation.dart'; 2 | 3 | class _ChannelBinding { 4 | AnimationChannel channel; 5 | Node node; 6 | 7 | _ChannelBinding(this.channel, this.node); 8 | } 9 | 10 | /// An instance of an [Animation] that has been bound to a specific [Node]. 11 | class AnimationClip { 12 | final Animation _animation; 13 | final List<_ChannelBinding> _bindings = []; 14 | 15 | double _playbackTime = 0; 16 | double get playbackTime => _playbackTime; 17 | set playbackTime(double timeInSeconds) { 18 | seek(timeInSeconds); 19 | } 20 | 21 | double playbackTimeScale = 1; 22 | 23 | double _weight = 1; 24 | double get weight => _weight; 25 | set weight(double value) { 26 | _weight = clampDouble(value, 0, 1); 27 | } 28 | 29 | bool playing = false; 30 | 31 | bool loop = false; 32 | 33 | AnimationClip(this._animation, Node bindTarget) { 34 | _bindToTarget(bindTarget); 35 | } 36 | 37 | void play() { 38 | playing = true; 39 | } 40 | 41 | void pause() { 42 | playing = false; 43 | } 44 | 45 | void stop() { 46 | playing = false; 47 | seek(0); 48 | } 49 | 50 | void seek(double time) { 51 | _playbackTime = clampDouble(time, 0, _animation.endTime); 52 | } 53 | 54 | void advance(double deltaTime) { 55 | if (!playing || deltaTime <= 0) { 56 | return; 57 | } 58 | deltaTime *= playbackTimeScale; 59 | _playbackTime += deltaTime; 60 | 61 | // Handle looping behavior. 62 | 63 | if (_animation.endTime == 0) { 64 | _playbackTime = 0; 65 | return; 66 | } 67 | if (!loop && (_playbackTime < 0 || _playbackTime > _animation.endTime)) { 68 | // If looping is disabled, clamp to the end (or beginning, if playing in 69 | // reverse) and pause. 70 | pause(); 71 | _playbackTime = clampDouble(_playbackTime, 0, _animation.endTime); 72 | } else if ( /* loop && */ _playbackTime > _animation.endTime) { 73 | // If looping is enabled and we ran off the end, loop to the beginning. 74 | _playbackTime = _playbackTime.abs() % _animation.endTime; 75 | } else if ( /* loop && */ _playbackTime < 0) { 76 | // If looping is enabled and we ran off the beginning, loop to the end. 77 | _playbackTime = 78 | _animation.endTime - (_playbackTime.abs() % _animation.endTime); 79 | } 80 | } 81 | 82 | void _bindToTarget(Node target) { 83 | final channels = _animation.channels; 84 | _bindings.clear(); 85 | for (var channel in channels) { 86 | Node channelTarget; 87 | if (channel.bindTarget.nodeName == target.name) { 88 | channelTarget = target; 89 | } 90 | Node? result = target.getChildByName(channel.bindTarget.nodeName); 91 | if (result != null) { 92 | channelTarget = result; 93 | } else { 94 | continue; 95 | } 96 | _bindings.add(_ChannelBinding(channel, channelTarget)); 97 | } 98 | } 99 | 100 | void applyToBindings( 101 | Map transformDecomps, 102 | double weightMultiplier, 103 | ) { 104 | for (var binding in _bindings) { 105 | final transforms = transformDecomps[binding.node]; 106 | if (transforms == null) { 107 | continue; 108 | } 109 | binding.channel.resolver.apply( 110 | transforms, 111 | _playbackTime, 112 | _weight * weightMultiplier, 113 | ); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/src/animation/animation_player.dart: -------------------------------------------------------------------------------- 1 | part of '../animation.dart'; 2 | 3 | class AnimationPlayer { 4 | final Map _targetTransforms = {}; 5 | final Map _clips = {}; 6 | int? _previousTimeInMilliseconds; 7 | 8 | AnimationClip createAnimationClip(Animation animation, Node bindTarget) { 9 | final clip = AnimationClip(animation, bindTarget); 10 | 11 | // Record all of the unique default transforms that this AnimationClip 12 | // will mutate. 13 | for (final binding in clip._bindings) { 14 | _targetTransforms[binding.node] = AnimationTransforms( 15 | bindPose: DecomposedTransform.fromMatrix(binding.node.localTransform), 16 | ); 17 | } 18 | 19 | _clips[animation.name] = clip; 20 | return clip; 21 | } 22 | 23 | AnimationClip? getClipByName(String name) { 24 | return _clips[name]; 25 | } 26 | 27 | void update() { 28 | // Initialize the previous time if it has not been set yet. 29 | _previousTimeInMilliseconds ??= DateTime.now().millisecondsSinceEpoch; 30 | 31 | int newTime = DateTime.now().millisecondsSinceEpoch; 32 | double deltaTime = (newTime - _previousTimeInMilliseconds!) / 1000.0; 33 | _previousTimeInMilliseconds = newTime; 34 | 35 | // Reset the animated pose state. 36 | for (final transforms in _targetTransforms.values) { 37 | transforms.animatedPose = transforms.bindPose.clone(); 38 | } 39 | 40 | // Compute a weight multiplier for normalizing the animation. 41 | double totalWeight = 0.0; 42 | for (final clip in _clips.values) { 43 | totalWeight += clip.weight; 44 | } 45 | double weightMultiplier = totalWeight > 1.0 ? 1.0 / totalWeight : 1.0; 46 | 47 | // Update and apply all clips to the animation pose state. 48 | for (final clip in _clips.values) { 49 | clip.advance(deltaTime); 50 | clip.applyToBindings(_targetTransforms, weightMultiplier); 51 | } 52 | 53 | // Apply the animated pose to the bound joints. 54 | for (final entry in _targetTransforms.entries) { 55 | final node = entry.key; 56 | final transforms = entry.value; 57 | node.localTransform = transforms.animatedPose.toMatrix4(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/animation/animation_transform.dart: -------------------------------------------------------------------------------- 1 | part of '../animation.dart'; 2 | 3 | /// A decomposed animation transform consisting of a translation, rotation, and scale. 4 | class DecomposedTransform { 5 | /// The translation component of the transform. 6 | Vector3 translation = Vector3.zero(); 7 | 8 | /// The rotation component of the transform. 9 | Quaternion rotation = Quaternion.identity(); 10 | 11 | /// The scale component of the transform. 12 | Vector3 scale = Vector3.all(1.0); 13 | 14 | /// Constructs a new instance of [DecomposedTransform]. 15 | DecomposedTransform({ 16 | required this.translation, 17 | required this.rotation, 18 | required this.scale, 19 | }); 20 | 21 | /// Constructs a new instance of [DecomposedTransform] from a [Matrix4]. 22 | DecomposedTransform.fromMatrix(Matrix4 matrix) { 23 | matrix.decompose(translation, rotation, scale); 24 | 25 | // TODO(bdero): Why do some of the bind pose quaternions end up being more 26 | // than 180 degrees? 27 | double angle = 2 * acos(rotation.w); 28 | if (angle >= pi) { 29 | rotation.setAxisAngle(-rotation.axis, 2 * pi - angle); 30 | } 31 | } 32 | 33 | /// Converts this [DecomposedTransform] to a [Matrix4]. 34 | Matrix4 toMatrix4() { 35 | return Matrix4.compose(translation, rotation, scale); 36 | } 37 | 38 | DecomposedTransform clone() { 39 | return DecomposedTransform( 40 | translation: translation.clone(), 41 | rotation: rotation.clone(), 42 | scale: scale.clone(), 43 | ); 44 | } 45 | } 46 | 47 | class AnimationTransforms { 48 | DecomposedTransform bindPose; 49 | DecomposedTransform animatedPose = DecomposedTransform( 50 | translation: Vector3.zero(), 51 | rotation: Quaternion.identity(), 52 | scale: Vector3.all(1.0), 53 | ); 54 | 55 | AnimationTransforms({required this.bindPose}); 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/animation/property_resolver.dart: -------------------------------------------------------------------------------- 1 | part of '../animation.dart'; 2 | 3 | abstract class PropertyResolver { 4 | /// Returns the end time of the property in seconds. 5 | double getEndTime(); 6 | 7 | /// Resolve and apply the property value to a target node. This 8 | /// operation is additive; a given node property may be amended by 9 | /// many different PropertyResolvers prior to rendering. For example, 10 | /// an AnimationPlayer may blend multiple Animations together by 11 | /// applying several AnimationClips. 12 | void apply(AnimationTransforms target, double timeInSeconds, double weight); 13 | 14 | static PropertyResolver makeTranslationTimeline( 15 | List times, 16 | List values, 17 | ) { 18 | return TranslationTimelineResolver._(times, values); 19 | } 20 | 21 | static PropertyResolver makeRotationTimeline( 22 | List times, 23 | List values, 24 | ) { 25 | return RotationTimelineResolver._(times, values); 26 | } 27 | 28 | static PropertyResolver makeScaleTimeline( 29 | List times, 30 | List values, 31 | ) { 32 | return ScaleTimelineResolver._(times, values); 33 | } 34 | } 35 | 36 | class _TimelineKey { 37 | /// The index of the closest previous keyframe. 38 | int index = 0; 39 | 40 | /// Used to interpolate between the resolved values for `timeline_index - 1` 41 | /// and `timeline_index`. The range of this value should always be `0>N>=1`. 42 | double lerp = 1.0; 43 | 44 | _TimelineKey(this.index, this.lerp); 45 | } 46 | 47 | abstract class TimelineResolver implements PropertyResolver { 48 | final List _times; 49 | 50 | TimelineResolver._(this._times); 51 | 52 | @override 53 | double getEndTime() { 54 | return _times.isEmpty ? 0.0 : _times.last; 55 | } 56 | 57 | _TimelineKey _getTimelineKey(double time) { 58 | if (_times.length <= 1 || time <= _times.first) { 59 | return _TimelineKey(0, 1); 60 | } 61 | if (time >= _times.last) { 62 | return _TimelineKey(_times.length - 1, 1); 63 | } 64 | int nextTimeIndex = _times.indexWhere((t) => t >= time); 65 | 66 | double previousTime = _times[nextTimeIndex - 1]; 67 | double nextTime = _times[nextTimeIndex]; 68 | 69 | double lerp = (time - previousTime) / (nextTime - previousTime); 70 | return _TimelineKey(nextTimeIndex, lerp); 71 | } 72 | } 73 | 74 | class TranslationTimelineResolver extends TimelineResolver { 75 | final List _values; 76 | 77 | TranslationTimelineResolver._(List times, this._values) 78 | : super._(times) { 79 | assert(times.length == _values.length); 80 | } 81 | 82 | @override 83 | void apply(AnimationTransforms target, double timeInSeconds, double weight) { 84 | if (_values.isEmpty) { 85 | return; 86 | } 87 | 88 | _TimelineKey key = _getTimelineKey(timeInSeconds); 89 | Vector3 value = _values[key.index]; 90 | if (key.lerp < 1) { 91 | value = _values[key.index - 1].lerp(value, key.lerp); 92 | } 93 | 94 | target.animatedPose.translation += 95 | (value - target.bindPose.translation) * weight; 96 | } 97 | } 98 | 99 | class RotationTimelineResolver extends TimelineResolver { 100 | final List _values; 101 | 102 | RotationTimelineResolver._(List times, this._values) 103 | : super._(times) { 104 | assert(times.length == _values.length); 105 | } 106 | 107 | @override 108 | void apply(AnimationTransforms target, double timeInSeconds, double weight) { 109 | if (_values.isEmpty) { 110 | return; 111 | } 112 | 113 | _TimelineKey key = _getTimelineKey(timeInSeconds); 114 | Quaternion value = _values[key.index]; 115 | if (key.lerp < 1) { 116 | value = _values[key.index - 1].slerp(value, key.lerp); 117 | } 118 | 119 | target.animatedPose.rotation = target.animatedPose.rotation.slerp( 120 | value, 121 | weight, 122 | ); 123 | } 124 | } 125 | 126 | class ScaleTimelineResolver extends TimelineResolver { 127 | final List _values; 128 | 129 | ScaleTimelineResolver._(List times, this._values) : super._(times) { 130 | assert(times.length == _values.length); 131 | } 132 | 133 | @override 134 | void apply(AnimationTransforms target, double timeInSeconds, double weight) { 135 | if (_values.isEmpty) { 136 | return; 137 | } 138 | 139 | _TimelineKey key = _getTimelineKey(timeInSeconds); 140 | Vector3 value = _values[key.index]; 141 | if (key.lerp < 1) { 142 | value = _values[key.index - 1].lerp(value, key.lerp); 143 | } 144 | 145 | Vector3 scale = Vector3( 146 | 1, 147 | 1, 148 | 1, 149 | ).lerp(value.divided(target.bindPose.scale), weight); 150 | 151 | target.animatedPose.scale = Vector3( 152 | target.animatedPose.scale.x * scale.x, 153 | target.animatedPose.scale.y * scale.y, 154 | target.animatedPose.scale.z * scale.z, 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/src/asset_helpers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_gpu/gpu.dart' as gpu; 5 | 6 | Future gpuTextureFromImage(ui.Image image) async { 7 | final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); 8 | if (byteData == null) { 9 | throw Exception('Failed to get RGBA data from image.'); 10 | } 11 | 12 | // Upload the RGBA image to a Flutter GPU texture. 13 | final texture = gpu.gpuContext.createTexture( 14 | gpu.StorageMode.hostVisible, 15 | image.width, 16 | image.height, 17 | ); 18 | texture.overwrite(byteData); 19 | 20 | return texture; 21 | } 22 | 23 | Future gpuTextureFromAsset(String assetPath) async { 24 | // Load resource from the asset bundle. Throws exception if the asset couldn't 25 | // be found in the bundle. 26 | final buffer = await rootBundle.loadBuffer(assetPath); 27 | 28 | // Decode the image. 29 | final codec = await ui.instantiateImageCodecFromBuffer(buffer); 30 | final frame = await codec.getNextFrame(); 31 | final image = frame.image; 32 | 33 | return await gpuTextureFromImage(image); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/camera.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui' as ui; 3 | 4 | import 'package:vector_math/vector_math.dart'; 5 | 6 | abstract class Camera { 7 | Vector3 get position; 8 | Matrix4 getViewTransform(ui.Size dimensions); 9 | } 10 | 11 | Matrix4 _matrix4LookAt(Vector3 position, Vector3 target, Vector3 up) { 12 | Vector3 forward = (target - position).normalized(); 13 | Vector3 right = up.cross(forward).normalized(); 14 | up = forward.cross(right).normalized(); 15 | 16 | return Matrix4( 17 | right.x, 18 | up.x, 19 | forward.x, 20 | 0.0, // 21 | right.y, 22 | up.y, 23 | forward.y, 24 | 0.0, // 25 | right.z, 26 | up.z, 27 | forward.z, 28 | 0.0, // 29 | -right.dot(position), 30 | -up.dot(position), 31 | -forward.dot(position), 32 | 1.0, // 33 | ); 34 | } 35 | 36 | Matrix4 _matrix4Perspective( 37 | double fovRadiansY, 38 | double aspectRatio, 39 | double zNear, 40 | double zFar, 41 | ) { 42 | double height = tan(fovRadiansY * 0.5); 43 | double width = height * aspectRatio; 44 | 45 | return Matrix4( 46 | 1.0 / width, 47 | 0.0, 48 | 0.0, 49 | 0.0, 50 | 0.0, 51 | 1.0 / height, 52 | 0.0, 53 | 0.0, 54 | 0.0, 55 | 0.0, 56 | zFar / (zFar - zNear), 57 | 1.0, 58 | 0.0, 59 | 0.0, 60 | -(zFar * zNear) / (zFar - zNear), 61 | 0.0, 62 | ); 63 | } 64 | 65 | class PerspectiveCamera extends Camera { 66 | PerspectiveCamera({ 67 | this.fovRadiansY = 45 * degrees2Radians, 68 | Vector3? position, 69 | Vector3? target, 70 | Vector3? up, 71 | this.fovNear = 0.1, 72 | this.fovFar = 1000.0, 73 | }) : position = position ?? Vector3(0, 0, -5), 74 | target = target ?? Vector3(0, 0, 0), 75 | up = up ?? Vector3(0, 1, 0); 76 | 77 | double fovRadiansY; 78 | @override 79 | Vector3 position = Vector3(0, 0, -5); 80 | Vector3 target; 81 | Vector3 up; 82 | double fovNear; 83 | double fovFar; 84 | 85 | @override 86 | Matrix4 getViewTransform(ui.Size dimensions) { 87 | return _matrix4Perspective( 88 | fovRadiansY, 89 | dimensions.width / dimensions.height, 90 | fovNear, 91 | fovFar, 92 | ) * 93 | _matrix4LookAt(position, target, up); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/geometry/geometry.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter_gpu/gpu.dart' as gpu; 5 | import 'package:vector_math/vector_math.dart' as vm; 6 | 7 | import 'package:flutter_scene/src/shaders.dart'; 8 | import 'package:flutter_scene_importer/constants.dart'; 9 | import 'package:flutter_scene_importer/flatbuffer.dart' as fb; 10 | 11 | abstract class Geometry { 12 | gpu.BufferView? _vertices; 13 | int _vertexCount = 0; 14 | 15 | gpu.BufferView? _indices; 16 | gpu.IndexType _indexType = gpu.IndexType.int16; 17 | int _indexCount = 0; 18 | 19 | gpu.Shader? _vertexShader; 20 | gpu.Shader get vertexShader { 21 | if (_vertexShader == null) { 22 | throw Exception('Vertex shader has not been set'); 23 | } 24 | return _vertexShader!; 25 | } 26 | 27 | static Geometry fromFlatbuffer(fb.MeshPrimitive fbPrimitive) { 28 | Uint8List vertices; 29 | bool isSkinned = 30 | fbPrimitive.vertices!.runtimeType == fb.SkinnedVertexBuffer; 31 | int perVertexBytes = 32 | isSkinned ? kSkinnedPerVertexSize : kUnskinnedPerVertexSize; 33 | 34 | switch (fbPrimitive.vertices!.runtimeType) { 35 | case const (fb.UnskinnedVertexBuffer): 36 | fb.UnskinnedVertexBuffer unskinned = 37 | (fbPrimitive.vertices as fb.UnskinnedVertexBuffer?)!; 38 | vertices = unskinned.vertices! as Uint8List; 39 | case const (fb.SkinnedVertexBuffer): 40 | fb.SkinnedVertexBuffer skinned = 41 | (fbPrimitive.vertices as fb.SkinnedVertexBuffer?)!; 42 | vertices = skinned.vertices! as Uint8List; 43 | default: 44 | throw Exception('Unknown vertex buffer type'); 45 | } 46 | 47 | if (vertices.length % perVertexBytes != 0) { 48 | debugPrint( 49 | 'OH NO: Encountered an vertex buffer of size ' 50 | '${vertices.lengthInBytes} bytes, which doesn\'t match the ' 51 | 'expected multiple of $perVertexBytes bytes. Possible data corruption! ' 52 | 'Attempting to use a vertex count of ${vertices.length ~/ perVertexBytes}. ' 53 | 'The last ${vertices.length % perVertexBytes} bytes will be ignored.', 54 | ); 55 | } 56 | int vertexCount = vertices.length ~/ perVertexBytes; 57 | 58 | gpu.IndexType indexType = fbPrimitive.indices!.type.toIndexType(); 59 | Uint8List indices = fbPrimitive.indices!.data! as Uint8List; 60 | 61 | Geometry geometry; 62 | switch (fbPrimitive.vertices!.runtimeType) { 63 | case const (fb.UnskinnedVertexBuffer): 64 | geometry = UnskinnedGeometry(); 65 | case const (fb.SkinnedVertexBuffer): 66 | geometry = SkinnedGeometry(); 67 | default: 68 | throw Exception('Unknown vertex buffer type'); 69 | } 70 | 71 | geometry.uploadVertexData( 72 | ByteData.sublistView(vertices), 73 | vertexCount, 74 | ByteData.sublistView(indices), 75 | indexType: indexType, 76 | ); 77 | return geometry; 78 | } 79 | 80 | void setVertices(gpu.BufferView vertices, int vertexCount) { 81 | _vertices = vertices; 82 | _vertexCount = vertexCount; 83 | } 84 | 85 | void setIndices(gpu.BufferView indices, gpu.IndexType indexType) { 86 | _indices = indices; 87 | _indexType = indexType; 88 | switch (indexType) { 89 | case gpu.IndexType.int16: 90 | _indexCount = indices.lengthInBytes ~/ 2; 91 | case gpu.IndexType.int32: 92 | _indexCount = indices.lengthInBytes ~/ 4; 93 | } 94 | } 95 | 96 | void uploadVertexData( 97 | ByteData vertices, 98 | int vertexCount, 99 | ByteData? indices, { 100 | gpu.IndexType indexType = gpu.IndexType.int16, 101 | }) { 102 | gpu.DeviceBuffer deviceBuffer = gpu.gpuContext.createDeviceBuffer( 103 | gpu.StorageMode.hostVisible, 104 | indices == null 105 | ? vertices.lengthInBytes 106 | : vertices.lengthInBytes + indices.lengthInBytes, 107 | ); 108 | 109 | deviceBuffer.overwrite(vertices, destinationOffsetInBytes: 0); 110 | setVertices( 111 | gpu.BufferView( 112 | deviceBuffer, 113 | offsetInBytes: 0, 114 | lengthInBytes: vertices.lengthInBytes, 115 | ), 116 | vertexCount, 117 | ); 118 | 119 | if (indices != null) { 120 | deviceBuffer.overwrite( 121 | indices, 122 | destinationOffsetInBytes: vertices.lengthInBytes, 123 | ); 124 | setIndices( 125 | gpu.BufferView( 126 | deviceBuffer, 127 | offsetInBytes: vertices.lengthInBytes, 128 | lengthInBytes: indices.lengthInBytes, 129 | ), 130 | indexType, 131 | ); 132 | } 133 | } 134 | 135 | void setVertexShader(gpu.Shader shader) { 136 | _vertexShader = shader; 137 | } 138 | 139 | void setJointsTexture(gpu.Texture? texture, int width) {} 140 | 141 | void bind( 142 | gpu.RenderPass pass, 143 | gpu.HostBuffer transientsBuffer, 144 | vm.Matrix4 modelTransform, 145 | vm.Matrix4 cameraTransform, 146 | vm.Vector3 cameraPosition, 147 | ); 148 | } 149 | 150 | class UnskinnedGeometry extends Geometry { 151 | UnskinnedGeometry() { 152 | setVertexShader(baseShaderLibrary['UnskinnedVertex']!); 153 | } 154 | 155 | @override 156 | void bind( 157 | gpu.RenderPass pass, 158 | gpu.HostBuffer transientsBuffer, 159 | vm.Matrix4 modelTransform, 160 | vm.Matrix4 cameraTransform, 161 | vm.Vector3 cameraPosition, 162 | ) { 163 | if (_vertices == null) { 164 | throw Exception( 165 | 'SetVertices must be called before GetBufferView for Geometry.', 166 | ); 167 | } 168 | 169 | pass.bindVertexBuffer(_vertices!, _vertexCount); 170 | if (_indices != null) { 171 | pass.bindIndexBuffer(_indices!, _indexType, _indexCount); 172 | } 173 | 174 | // Unskinned vertex UBO. 175 | final frameInfoSlot = vertexShader.getUniformSlot('FrameInfo'); 176 | final frameInfoFloats = Float32List.fromList([ 177 | modelTransform.storage[0], 178 | modelTransform.storage[1], 179 | modelTransform.storage[2], 180 | modelTransform.storage[3], 181 | modelTransform.storage[4], 182 | modelTransform.storage[5], 183 | modelTransform.storage[6], 184 | modelTransform.storage[7], 185 | modelTransform.storage[8], 186 | modelTransform.storage[9], 187 | modelTransform.storage[10], 188 | modelTransform.storage[11], 189 | modelTransform.storage[12], 190 | modelTransform.storage[13], 191 | modelTransform.storage[14], 192 | modelTransform.storage[15], 193 | cameraTransform.storage[0], 194 | cameraTransform.storage[1], 195 | cameraTransform.storage[2], 196 | cameraTransform.storage[3], 197 | cameraTransform.storage[4], 198 | cameraTransform.storage[5], 199 | cameraTransform.storage[6], 200 | cameraTransform.storage[7], 201 | cameraTransform.storage[8], 202 | cameraTransform.storage[9], 203 | cameraTransform.storage[10], 204 | cameraTransform.storage[11], 205 | cameraTransform.storage[12], 206 | cameraTransform.storage[13], 207 | cameraTransform.storage[14], 208 | cameraTransform.storage[15], 209 | cameraPosition.x, 210 | cameraPosition.y, 211 | cameraPosition.z, 212 | ]); 213 | final frameInfoView = transientsBuffer.emplace( 214 | frameInfoFloats.buffer.asByteData(), 215 | ); 216 | pass.bindUniform(frameInfoSlot, frameInfoView); 217 | } 218 | } 219 | 220 | class SkinnedGeometry extends Geometry { 221 | gpu.Texture? _jointsTexture; 222 | int _jointsTextureWidth = 0; 223 | 224 | SkinnedGeometry() { 225 | setVertexShader(baseShaderLibrary['SkinnedVertex']!); 226 | } 227 | 228 | @override 229 | void setJointsTexture(gpu.Texture? texture, int width) { 230 | _jointsTexture = texture; 231 | _jointsTextureWidth = width; 232 | } 233 | 234 | @override 235 | void bind( 236 | gpu.RenderPass pass, 237 | gpu.HostBuffer transientsBuffer, 238 | vm.Matrix4 modelTransform, 239 | vm.Matrix4 cameraTransform, 240 | vm.Vector3 cameraPosition, 241 | ) { 242 | if (_jointsTexture == null) { 243 | throw Exception('Joints texture must be set for skinned geometry.'); 244 | } 245 | 246 | pass.bindTexture( 247 | vertexShader.getUniformSlot('joints_texture'), 248 | _jointsTexture!, 249 | sampler: gpu.SamplerOptions( 250 | minFilter: gpu.MinMagFilter.nearest, 251 | magFilter: gpu.MinMagFilter.nearest, 252 | mipFilter: gpu.MipFilter.nearest, 253 | widthAddressMode: gpu.SamplerAddressMode.clampToEdge, 254 | heightAddressMode: gpu.SamplerAddressMode.clampToEdge, 255 | ), 256 | ); 257 | 258 | if (_vertices == null) { 259 | throw Exception( 260 | 'SetVertices must be called before GetBufferView for Geometry.', 261 | ); 262 | } 263 | 264 | pass.bindVertexBuffer(_vertices!, _vertexCount); 265 | if (_indices != null) { 266 | pass.bindIndexBuffer(_indices!, _indexType, _indexCount); 267 | } 268 | 269 | // Skinned vertex UBO. 270 | final frameInfoSlot = vertexShader.getUniformSlot('FrameInfo'); 271 | final frameInfoFloats = Float32List.fromList([ 272 | modelTransform.storage[0], 273 | modelTransform.storage[1], 274 | modelTransform.storage[2], 275 | modelTransform.storage[3], 276 | modelTransform.storage[4], 277 | modelTransform.storage[5], 278 | modelTransform.storage[6], 279 | modelTransform.storage[7], 280 | modelTransform.storage[8], 281 | modelTransform.storage[9], 282 | modelTransform.storage[10], 283 | modelTransform.storage[11], 284 | modelTransform.storage[12], 285 | modelTransform.storage[13], 286 | modelTransform.storage[14], 287 | modelTransform.storage[15], 288 | cameraTransform.storage[0], 289 | cameraTransform.storage[1], 290 | cameraTransform.storage[2], 291 | cameraTransform.storage[3], 292 | cameraTransform.storage[4], 293 | cameraTransform.storage[5], 294 | cameraTransform.storage[6], 295 | cameraTransform.storage[7], 296 | cameraTransform.storage[8], 297 | cameraTransform.storage[9], 298 | cameraTransform.storage[10], 299 | cameraTransform.storage[11], 300 | cameraTransform.storage[12], 301 | cameraTransform.storage[13], 302 | cameraTransform.storage[14], 303 | cameraTransform.storage[15], 304 | cameraPosition.x, 305 | cameraPosition.y, 306 | cameraPosition.z, 307 | _jointsTexture != null ? 1 : 0, 308 | _jointsTexture != null ? _jointsTextureWidth.toDouble() : 1.0, 309 | ]); 310 | final frameInfoView = transientsBuffer.emplace( 311 | frameInfoFloats.buffer.asByteData(), 312 | ); 313 | pass.bindUniform(frameInfoSlot, frameInfoView); 314 | } 315 | } 316 | 317 | class CuboidGeometry extends UnskinnedGeometry { 318 | CuboidGeometry(vm.Vector3 extents) { 319 | final e = extents / 2; 320 | // Layout: Position, normal, uv, color 321 | final vertices = Float32List.fromList([ 322 | -e.x, -e.y, -e.z, /* */ 0, 0, -1, /* */ 0, 0, /* */ 1, 0, 0, 1, // 323 | e.x, -e.y, -e.z, /* */ 0, 0, -1, /* */ 1, 0, /* */ 0, 1, 0, 1, // 324 | e.x, e.y, -e.z, /* */ 0, 0, -1, /* */ 1, 1, /* */ 0, 0, 1, 1, // 325 | -e.x, e.y, -e.z, /* */ 0, 0, -1, /* */ 0, 1, /* */ 0, 0, 0, 1, // 326 | -e.x, -e.y, e.z, /* */ 0, 0, -1, /* */ 0, 0, /* */ 0, 1, 1, 1, // 327 | e.x, -e.y, e.z, /* */ 0, 0, -1, /* */ 1, 0, /* */ 1, 0, 1, 1, // 328 | e.x, e.y, e.z, /* */ 0, 0, -1, /* */ 1, 1, /* */ 1, 1, 0, 1, // 329 | -e.x, e.y, e.z, /* */ 0, 0, -1, /* */ 0, 1, /* */ 1, 1, 1, 1, // 330 | ]); 331 | 332 | final indices = Uint16List.fromList([ 333 | 0, 1, 3, 3, 1, 2, // 334 | 1, 5, 2, 2, 5, 6, // 335 | 5, 4, 6, 6, 4, 7, // 336 | 4, 0, 7, 7, 0, 3, // 337 | 3, 2, 7, 7, 2, 6, // 338 | 4, 5, 0, 0, 5, 1, // 339 | ]); 340 | 341 | uploadVertexData( 342 | ByteData.sublistView(vertices), 343 | 8, 344 | ByteData.sublistView(indices), 345 | indexType: gpu.IndexType.int16, 346 | ); 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /lib/src/material/environment.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:flutter_gpu/gpu.dart' as gpu; 4 | import 'package:flutter_scene/src/asset_helpers.dart'; 5 | import 'package:flutter_scene/src/material/material.dart'; 6 | 7 | base class EnvironmentMap { 8 | EnvironmentMap._(this._radianceTexture, this._irradianceTexture); 9 | 10 | factory EnvironmentMap.empty() { 11 | return EnvironmentMap._(null, null); 12 | } 13 | 14 | factory EnvironmentMap.fromGpuTextures({ 15 | required gpu.Texture radianceTexture, 16 | gpu.Texture? irradianceTexture, 17 | }) { 18 | return EnvironmentMap._(radianceTexture, irradianceTexture); 19 | } 20 | 21 | static Future fromUIImages({ 22 | required ui.Image radianceImage, 23 | ui.Image? irradianceImage, 24 | }) async { 25 | final radianceTexture = await gpuTextureFromImage(radianceImage); 26 | gpu.Texture? irradianceTexture; 27 | 28 | if (irradianceImage != null) { 29 | irradianceTexture = await gpuTextureFromImage(irradianceImage); 30 | } 31 | 32 | return EnvironmentMap.fromGpuTextures( 33 | radianceTexture: radianceTexture, 34 | irradianceTexture: irradianceTexture, 35 | ); 36 | } 37 | 38 | static Future fromAssets({ 39 | required String radianceImagePath, 40 | String? irradianceImagePath, 41 | }) async { 42 | final radianceTexture = await gpuTextureFromAsset(radianceImagePath); 43 | gpu.Texture? irradianceTexture; 44 | 45 | if (irradianceImagePath != null) { 46 | irradianceTexture = await gpuTextureFromAsset(irradianceImagePath); 47 | } 48 | 49 | return EnvironmentMap.fromGpuTextures( 50 | radianceTexture: radianceTexture, 51 | irradianceTexture: irradianceTexture, 52 | ); 53 | } 54 | 55 | bool isEmpty() => _radianceTexture == null; 56 | 57 | gpu.Texture? _radianceTexture; 58 | gpu.Texture? _irradianceTexture; 59 | 60 | // TODO(bdero): Once cubemaps are supported, change this to be an environment cubemap. (Cubemaps are missing from Flutter GPU at the time of writing: https://github.com/flutter/flutter/issues/145027) 61 | /// Represents the light being emitted by the environment from any direction. 62 | /// 63 | /// Currently expected to be an equirectangular map. 64 | gpu.Texture get radianceTexture => 65 | Material.whitePlaceholder(_radianceTexture); 66 | 67 | // TODO(bdero): Once cubemaps are supported, change this to be an environment cubemap. (Cubemaps are missing from Flutter GPU at the time of writing: https://github.com/flutter/flutter/issues/145027) 68 | // TODO(bdero): Generate Gaussian blurred mipmaps for this texture for accurate roughness sampling. 69 | /// The integral of all light being received by a given surface at any direction. 70 | /// 71 | /// Currently expected to be an equirectangular map. 72 | gpu.Texture get irradianceTexture => 73 | Material.whitePlaceholder(_irradianceTexture); 74 | } 75 | 76 | /// Shared material rendering properties. 77 | /// 78 | /// A default environment can be set on the [Scene], which is automatically 79 | /// applied to all materials. Individual [Material]s may optionally override the 80 | /// default environment. 81 | base class Environment { 82 | Environment({ 83 | EnvironmentMap? environmentMap, 84 | this.intensity = 1.0, 85 | this.exposure = 2.0, 86 | }) : environmentMap = environmentMap ?? EnvironmentMap.empty(); 87 | 88 | Environment withNewEnvironmentMap(EnvironmentMap environmentMap) { 89 | return Environment( 90 | environmentMap: environmentMap, 91 | intensity: intensity, 92 | exposure: exposure, 93 | ); 94 | } 95 | 96 | /// The environment map to use for image-based-lighting. 97 | /// 98 | /// This must be an equirectangular map. 99 | EnvironmentMap environmentMap; 100 | 101 | /// The intensity of the environment map. 102 | double intensity; 103 | 104 | /// The exposure level used for tone mapping. 105 | double exposure; 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/material/material.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter_gpu/gpu.dart' as gpu; 4 | import 'package:flutter_scene/src/asset_helpers.dart'; 5 | 6 | import 'package:flutter_scene/src/material/environment.dart'; 7 | import 'package:flutter_scene/src/material/physically_based_material.dart'; 8 | import 'package:flutter_scene/src/material/unlit_material.dart'; 9 | import 'package:flutter_scene_importer/flatbuffer.dart' as fb; 10 | 11 | abstract class Material { 12 | static gpu.Texture? _whitePlaceholderTexture; 13 | 14 | static gpu.Texture getWhitePlaceholderTexture() { 15 | if (_whitePlaceholderTexture != null) { 16 | return _whitePlaceholderTexture!; 17 | } 18 | _whitePlaceholderTexture = gpu.gpuContext.createTexture( 19 | gpu.StorageMode.hostVisible, 20 | 1, 21 | 1, 22 | ); 23 | if (_whitePlaceholderTexture == null) { 24 | throw Exception('Failed to create white placeholder texture.'); 25 | } 26 | _whitePlaceholderTexture!.overwrite( 27 | Uint32List.fromList([0xFFFF7F7F]).buffer.asByteData(), 28 | ); 29 | return _whitePlaceholderTexture!; 30 | } 31 | 32 | static gpu.Texture whitePlaceholder(gpu.Texture? texture) { 33 | return texture ?? getWhitePlaceholderTexture(); 34 | } 35 | 36 | static gpu.Texture? _normalPlaceholderTexture; 37 | 38 | static gpu.Texture getNormalPlaceholderTexture() { 39 | if (_normalPlaceholderTexture != null) { 40 | return _normalPlaceholderTexture!; 41 | } 42 | _normalPlaceholderTexture = gpu.gpuContext.createTexture( 43 | gpu.StorageMode.hostVisible, 44 | 1, 45 | 1, 46 | ); 47 | if (_normalPlaceholderTexture == null) { 48 | throw Exception('Failed to create normal placeholder texture.'); 49 | } 50 | _normalPlaceholderTexture!.overwrite( 51 | Uint32List.fromList([0xFFFF7574]).buffer.asByteData(), 52 | ); 53 | return _normalPlaceholderTexture!; 54 | } 55 | 56 | static gpu.Texture normalPlaceholder(gpu.Texture? texture) { 57 | return texture ?? getNormalPlaceholderTexture(); 58 | } 59 | 60 | static gpu.Texture? _brdfLutTexture; 61 | static gpu.Texture? _defaultRadianceTexture; 62 | static gpu.Texture? _defaultIrradianceTexture; 63 | 64 | static gpu.Texture getBrdfLutTexture() { 65 | if (_brdfLutTexture == null) { 66 | throw Exception('BRDF LUT texture has not been initialized.'); 67 | } 68 | return _brdfLutTexture!; 69 | } 70 | 71 | static EnvironmentMap getDefaultEnvironmentMap() { 72 | if (_defaultRadianceTexture == null || _defaultIrradianceTexture == null) { 73 | throw Exception('Default environment map has not been initialized.'); 74 | } 75 | return EnvironmentMap.fromGpuTextures( 76 | radianceTexture: _defaultRadianceTexture!, 77 | irradianceTexture: _defaultIrradianceTexture!, 78 | ); 79 | } 80 | 81 | static Future initializeStaticResources() { 82 | List> futures = [ 83 | gpuTextureFromAsset( 84 | 'packages/flutter_scene/assets/ibl_brdf_lut.png', 85 | ).then((gpu.Texture value) { 86 | _brdfLutTexture = value; 87 | }), 88 | gpuTextureFromAsset( 89 | 'packages/flutter_scene/assets/royal_esplanade.png', 90 | ).then((gpu.Texture value) { 91 | _defaultRadianceTexture = value; 92 | }), 93 | gpuTextureFromAsset( 94 | 'packages/flutter_scene/assets/royal_esplanade_irradiance.png', 95 | ).then((gpu.Texture value) { 96 | _defaultIrradianceTexture = value; 97 | }), 98 | ]; 99 | return Future.wait(futures); 100 | } 101 | 102 | static Material fromFlatbuffer( 103 | fb.Material fbMaterial, 104 | List textures, 105 | ) { 106 | switch (fbMaterial.type) { 107 | case fb.MaterialType.kUnlit: 108 | return UnlitMaterial.fromFlatbuffer(fbMaterial, textures); 109 | case fb.MaterialType.kPhysicallyBased: 110 | return PhysicallyBasedMaterial.fromFlatbuffer(fbMaterial, textures); 111 | default: 112 | throw Exception('Unknown material type'); 113 | } 114 | } 115 | 116 | gpu.Shader? _fragmentShader; 117 | gpu.Shader get fragmentShader { 118 | if (_fragmentShader == null) { 119 | throw Exception('Fragment shader has not been set'); 120 | } 121 | return _fragmentShader!; 122 | } 123 | 124 | void setFragmentShader(gpu.Shader shader) { 125 | _fragmentShader = shader; 126 | } 127 | 128 | void bind( 129 | gpu.RenderPass pass, 130 | gpu.HostBuffer transientsBuffer, 131 | Environment environment, 132 | ) { 133 | pass.setCullMode(gpu.CullMode.backFace); 134 | pass.setWindingOrder(gpu.WindingOrder.counterClockwise); 135 | } 136 | 137 | bool isOpaque() { 138 | return true; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/src/material/physically_based_material.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter_gpu/gpu.dart' as gpu; 3 | import 'package:flutter_scene/src/material/environment.dart'; 4 | import 'package:flutter_scene/src/material/material.dart'; 5 | import 'package:flutter_scene/src/shaders.dart'; 6 | 7 | import 'package:flutter_scene_importer/flatbuffer.dart' as fb; 8 | import 'package:vector_math/vector_math.dart'; 9 | 10 | class PhysicallyBasedMaterial extends Material { 11 | static PhysicallyBasedMaterial fromFlatbuffer( 12 | fb.Material fbMaterial, 13 | List textures, 14 | ) { 15 | if (fbMaterial.type != fb.MaterialType.kPhysicallyBased) { 16 | throw Exception('Cannot unpack PBR material from non-PBR material'); 17 | } 18 | 19 | PhysicallyBasedMaterial material = PhysicallyBasedMaterial(); 20 | 21 | // Base color. 22 | 23 | if (fbMaterial.baseColorFactor != null) { 24 | material.baseColorFactor = Vector4( 25 | fbMaterial.baseColorFactor!.r, 26 | fbMaterial.baseColorFactor!.g, 27 | fbMaterial.baseColorFactor!.b, 28 | fbMaterial.baseColorFactor!.a, 29 | ); 30 | } 31 | 32 | if (fbMaterial.baseColorTexture >= 0 && 33 | fbMaterial.baseColorTexture < textures.length) { 34 | material.baseColorTexture = textures[fbMaterial.baseColorTexture]; 35 | } 36 | 37 | // Metallic-roughness. 38 | 39 | material.metallicFactor = fbMaterial.metallicFactor; 40 | material.roughnessFactor = fbMaterial.roughnessFactor; 41 | 42 | debugPrint('Total texture count: ${textures.length}'); 43 | if (fbMaterial.metallicRoughnessTexture >= 0 && 44 | fbMaterial.metallicRoughnessTexture < textures.length) { 45 | material.metallicRoughnessTexture = 46 | textures[fbMaterial.metallicRoughnessTexture]; 47 | } 48 | 49 | // Normal. 50 | 51 | if (fbMaterial.normalTexture >= 0 && 52 | fbMaterial.normalTexture < textures.length) { 53 | material.normalTexture = textures[fbMaterial.normalTexture]; 54 | } 55 | 56 | material.normalScale = fbMaterial.normalScale; 57 | 58 | // Emissive. 59 | 60 | if (fbMaterial.emissiveFactor != null) { 61 | material.emissiveFactor = Vector4( 62 | fbMaterial.emissiveFactor!.x, 63 | fbMaterial.emissiveFactor!.y, 64 | fbMaterial.emissiveFactor!.z, 65 | 1, 66 | ); 67 | } 68 | 69 | if (fbMaterial.emissiveTexture >= 0 && 70 | fbMaterial.emissiveTexture < textures.length) { 71 | material.emissiveTexture = textures[fbMaterial.emissiveTexture]; 72 | } 73 | 74 | // Occlusion. 75 | 76 | material.occlusionStrength = fbMaterial.occlusionStrength; 77 | 78 | if (fbMaterial.occlusionTexture >= 0 && 79 | fbMaterial.occlusionTexture < textures.length) { 80 | material.occlusionTexture = textures[fbMaterial.occlusionTexture]; 81 | } 82 | 83 | return material; 84 | } 85 | 86 | PhysicallyBasedMaterial({ 87 | this.baseColorTexture, 88 | this.metallicRoughnessTexture, 89 | this.normalTexture, 90 | this.emissiveTexture, 91 | this.occlusionTexture, 92 | this.environment, 93 | }) { 94 | setFragmentShader(baseShaderLibrary['StandardFragment']!); 95 | } 96 | 97 | gpu.Texture? baseColorTexture; 98 | Vector4 baseColorFactor = Colors.white; 99 | double vertexColorWeight = 1.0; 100 | 101 | gpu.Texture? metallicRoughnessTexture; 102 | double metallicFactor = 1.0; 103 | double roughnessFactor = 1.0; 104 | 105 | gpu.Texture? normalTexture; 106 | double normalScale = 1.0; 107 | 108 | gpu.Texture? emissiveTexture; 109 | Vector4 emissiveFactor = Vector4.zero(); 110 | 111 | gpu.Texture? occlusionTexture; 112 | double occlusionStrength = 1.0; 113 | 114 | Environment? environment; 115 | 116 | @override 117 | void bind( 118 | gpu.RenderPass pass, 119 | gpu.HostBuffer transientsBuffer, 120 | Environment environment, 121 | ) { 122 | super.bind(pass, transientsBuffer, environment); 123 | 124 | Environment env = this.environment ?? environment; 125 | 126 | var fragInfo = Float32List.fromList([ 127 | baseColorFactor.r, baseColorFactor.g, 128 | baseColorFactor.b, baseColorFactor.a, // color 129 | emissiveFactor.r, emissiveFactor.g, 130 | emissiveFactor.b, 131 | emissiveFactor.a, // emissive_factor 132 | vertexColorWeight, // vertex_color_weight 133 | environment.exposure, // exposure 134 | metallicFactor, // metallic 135 | roughnessFactor, // roughness 136 | normalTexture != null ? 1.0 : 0.0, // has_normal_map 137 | normalScale, // normal_scale 138 | occlusionStrength, // occlusion_strength 139 | environment.intensity, // environment_intensity 140 | ]); 141 | pass.bindUniform( 142 | fragmentShader.getUniformSlot("FragInfo"), 143 | transientsBuffer.emplace(ByteData.sublistView(fragInfo)), 144 | ); 145 | pass.bindTexture( 146 | fragmentShader.getUniformSlot('base_color_texture'), 147 | Material.whitePlaceholder(baseColorTexture), 148 | sampler: gpu.SamplerOptions( 149 | widthAddressMode: gpu.SamplerAddressMode.repeat, 150 | heightAddressMode: gpu.SamplerAddressMode.repeat, 151 | ), 152 | ); 153 | pass.bindTexture( 154 | fragmentShader.getUniformSlot('emissive_texture'), 155 | Material.whitePlaceholder(emissiveTexture), 156 | sampler: gpu.SamplerOptions( 157 | widthAddressMode: gpu.SamplerAddressMode.repeat, 158 | heightAddressMode: gpu.SamplerAddressMode.repeat, 159 | ), 160 | ); 161 | pass.bindTexture( 162 | fragmentShader.getUniformSlot('metallic_roughness_texture'), 163 | Material.whitePlaceholder(metallicRoughnessTexture), 164 | sampler: gpu.SamplerOptions( 165 | widthAddressMode: gpu.SamplerAddressMode.repeat, 166 | heightAddressMode: gpu.SamplerAddressMode.repeat, 167 | ), 168 | ); 169 | pass.bindTexture( 170 | fragmentShader.getUniformSlot('normal_texture'), 171 | Material.normalPlaceholder(normalTexture), 172 | sampler: gpu.SamplerOptions( 173 | widthAddressMode: gpu.SamplerAddressMode.repeat, 174 | heightAddressMode: gpu.SamplerAddressMode.repeat, 175 | ), 176 | ); 177 | pass.bindTexture( 178 | fragmentShader.getUniformSlot('occlusion_texture'), 179 | Material.whitePlaceholder(occlusionTexture), 180 | sampler: gpu.SamplerOptions( 181 | widthAddressMode: gpu.SamplerAddressMode.repeat, 182 | heightAddressMode: gpu.SamplerAddressMode.repeat, 183 | ), 184 | ); 185 | pass.bindTexture( 186 | fragmentShader.getUniformSlot('radiance_texture'), 187 | env.environmentMap.radianceTexture, 188 | sampler: gpu.SamplerOptions( 189 | minFilter: gpu.MinMagFilter.linear, 190 | magFilter: gpu.MinMagFilter.linear, 191 | widthAddressMode: gpu.SamplerAddressMode.clampToEdge, 192 | heightAddressMode: gpu.SamplerAddressMode.clampToEdge, 193 | ), 194 | ); 195 | pass.bindTexture( 196 | fragmentShader.getUniformSlot('irradiance_texture'), 197 | env.environmentMap.irradianceTexture, 198 | sampler: gpu.SamplerOptions( 199 | minFilter: gpu.MinMagFilter.linear, 200 | magFilter: gpu.MinMagFilter.linear, 201 | widthAddressMode: gpu.SamplerAddressMode.clampToEdge, 202 | heightAddressMode: gpu.SamplerAddressMode.clampToEdge, 203 | ), 204 | ); 205 | pass.bindTexture( 206 | fragmentShader.getUniformSlot('brdf_lut'), 207 | Material.getBrdfLutTexture(), 208 | sampler: gpu.SamplerOptions( 209 | minFilter: gpu.MinMagFilter.linear, 210 | magFilter: gpu.MinMagFilter.linear, 211 | widthAddressMode: gpu.SamplerAddressMode.clampToEdge, 212 | heightAddressMode: gpu.SamplerAddressMode.clampToEdge, 213 | ), 214 | ); 215 | } 216 | 217 | @override 218 | bool isOpaque() { 219 | return baseColorFactor.a == 1; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /lib/src/material/unlit_material.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter_gpu/gpu.dart' as gpu; 4 | import 'package:flutter_scene/src/material/environment.dart'; 5 | import 'package:flutter_scene/src/material/material.dart'; 6 | import 'package:flutter_scene/src/shaders.dart'; 7 | 8 | import 'package:flutter_scene_importer/flatbuffer.dart' as fb; 9 | import 'package:vector_math/vector_math.dart'; 10 | 11 | class UnlitMaterial extends Material { 12 | static UnlitMaterial fromFlatbuffer( 13 | fb.Material fbMaterial, 14 | List textures, 15 | ) { 16 | if (fbMaterial.type != fb.MaterialType.kUnlit) { 17 | throw Exception('Cannot unpack unlit material from non-unlit material'); 18 | } 19 | 20 | UnlitMaterial material = UnlitMaterial(); 21 | 22 | if (fbMaterial.baseColorFactor != null) { 23 | material.baseColorFactor = Vector4( 24 | fbMaterial.baseColorFactor!.r, 25 | fbMaterial.baseColorFactor!.g, 26 | fbMaterial.baseColorFactor!.b, 27 | fbMaterial.baseColorFactor!.a, 28 | ); 29 | } 30 | 31 | if (fbMaterial.baseColorTexture >= 0 && 32 | fbMaterial.baseColorTexture < textures.length) { 33 | material.baseColorTexture = textures[fbMaterial.baseColorTexture]; 34 | } 35 | 36 | return material; 37 | } 38 | 39 | UnlitMaterial({gpu.Texture? colorTexture}) { 40 | setFragmentShader(baseShaderLibrary['UnlitFragment']!); 41 | baseColorTexture = Material.whitePlaceholder(colorTexture); 42 | } 43 | 44 | late gpu.Texture baseColorTexture; 45 | Vector4 baseColorFactor = Colors.white; 46 | double vertexColorWeight = 1.0; 47 | 48 | @override 49 | void bind( 50 | gpu.RenderPass pass, 51 | gpu.HostBuffer transientsBuffer, 52 | Environment environment, 53 | ) { 54 | super.bind(pass, transientsBuffer, environment); 55 | 56 | var fragInfo = Float32List.fromList([ 57 | baseColorFactor.r, baseColorFactor.g, 58 | baseColorFactor.b, baseColorFactor.a, // color 59 | vertexColorWeight, // vertex_color_weight 60 | ]); 61 | pass.bindUniform( 62 | fragmentShader.getUniformSlot("FragInfo"), 63 | transientsBuffer.emplace(ByteData.sublistView(fragInfo)), 64 | ); 65 | pass.bindTexture( 66 | fragmentShader.getUniformSlot('base_color_texture'), 67 | baseColorTexture, 68 | sampler: gpu.SamplerOptions( 69 | widthAddressMode: gpu.SamplerAddressMode.repeat, 70 | heightAddressMode: gpu.SamplerAddressMode.repeat, 71 | ), 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/math_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:vector_math/vector_math.dart'; 4 | 5 | extension Vector3Lerp on Vector3 { 6 | Vector3 lerp(Vector3 to, double weight) { 7 | return Vector3( 8 | x + (to.x - x) * weight, 9 | y + (to.y - y) * weight, 10 | z + (to.z - z) * weight, 11 | ); 12 | } 13 | 14 | Vector3 divided(Vector3 other) { 15 | return Vector3(x / other.x, y / other.y, z / other.z); 16 | } 17 | } 18 | 19 | extension QuaternionSlerp on Quaternion { 20 | double dot(Quaternion other) { 21 | return x * other.x + y * other.y + z * other.z + w * other.w; 22 | } 23 | 24 | Quaternion slerp(Quaternion to, double weight) { 25 | double cosine = dot(to); 26 | if (cosine.abs() < 1.0 - 1e-3 /* epsilon */ ) { 27 | // Spherical interpolation. 28 | double sine = sqrt(1.0 - cosine * cosine); 29 | double angle = atan2(sine, cosine); 30 | double sineInverse = 1.0 / sine; 31 | double c0 = sin((1.0 - weight) * angle) * sineInverse; 32 | double c1 = sin(weight * angle) * sineInverse; 33 | return scaled(c0) + to.scaled(c1); 34 | } else { 35 | // Linear interpolation. 36 | return (scaled(1.0 - weight) + to.scaled(weight)).normalized(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/mesh.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gpu/gpu.dart' as gpu; 2 | import 'package:flutter_scene/src/geometry/geometry.dart'; 3 | import 'package:flutter_scene/src/material/material.dart'; 4 | import 'package:flutter_scene/src/scene_encoder.dart'; 5 | import 'package:vector_math/vector_math.dart'; 6 | 7 | /// Represents a single part of a [Mesh], containing both [Geometry] and [Material] properties. 8 | /// 9 | /// A `MeshPrimitive` defines the [Geometry] and [Material] of one specific part of the model. 10 | /// By combining multiple `MeshPrimitive` objects, a full 3D model can be created, with different 11 | /// parts of the model having different [Geometry] and [Material]. 12 | /// 13 | /// For example, imagine a 3D model of a car. The body of the car, the windows, and the wheels 14 | /// could each be represented by different `MeshPrimitive` objects. The body might have a red 15 | /// paint [Material], the windows a transparent glass [Material], and the wheels a black rubber [Material]. 16 | /// Each of these parts of the car has its own [Geometry] and [Material], and together 17 | /// they form the complete model. 18 | base class MeshPrimitive { 19 | MeshPrimitive(this.geometry, this.material); 20 | 21 | Geometry geometry; 22 | Material material; 23 | } 24 | 25 | /// Defines the shape and appearance of a 3D model in the scene. 26 | /// 27 | /// It consists of a list of [MeshPrimitive] instances, where each primitive 28 | /// contains the [Geometry] and the [Material] to render a specific part of 29 | /// the 3d model. 30 | base class Mesh { 31 | /// Creates a `Mesh` consisting of a single [MeshPrimitive] with the given [Geometry] and [Material]. 32 | Mesh(Geometry geometry, Material material) 33 | : primitives = [MeshPrimitive(geometry, material)]; 34 | 35 | Mesh.primitives({required this.primitives}); 36 | 37 | /// The list of [MeshPrimitive] objects that make up the [Geometry] and [Material] of the 3D model. 38 | final List primitives; 39 | 40 | /// Draws the [Geometry] and [Material] data of each [MeshPrimitive] onto the screen. 41 | /// 42 | /// This method prepares the [Mesh] for rendering by passing its data to a [SceneEncoder]. 43 | /// For skinned meshes, which are typically used in animations, 44 | /// the joint [gpu.Texture] data is also included to ensure proper rendering of animated features. 45 | void render( 46 | SceneEncoder encoder, 47 | Matrix4 worldTransform, 48 | gpu.Texture? jointsTexture, 49 | int jointTextureWidth, 50 | ) { 51 | for (var primitive in primitives) { 52 | primitive.geometry.setJointsTexture(jointsTexture, jointTextureWidth); 53 | encoder.encode(worldTransform, primitive.geometry, primitive.material); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/scene.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | import 'dart:ui' as ui; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter_gpu/gpu.dart' as gpu; 6 | import 'camera.dart'; 7 | import 'material/environment.dart'; 8 | import 'material/material.dart'; 9 | import 'mesh.dart'; 10 | import 'node.dart'; 11 | import 'scene_encoder.dart'; 12 | import 'surface.dart'; 13 | import 'package:vector_math/vector_math.dart'; 14 | 15 | /// Defines a common interface for managing a scene graph, allowing the addition and removal of [Nodes]. 16 | /// 17 | /// `SceneGraph` provides a set of methods that can be implemented by a class 18 | /// to manage a hierarchy of nodes within a 3D scene. 19 | mixin SceneGraph { 20 | /// Add a child node. 21 | void add(Node child); 22 | 23 | /// Add a list of child nodes. 24 | void addAll(Iterable children); 25 | 26 | /// Add a mesh as a child node. 27 | void addMesh(Mesh mesh); 28 | 29 | /// Remove a child node. 30 | void remove(Node child); 31 | 32 | /// Remove all children nodes. 33 | void removeAll(); 34 | } 35 | 36 | enum AntiAliasingMode { none, msaa } 37 | 38 | /// Represents a 3D scene, which is a collection of nodes that can be rendered onto the screen. 39 | /// 40 | /// `Scene` manages the scene graph and handles rendering operations. 41 | /// It contains a root [Node] that serves as the entry point for all nodes in this `Scene`, and 42 | /// it provides methods for adding and removing nodes from the scene graph. 43 | base class Scene implements SceneGraph { 44 | Scene() { 45 | initializeStaticResources(); 46 | root.registerAsRoot(this); 47 | antiAliasingMode = AntiAliasingMode.msaa; 48 | } 49 | 50 | static Future? _initializeStaticResources; 51 | static bool _readyToRender = false; 52 | 53 | AntiAliasingMode _antiAliasingMode = AntiAliasingMode.none; 54 | 55 | set antiAliasingMode(AntiAliasingMode value) { 56 | switch (value) { 57 | case AntiAliasingMode.none: 58 | break; 59 | case AntiAliasingMode.msaa: 60 | if (!gpu.gpuContext.doesSupportOffscreenMSAA) { 61 | debugPrint("MSAA is not currently supported on this backend."); 62 | return; 63 | } 64 | break; 65 | } 66 | 67 | _antiAliasingMode = value; 68 | } 69 | 70 | AntiAliasingMode get antiAliasingMode { 71 | return _antiAliasingMode; 72 | } 73 | 74 | /// Prepares the rendering resources, such as textures and shaders, 75 | /// that are used to display models in this [Scene]. 76 | /// 77 | /// This method ensures all necessary resources are loaded and ready to be used in the rendering pipeline. 78 | /// If the initialization fails, the resources are reset, and the scene 79 | /// will not be marked as ready to render. 80 | /// 81 | /// Returns a [Future] that completes when the initialization is finished. 82 | static Future initializeStaticResources() { 83 | if (_initializeStaticResources != null) { 84 | return _initializeStaticResources!; 85 | } 86 | _initializeStaticResources = Material.initializeStaticResources() 87 | .onError((e, stacktrace) { 88 | log( 89 | 'Failed to initialize static Flutter Scene resources', 90 | error: e, 91 | stackTrace: stacktrace, 92 | ); 93 | _initializeStaticResources = null; 94 | }) 95 | .then((_) { 96 | _readyToRender = true; 97 | }); 98 | return _initializeStaticResources!; 99 | } 100 | 101 | /// The root [Node] of the scene graph. 102 | /// 103 | /// All [Node] objects in the scene are connected to this node, either directly or indirectly. 104 | /// Transformations applied to this [Node] affect all child [Node] objects. 105 | final Node root = Node(); 106 | 107 | /// Handles the creation and management of render targets for this [Scene]. 108 | final Surface surface = Surface(); 109 | 110 | /// Manages the lighting for this [Scene]. 111 | final Environment environment = Environment(); 112 | 113 | @override 114 | void add(Node child) { 115 | root.add(child); 116 | } 117 | 118 | @override 119 | void addAll(Iterable children) { 120 | root.addAll(children); 121 | } 122 | 123 | @override 124 | void addMesh(Mesh mesh) { 125 | final node = Node(mesh: mesh); 126 | add(node); 127 | } 128 | 129 | @override 130 | void remove(Node child) { 131 | root.remove(child); 132 | } 133 | 134 | @override 135 | void removeAll() { 136 | root.removeAll(); 137 | } 138 | 139 | /// Renders the current state of this [Scene] onto the given [ui.Canvas] using the specified [Camera]. 140 | /// 141 | /// The [Camera] provides the perspective from which the scene is viewed, and the [ui.Canvas] 142 | /// is the drawing surface onto which this [Scene] will be rendered. 143 | /// 144 | /// Optionally, a [ui.Rect] can be provided to define a viewport, limiting the rendering area on the canvas. 145 | /// If no [ui.Rect] is specified, the entire canvas will be rendered. 146 | void render(Camera camera, ui.Canvas canvas, {ui.Rect? viewport}) { 147 | if (!_readyToRender) { 148 | debugPrint('Flutter Scene is not ready to render. Skipping frame.'); 149 | debugPrint( 150 | 'You may wait on the Future returned by Scene.initializeStaticResources() before rendering.', 151 | ); 152 | return; 153 | } 154 | 155 | final drawArea = viewport ?? canvas.getLocalClipBounds(); 156 | if (drawArea.isEmpty) { 157 | return; 158 | } 159 | final enableMsaa = _antiAliasingMode == AntiAliasingMode.msaa; 160 | final gpu.RenderTarget renderTarget = surface.getNextRenderTarget( 161 | drawArea.size, 162 | enableMsaa, 163 | ); 164 | 165 | final env = 166 | environment.environmentMap.isEmpty() 167 | ? environment.withNewEnvironmentMap( 168 | Material.getDefaultEnvironmentMap(), 169 | ) 170 | : environment; 171 | 172 | final encoder = SceneEncoder(renderTarget, camera, drawArea.size, env); 173 | root.render(encoder, Matrix4.identity()); 174 | encoder.finish(); 175 | 176 | final gpu.Texture texture = 177 | enableMsaa 178 | ? renderTarget.colorAttachments[0].resolveTexture! 179 | : renderTarget.colorAttachments[0].texture; 180 | final image = texture.asImage(); 181 | canvas.drawImage(image, drawArea.topLeft, ui.Paint()); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /lib/src/scene_encoder.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:flutter_gpu/gpu.dart' as gpu; 4 | import 'package:vector_math/vector_math.dart'; 5 | 6 | import 'package:flutter_scene/src/camera.dart'; 7 | import 'package:flutter_scene/src/geometry/geometry.dart'; 8 | import 'package:flutter_scene/src/material/environment.dart'; 9 | import 'package:flutter_scene/src/material/material.dart'; 10 | 11 | base class _TranslucentRecord { 12 | _TranslucentRecord(this.worldTransform, this.geometry, this.material); 13 | final Matrix4 worldTransform; 14 | final Geometry geometry; 15 | final Material material; 16 | } 17 | 18 | base class SceneEncoder { 19 | SceneEncoder( 20 | gpu.RenderTarget renderTarget, 21 | this._camera, 22 | ui.Size dimensions, 23 | this._environment, 24 | ) { 25 | _cameraTransform = _camera.getViewTransform(dimensions); 26 | _commandBuffer = gpu.gpuContext.createCommandBuffer(); 27 | _transientsBuffer = gpu.gpuContext.createHostBuffer(); 28 | 29 | // Begin the opaque render pass. 30 | _renderPass = _commandBuffer.createRenderPass(renderTarget); 31 | _renderPass.setDepthWriteEnable(true); 32 | _renderPass.setColorBlendEnable(false); 33 | _renderPass.setDepthCompareOperation(gpu.CompareFunction.lessEqual); 34 | } 35 | 36 | final Camera _camera; 37 | final Environment _environment; 38 | late final Matrix4 _cameraTransform; 39 | late final gpu.CommandBuffer _commandBuffer; 40 | late final gpu.HostBuffer _transientsBuffer; 41 | late final gpu.RenderPass _renderPass; 42 | final List<_TranslucentRecord> _translucentRecords = []; 43 | 44 | void encode(Matrix4 worldTransform, Geometry geometry, Material material) { 45 | if (material.isOpaque()) { 46 | _encode(worldTransform, geometry, material); 47 | return; 48 | } 49 | _translucentRecords.add( 50 | _TranslucentRecord(worldTransform, geometry, material), 51 | ); 52 | } 53 | 54 | void _encode(Matrix4 worldTransform, Geometry geometry, Material material) { 55 | _renderPass.clearBindings(); 56 | var pipeline = gpu.gpuContext.createRenderPipeline( 57 | geometry.vertexShader, 58 | material.fragmentShader, 59 | ); 60 | _renderPass.bindPipeline(pipeline); 61 | 62 | geometry.bind( 63 | _renderPass, 64 | _transientsBuffer, 65 | worldTransform, 66 | _cameraTransform, 67 | _camera.position, 68 | ); 69 | material.bind(_renderPass, _transientsBuffer, _environment); 70 | _renderPass.draw(); 71 | } 72 | 73 | void finish() { 74 | _translucentRecords.sort((a, b) { 75 | var aDistance = a.worldTransform.getTranslation().distanceTo( 76 | _camera.position, 77 | ); 78 | var bDistance = b.worldTransform.getTranslation().distanceTo( 79 | _camera.position, 80 | ); 81 | return bDistance.compareTo(aDistance); 82 | }); 83 | _renderPass.setDepthWriteEnable(false); 84 | _renderPass.setColorBlendEnable(true); 85 | // Additive source-over blending. 86 | // Note: Expects premultiplied alpha output from the fragment stage! 87 | _renderPass.setColorBlendEquation( 88 | gpu.ColorBlendEquation( 89 | colorBlendOperation: gpu.BlendOperation.add, 90 | sourceColorBlendFactor: gpu.BlendFactor.one, 91 | destinationColorBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha, 92 | alphaBlendOperation: gpu.BlendOperation.add, 93 | sourceAlphaBlendFactor: gpu.BlendFactor.one, 94 | destinationAlphaBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha, 95 | ), 96 | ); 97 | for (var record in _translucentRecords) { 98 | _encode(record.worldTransform, record.geometry, record.material); 99 | } 100 | _translucentRecords.clear(); 101 | _commandBuffer.submit(); 102 | _transientsBuffer.reset(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/src/shaders.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gpu/gpu.dart' as gpu; 2 | 3 | const String _kBaseShaderBundlePath = 4 | 'packages/flutter_scene/build/shaderbundles/base.shaderbundle'; 5 | 6 | gpu.ShaderLibrary? _baseShaderLibrary; 7 | gpu.ShaderLibrary get baseShaderLibrary { 8 | if (_baseShaderLibrary != null) { 9 | return _baseShaderLibrary!; 10 | } 11 | _baseShaderLibrary = gpu.ShaderLibrary.fromAsset(_kBaseShaderBundlePath); 12 | if (_baseShaderLibrary != null) { 13 | return _baseShaderLibrary!; 14 | } 15 | 16 | throw Exception( 17 | "Failed to load base shader bundle! ($_kBaseShaderBundlePath)", 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/skin.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter_scene/src/node.dart'; 6 | import 'package:vector_math/vector_math.dart'; 7 | import 'package:flutter_scene_importer/flatbuffer.dart' as fb; 8 | import 'package:flutter_gpu/gpu.dart' as gpu; 9 | 10 | int _getNextPowerOfTwoSize(int x) { 11 | if (x == 0) { 12 | return 1; 13 | } 14 | 15 | --x; 16 | 17 | x |= x >> 1; 18 | x |= x >> 2; 19 | x |= x >> 4; 20 | x |= x >> 8; 21 | x |= x >> 16; 22 | 23 | return x + 1; 24 | } 25 | 26 | base class Skin { 27 | final List joints = []; 28 | final List inverseBindMatrices = []; 29 | 30 | static Skin fromFlatbuffer(fb.Skin skin, List sceneNodes) { 31 | if (skin.joints == null || 32 | skin.inverseBindMatrices == null || 33 | skin.joints!.length != skin.inverseBindMatrices!.length) { 34 | throw Exception('Skin data is missing joints or bind matrices.'); 35 | } 36 | 37 | Skin result = Skin(); 38 | for (int jointIndex in skin.joints!) { 39 | if (jointIndex < 0 || jointIndex > sceneNodes.length) { 40 | throw Exception('Skin join index out of range'); 41 | } 42 | sceneNodes[jointIndex].isJoint = true; 43 | result.joints.add(sceneNodes[jointIndex]); 44 | } 45 | 46 | for ( 47 | int matrixIndex = 0; 48 | matrixIndex < skin.inverseBindMatrices!.length; 49 | matrixIndex++ 50 | ) { 51 | final matrix = skin.inverseBindMatrices![matrixIndex].toMatrix4(); 52 | 53 | result.inverseBindMatrices.add(matrix); 54 | } 55 | 56 | return result; 57 | } 58 | 59 | gpu.Texture getJointsTexture() { 60 | // Each joint has a matrix. 1 matrix = 16 floats. 1 pixel = 4 floats. 61 | // Therefore, each joint needs 4 pixels. 62 | int requiredPixels = joints.length * 4; 63 | int dimensionSize = max( 64 | 2, 65 | _getNextPowerOfTwoSize(sqrt(requiredPixels).ceil()), 66 | ); 67 | 68 | gpu.Texture texture = gpu.gpuContext.createTexture( 69 | gpu.StorageMode.hostVisible, 70 | dimensionSize, 71 | dimensionSize, 72 | format: gpu.PixelFormat.r32g32b32a32Float, 73 | ); 74 | // 64 bytes per matrix. 4 bytes per pixel. 75 | Float32List jointMatrixFloats = Float32List( 76 | dimensionSize * dimensionSize * 4, 77 | ); 78 | // Initialize with identity matrices. 79 | for (int i = 0; i < jointMatrixFloats.length; i += 16) { 80 | jointMatrixFloats[i] = 1.0; 81 | jointMatrixFloats[i + 5] = 1.0; 82 | jointMatrixFloats[i + 10] = 1.0; 83 | jointMatrixFloats[i + 15] = 1.0; 84 | } 85 | 86 | for (int jointIndex = 0; jointIndex < joints.length; jointIndex++) { 87 | Node? joint = joints[jointIndex]; 88 | 89 | // Compute a model space matrix for the joint by walking up the bones to the 90 | // skeleton root. 91 | final floatOffset = jointIndex * 16; 92 | while (joint != null && joint.isJoint) { 93 | final Matrix4 matrix = 94 | joint.localTransform * 95 | Matrix4.fromFloat32List( 96 | jointMatrixFloats.sublist(floatOffset, floatOffset + 16), 97 | ); 98 | 99 | jointMatrixFloats.setRange( 100 | floatOffset, 101 | floatOffset + 16, 102 | matrix.storage, 103 | ); 104 | 105 | joint = joint.parent; 106 | } 107 | 108 | // Get the joint transform relative to the default pose of the bone by 109 | // incorporating the joint's inverse bind matrix. The inverse bind matrix 110 | // transforms from model space to the default pose space of the joint. The 111 | // result is a model space matrix that only captures the difference between 112 | // the joint's default pose and the joint's current pose in the scene. This 113 | // is necessary because the skinned model's vertex positions (which _define_ 114 | // the default pose) are all in model space. 115 | final Matrix4 matrix = 116 | Matrix4.fromFloat32List( 117 | jointMatrixFloats.sublist(floatOffset, floatOffset + 16), 118 | ) * 119 | inverseBindMatrices[jointIndex]; 120 | 121 | jointMatrixFloats.setRange(floatOffset, floatOffset + 16, matrix.storage); 122 | } 123 | 124 | texture.overwrite(jointMatrixFloats.buffer.asByteData()); 125 | return texture; 126 | } 127 | 128 | int getTextureWidth() { 129 | return _getNextPowerOfTwoSize(sqrt(joints.length * 4).ceil()); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/src/surface.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_gpu/gpu.dart' as gpu; 4 | 5 | class Surface { 6 | // TODO(bdero): There should be a method on the Flutter GPU context to pull 7 | // this information. 8 | final int _maxFramesInFlight = 2; 9 | // TODO(bdero): There's no need to track whole RenderTargets in a rotating 10 | // list. Only the color texture needs to be swapped out for 11 | // properly synchronizing with the canvas. 12 | final List _renderTargets = []; 13 | int _cursor = 0; 14 | Size _previousSize = const Size(0, 0); 15 | 16 | gpu.RenderTarget getNextRenderTarget(Size size, bool enableMsaa) { 17 | if (size != _previousSize) { 18 | _cursor = 0; 19 | _renderTargets.clear(); 20 | _previousSize = size; 21 | } 22 | if (_cursor == _renderTargets.length) { 23 | final gpu.Texture colorTexture = gpu.gpuContext.createTexture( 24 | gpu.StorageMode.devicePrivate, 25 | size.width.toInt(), 26 | size.height.toInt(), 27 | enableRenderTargetUsage: true, 28 | enableShaderReadUsage: true, 29 | coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture, 30 | ); 31 | final colorAttachment = gpu.ColorAttachment(texture: colorTexture); 32 | if (enableMsaa) { 33 | final gpu.Texture msaaColorTexture = gpu.gpuContext.createTexture( 34 | gpu.StorageMode.deviceTransient, 35 | size.width.toInt(), 36 | size.height.toInt(), 37 | sampleCount: 4, 38 | enableRenderTargetUsage: true, 39 | coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture, 40 | ); 41 | colorAttachment.resolveTexture = colorAttachment.texture; 42 | colorAttachment.texture = msaaColorTexture; 43 | colorAttachment.storeAction = gpu.StoreAction.multisampleResolve; 44 | } 45 | final gpu.Texture depthTexture = gpu.gpuContext.createTexture( 46 | gpu.StorageMode.deviceTransient, 47 | size.width.toInt(), 48 | size.height.toInt(), 49 | sampleCount: enableMsaa ? 4 : 1, 50 | format: gpu.gpuContext.defaultDepthStencilFormat, 51 | enableRenderTargetUsage: true, 52 | coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture, 53 | ); 54 | final renderTarget = gpu.RenderTarget.singleColor( 55 | colorAttachment, 56 | depthStencilAttachment: gpu.DepthStencilAttachment( 57 | texture: depthTexture, 58 | depthClearValue: 1.0, 59 | ), 60 | ); 61 | _renderTargets.add(renderTarget); 62 | } 63 | gpu.RenderTarget result = _renderTargets[_cursor]; 64 | _cursor = (_cursor + 1) % _maxFramesInFlight; 65 | return result; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | 9 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 10 | } 11 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | FLUTTER_SCENE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 4 | cd "${FLUTTER_SCENE_DIR}" || exit 1 5 | 6 | # Copy .pubignore file. 7 | # We do this to prevent issues when publishing the importer package (which 8 | # exists in a child directory). 9 | cp flutter_scene_pubignore .pubignore 10 | 11 | # Publish 12 | flutter pub publish 13 | 14 | # Remove .pubignore file 15 | rm .pubignore 16 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_scene 2 | description: 3D rendering library for Flutter. Currently only supported when Impeller is enabled. 3 | version: 0.9.2-0 4 | repository: https://github.com/bdero/flutter_scene 5 | homepage: https://github.com/bdero/flutter_scene 6 | platforms: 7 | macos: 8 | ios: 9 | android: 10 | windows: 11 | linux: 12 | 13 | environment: 14 | sdk: '>=3.7.0-75.0.dev <4.0.0' 15 | flutter: ">=3.29.0-1.0.pre.242" 16 | 17 | dependencies: 18 | collection: ^1.19.0 19 | flutter: 20 | sdk: flutter 21 | flutter_gpu: 22 | sdk: flutter 23 | flutter_gpu_shaders: ^0.3.0 24 | #flutter_gpu_shaders: 25 | # path: ../flutter_gpu_shaders 26 | #flutter_scene_importer: ^0.9.0-0 27 | flutter_scene_importer: 28 | path: ./importer 29 | native_assets_cli: '>=0.13.0 <0.14.0' 30 | vector_math: ^2.1.4 31 | 32 | dev_dependencies: 33 | flutter_test: 34 | sdk: flutter 35 | flutter_lints: ^5.0.0 36 | test: ^1.25.15 37 | 38 | flutter: 39 | assets: 40 | - build/shaderbundles/base.shaderbundle 41 | - assets/ibl_brdf_lut.png 42 | 43 | # Default environment map. 44 | - assets/royal_esplanade.png 45 | - assets/royal_esplanade_irradiance.png 46 | -------------------------------------------------------------------------------- /screenshots/flutter_scene_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdero/flutter_scene/547df3e718f16ff5b7425dc136e6ca0aca05441d/screenshots/flutter_scene_logo.png -------------------------------------------------------------------------------- /shaders/base.shaderbundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "UnskinnedVertex": { 3 | "type": "vertex", 4 | "file": "shaders/flutter_scene_unskinned.vert" 5 | }, 6 | "SkinnedVertex": { 7 | "type": "vertex", 8 | "file": "shaders/flutter_scene_skinned.vert" 9 | }, 10 | "UnlitFragment": { 11 | "type": "fragment", 12 | "file": "shaders/flutter_scene_unlit.frag" 13 | }, 14 | "StandardFragment": { 15 | "type": "fragment", 16 | "file": "shaders/flutter_scene_standard.frag" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /shaders/flutter_scene_skinned.vert: -------------------------------------------------------------------------------- 1 | uniform FrameInfo { 2 | mat4 model_transform; 3 | mat4 camera_transform; 4 | vec3 camera_position; 5 | float enable_skinning; 6 | float joint_texture_size; 7 | } 8 | frame_info; 9 | 10 | uniform sampler2D joints_texture; 11 | 12 | // This attribute layout is expected to be identical to `SkinnedVertex` within 13 | // `impeller/scene/importer/scene.fbs`. 14 | in vec3 position; 15 | in vec3 normal; 16 | in vec2 texture_coords; 17 | in vec4 color; 18 | in vec4 joints; 19 | in vec4 weights; 20 | 21 | out vec3 v_position; 22 | out vec3 v_normal; 23 | out vec3 v_viewvector; // camera_position - vertex_position 24 | out vec2 v_texture_coords; 25 | out vec4 v_color; 26 | 27 | const int kMatrixTexelStride = 4; 28 | 29 | mat4 GetJoint(float joint_index) { 30 | // The size of one texel in UV space. The joint texture should always be 31 | // square, so the answer is the same in both dimensions. 32 | float texel_size_uv = 1 / frame_info.joint_texture_size; 33 | 34 | // Each joint matrix takes up 4 pixels (16 floats), so we jump 4 pixels per 35 | // joint matrix. 36 | float matrix_start = joint_index * kMatrixTexelStride; 37 | 38 | // The texture space coordinates at the start of the matrix. 39 | float x = mod(matrix_start, frame_info.joint_texture_size); 40 | float y = floor(matrix_start / frame_info.joint_texture_size); 41 | 42 | // Nearest sample the middle of each the texel by adding `0.5 * texel_size_uv` 43 | // to both dimensions. 44 | y = (y + 0.5) * texel_size_uv; 45 | mat4 joint = 46 | mat4(texture(joints_texture, vec2((x + 0.5) * texel_size_uv, y)), 47 | texture(joints_texture, vec2((x + 1.5) * texel_size_uv, y)), 48 | texture(joints_texture, vec2((x + 2.5) * texel_size_uv, y)), 49 | texture(joints_texture, vec2((x + 3.5) * texel_size_uv, y))); 50 | 51 | return joint; 52 | } 53 | 54 | void main() { 55 | mat4 skin_matrix; 56 | if (frame_info.enable_skinning == 1) { 57 | skin_matrix = 58 | GetJoint(joints.x) * weights.x + GetJoint(joints.y) * weights.y + 59 | GetJoint(joints.z) * weights.z + GetJoint(joints.w) * weights.w; 60 | } else { 61 | skin_matrix = mat4(1); // Identity matrix. 62 | } 63 | 64 | vec4 model_position = 65 | frame_info.model_transform * skin_matrix * vec4(position, 1.0); 66 | v_position = model_position.xyz; 67 | gl_Position = frame_info.camera_transform * model_position; 68 | v_viewvector = frame_info.camera_position - v_position; 69 | v_normal = 70 | (mat3(frame_info.model_transform) * mat3(skin_matrix) * normal).xyz; 71 | v_texture_coords = texture_coords; 72 | v_color = color; 73 | } 74 | -------------------------------------------------------------------------------- /shaders/flutter_scene_standard.frag: -------------------------------------------------------------------------------- 1 | uniform FragInfo { 2 | vec4 color; 3 | vec4 emissive_factor; 4 | float vertex_color_weight; 5 | float exposure; 6 | float metallic_factor; 7 | float roughness_factor; 8 | float has_normal_map; 9 | float normal_scale; 10 | float occlusion_strength; 11 | float environment_intensity; 12 | } 13 | frag_info; 14 | 15 | uniform sampler2D base_color_texture; 16 | uniform sampler2D emissive_texture; 17 | uniform sampler2D metallic_roughness_texture; 18 | uniform sampler2D normal_texture; 19 | uniform sampler2D occlusion_texture; 20 | 21 | uniform sampler2D radiance_texture; 22 | uniform sampler2D irradiance_texture; 23 | 24 | uniform sampler2D brdf_lut; 25 | 26 | in vec3 v_position; 27 | in vec3 v_normal; 28 | in vec3 v_viewvector; // camera_position - vertex_position 29 | in vec2 v_texture_coords; 30 | in vec4 v_color; 31 | 32 | out vec4 frag_color; 33 | 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | void main() { 40 | vec4 vertex_color = mix(vec4(1), v_color, frag_info.vertex_color_weight); 41 | vec4 base_color_srgb = texture(base_color_texture, v_texture_coords); 42 | vec3 albedo = SRGBToLinear(base_color_srgb.rgb) * vertex_color.rgb * 43 | frag_info.color.rgb; 44 | float alpha = base_color_srgb.a * vertex_color.a * frag_info.color.a; 45 | // Note: PerturbNormal needs the non-normalized view vector 46 | // (camera_position - vertex_position). 47 | vec3 normal = normalize(v_normal); 48 | if (frag_info.has_normal_map > 0.5) { 49 | normal = 50 | PerturbNormal(normal_texture, normal, v_viewvector, v_texture_coords); 51 | } 52 | 53 | vec4 metallic_roughness = 54 | texture(metallic_roughness_texture, v_texture_coords); 55 | float metallic = metallic_roughness.b * frag_info.metallic_factor; 56 | float roughness = metallic_roughness.g * frag_info.roughness_factor; 57 | 58 | float occlusion = texture(occlusion_texture, v_texture_coords).r; 59 | occlusion = 1.0 - (1.0 - occlusion) * frag_info.occlusion_strength; 60 | 61 | vec3 camera_normal = normalize(v_viewvector); 62 | 63 | vec3 reflectance = mix(vec3(0.04), albedo, metallic); 64 | 65 | // 1 when the surface is facing the camera, 0 when it's perpendicular to the 66 | // camera. 67 | float n_dot_v = max(dot(normal, camera_normal), 0.0); 68 | 69 | vec3 reflection_normal = reflect(camera_normal, normal); 70 | 71 | vec3 fresnel = FresnelSchlickRoughness(n_dot_v, reflectance, roughness); 72 | vec3 indirect_diffuse_factor = 1.0 - fresnel; 73 | indirect_diffuse_factor *= 1.0 - metallic; 74 | 75 | // TODO(bdero): This multiplier is here because the environment look too dim. 76 | // But this might be resolved once we actually support HDRs. 77 | const float kEnvironmentMultiplier = 2.0; 78 | vec3 irradiance = 79 | SRGBToLinear(SampleEnvironmentTexture(irradiance_texture, normal)) * 80 | frag_info.environment_intensity * kEnvironmentMultiplier; 81 | vec3 indirect_diffuse = irradiance * albedo * indirect_diffuse_factor; 82 | 83 | const float kMaxReflectionLod = 4.0; 84 | vec3 prefiltered_color = 85 | SRGBToLinear(SampleEnvironmentTextureLod(radiance_texture, 86 | reflection_normal, 87 | roughness * kMaxReflectionLod) 88 | .rgb) * 89 | frag_info.environment_intensity * kEnvironmentMultiplier; 90 | 91 | // Hack to replace rough surfaces with irradiance because roughness LoDs are 92 | // not being generated yet. 93 | // TODO(bdero): Remove this hack once roughness LoDs are generated. 94 | // float roughness_map = 1 / (1 + exp(-52.3 * (roughness - 0.786))); 95 | // prefiltered_color = mix(prefiltered_color, irradiance, roughness_map); 96 | prefiltered_color = 97 | mix(irradiance, prefiltered_color, pow(1.02 - roughness, 12)); 98 | 99 | float brdf_x = mix(0.0, 0.99, n_dot_v); 100 | float brdf_y = mix(0.0, 0.99, roughness); 101 | vec2 environment_brdf = texture(brdf_lut, vec2(brdf_x, brdf_y)).rg; 102 | vec3 indirect_specular = 103 | prefiltered_color * (fresnel * environment_brdf.x + environment_brdf.y); 104 | 105 | vec3 ambient = (indirect_diffuse + indirect_specular) * occlusion; 106 | 107 | vec3 emissive = 108 | SRGBToLinear(texture(emissive_texture, v_texture_coords).rgb) * 109 | frag_info.emissive_factor.rgb; 110 | 111 | vec3 out_color = ambient + emissive; 112 | 113 | // Tone mapping. 114 | // frag_color = vec4(frag_info.exposure, 0, 0, 0); 115 | out_color = ACESFilmicToneMapping(out_color, frag_info.exposure); 116 | 117 | #ifndef IMPELLER_TARGET_METAL 118 | out_color = pow(out_color, vec3(1.0 / kGamma)); 119 | #endif 120 | 121 | // // Catch-all for unused uniforms (useful when debugging because unused 122 | // //uniforms are automatically culled from the shader). 123 | // frag_color = 124 | // vec4(albedo, alpha) + vec4(normal, 1) + vec4(ambient, 1) + 125 | // vec4(emissive, 1) + 126 | // metallic_roughness // 127 | // * frag_info.color * frag_info.emissive_factor * frag_info.exposure 128 | // * frag_info.metallic_factor * frag_info.roughness_factor * 129 | // frag_info.normal_scale * frag_info.occlusion_strength * 130 | // frag_info.environment_intensity; 131 | 132 | frag_color = vec4(out_color, 1) * alpha; 133 | } 134 | -------------------------------------------------------------------------------- /shaders/flutter_scene_unlit.frag: -------------------------------------------------------------------------------- 1 | uniform FragInfo { 2 | vec4 color; 3 | float vertex_color_weight; 4 | } 5 | frag_info; 6 | 7 | uniform sampler2D base_color_texture; 8 | 9 | in vec3 v_position; 10 | in vec3 v_normal; 11 | in vec3 v_viewvector; // camera_position - vertex_position 12 | in vec2 v_texture_coords; 13 | in vec4 v_color; 14 | 15 | out vec4 frag_color; 16 | 17 | void main() { 18 | vec4 vertex_color = mix(vec4(1), v_color, frag_info.vertex_color_weight); 19 | frag_color = texture(base_color_texture, v_texture_coords) * vertex_color * 20 | frag_info.color; 21 | } 22 | -------------------------------------------------------------------------------- /shaders/flutter_scene_unskinned.vert: -------------------------------------------------------------------------------- 1 | uniform FrameInfo { 2 | mat4 model_transform; 3 | mat4 camera_transform; 4 | vec3 camera_position; 5 | } 6 | frame_info; 7 | 8 | // This attribute layout is expected to be identical to that within 9 | // `impeller/scene/importer/scene.fbs`. 10 | in vec3 position; 11 | in vec3 normal; 12 | in vec2 texture_coords; 13 | in vec4 color; 14 | 15 | out vec3 v_position; 16 | out vec3 v_normal; 17 | out vec3 v_viewvector; // camera_position - vertex_position 18 | out vec2 v_texture_coords; 19 | out vec4 v_color; 20 | 21 | void main() { 22 | vec4 model_position = frame_info.model_transform * vec4(position, 1.0); 23 | v_position = model_position.xyz; 24 | gl_Position = frame_info.camera_transform * model_position; 25 | v_viewvector = frame_info.camera_position - v_position; 26 | v_normal = (mat3(frame_info.model_transform) * normal).xyz; 27 | v_texture_coords = texture_coords; 28 | v_color = color; 29 | } 30 | -------------------------------------------------------------------------------- /shaders/normals.glsl: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /// Normal resolution. 3 | /// See also: http://www.thetenthplanet.de/archives/1180 4 | /// 5 | 6 | mat3 CotangentFrame(vec3 normal, vec3 view_vector, vec2 uv) { 7 | // Get edge vectors of the pixel triangle. 8 | vec3 d_view_x = dFdx(view_vector); 9 | vec3 d_view_y = dFdy(view_vector); 10 | vec2 d_uv_x = dFdx(uv); 11 | vec2 d_uv_y = dFdy(uv); 12 | 13 | // Force the UV derivatives to be non-zero. This is a hack to force correct 14 | // behavior when UV islands are concentrated to a single point. 15 | if (length(d_uv_x) == 0.0) { 16 | d_uv_x = vec2(1.0, 0.0); 17 | } 18 | if (length(d_uv_y) == 0.0) { 19 | d_uv_y = vec2(0.0, 1.0); 20 | } 21 | 22 | // Solve the linear system. 23 | vec3 view_y_perp = cross(d_view_y, normal); 24 | vec3 view_x_perp = cross(normal, d_view_x); 25 | vec3 T = view_y_perp * d_uv_x.x + view_x_perp * d_uv_y.x; 26 | vec3 B = view_y_perp * d_uv_x.y + view_x_perp * d_uv_y.y; 27 | 28 | // Construct a scale-invariant frame. 29 | float invmax = inversesqrt(max(dot(T, T), dot(B, B))); 30 | return mat3(T * invmax, B * invmax, normal); 31 | } 32 | 33 | vec3 PerturbNormal(sampler2D normal_tex, vec3 normal, vec3 view_vector, 34 | vec2 texcoord) { 35 | vec3 map = texture(normal_tex, texcoord).xyz; 36 | map = map * 255. / 127. - 128. / 127.; 37 | // map.z = sqrt(1. - dot(map.xy, map.xy)); 38 | // map.y = -map.y; 39 | mat3 TBN = CotangentFrame(normal, -view_vector, texcoord); 40 | return normalize(TBN * map).xyz; 41 | } 42 | -------------------------------------------------------------------------------- /shaders/pbr.glsl: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | const float kPi = 3.14159265358979323846; 4 | 5 | //------------------------------------------------------------------------------ 6 | /// Lighting equation. 7 | /// See also: https://learnopengl.com/PBR/Lighting 8 | /// 9 | 10 | const float kGamma = 2.2; 11 | 12 | // Convert from sRGB to linear space. 13 | // This can be removed once Impeller supports sRGB texture inputs. 14 | vec3 SRGBToLinear(vec3 color) { return pow(color, vec3(kGamma)); } 15 | 16 | vec3 FresnelSchlick(float cos_theta, vec3 reflectance) { 17 | return reflectance + 18 | (1.0 - reflectance) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0); 19 | } 20 | 21 | vec3 FresnelSchlickRoughness(float cos_theta, vec3 reflectance, 22 | float roughness) { 23 | return reflectance + (max(vec3(1.0 - roughness), reflectance) - reflectance) * 24 | pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0); 25 | } 26 | 27 | float DistributionGGX(vec3 normal, vec3 half_vector, float roughness) { 28 | float a = roughness * roughness; 29 | float a2 = a * a; 30 | float NdotH = max(dot(normal, half_vector), 0.0); 31 | float NdotH2 = NdotH * NdotH; 32 | 33 | float num = a2; 34 | float denom = (NdotH2 * (a2 - 1.0) + 1.0); 35 | denom = kPi * denom * denom; 36 | 37 | return num / denom; 38 | } 39 | 40 | float GeometrySchlickGGX(float NdotV, float roughness) { 41 | float r = (roughness + 1.0); 42 | float k = (r * r) / 8.0; 43 | 44 | float num = NdotV; 45 | float denom = NdotV * (1.0 - k) + k; 46 | 47 | return num / denom; 48 | } 49 | 50 | float GeometrySmith(vec3 normal, vec3 camera_normal, vec3 light_normal, 51 | float roughness) { 52 | float camera_ggx = 53 | GeometrySchlickGGX(max(dot(normal, camera_normal), 0.0), roughness); 54 | float light_ggx = 55 | GeometrySchlickGGX(max(dot(normal, light_normal), 0.0), roughness); 56 | return camera_ggx * light_ggx; 57 | } 58 | -------------------------------------------------------------------------------- /shaders/texture.glsl: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /// Equirectangular projection. 3 | /// See also: https://learnopengl.com/PBR/IBL/Diffuse-irradiance 4 | /// 5 | 6 | const vec2 kInvAtan = vec2(0.1591, 0.3183); 7 | 8 | vec2 SphericalToEquirectangular(vec3 direction) { 9 | vec2 uv = vec2(atan(direction.z, direction.x), asin(direction.y)); 10 | uv *= kInvAtan; 11 | uv += 0.5; 12 | return uv; 13 | } 14 | 15 | vec3 SampleEnvironmentTexture(sampler2D tex, vec3 direction) { 16 | vec2 uv = SphericalToEquirectangular(direction); 17 | return texture(tex, uv).rgb; 18 | } 19 | 20 | vec3 SampleEnvironmentTextureLod(sampler2D tex, vec3 direction, float lod) { 21 | vec2 uv = SphericalToEquirectangular(direction); 22 | // textureLod is not supported in GLSL ES 1.0. But it doesn't matter anyway, 23 | // since this function will eventually use `textureCubeLod` once environment 24 | // maps are fixed. 25 | //return textureLod(tex, uv, lod).rgb; 26 | return texture(tex, uv).rgb; 27 | } 28 | -------------------------------------------------------------------------------- /shaders/tone_mapping.glsl: -------------------------------------------------------------------------------- 1 | // source: 2 | // https://github.com/selfshadow/ltc_code/blob/master/webgl/shaders/ltc/ltc_blit.fs 3 | vec3 RRTAndODTFit(vec3 v) { 4 | vec3 a = v * (v + 0.0245786) - 0.000090537; 5 | vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081; 6 | return a / b; 7 | } 8 | 9 | // source: 10 | // https://github.com/mrdoob/three.js/blob/364f90e7b0207564ab4e163daa968ce06af8ff99/src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js#L34-L74 11 | vec3 ACESFilmicToneMapping(vec3 color, float exposure) { 12 | // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT 13 | const mat3 ACESInputMat = 14 | mat3(vec3(0.59719, 0.07600, 0.02840), // transposed from source 15 | vec3(0.35458, 0.90834, 0.13383), // 16 | vec3(0.04823, 0.01566, 0.83777)); 17 | 18 | // ODT_SAT => XYZ => D60_2_D65 => sRGB 19 | const mat3 ACESOutputMat = 20 | mat3(vec3(1.60475, -0.10208, -0.00327), // transposed from source 21 | vec3(-0.53108, 1.10813, -0.07276), // 22 | vec3(-0.07367, -0.00605, 1.07602)); 23 | 24 | color *= frag_info.exposure / 0.6; 25 | 26 | color = ACESInputMat * color; 27 | 28 | // Apply RRT and ODT 29 | color = RRTAndODTFit(color); 30 | 31 | color = ACESOutputMat * color; 32 | 33 | // Clamp to [0, 1] 34 | return clamp(color, 0.0, 1.0); 35 | } 36 | -------------------------------------------------------------------------------- /test/node_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_scene/scene.dart'; 2 | import 'package:test/test.dart'; 3 | import 'package:vector_math/vector_math.dart'; 4 | 5 | void main() { 6 | group('globalTransform', () { 7 | test('globalTransform defaults to identity', () { 8 | final node = Node(); 9 | expect(node.globalTransform, Matrix4.identity()); 10 | }); 11 | 12 | test('globalTransform propagates to child node', () { 13 | final parentNode = Node(); 14 | final childNode = Node(); 15 | parentNode.add(childNode); 16 | parentNode.localTransform.setTranslationRaw(1.0, 2.0, 3.0); 17 | 18 | expect(childNode.globalTransform, parentNode.globalTransform); 19 | }); 20 | 21 | test('globalTransform applies local transforms in correct order', () { 22 | final parentNode = Node(); 23 | final childNode = Node(); 24 | parentNode.add(childNode); 25 | 26 | parentNode.localTransform.scale(2.0); 27 | childNode.localTransform.translate(1.0, 2.0, 3.0); 28 | 29 | // In addition to the basis vectors being scaled up, the, the child's 30 | // translation (last column) is magnified by the parent's scale. 31 | final expectedTransform = Matrix4.columns( 32 | Vector4(2.0, 0.0, 0.0, 0.0), 33 | Vector4(0.0, 2.0, 0.0, 0.0), 34 | Vector4(0.0, 0.0, 2.0, 0.0), 35 | Vector4(2.0, 4.0, 6.0, 1.0), 36 | ); 37 | 38 | expect(childNode.globalTransform, expectedTransform); 39 | }); 40 | }); 41 | } 42 | --------------------------------------------------------------------------------