├── .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 |
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 App — Example Game — Docs — FAQ
23 |
24 | ---
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
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 |
--------------------------------------------------------------------------------