MORE RE-ORGANIZING
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: flutter_scene-idioms
|
||||
version: 4
|
||||
description: Write correct flutter_scene code. Use this whenever building 3D with the flutter_scene Dart/Flutter engine (rendering a scene, geometry, materials, lighting, loading a .glb model, animation, custom shaders). It corrects the wrong assumptions models carry from three.js, Godot, and Unity, and names the APIs and traps that are specific to this engine.
|
||||
---
|
||||
|
||||
# Building with flutter_scene
|
||||
|
||||
flutter_scene is a realtime 3D engine for Flutter, built on Flutter GPU. It has a retained scene graph (`Scene` holds `Node`s, nodes carry a `Mesh`), physically based materials, image-based lighting, and a deep post-processing stack.
|
||||
|
||||
**The one thing to internalize: this is not three.js, Godot, or Unity, and the API diverges from all three in specific ways.** Most first-attempt failures come from reaching for another engine's spelling. The corrections below are the highest-value part of this skill; read them before writing code.
|
||||
|
||||
## Do not reach for these (they do not exist or will break the build)
|
||||
|
||||
- **Not the master channel.** flutter_scene runs on **Flutter 3.47 stable or newer**. Do not run `flutter channel master`; it resolves worse, not better.
|
||||
- **Not `--enable-impeller`, not `--enable-experiment=native-assets`.** The run flag is **`--enable-flutter-gpu`** and nothing else. `--enable-experiment=native-assets` actively breaks the build on Dart 3.10+.
|
||||
- **Not `package:vector_math/vector_math_64.dart`.** flutter_scene uses **`package:vector_math/vector_math.dart`**. The `_64` types are a different, incompatible `Vector3`.
|
||||
- **Not `Node.fromAsset(...)`, not `loadModel(...)`.** Load a preprocessed model with **`loadScene('assets/x.glb')`** (returns `Future<Node>`), or a runtime glTF with `Node.fromGlbAsset` / `Node.fromGlbBytes`.
|
||||
- **Not a hand-rolled `CustomPainter` + `Ticker`.** Display a scene with the **`SceneView`** widget; it drives the per-frame loop for you.
|
||||
- **Not `node.position.set(x, y, z)`.** See transforms below.
|
||||
- **Not `.model` files or `buildModels`.** The offline format is `.fsceneb`, produced by the `flutter_scene:init` build hook; you load it by source path with `loadScene`.
|
||||
- **Not the removed `Environment` class.** Environment lighting is `EnvironmentMap` on `Scene.environment`.
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
flutter pub add flutter_scene
|
||||
dart run flutter_scene:init # installs the build hook, sets up assets
|
||||
flutter run --enable-flutter-gpu # native; add -d chrome for web
|
||||
```
|
||||
|
||||
`flutter_scene:init` is required setup, not optional. Rendering is gated on `Scene.initializeStaticResources()`; until it completes the engine prints "Flutter Scene is not ready to render. Skipping frame."
|
||||
|
||||
## Minimal scene (this compiles as-is)
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_scene/scene.dart';
|
||||
import 'package:vector_math/vector_math.dart' as vm;
|
||||
|
||||
void main() => runApp(const MaterialApp(home: CubeView()));
|
||||
|
||||
class CubeView extends StatefulWidget {
|
||||
const CubeView({super.key});
|
||||
@override
|
||||
State<CubeView> createState() => _CubeViewState();
|
||||
}
|
||||
|
||||
class _CubeViewState extends State<CubeView> {
|
||||
final Scene scene = Scene();
|
||||
bool ready = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Geometry and materials touch the shader bundle, so build them only
|
||||
// after the engine's static resources are up.
|
||||
Scene.initializeStaticResources().then((_) {
|
||||
scene.add(Node(
|
||||
mesh: Mesh(CuboidGeometry(vm.Vector3(1, 1, 1)), PhysicallyBasedMaterial()),
|
||||
));
|
||||
if (mounted) setState(() => ready = true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!ready) return const SizedBox.expand();
|
||||
return SceneView(scene, camera: PerspectiveCamera(position: vm.Vector3(2, 2, -4)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
An unset `Scene.environment` still gives image-based lighting (a default studio map is resolved at render), so a bare `PhysicallyBasedMaterial()` is lit without any light setup.
|
||||
|
||||
## Choosing declarative or imperative
|
||||
|
||||
flutter_scene has two ways to build a scene, and picking the wrong one is a structural decision that is expensive to undo later. Choose up front by what the app does, not by which reads nicer.
|
||||
|
||||
**Declarative (inline widgets).** Describe the scene as Flutter widgets under `SceneView.declarative(children: [...])`, using `SceneMesh`, `SceneNode`, and `SceneModel`. Flutter's own rebuild diffing keeps the rendered scene in sync with your widget state, the same way it keeps the UI in sync. Reach for this when app state maps cleanly onto a fixed set of objects on screen and nothing is simulation-like, for example a product configurator where a few `SceneMesh`es track some `setState` values.
|
||||
|
||||
**Imperative (retained scene graph).** Own a `Scene`, add `Node`s, attach `Component`s, and display it with `SceneView(scene, camera: ..., onTick: ...)`. Track state the way you would in another game engine. The cleanest shape is a plain Dart `Game` class that owns the `Scene` and holds the game state, with the scene build and per-frame tick routed into it, and behavior living in custom `Component`s attached to nodes that the engine runs through the component lifecycle hooks. Reach for this for anything with real simulation.
|
||||
|
||||
**Go imperative when** the scene has complex physics, network replication, a character walking around, or procedural generation. Any one of these means imperative.
|
||||
**Stay declarative when** state maps directly to a fixed set of shown objects and nothing ticks or simulates.
|
||||
|
||||
The two interoperate. A mostly-declarative scene can drop to an imperative node where it needs one, and an imperative scene can mount declarative subtrees. See `references/architecture.md` for the `Game`-class pattern, component-driven nodes, and the hybrid seam.
|
||||
|
||||
## The API shape (where it diverges from what you expect)
|
||||
|
||||
**Transforms.** `Node` has `position`, `rotation` (a `Quaternion`), and `scale`, but they are whole-value get/set, not the mutable spelling other engines use. Assign the whole vector (`node.position = vm.Vector3(0, 1, 0)` or `node.position += ...`). The getters return copies, so `node.position.x = 5` does nothing and throws in debug. For a raw matrix edit use `node.localTransform = matrix` or `node.mutateLocalTransform((m) => m.translateByVector3(...))`; a bare in-place edit of `node.localTransform` never moves the node, because the cache is not told.
|
||||
|
||||
**Geometry.** Ten built-in primitives (`CuboidGeometry`, `SphereGeometry`, `IcosphereGeometry`, `CylinderGeometry` with separate top/bottom radii so cones are free, `CapsuleGeometry`, `TorusGeometry`, `PlaneGeometry`, `DiscGeometry`, `RingGeometry`, `WedgeGeometry`), plus swept geometry (`ExtrudeGeometry`, `TubeGeometry`, `RibbonGeometry`), lines (`PolylineGeometry`), and `GeometryBuilder`/`MeshData` for custom meshes. Do not hand-pack a `ByteData` vertex buffer before checking these.
|
||||
|
||||
**Materials.** `PhysicallyBasedMaterial` (base color, metallic, roughness, normal, emissive, plus clearcoat/sheen/transmission/etc.), `UnlitMaterial`, `ShaderMaterial` for custom shaders. Texture slots take a `TextureSource` (from `loadTexture(path)`), not a raw `gpu.Texture`.
|
||||
|
||||
**Camera.** `PerspectiveCamera(position: ..., target: ...)`. There is no orthographic camera built in.
|
||||
|
||||
## What you are probably underestimating (it is all here)
|
||||
|
||||
Models trained on older or thinner information assume flutter_scene has no lighting, no shadows, and no post-processing. It has all of it. Before hand-rolling any of these, know they exist: **directional/point/spot/area lights, shadows (PCSS, contact shadows), GTAO ambient occlusion, screen-space reflections, parallax-corrected reflection probes, SSGI, depth of field, god rays, fog, auto exposure, LUT color grading, bloom, lens flares, MSAA/SMAA/FXAA, tone mapping, instancing, LOD.** See `references/what-exists.md` for the full surface with the class names.
|
||||
|
||||
## Traps that fail silently (wrong pixels, no error)
|
||||
|
||||
- **Custom `ShaderMaterial` output is linear HDR premultiplied by alpha.** No tone mapping or gamma in your shader; the `ResolvePass` applies exposure, tone mapping, and the display transform. Linearize sRGB texture samples yourself. See `MATERIALS.md`.
|
||||
- **Never hand-roll a per-triangle winding flip to fix glTF orientation.** The importers handle the coordinate conversion; a manual flip leaves normals and IBL wrong.
|
||||
- **Do not emit a vertex buffer at the wrong stride.** Unskinned is 72 bytes/vertex, skinned is 104; the attribute order is fixed. Use `GeometryBuilder`, do not guess the layout.
|
||||
|
||||
## More depth
|
||||
|
||||
- `references/architecture.md` for the declarative-vs-imperative choice in depth, the `Game`-class pattern, component-driven nodes, and hybrid interop.
|
||||
- `references/what-exists.md` for the full API surface (the false-absence fix).
|
||||
- `references/traps.md` for the complete silent-failure list.
|
||||
- The repo-root `MATERIALS.md` for the custom-shader contract.
|
||||
|
||||
## Keeping this skill current
|
||||
|
||||
This skill ships inside the flutter_scene package, so upgrading flutter_scene can carry a newer revision of it than the copy installed in the project. To check, run:
|
||||
|
||||
```sh
|
||||
dart run flutter_scene:skills --check
|
||||
```
|
||||
|
||||
It reports the installed and bundled skill versions and exits non-zero when an update is available. If the installed flutter_scene ships a newer skill than what is installed, tell the user, since they are working against out-of-date guidance, and offer to update it with `dart run flutter_scene:skills` (which touches only the skill, not their build hook or pubspec). Worth a check when you start substantial flutter_scene work or when the user mentions upgrading the package.
|
||||
@@ -0,0 +1,297 @@
|
||||
# Structuring a flutter_scene app
|
||||
|
||||
Which API to build the scene with, and how to combine them. The short version is in SKILL.md;
|
||||
this is the depth, with patterns that compile against 0.22.0.
|
||||
|
||||
flutter_scene exposes the same scene graph two ways. The declarative widgets describe it as Flutter
|
||||
widgets that rebuild-diff into the graph; the imperative API hands you the retained `Scene`/`Node`/
|
||||
`Component` graph directly. They are not competing renderers, they drive the same engine. The choice
|
||||
is about how your app tracks state, and it is worth making deliberately because reworking a large
|
||||
scene from one to the other is a rewrite.
|
||||
|
||||
---
|
||||
|
||||
## The decision
|
||||
|
||||
Go **declarative** when app state maps directly onto a fixed set of shown objects and nothing
|
||||
simulates. A product configurator, a data-driven diagram, a few models whose transforms follow some
|
||||
`setState` values. Flutter already owns the state, and the widgets keep the scene tracking it for
|
||||
free.
|
||||
|
||||
Go **imperative** when the scene simulates. Complex physics, network replication, a character
|
||||
walking around under input, procedural generation. Any one of these means imperative. Here the
|
||||
scene's state is the app's state, it changes every frame, and you want to own the loop rather than
|
||||
express each frame as a widget rebuild.
|
||||
|
||||
If you are unsure, ask whether anything in the scene changes on its own between user actions. If yes,
|
||||
imperative. If the scene only changes when the user changes a value, declarative.
|
||||
|
||||
---
|
||||
|
||||
## Declarative
|
||||
|
||||
`SceneView.declarative` owns an internal `Scene`; its `children` are the whole scene description.
|
||||
|
||||
```dart
|
||||
class Configurator extends StatefulWidget {
|
||||
const Configurator({super.key});
|
||||
@override
|
||||
State<Configurator> createState() => _ConfiguratorState();
|
||||
}
|
||||
|
||||
class _ConfiguratorState extends State<Configurator> {
|
||||
// Build engine objects once, not per rebuild. Constructing GPU resources
|
||||
// every build is the main performance hazard of the declarative layer.
|
||||
final Geometry _geometry = CuboidGeometry(vm.Vector3(1, 1, 1));
|
||||
final PhysicallyBasedMaterial _material = PhysicallyBasedMaterial();
|
||||
double _spin = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
Expanded(
|
||||
child: SceneView.declarative(
|
||||
camera: PerspectiveCamera(position: vm.Vector3(2, 2, -4)),
|
||||
children: [
|
||||
SceneMesh(
|
||||
geometry: _geometry,
|
||||
material: _material,
|
||||
rotation: vm.Quaternion.axisAngle(vm.Vector3(0, 1, 0), _spin),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
value: _spin,
|
||||
max: 6.28,
|
||||
onChanged: (v) => setState(() => _spin = v),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The scene tracks widget state through the normal rebuild path. Note two things the example shows:
|
||||
engine objects (`geometry`, `material`) are created once and held as fields (they are diffed by
|
||||
identity, and rebuilding them every frame is the classic mistake), while cheap value props
|
||||
(`rotation`) are fine to pass fresh each build.
|
||||
|
||||
Declarative building blocks (all under `SceneView.declarative` or a `SceneView` with `children`):
|
||||
|
||||
- `SceneMesh(geometry:, material:, ...)` a node with a mesh.
|
||||
- `SceneNode(...)` a bare transform node, for grouping children.
|
||||
- `SceneModel('assets/x.glb', animations: [...])` a loaded model (runtime glTF path).
|
||||
- `SceneSubtree(parent:, children:)` mounts children under a given imperative `Node`.
|
||||
- Every node widget takes `position`/`rotation`/`scale` (or a full `transform`), `visible`,
|
||||
`components:` (attach imperative `Component`s), `controller:` (a `SceneNodeController` handle), and
|
||||
`children:`.
|
||||
|
||||
---
|
||||
|
||||
## Imperative
|
||||
|
||||
Own the `Scene`, add `Node`s, attach `Component`s, display with `SceneView(scene, camera:, onTick:)`.
|
||||
For anything beyond a demo, do not scatter this across a `StatefulWidget`. Put it in a plain Dart
|
||||
class that owns the scene and the game state, and keep the widget thin.
|
||||
|
||||
```dart
|
||||
// Pure Dart, no Flutter import. Owns the scene and the game state.
|
||||
class Game {
|
||||
final Scene scene = Scene();
|
||||
late final Node player;
|
||||
|
||||
Future<void> load() async {
|
||||
await Scene.initializeStaticResources();
|
||||
|
||||
// The camera lives in the scene as a node, not on the widget. A camera
|
||||
// node's transform is its view: the translation is the eye, local +Z is
|
||||
// the look direction, +Y is up. lookAtFrom sets both at once, so there is
|
||||
// no view-matrix math to hand-roll.
|
||||
final cameraNode = Node()
|
||||
..addComponent(CameraComponent(activateOnMount: true))
|
||||
..lookAtFrom(vm.Vector3(0, 3, -8), vm.Vector3.zero());
|
||||
scene.add(cameraNode);
|
||||
// activateOnMount makes this the scene's primary camera when the node
|
||||
// mounts, so SceneView needs no `camera:` argument. (The first mounted
|
||||
// camera auto-promotes anyway; this states the intent explicitly, and is
|
||||
// how you pick one when several cameras exist.)
|
||||
|
||||
player = Node(mesh: Mesh(CuboidGeometry(vm.Vector3(1, 1, 1)),
|
||||
PhysicallyBasedMaterial()));
|
||||
player.addComponent(PlayerController());
|
||||
scene.add(player);
|
||||
}
|
||||
|
||||
// Per-frame app logic that is not tied to one node. Component updates run
|
||||
// on their own (see below), so this is for whole-game concerns.
|
||||
void tick(double dt) {
|
||||
// advance timers, spawn waves, read input, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// Thin widget: builds the game, forwards ticks, renders the scene.
|
||||
class GameView extends StatefulWidget {
|
||||
const GameView({super.key});
|
||||
@override
|
||||
State<GameView> createState() => _GameViewState();
|
||||
}
|
||||
|
||||
class _GameViewState extends State<GameView> {
|
||||
final Game game = Game();
|
||||
bool _ready = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
game.load().then((_) {
|
||||
if (mounted) setState(() => _ready = true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_ready) return const SizedBox.expand();
|
||||
// No `camera:` here: the view resolves the scene's active camera, which is
|
||||
// the CameraComponent added in Game.load. Resolution order is the explicit
|
||||
// `camera:` (absent), then `cameraBuilder`, then `scene.camera` (the active
|
||||
// CameraComponent), then a default camera.
|
||||
return SceneView(
|
||||
game.scene,
|
||||
onTick: (elapsed, dt) => game.tick(dt),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### The active camera
|
||||
|
||||
The scene owns which camera is active, and there are three levers:
|
||||
|
||||
- **`CameraComponent(activateOnMount: true)`** (above) selects this camera when its node mounts.
|
||||
- **`cameraComponent.makeActive()`** switches to it at runtime, for example a chase-cam to a
|
||||
cutscene camera. Before its node mounts the choice is deferred and applied on mount.
|
||||
- **`scene.camera = someCamera`** sets any `Camera` as the override directly, and `scene.camera`
|
||||
reads the active one back.
|
||||
|
||||
With no camera set at all, the first mounted `CameraComponent` auto-promotes, and a scene with none
|
||||
still renders through a default camera. Move or rotate a `CameraComponent`'s node to move the view;
|
||||
the `NodeCamera` reads the node's world transform live each frame. Aim it with `node.lookAt(target)`
|
||||
(rotate toward a world point) or `node.lookAtFrom(eye, target)` (position and aim in one call); +Z is
|
||||
the forward axis, so the same helpers aim lights and imported models. A follow-cam is then a one-line
|
||||
component that calls `node.lookAtFrom(...)` in `update` each frame.
|
||||
|
||||
### Camera controllers (interactive cameras)
|
||||
|
||||
For a user-controlled camera, do not hand-roll the drag/scroll/key math: attach a camera controller
|
||||
component to the camera node. `OrbitCameraController` (turntable around a target, drag rotates, scroll
|
||||
dollies), `FlyCameraController` (WASD + drag free flight, `moveVertical: false` gives grounded
|
||||
first-person), and `FollowCameraController` (third-person that eases behind a target node). Each holds
|
||||
the camera state, eases toward it with frame-rate-independent smoothing, clamps pitch so the view
|
||||
never flips, and writes the node via `lookAtFrom`.
|
||||
|
||||
Wire input with the `CameraControls` widget wrapping the view; it forwards Flutter gestures and keys
|
||||
to the controller. `SceneView` itself has no camera-input knobs, so nothing camera-specific leaks into
|
||||
it.
|
||||
|
||||
```dart
|
||||
final camera = Node()
|
||||
..addComponent(CameraComponent(activateOnMount: true))
|
||||
..addComponent(OrbitCameraController(target: vm.Vector3.zero(), distance: 8));
|
||||
scene.add(camera);
|
||||
|
||||
// In build:
|
||||
return CameraControls(
|
||||
controller: camera.getComponent<OrbitCameraController>()!,
|
||||
child: SceneView(scene),
|
||||
);
|
||||
```
|
||||
|
||||
The controllers also expose intent methods (`orbitBy`, `dollyBy`, `panBy`, `look`), so an app with its
|
||||
own input handling can drive them without the widget.
|
||||
|
||||
### Behavior lives in components, not in the tick
|
||||
|
||||
The bulk of per-object logic should be custom `Component`s, not a giant `onTick`. A component is
|
||||
attached to a node and the engine runs it through the lifecycle. Crucially, component ticks are
|
||||
driven automatically by the render path, so you do not call them yourself, and `onTick` is only for
|
||||
game-wide concerns that do not belong to a single node.
|
||||
|
||||
```dart
|
||||
class PlayerController extends Component {
|
||||
vm.Vector3 velocity = vm.Vector3.zero();
|
||||
|
||||
@override
|
||||
void onMount() {
|
||||
// node is available here; wire up input, cache references.
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double deltaSeconds) {
|
||||
// `node` is the node this component is attached to.
|
||||
node.mutateLocalTransform(
|
||||
(m) => m.translateByVector3(velocity * deltaSeconds),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Component lifecycle hooks (subclass `Component`, override what you need):
|
||||
|
||||
- `onAttach()` added to a node, before it is in a live scene.
|
||||
- `Future<void> onLoad()` async setup (await assets); mount waits for it.
|
||||
- `onMount()` the node entered a live scene; `node` is usable.
|
||||
- `update(double deltaSeconds)` per rendered frame.
|
||||
- `fixedUpdate(double fixedDt)` fixed-step, driven by the physics accumulator when a `PhysicsWorld`
|
||||
is present. Put physics-coupled logic here, not in `update`.
|
||||
- `onUnmount()` / `onDetach()` teardown.
|
||||
|
||||
This is the structure that scales. A character is a node with a controller component; an enemy is a
|
||||
node with an AI component; a pickup is a node with a trigger component. The `Game` class holds what
|
||||
is genuinely global (score, wave state, the input map), and everything spatial is a component on a
|
||||
node.
|
||||
|
||||
---
|
||||
|
||||
## Hybrid interop
|
||||
|
||||
The two APIs share one graph, so you can mix them at the seam that suits the app.
|
||||
|
||||
**Declarative shell, imperative pockets.** A declarative node accepts `components:`, so an otherwise
|
||||
declarative scene can attach imperative behavior to any node without leaving the widget tree.
|
||||
|
||||
```dart
|
||||
SceneMesh(
|
||||
geometry: _geometry,
|
||||
material: _material,
|
||||
components: [Spinner()], // a custom Component, ticked by the engine
|
||||
)
|
||||
```
|
||||
|
||||
**Imperative scene, declarative subtrees.** `SceneView(scene, children: [...])` mounts declarative
|
||||
widgets over an app-owned scene. Use `SceneSubtree(parent: someNode, children: [...])` to attach a
|
||||
declarative subtree under a specific imperative node, for example UI-like markers that follow a
|
||||
game object.
|
||||
|
||||
```dart
|
||||
SceneView(
|
||||
game.scene,
|
||||
camera: PerspectiveCamera(position: vm.Vector3(0, 3, -8)),
|
||||
children: [
|
||||
SceneSubtree(
|
||||
parent: game.player,
|
||||
children: [SceneModel('assets/hat.glb')],
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
**Reaching an imperative node from a declarative widget.** Pass a `SceneNodeController` as
|
||||
`controller:` and read `controller.node` for the managed `Node` (null while unmounted). This is the
|
||||
escape hatch when a declarative node needs an imperative handle for a one-off operation.
|
||||
|
||||
The rule of thumb: pick the mode that matches how the *majority* of the scene is driven, then use the
|
||||
seam above for the exceptions. Do not build a whole simulation out of declarative widgets to avoid
|
||||
the imperative API, and do not hand-roll a diffing layer over the imperative graph to avoid the
|
||||
declarative one.
|
||||
@@ -0,0 +1,602 @@
|
||||
# flutter_scene silent-failure traps
|
||||
|
||||
Mistakes that produce wrong pixels with no exception and no console message. Each entry gives the
|
||||
mistake, what you see, and what to do instead. Sorted worst-first (most likely to hit, hardest to
|
||||
diagnose from the symptom).
|
||||
|
||||
Some of these are now caught by the engine in version 0.22.0. Those are tagged **[0.22.0 catches
|
||||
this]** with what the engine does, so if you see that error you know what it means. The rest are
|
||||
still silent, so you have to recognize them yourself.
|
||||
|
||||
---
|
||||
|
||||
## 1. Editing a transform in place instead of assigning it
|
||||
|
||||
**Mistake.** `node.localTransform.setTranslation(v)`, `node.localTransform..rotateY(t)`,
|
||||
`node.position.x = 5`, or any edit of the matrix/vector a getter returns. This is the natural
|
||||
`vector_math` style and the first thing most people reach for.
|
||||
|
||||
**Symptom.** The node does not move. Not "moves wrong", nothing happens, forever, including its
|
||||
children and bounds. Reading `node.localTransform`/`node.position` back shows the value you wrote, so
|
||||
the state looks correct while the render disagrees.
|
||||
|
||||
**Do instead.** Assign a fresh value (`node.position = ...`, `node.localTransform = node.localTransform.clone()..translateByVector3(v)`),
|
||||
use the component setters `node.position`/`node.rotation`/`node.scale`, or edit the raw matrix
|
||||
through `node.mutateLocalTransform((m) => m.translateByVector3(v))`, which dirties the cache for you.
|
||||
|
||||
**[0.22.0 catches this]** Debug builds throw a `StateError` naming the node and the fix, both for an
|
||||
in-place `localTransform` edit and for editing a copy returned by `position`/`rotation`/`scale`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Passing a normal or metallic-roughness map as `TextureContent.color`
|
||||
|
||||
**Mistake.** `material.normalTexture = await Texture2D.fromAsset('brick_normal.png')` without
|
||||
`content: TextureContent.normal`. The `content` parameter defaults to `color`.
|
||||
|
||||
**Symptom.** The base mip is fine, so it looks right up close and progressively wrong with distance:
|
||||
normals flatten and skew, roughness reads too smooth at range, specular shimmers. A
|
||||
distance-dependent symptom is nearly the worst case for screenshot-driven iteration.
|
||||
|
||||
**Do instead.** Build non-color maps with the right content: `Texture2D.fromAsset(path, content:
|
||||
TextureContent.normal)` for normal maps, `TextureContent.data` for metallic-roughness, AO, and other
|
||||
linear data. Still silent, so this is on you.
|
||||
|
||||
---
|
||||
|
||||
## 3. Non-uniform scale on a lit mesh
|
||||
|
||||
**Mistake.** Any non-uniform scale on the node or an ancestor, e.g. `node.scale = Vector3(1, 3, 1)`.
|
||||
|
||||
**Symptom.** Lighting, specular, and reflections are wrong across the whole mesh. It reads as a
|
||||
shading or material bug, so it sends you into the materials, never the transform.
|
||||
|
||||
**Do instead.** Use a uniform scale, or bake the non-uniform scale into the geometry with
|
||||
`MeshData.transformed(matrix)` and build a fresh `MeshGeometry` from it. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 4. Moving a skinned mesh node
|
||||
|
||||
**Mistake.** `skinnedNode.localTransform = Matrix4.translation(v)` on a node that carries a `Skin`.
|
||||
|
||||
**Symptom.** The mesh does not move. Worse, it *does* move when the node you transformed happens to
|
||||
be an ancestor of the skeleton's joints, so it looks intermittent across models.
|
||||
|
||||
**Do instead.** glTF requires a skinned mesh node's own transform to be ignored, so the engine passes
|
||||
identity. Move the skeleton root (the common ancestor of `skin.joints`) instead, or parent both the
|
||||
mesh node and the skeleton under a shared node and move that. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 5. Replacing the transform of a runtime-imported model root
|
||||
|
||||
**Mistake.**
|
||||
```dart
|
||||
final model = await Node.fromGlbAsset('assets/ship.glb');
|
||||
model.localTransform = Matrix4.translation(v); // wipes the handedness flip
|
||||
```
|
||||
|
||||
**Symptom.** The model renders mirrored through Z (asymmetric geometry reversed, text backwards) with
|
||||
normals and IBL wrong for the mirrored orientation. It still draws and is not obviously broken.
|
||||
|
||||
**Do instead.** The runtime glTF importer synthesizes a root carrying a `scale(1, 1, -1)` handedness
|
||||
flip. Do not overwrite it. Parent that node under a new `Node` and transform the parent, or
|
||||
pre-multiply your transform by the existing `localTransform`. (The offline `.fscene`/`loadScene` path
|
||||
bakes handedness into the vertices, so its roots are identity and do not have this trap.) Still
|
||||
silent.
|
||||
|
||||
---
|
||||
|
||||
## 6. `EnvironmentMap.fromGpuTextures` with a raw panorama
|
||||
|
||||
**Mistake.**
|
||||
```dart
|
||||
final tex = await gpuTextureFromAsset('assets/panorama.png');
|
||||
scene.environment = EnvironmentMap.fromGpuTextures(prefilteredRadiance: tex);
|
||||
```
|
||||
|
||||
**Symptom.** Every reflective surface (and the sky) shows the top 1/8 of the panorama stretched 8x
|
||||
vertically, cross-fading between slices as roughness varies. Diffuse is fully black.
|
||||
|
||||
**Do instead.** `fromGpuTextures` expects an already-prefiltered radiance atlas, not a plain image.
|
||||
Run the source through `prefilterEquirectRadiance()` first, or just use
|
||||
`EnvironmentMap.fromEquirectImageAsset(assetPath: ...)`/`fromUIImages`, which prefilter and project
|
||||
SH for you. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 7. `ShaderMaterial(cullingMode: none)` for a double-sided custom material
|
||||
|
||||
**Mistake.** `ShaderMaterial(cullingMode: gpu.CullMode.none)`, or setting `doubleSided = true` on a
|
||||
`ShaderMaterial`. The two do not agree.
|
||||
|
||||
**Symptom.** `cullingMode.none` draws back faces in color but leaves them out of the depth prepass, so
|
||||
SSAO, SSR, and contact shadows sample the wrong surface exactly where a back face shows: dark halos,
|
||||
wrong reflections, occlusion bleeding through a leaf card. `doubleSided = true` does the opposite: the
|
||||
color pass still culls while the prepass draws the extra faces.
|
||||
|
||||
**Do instead.** For a truly double-sided `ShaderMaterial`, understand that the color cull and the
|
||||
prepass cull are driven separately today; keep the mesh single-sided where the depth-based effects
|
||||
need to match, or split it. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 8. Caller-supplied `bounds` that do not cover the geometry
|
||||
|
||||
**Mistake.** `MeshGeometry.fromArrays(positions: p, bounds: someAabb)` where the AABB does not contain
|
||||
every position, or `setLocalBounds` with a guessed or stale box.
|
||||
|
||||
**Symptom.** The mesh pops out of existence at some camera angles and reappears at others; part of a
|
||||
large mesh vanishes; shadows disappear before the caster does. Intermittent and view-dependent, so a
|
||||
single screenshot can look fine.
|
||||
|
||||
**Do instead.** Widen the bounds to cover every vertex, or just omit the `bounds` argument and let the
|
||||
constructor scan the positions. An absent bounds is safe (it means always-visible); only a wrong one
|
||||
is dangerous. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 9. Binding a mipless texture to a material
|
||||
|
||||
**Mistake.** `material.baseColorTexture = GpuTextureSource(await gpuTextureFromAsset('brick.png'))`.
|
||||
The helper's own doc even recommends this.
|
||||
|
||||
**Symptom.** Severe minification aliasing: crawling and shimmering on any surface at an angle or
|
||||
distance, sparkling on a metallic-roughness map. `Texture2D.fromAsset` on the same file looks fine, so
|
||||
two seemingly equivalent APIs disagree visually.
|
||||
|
||||
**Do instead.** Use `Texture2D.fromAsset`/`fromImage`/`fromPixels`, which generate a mip chain, or
|
||||
supply a texture you mipped yourself. Reserve `gpuTextureFromAsset` for non-material uses. Still
|
||||
silent.
|
||||
|
||||
---
|
||||
|
||||
## 10. `RenderView.layerMask`/`Node.layers` mismatch, or a zero mask
|
||||
|
||||
**Mistake.** Putting a node on a non-default layer and forgetting the view side (or vice versa), or
|
||||
`layerMask: 0`, or `Node.layers = 2` meaning to select "layer 2" (which is actually `1 << 2 == 4`).
|
||||
|
||||
**Symptom.** A node, a group, or the whole scene is simply absent, with no hint a mask is involved. A
|
||||
`layerMask` of 0 renders nothing.
|
||||
|
||||
**Do instead.** `Node.layers` is a bitmask and is NOT inherited by children, so set it on each node
|
||||
you want the view to see. Use `kRenderLayerAll` to see everything, or a bitmask like `(1 << 2)`.
|
||||
Match the view's `layerMask` to the nodes' `layers`. Still silent (but see #23 for the
|
||||
draws-nothing diagnostic).
|
||||
|
||||
---
|
||||
|
||||
## 11. Mutating a `TextureTransform` in place
|
||||
|
||||
**Mistake.** `material.baseColorTextureTransform.offset.x = 0.5` instead of assigning a fresh
|
||||
`TextureTransform`. Same shape as trap #1, for materials.
|
||||
|
||||
**Symptom.** UV scroll/rotation animation freezes at the first value, but ONLY for materials on the
|
||||
physical-variant path (any material with clearcoat/sheen/transmission/etc). The identical code works
|
||||
on a plain PBR material, so it reads as a shader bug.
|
||||
|
||||
**Do instead.** Assign a new transform each frame: `material.baseColorTextureTransform =
|
||||
TextureTransform(offset: ...)`. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 12. Environment image that is not 2:1 equirectangular
|
||||
|
||||
**Mistake.** Passing a cube cross, a 1:1 angular light probe, or a cropped panorama to any environment
|
||||
entry point. HDRI downloads are not reliably 2:1.
|
||||
|
||||
**Symptom.** The scene is lit from wildly wrong directions, reflections show mirrored or duplicated
|
||||
content, the sky is smeared.
|
||||
|
||||
**Do instead.** Re-project the source to a 2:1 latitude-longitude panorama before loading. Cube
|
||||
crosses and angular probes are not supported. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 13. Hand-built triangles wound clockwise
|
||||
|
||||
**Mistake.** Generating triangles with clockwise winding instead of the standard Counter-Clockwise (CCW)
|
||||
right-handed convention when feeding `MeshGeometry.fromArrays` or `GeometryBuilder`.
|
||||
|
||||
**Symptom.** The mesh is invisible from outside and visible from inside; a closed shape looks hollow
|
||||
or inside-out; lighting is inverted where it shows. ("See-through faces.")
|
||||
|
||||
**Do instead.** flutter_scene's front faces wind COUNTER-CLOCKWISE (CCW) in model space, matching glTF
|
||||
and standard 3D conventions. Ensure triangle indices wind CCW around the outward face normal, or omit
|
||||
`normals` and let `GeometryBuilder` derive them from your winding. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 14. Out-of-range indices in `fromArrays`
|
||||
|
||||
**Mistake.** `MeshGeometry.fromArrays(positions: p /* 100 verts */, indices: [0, 1, 100])`, e.g. from
|
||||
an off-by-one or an index list built against a different vertex array.
|
||||
|
||||
**Symptom.** Stray triangles stretching to the origin or infinity, holes, flicker. On some backends
|
||||
the fetch is clamped and on others it reads adjacent memory, so the symptom differs per backend.
|
||||
|
||||
**Do instead.** Keep every index in `0 .. vertexCount - 1`. (`GeometryBuilder.addTriangle` range-checks
|
||||
for you and throws; the `fromArrays` index path does not.) Still silent on the `fromArrays` path.
|
||||
|
||||
---
|
||||
|
||||
## 15. A `vertexCount` that does not match the buffer in `setVertices`
|
||||
|
||||
**Mistake.** `geometry.setVertices(bufferView, vertexCount)` where `vertexCount` is a byte count, a
|
||||
float count, or a triangle count rather than a vertex count.
|
||||
|
||||
**Symptom.** Too small: part of the mesh is missing. Too large: the draw reads past the buffer, giving
|
||||
stray geometry or a dropped draw depending on backend. The buffer is fine, so the investigation goes
|
||||
to the packing code.
|
||||
|
||||
**Do instead.** `vertexCount` is a count of vertices. Prefer `uploadVertexData` (which validates the
|
||||
stride, see #17) or `fromArrays` over the caller-managed `setVertices` path unless you really own the
|
||||
GPU buffer. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 16. Oversized texture on a low-end device
|
||||
|
||||
**Mistake.** `EnvironmentMap.fromEquirectImageAsset(assetPath: 'pano_16k.hdr', maxWidth: 16384)` or
|
||||
`EnvironmentMap.radianceCubeSize = 4096` on a device whose max texture size is lower.
|
||||
|
||||
**Symptom.** A completely black environment: no IBL, no reflections, black sky. Works on the dev
|
||||
machine, black on a phone.
|
||||
|
||||
**Do instead.** Keep environment and texture sizes within the device limit; lower `maxWidth` or
|
||||
`radianceCubeSize`. Test on the lowest-end target you support. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 17. Hand-packing vertex bytes at the wrong stride
|
||||
|
||||
**Mistake.** `SkinnedGeometry()..uploadVertexData(bytes, vertexCount, indices)` with the wrong stride
|
||||
(a common one is 96 bytes having forgotten UV1, or the legacy 80-byte layout).
|
||||
|
||||
**Symptom.** Washed-out colors, see-through faces, geometry smeared toward the origin.
|
||||
|
||||
**Do instead.** Unskinned vertices are 72 bytes (position 3, normal 3, tex_coords 2, tex_coords_1 2,
|
||||
color 4, tangent 4, all float32), skinned are 104 (+ joints 4, weights 4). Better, do not hand-pack:
|
||||
use `MeshGeometry.fromArrays`, `fromMeshData`, or `GeometryBuilder`.
|
||||
|
||||
**[0.22.0 catches this]** `uploadVertexData` on both `SkinnedGeometry` and `UnskinnedGeometry` now
|
||||
throws an `ArgumentError` when the byte length does not match `vertexCount * stride`, naming the
|
||||
expected layout.
|
||||
|
||||
---
|
||||
|
||||
## 18. Custom attribute length not matching the vertex count
|
||||
|
||||
**Mistake.** `geometry.setCustomAttribute('a_wind', data, components: 3)` where `data` has the wrong
|
||||
length, or set before uploading vertices, or not re-set after a `rebuild` changed the count.
|
||||
|
||||
**Symptom.** The attribute is read at the wrong stride, so every vertex gets a neighbor's value: a
|
||||
displacement shader shears the mesh, a color attribute smears. Nearly right, so hard to spot.
|
||||
|
||||
**Do instead.** `data.length` must equal `vertexCount * components`. Set the attribute after uploading
|
||||
vertices, and re-set it after any rebuild. Also note custom attributes are not fetched by depth/shadow
|
||||
passes, so an attribute-driven displacement will not show in shadows.
|
||||
|
||||
**[0.22.0 catches this]** `setCustomAttribute` now throws an `ArgumentError` on a length mismatch
|
||||
(once the vertex count is known).
|
||||
|
||||
---
|
||||
|
||||
## 19. `UnlitMaterial` with `AlphaMode.mask`
|
||||
|
||||
**Mistake.** `UnlitMaterial(colorTexture: foliage)..alphaMode = AlphaMode.mask` for cutout foliage.
|
||||
|
||||
**Symptom.** No alpha test. Cutout edges render soft and blended, the material goes through the
|
||||
translucent pass, writes no depth, sorts badly against itself, and casts no cutout shadow.
|
||||
|
||||
**Do instead.** `UnlitMaterial` does not implement `mask` (it behaves as `blend`). Use
|
||||
`PhysicallyBasedMaterial` for cutouts, or a `.fmat` unlit material that discards below your cutoff.
|
||||
Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 20. `vertexColorWeight` on a material that took a physical variant
|
||||
|
||||
**Mistake.**
|
||||
```dart
|
||||
final m = PhysicallyBasedMaterial()..vertexColorWeight = 0.0;
|
||||
m.clearcoat = 1.0; // or sheen/transmission/anisotropy/ior != 1.5/any extension texture
|
||||
```
|
||||
|
||||
**Symptom.** Vertex colors snap back to full strength the moment an unrelated extension is enabled.
|
||||
On a vertex-colored import, an abrupt tint change with no plausible cause. (The same gap silently
|
||||
drops `specularAntiAliasingVariance` and `specularAntiAliasingThreshold` on the variant path.)
|
||||
|
||||
**Do instead.** Leave `vertexColorWeight` at 1.0 when using any advanced PBR feature, or drop the
|
||||
extension. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 21. Vertex-stage binding on a `ShaderMaterial` with no vertex shader
|
||||
|
||||
**Mistake.**
|
||||
```dart
|
||||
final m = ShaderMaterial(fragmentShader: frag);
|
||||
m.setUniformBlock('WaveInfo', bytes, stage: ShaderStage.vertex); // never set a vertex shader
|
||||
```
|
||||
|
||||
**Symptom.** The vertex-stage parameter has no effect; geometry stays undisplaced while the fragment
|
||||
stage looks right. Reads as "my vertex shader is not running."
|
||||
|
||||
**Do instead.** Pass a `vertexShader` (and `skinnedVertexShader`/`depthVertexShader` for those mesh
|
||||
kinds) to the constructor before binding vertex-stage blocks, or bind the block on
|
||||
`ShaderStage.fragment`. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 22. A `ShaderMaterial` vertex shader on line/trail/polyline geometry
|
||||
|
||||
**Mistake.** Attaching a `ShaderMaterial` that supplies a vertex shader to a `LineSegmentsGeometry`, a
|
||||
trail, or a polyline.
|
||||
|
||||
**Symptom.** Lines vanish or explode into garbage. The unskinned vertex shader is paired with the
|
||||
line-segments instanced layout and never does the ribbon expansion.
|
||||
|
||||
**Do instead.** These geometries do their vertex expansion in the engine's own shader; a material
|
||||
vertex shader cannot be used with them. Drop the vertex shader for line/trail/polyline geometry, or
|
||||
use a mesh geometry. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 23. Four different causes of a blank frame
|
||||
|
||||
**Mistake.** Any of: a degenerate camera (target equals position, or `up` parallel to the view
|
||||
direction, e.g. a top-down camera left at the default `up`), a field of view passed in degrees
|
||||
(`fovRadiansY: 60`), an inverted or zero frustum, `layerMask: 0`, a zero-area draw region, or
|
||||
rendering before `Scene.isReadyToRender`.
|
||||
|
||||
**Symptom.** The entire scene is empty. Every one of these looks identical, so it is easy to "fix"
|
||||
lighting, materials, and geometry for many iterations before suspecting the camera or the mask.
|
||||
|
||||
**Do instead.** For a top-down/bottom-up camera set `up` to `Vector3(0, 0, 1)` or `Vector3(0, 0,
|
||||
-1)`, not the default `(0, 1, 0)`. Pass FOV in radians (`60 * degrees2Radians`). Keep `near > 0` and
|
||||
`far > near`. Give the view a non-zero `layerMask` and a non-empty draw region.
|
||||
|
||||
**[0.22.0 catches most of this]** Degenerate cameras (zero view direction, parallel `up`, degrees-valued
|
||||
FOV, degenerate near/far) assert in debug. And a frame that issues zero draw calls now prints once in
|
||||
debug naming the likely cause (not ready, empty region, no views, no visible meshes, or a layer mask
|
||||
matching nothing).
|
||||
|
||||
---
|
||||
|
||||
## 24. Missing bounds after swapping a primitive's geometry
|
||||
|
||||
**Mistake.** `mesh.primitives[0].geometry = newGeometry` for hand LOD, a rebuilt procedural mesh, or a
|
||||
variant swap.
|
||||
|
||||
**Symptom.** The new geometry is culled against the old geometry's bounds; if it is larger or
|
||||
displaced, it pops in and out exactly like trap #8.
|
||||
|
||||
**Do instead.** Nothing extra is needed anymore.
|
||||
|
||||
**[0.22.0 catches this]** A `Mesh` now recomputes its bounds on its own when a primitive's geometry
|
||||
identity changes, so the manual `markLocalBoundsDirty()` is no longer required.
|
||||
|
||||
---
|
||||
|
||||
## 25. A `.fmat` material that overruns the 15-sampler budget
|
||||
|
||||
**Mistake.** A `lit` or `physical` `.fmat` declaring several `sampler2d` parameters plus
|
||||
`engine_inputs: [scene_color, scene_depth]`, on top of the lit framework's own textures.
|
||||
|
||||
**Symptom.** Geometry disappears on a mid-range Android device while everything is correct on Metal,
|
||||
with no build-time signal. The draw is rejected on GLES drivers reporting the 16-unit minimum.
|
||||
|
||||
**Do instead.** The lit fragment shader budgets 15 fragment samplers. Pack channels into one texture
|
||||
(an ORM-style atlas), drop an `engine_input`, or make the material unlit. Still silent (fails at
|
||||
runtime on the device, not at build).
|
||||
|
||||
---
|
||||
|
||||
## 26. `RenderView.viewport` with a `target` set
|
||||
|
||||
**Mistake.** `RenderView(camera: cam, target: myRenderTexture, viewport: Rect.fromLTWH(0, 0, 0.5, 1))`
|
||||
expecting a half-width render into the texture.
|
||||
|
||||
**Symptom.** The view fills the entire render texture; the passed `viewport` is ignored. Reads as
|
||||
"my viewport math is off."
|
||||
|
||||
**Do instead.** `viewport` is ignored when `target` is set. Size the `RenderTexture` to the region you
|
||||
want, or drop the target to render a sub-rect of the screen. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 27. Scaled or mirrored camera node
|
||||
|
||||
**Mistake.** Attaching a `CameraComponent` to a scaled node, or parenting a camera node under a scaled
|
||||
one.
|
||||
|
||||
**Symptom.** A uniform scale rescales the world in view; a negative scale mirrors the view, so every
|
||||
surface goes back-facing and the scene renders inside out. The camera's reported `forward`/`up` look
|
||||
correct, which makes it hard.
|
||||
|
||||
**Do instead.** A camera node must carry only rotation and translation, and no ancestor may be scaled.
|
||||
Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 28. Hand-built `Skin` with mismatched joints and inverse-bind matrices
|
||||
|
||||
**Mistake.** `skin.joints.add(n)` without a matching `skin.inverseBindMatrices.add(...)` (both are
|
||||
plain mutable lists).
|
||||
|
||||
**Symptom.** Extra inverse bind matrices are silently ignored and the mesh deforms wrongly. (Too few
|
||||
throws a `RangeError`, so only the extra-matrices direction is silent.)
|
||||
|
||||
**Do instead.** Keep the two lists parallel: one inverse bind matrix per joint (`Matrix4.identity()`
|
||||
if the joint's rest pose is the mesh's model space). Imported skins are validated; hand-built ones are
|
||||
not. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 29. Cloning a mesh node whose skeleton is a sibling
|
||||
|
||||
**Mistake.** `meshNode.clone()` when the skeleton lives outside the cloned subtree.
|
||||
|
||||
**Symptom.** The clone renders collapsed or in bind-pose garbage. There is a `debugPrint`, but it says
|
||||
only "Index path formation failed" and names neither the skin nor the consequence.
|
||||
|
||||
**Do instead.** Clone the common ancestor of the mesh node and its skeleton, not the mesh node alone.
|
||||
Still effectively silent.
|
||||
|
||||
---
|
||||
|
||||
## 30. `updateInstanceTransforms(recomputeWinding: false)` with a mirroring edit
|
||||
|
||||
**Mistake.** Editing an instance transform to a negative determinant while asking the engine to skip
|
||||
the parity refresh.
|
||||
|
||||
**Symptom.** Those instances render inside out (front faces culled, back faces lit).
|
||||
|
||||
**Do instead.** Drop `recomputeWinding: false`, or keep every instance edit orientation-preserving
|
||||
(no negative/mirrored scale). Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 31. Flipbook frame count vs atlas grid mismatch
|
||||
|
||||
**Mistake.** A `FlipbookModule(frameCount: 16)` without `emitter.flipbookColumns = 4;
|
||||
emitter.flipbookRows = 4`.
|
||||
|
||||
**Symptom.** Particles sample the wrong atlas cells, or only the first cell; the effect animates but
|
||||
shows the wrong art.
|
||||
|
||||
**Do instead.** Set `flipbookColumns * flipbookRows` equal to the module's `frameCount`. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 32. `LodComponent` blend bands overlapping
|
||||
|
||||
**Mistake.** A `blendRange` larger than the gap between adjacent LOD thresholds.
|
||||
|
||||
**Symptom.** An object sits permanently in the wrong cross-fade pair, dither-blending two levels that
|
||||
should not blend, or skipping a level.
|
||||
|
||||
**Do instead.** Keep `blendRange` smaller than the smallest gap between adjacent `screenSize`
|
||||
thresholds. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 33. `TextureAtlas` grid not matching its texture
|
||||
|
||||
**Mistake.** `TextureAtlas(columns: 16, rows: 16, tileSize: 32, padding: 2, baseColor: eightBySix)`
|
||||
where the grid does not match the image, or an out-of-range tile `index`.
|
||||
|
||||
**Symptom.** Every UV points at the wrong tile. With the default `repeat` addressing, an out-of-range
|
||||
index in release wraps to a different valid-looking tile rather than failing.
|
||||
|
||||
**Do instead.** Make the grid parameters produce exactly the texture's dimensions
|
||||
(`columns * (tileSize + 2*padding)` etc), keep tile indices in range, and set
|
||||
`TextureSampling.maxMipmapLevels` so tiles do not bleed across the padding gutter at high mips. Still
|
||||
silent.
|
||||
|
||||
---
|
||||
|
||||
## 34. `useEnvironment` sky with no cube-radiance variant
|
||||
|
||||
**Mistake.** `ShaderSkySource(fragmentShader: myShader, useEnvironment: true)` with
|
||||
`radianceCubeFragmentShader` left null.
|
||||
|
||||
**Symptom.** The sky contributes no image-based specular on any backend that builds the cube layout
|
||||
(the default nearly everywhere), so the scene loses its reflections.
|
||||
|
||||
**Do instead.** Supply `radianceCubeFragmentShader`, the entry built with
|
||||
`FLUTTER_SCENE_RADIANCE_CUBE`. Debug builds warn about this at bind; release builds are silent, so do
|
||||
not rely on the warning.
|
||||
|
||||
---
|
||||
|
||||
## 35. `radianceCubeFragmentShader` that is not the cube build
|
||||
|
||||
**Mistake.** `ShaderMaterial(fragmentShader: f, radianceCubeFragmentShader: f)` (the same shader
|
||||
twice), or naming the non-cube entry as the cube twin.
|
||||
|
||||
**Symptom.** The engine binds a cubemap into a shader whose sampler is a `sampler2D`: nothing on some
|
||||
backends, garbage specular on others.
|
||||
|
||||
**Do instead.** The cube variant must be the entry compiled with `FLUTTER_SCENE_RADIANCE_CUBE`, whose
|
||||
`prefiltered_radiance` sampler is a `samplerCube`. Pass the distinct `...Cube` entry from your bundle.
|
||||
(The engine can only catch the identical-shader case, and only in debug.) Still effectively silent.
|
||||
|
||||
---
|
||||
|
||||
## 36. Reading `int`/`bool`/`uint` shader members through `setUniformBlockFromFloats`
|
||||
|
||||
**Mistake.** `setUniformBlockFromFloats('FragInfo', [1.0, 0.5])` where the shader declares `int mode;
|
||||
float amount;`.
|
||||
|
||||
**Symptom.** The shader reads `mode` as the float bit pattern of `1.0` (a huge integer), so every
|
||||
`if (mode == 1)` branch misses and the material takes its fallback path. The block size is correct, so
|
||||
nothing complains.
|
||||
|
||||
**Do instead.** Pack integer members with `ByteData.setInt32` at the member's offset, or use a `.fmat`
|
||||
material whose `MaterialParameters` type-checks every assignment. Unenforceable at runtime.
|
||||
|
||||
---
|
||||
|
||||
## 37. A custom fragment shader that tone-maps or writes straight alpha
|
||||
|
||||
**Mistake.** Ending a `ShaderMaterial`/`ShaderSkySource`/`beforeTonemap` `PostEffect` fragment
|
||||
shader with `frag_color = vec4(color, alpha)` (straight alpha) or `pow(color, vec3(1.0/2.2))`
|
||||
(gamma-encoded).
|
||||
|
||||
**Symptom.** Straight alpha gives edge halos and over-bright overlaps. sRGB output is tone-mapped and
|
||||
EOTF-encoded a second time by the resolve pass, giving washed-out low-contrast color that looks like a
|
||||
bad exposure.
|
||||
|
||||
**Do instead.** Output linear HDR premultiplied by alpha. Exposure, tone mapping, and the display
|
||||
encode are applied later by the full-screen resolve pass. When sampling an sRGB texture, linearize
|
||||
first. `.fmat` materials get the premultiply for free. Unenforceable at runtime.
|
||||
|
||||
---
|
||||
|
||||
## 38. `MaterialParameters.copyStateFrom` across a changed layout
|
||||
|
||||
**Mistake.** Applying a re-realized material onto a live instance whose shader layout changed (an
|
||||
editor hot reload where the `.fmat` gained or lost a parameter).
|
||||
|
||||
**Symptom.** Every parameter reverts to its sidecar default while `assignedValues` still reports your
|
||||
overrides, so the inspector shows the right numbers and the render shows the wrong ones.
|
||||
|
||||
**Do instead.** `copyStateFrom` needs both sides to come from the same compiled shader entry; remap by
|
||||
name through `updateFromMetadata` across a layout change instead. Still silent.
|
||||
|
||||
---
|
||||
|
||||
## 39. Environment or widget textures with sub-255 alpha
|
||||
|
||||
**Mistake.** Passing an equirect image carrying alpha below 255 (an unfilled sky dome, a masked
|
||||
panorama) to `fromUIImages`/`fromEquirectImageAsset`. Or, for `WidgetTexture`/`WidgetComponent`,
|
||||
simply using a widget with anti-aliased or translucent edges.
|
||||
|
||||
**Symptom.** For environments, diffuse ambient comes out darker than the specular reflections of the
|
||||
same environment, so objects look lit by two environments. For widget textures, dark halos around
|
||||
anti-aliased text and rounded corners on the zero-copy path (correct on the web readback path, so it
|
||||
reads as a platform quirk).
|
||||
|
||||
**Do instead.** Use opaque (alpha 255) environment sources. The widget-alpha double-multiply is a
|
||||
backend difference you cannot fully control from the API; keep widget content opaque where you can.
|
||||
Still silent.
|
||||
|
||||
---
|
||||
|
||||
## Now caught by the engine, in one place
|
||||
|
||||
For quick reference, these traps became loud in 0.22.0. If you hit one you get an error, not silent
|
||||
wrong pixels:
|
||||
|
||||
- In-place edit of `localTransform`/`position`/`rotation`/`scale` -> throws in debug (#1).
|
||||
- Degenerate camera and a frame that draws nothing -> asserts/prints once in debug (#23).
|
||||
- `uploadVertexData` and `setCustomAttribute` length mismatches -> throw always (#17, #18).
|
||||
- A `Mesh` whose primitive geometry is swapped -> recomputes bounds itself (#24).
|
||||
- An `AnimationClip` binding zero of its channels -> asserts in debug naming the wanted nodes.
|
||||
- A web-backend bind to a shader uniform/texture name the shader does not declare -> throws (matches
|
||||
native), instead of silently sampling whatever was bound last.
|
||||
- Also fixed outright: `Node.clone()` sharing the original's matrix, the skinning joints texture being
|
||||
too narrow for small joint counts, and `ParticleSystem.reset()` not restarting its random stream.
|
||||
@@ -0,0 +1,474 @@
|
||||
# What exists in flutter_scene
|
||||
|
||||
Complete public API inventory (package version 0.22.0). flutter_scene has lights, shadows, PBR
|
||||
materials, instancing, LOD, skeletal animation, and a full post-processing stack. If you think a
|
||||
feature is missing, it is almost certainly here under the name below. Look before you hand-roll.
|
||||
|
||||
The public surface is the explicit `show` lists in `lib/scene.dart` (plus the separate barrels
|
||||
`gpu.dart`, `fscene.dart`, `build_hooks.dart`, `physics.dart`, `audio.dart`). Nothing under
|
||||
`lib/src` is public unless a barrel shows it.
|
||||
|
||||
Import:
|
||||
```dart
|
||||
import 'package:flutter_scene/scene.dart';
|
||||
import 'package:vector_math/vector_math.dart' as vm; // NOT vector_math_64
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Node and scene graph
|
||||
|
||||
`Node` (`base class Node implements SceneGraph`). Construct `Node({String name = '', Matrix4?
|
||||
localTransform, Mesh? mesh})`. A non-null `mesh` is wrapped in a `MeshComponent`.
|
||||
|
||||
Transform API (0.22.0 added the component properties; older docs say only `localTransform` exists):
|
||||
|
||||
| Member | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `position` | `Vector3` get/set | Getter returns a copy; editing the copy in place throws in debug. Assign to move. |
|
||||
| `rotation` | `Quaternion` get/set | Same copy rule. |
|
||||
| `scale` | `Vector3` get/set | Same copy rule. |
|
||||
| `localTransform` | `Matrix4` get/set | Getter returns the LIVE matrix; in-place edit throws in debug on next read. Assign a fresh matrix. |
|
||||
| `mutateLocalTransform(void Function(Matrix4) edit)` | method | Edits in place AND dirties the cache. Correct raw-matrix path. |
|
||||
| `globalTransform` | `Matrix4` get/set | Cached world transform; setter solves for the needed local. |
|
||||
| `lookAt(target, {up})` | method | Orients the node's forward axis (local +Z) at a world-space target; preserves world position and scale. |
|
||||
| `lookAtFrom(eye, target, {up})` | method | Positions at `eye` and aims +Z at `target` in one call (the imperative camera one-liner). |
|
||||
| `Node.lookAtTransform(eye, target, {up})` | static -> `Matrix4` | The `lookAt` basis as a local transform, for `Node(localTransform:)` and declarative `transform:`. |
|
||||
|
||||
+Z is the forward axis engine-wide (cameras, directional/spot lights, imported models), so the
|
||||
lookAt helpers aim any of them. Compose a plain matrix with `vm.Matrix4.translation(v)`,
|
||||
`vm.Matrix4.rotationY(a)`, `vm.Matrix4.compose(t, q, s)`. There is no `translate`/`rotateX` on Node.
|
||||
|
||||
Hierarchy (`SceneGraph` is a mixin): `add`, `addAll`, `addMesh`, `remove`, `removeAll`. `add` throws
|
||||
if the child already has a parent. `parent`, `children`, `detach()`, `getRoot()`, `getDepth()`.
|
||||
|
||||
Lookup: `getChildByName(name, {excludeAnimationPlayers})`, `getChildByNamePath`,
|
||||
`getChildByIndexPath`, static `getNamePath`/`getIndexPath`, `meshNodes`, `clone({recursive = true})`.
|
||||
|
||||
Per-node flags: `visible` (true), `frustumCulled` (true), `layers` (`kRenderLayerDefault`, a 32-bit
|
||||
mask, NOT inherited), `castsShadows` (true, not inherited), `shadowStatic` (false), `raycastable`
|
||||
(true), `highlightColor` (`Vector4?`), `skin` (`Skin?`, set by importers).
|
||||
|
||||
Bounds: `combinedLocalBounds`, `combinedWorldBounds`, `markBoundsDirty()`, `isVisibleTo(camera,
|
||||
size)`. A `null` bounds means always-visible.
|
||||
|
||||
Loading models (see Assets): `Node.fromGlbAsset`, `Node.fromGlbBytes`, `Node.fromGltfBytes`.
|
||||
|
||||
Geometry readback: `extractMeshData({Matrix4? transform})` flattens the subtree to one `MeshData`.
|
||||
Throws on instanced meshes, non-triangle primitives, caller-managed geometry, or an empty subtree.
|
||||
|
||||
`Scene` (`base class Scene implements SceneGraph`, cannot be subclassed). See the render/lighting/
|
||||
post sections. `Mesh(geometry, material)`/`Mesh.primitives({primitives})`; `MeshPrimitive(geometry,
|
||||
material)`; `Mesh.clone()` (shallow, shares geometry+material); `Mesh.localBounds`,
|
||||
`Mesh.markLocalBoundsDirty()`.
|
||||
|
||||
### Camera
|
||||
|
||||
- `PerspectiveCamera({double fovRadiansY = 45 * degrees2Radians, Vector3? position /*(0,0,-5)*/,
|
||||
Vector3? target /*(0,0,0)*/, Vector3? up /*(0,1,0)*/, double fovNear = 0.1, double fovFar =
|
||||
1000.0})`. Field names are `fovNear`/`fovFar`, NOT `near`/`far`.
|
||||
- `PerspectiveCamera.framing(Aabb3 bounds, {direction, fovRadiansY, up, margin = 1.1})`.
|
||||
- `PerspectiveProjection({fovRadiansY, near = 0.1, far = 1000.0})` and abstract `CameraProjection`,
|
||||
`Camera`. Camera helpers: `screenPointToRay`, `worldToScreen`, `getViewMatrix`, `getFrustum`.
|
||||
- There is NO `OrthographicCamera`. Implement `CameraProjection`/`Camera` for other projections.
|
||||
- Node-driven: `CameraComponent({CameraProjection? projection, activateOnMount = false})` ->
|
||||
`toCamera()` gives a `NodeCamera`. Camera node must not be scaled.
|
||||
- Interactive cameras: `CameraController` components attached to the camera node. `OrbitCameraController`
|
||||
(turntable around `target`; `orbitBy`/`dollyBy`/`panBy`/`frame`), `FlyCameraController` (WASD + drag
|
||||
free flight; `moveVertical: false` = grounded first-person; `look`), `FollowCameraController`
|
||||
(third-person easing behind `followTarget` node; `orbitBy`/`dollyBy`). All ease with frame-rate
|
||||
independent `smoothing` (settle seconds), clamp pitch short of vertical, and write the node via
|
||||
`lookAtFrom`. Wire input with the `CameraControls({required controller, enabled, autofocus, child})`
|
||||
widget (Focus + gestures + wheel); `SceneView` has no camera-input params by design.
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
`abstract class Component`. Lifecycle hooks (exact names): `onAttach`, `onLoad` (async), `onMount`,
|
||||
`update(double deltaSeconds)` (NOT `onUpdate`), `fixedUpdate(double)`, `onUnmount`, `onDetach`,
|
||||
`cloneFor(Node)`. Node side: `addComponent`, `removeComponent`, `getComponent<T>()`,
|
||||
`getComponents<T>()`.
|
||||
|
||||
| Component | Constructor/notes |
|
||||
| --- | --- |
|
||||
| `MeshComponent` | `MeshComponent(mesh)`; `mesh` get/set, `refreshMaterials()` |
|
||||
| `InstancedMeshComponent` | `InstancedMeshComponent(instancedMesh)` |
|
||||
| `LodComponent` | `LodComponent(List<LodLevel>, {lodBias = 1.0, hysteresis = 0.1, blendRange = 0.0})`; extends MeshComponent |
|
||||
| `CameraComponent`, `NodeCamera` | see Camera |
|
||||
| `DirectionalLightComponent` | `(light)` aims down node local +Z; `.aimed(light, localDir)`; `.fromLightDirection(light)` |
|
||||
| `PointLightComponent` | `(light)`; `worldPosition` |
|
||||
| `SpotLightComponent` | `(light)`; `worldPosition`, `worldDirection` |
|
||||
| `RectAreaLightComponent` | `(light)`; `worldPosition`, `worldRight`, `worldUp` |
|
||||
| `EnvironmentVolumeComponent` | `({required settings, shape = box, extents, radius = 5.0, blendDistance = 1.0, priority = 0.0, weight = 1.0})` |
|
||||
| `ReflectionProbeComponent` | `({extents = Vector3.all(5), blendDistance = 1.0, priority = 10.0, weight = 1.0, faceResolution = 128, captureOnActivate = true})`; parallax-corrected local reflections in the box; `requestCapture()` re-captures |
|
||||
| `MaterialsVariantsComponent` | No public ctor. `MaterialsVariantsComponent.of(root)`/`.allOf(root)`, then `select(name)`, `variants`, `selected` |
|
||||
| `SemanticsComponent` | `({label, value, hint, button, onTap, ... boundsOverride, properties})` |
|
||||
| `WidgetComponent` | `({required Widget child, required Size size, pixelRatio = 1.0, worldHeight = 1.0, update = everyFrame, input = automatic, ...})`; `.bindOnly(...)` |
|
||||
| `SplatComponent` | `SplatComponent(GaussianSplats)`; `opacity`, `splatScale`, `tint`, `shDegree`, `cropBox`, `cropMode` |
|
||||
| `ParticleEmitterComponent` | `({required system, SpriteMaterial? material})`; `facing`, `flipbookColumns/Rows/Blend`, `paused` |
|
||||
| `MeshParticleEmitterComponent` | `({required system, required List<Geometry> geometries, required material, facing = tumble})` |
|
||||
| `TrailComponent` | `({width = 0.25, lifetime = 0.6, minVertexDistance = 0.05, maxPoints = 48, ...})`; `emitting`, `clear()` |
|
||||
|
||||
`SemanticsComponent`, `SplatComponent`, particle emitters, `TrailComponent`, `WidgetInput`,
|
||||
`MeshParticleFacing`, `LodLevel`, `EnvironmentVolumeShape` are all exported.
|
||||
|
||||
---
|
||||
|
||||
## Geometry
|
||||
|
||||
### Primitives (`primitives.dart`, all factory constructors, all `extends MeshGeometry`)
|
||||
|
||||
| Class | Constructor | Facing/notes |
|
||||
| --- | --- | --- |
|
||||
| `CuboidGeometry` | `CuboidGeometry(Vector3 extents, {debugColors = false})` | positional extents; box `-extents/2..+extents/2`; debugColors off |
|
||||
| `WedgeGeometry` | `WedgeGeometry(Vector3 size)` | triangular prism; base on `y=0`, not Y-centered |
|
||||
| `PlaneGeometry` | `({width = 1.0, depth = 1.0, segmentsX = 1, segmentsZ = 1})` | XZ plane, faces +Y; no collisionShape |
|
||||
| `SphereGeometry` | `({radius = 0.5, segments = 32, rings = 16})` | UV sphere |
|
||||
| `CylinderGeometry` | `({bottomRadius = 0.5, topRadius = 0.5, height = 1.0, radialSegments = 32, heightSegments = 1, bottomCap = true, topCap = true})` | topRadius 0 = cone |
|
||||
| `CapsuleGeometry` | `({radius = 0.5, height = 1.0, radialSegments = 32, capRings = 8})` | `height` is the mid-section; total Y = height + 2*radius |
|
||||
| `TorusGeometry` | `({radius = 0.5, tubeRadius = 0.2, radialSegments = 32, tubularSegments = 16})` | XZ plane |
|
||||
| `DiscGeometry` | `({radius = 0.5, segments = 32})` | XZ, faces +Y |
|
||||
| `RingGeometry` | `({innerRadius = 0.25, outerRadius = 0.5, segments = 32})` | annulus, XZ, +Y |
|
||||
| `IcosphereGeometry` | `({radius = 0.5, subdivisions = 2})` | subdivided icosahedron |
|
||||
|
||||
Every primitive except `PlaneGeometry` has a `Shape get collisionShape`.
|
||||
|
||||
### Swept/procedural (sweep a `ScenePath`; also `BezierPath`, `CatmullRomPath`, `PolylinePath`)
|
||||
|
||||
- `RibbonGeometry(path, {width = 1.0, stations = 64, alignment = RibbonAlignment.ground, up, storage = fixed})`; `updatePath(path)`. `RibbonAlignment` = `ground` | `path`.
|
||||
- `TubeGeometry(path, {radius = 0.5, radialSegments = 12, stations = 64, caps = true, storage})`.
|
||||
- `ExtrudeGeometry(path, {required List<Vector2> profile, stations = 64, caps = true, storage})`.
|
||||
- `PolylineGeometry(List<Vector3> points, {width = 8.0, widthMode = screenPixels, cap = butt, dash, perVertexWidth, perVertexColor})`. INERT until `updateForCamera(camera, viewportSize)` is called every frame. `PolylineWidthMode` = `screenPixels` | `worldUnits`; `PolylineCap` = `butt` | `round`; `DashPattern({dashLength, gapLength, cap})`.
|
||||
- `LineSegmentsGeometry(LineSegmentData segments, {width = 0.01, normalOffset = 0.0})`. `extends Geometry`, GPU-expanded, no per-frame CPU work. For large independent-segment sets.
|
||||
- `BillboardGeometry({capacity = 256})`. `floatsPerInstance = 14`; `BillboardFacing` = `spherical` | `axisLocked` | `velocityStretched`.
|
||||
|
||||
### MeshGeometry, GeometryBuilder, MeshData
|
||||
|
||||
`MeshGeometry.fromArrays({required Float32List positions, Float32List? normals, texCoords,
|
||||
texCoords1, colors, tangents, List<int>? indices, primitiveType = triangle, Aabb3? bounds, storage =
|
||||
fixed, GeometryBufferArena? bufferArena, retainCpuData = true})`. Components per vertex: positions 3,
|
||||
normals 3, texCoords/texCoords1 2, colors/tangents 4. Omitted normals on a triangle list are
|
||||
generated. Omitted indices need a vertex count divisible by 3. `bounds` skips the position scan (it
|
||||
must actually cover every vertex).
|
||||
|
||||
`MeshGeometry.fromMeshData(MeshData data, {storage, bufferArena, retainCpuData})`.
|
||||
|
||||
In-place update (require `GeometryStorage.updatable`, all take `{dirtyStart, dirtyCount}`):
|
||||
`updatePositions`, `updateNormals`, `updateTexCoords`, `updateTexCoords1`, `updateColors`,
|
||||
`updateTangents`. `rebuild({positions, normals, ...})` may change the vertex/index count.
|
||||
`applyMeshData(data)`. `GeometryStorage` = `fixed` | `updatable`.
|
||||
|
||||
`GeometryBuilder({deduplicate = true})`: `normal(v)`, `texCoord(v)`, `texCoord1(v)`, `color(v)`,
|
||||
`tangent(v)`, `addVertex(Vector3) -> int`, `addTriangle(a, b, c)` (throws RangeError on bad index),
|
||||
`packVertices()`, `build({storage, bufferArena, retainCpuData})`. Attribute setters are STICKY.
|
||||
Calling `normal()` once disables generated normals for the whole mesh.
|
||||
|
||||
`MeshData` (isolate-transferable, pure): `MeshData({required positions, required vertexCount,
|
||||
normals, ..., customAttributes})`, `MeshData.build({required positions, ...})` (derives vertexCount,
|
||||
generates normals). Derivations: `triangleCount`, `triangles`, `transformed(Matrix4)` (inverse
|
||||
transpose for normals; a mirror flips winding), `toTriMeshShape()` (hollow static collider),
|
||||
`toConvexHullShape()` (dynamic body), `unweld({attributes})`, `extractEdges({creaseAngleDegrees})`,
|
||||
static `merge(parts)`. `MeshAttributeData(data, {components})`; `UnweldAttribute` = `centroid` |
|
||||
`seed` | `triangleIndex` | `barycentric`; `LineSegmentData({positions, normals})`.
|
||||
|
||||
`Geometry` base: `primitiveType`, `localBounds`, `localBoundingSphere`, `setLocalBounds(aabb,
|
||||
sphere)`, `setVertices(BufferView, vertexCount)`, `setIndices(BufferView, indexType)`,
|
||||
`setCustomAttribute(name, Float32List, {required components})` (1..4; not fetched by depth passes so
|
||||
it does not affect shadows), `uploadVertexData(ByteData, vertexCount, ByteData? indices, {indexType =
|
||||
int16})`, `isReadable`, `extractMeshData()`, `setVertexShader`/`setVertexShaderName`,
|
||||
`setVertexLayout(descriptor, {bindsModelTransform = true})`, `draw(pass, {instanceCount = 1})`.
|
||||
`SkinnedGeometry`/`UnskinnedGeometry` subclasses. `GeometryBufferArena({blockSizeInBytes = 16MB})`.
|
||||
|
||||
Vertex layout: unskinned 72 bytes/18 floats = position(3) normal(3) texture_coords(2)
|
||||
texture_coords_1(2) color(4) tangent(4). Skinned 104 bytes/26 floats = + joints(4) weights(4). Do
|
||||
not hand-pack; use `fromArrays`/`fromMeshData`/`GeometryBuilder`.
|
||||
|
||||
### Instancing and LOD
|
||||
|
||||
`InstancedMesh({required geometry, required material, cullInstances = false,
|
||||
sortTransparentInstances = true})`: `instanceCount`, `addInstance(Matrix4, {Vector4? color}) -> int`
|
||||
(clones the matrix), `setInstanceTransform(i, m)`, `updateInstanceTransforms(update,
|
||||
{recomputeWinding = true})`, `setInstanceColor(i, color)`, `removeInstanceAt(i)`, `clearInstances()`.
|
||||
Attach via `InstancedMeshComponent`.
|
||||
|
||||
`LodLevel({required geometry, required material, required double screenSize})` (screenSize = projected
|
||||
bounding-sphere diameter as a fraction of viewport height, descending, last is the cull floor).
|
||||
Attach via `LodComponent`. Shadow/depth passes always draw level 0.
|
||||
|
||||
---
|
||||
|
||||
## Materials and textures
|
||||
|
||||
`Material` (abstract): `name`, `doubleSided` (false), `depthBias` (0.0), `setFragmentShader`,
|
||||
`setFragmentShaderName(name, {cubeName})`, `setRadianceCubeFragmentShader`, `isOpaque()`.
|
||||
|
||||
### UnlitMaterial
|
||||
|
||||
`UnlitMaterial({TextureSource? colorTexture})`. `baseColorTexture` (field name differs from the ctor
|
||||
arg), `baseColorTextureTransform`, `baseColorTextureTexCoord` (0), `alphaMode` (`opaque`; `mask` not
|
||||
implemented, behaves as blend), `baseColorFactor` (white), `vertexColorWeight` (1.0). Fog applies.
|
||||
|
||||
### PhysicallyBasedMaterial
|
||||
|
||||
`PhysicallyBasedMaterial({baseColorTexture, metallicRoughnessTexture, normalTexture, emissiveTexture,
|
||||
occlusionTexture, EnvironmentMap? environment})`. Every texture slot is a `TextureSource?` with a
|
||||
`<slot>TextureTransform` and `<slot>TextureTexCoord`.
|
||||
|
||||
Core: `baseColorFactor` (white), `vertexColorWeight` (1.0), `metallicFactor` (1.0), `roughnessFactor`
|
||||
(1.0), `normalScale` (1.0), `emissiveFactor` (`Vector4.zero()`), `emissiveStrength` (1.0),
|
||||
`occlusionStrength` (1.0), `environment` (null, falls back to `Scene.environment`), `alphaMode`
|
||||
(opaque), `alphaCutoff` (0.5), `specularAntiAliasingVariance` (0.15), `specularAntiAliasingThreshold`
|
||||
(0.2).
|
||||
|
||||
Advanced KHR_materials_* (setting any flips onto an internal physical-variant shader; each has a
|
||||
`<name>Texture`): `specular` (1.0), `specularColor`, `ior` (1.5), `clearcoat` (0.0),
|
||||
`clearcoatRoughness` (0.0), `clearcoatNormalScale`, `sheenColor` (zero), `sheenRoughness` (0.0),
|
||||
`transmission` (0.0), `diffuseTransmission`, `diffuseTransmissionColor`, `thickness` (0.0),
|
||||
`attenuationDistance` (inf), `attenuationColor`, `dispersion` (0.0), `iridescence` (0.0),
|
||||
`iridescenceIor` (1.3), `iridescenceThicknessMinimum` (100.0), `iridescenceThicknessMaximum` (400.0),
|
||||
`anisotropy` (0.0), `anisotropyRotation` (0.0).
|
||||
|
||||
`isOpaque()` is false when `transmission > 0`, `alphaMode == blend`, or `baseColorFactor.a < 1.0`.
|
||||
|
||||
`AlphaMode` = `opaque` | `mask` | `blend`. `TextureTransform({offset, scale, rotation})` (glTF
|
||||
KHR_texture_transform order).
|
||||
|
||||
### SpriteMaterial
|
||||
|
||||
`SpriteMaterial({TextureSource? colorTexture})`: `colorTexture`, `tint` (white), `blendMode`
|
||||
(`SpriteBlendMode.alpha` | `additive`), `softDepthFade` (0.0), `cameraNearFade` (0.0), `sampler`.
|
||||
Always non-opaque, always cull none.
|
||||
|
||||
### ShaderMaterial (raw GLSL escape hatch)
|
||||
|
||||
`ShaderMaterial({gpu.Shader? fragmentShader, radianceCubeFragmentShader, vertexShader,
|
||||
skinnedVertexShader, depthVertexShader, useEnvironment = false, cullingMode = backFace, windingOrder =
|
||||
counterClockwise, isOpaqueOverride = true})`. `setVertexShader(shader, {variant = unskinned})`,
|
||||
`vertexShaderFor(variant)`, `setUniformBlock(name, ByteData?, {stage = fragment})`,
|
||||
`setUniformBlockFromFloats(name, List<double>, {stage})`, `getUniformBlock`, `uniformBlockNames`,
|
||||
`setTexture(name, texture, {sampler, stage})` (accepts `gpu.Texture`/`Texture2D`/`RenderTexture`),
|
||||
`getTexture`, `textureNames`. `ShaderStage` = `vertex` | `fragment`; `MeshVariant` = `unskinned` |
|
||||
`skinned` | `depth`.
|
||||
|
||||
Fragment shaders MUST output linear HDR premultiplied by alpha (exposure, tone mapping, and the
|
||||
display encode are applied later by the resolve pass). Same contract for `ShaderSkySource` and
|
||||
`PostInsertion.beforeTonemap` effects. std140 packing is by hand.
|
||||
|
||||
### .fmat (declarative, recommended over ShaderMaterial)
|
||||
|
||||
`loadFmatMaterial(sourcePath) -> PreprocessedMaterial`, `loadFmatSky(...) -> PreprocessedSky`.
|
||||
`PreprocessedMaterial`: `parameters` (`MaterialParameters`), `shadingModel`, `environment`.
|
||||
`MaterialParameters` (typed, reflection-backed, throws on wrong type/name): `setFloat`, `setInt`,
|
||||
`setVec2/3/4`, `setMat4`, `setColor(name, Color)`, `setTexture(name, gpu.Texture, {sampler})`,
|
||||
`operator []=`, `parameterNames`, `samplerNames`, `hasUniformBlock`.
|
||||
|
||||
### Textures
|
||||
|
||||
`TextureSource` (interface): implementers are `Texture2D`, `RenderTexture`, `GpuTextureSource`. Every
|
||||
built-in material slot takes a `TextureSource`, not a raw `gpu.Texture`.
|
||||
|
||||
`Texture2D` (factories, generates a mip chain): `Texture2D.fromPixels(Uint8List, w, h, {content =
|
||||
color, sampling})`, `fromImage(ui.Image, {...})`, `fromAsset(String, {content = color, sampling,
|
||||
bundle})`. `TextureContent` = `color` (sRGB) | `data` (linear, e.g. metallic-roughness/AO) | `normal`
|
||||
(vector-averaged). `TextureSampling({mipmaps = true, maxMipmapLevels, minFilter = linear, magFilter =
|
||||
linear, mipFilter = linear, maxAnisotropy = 8, addressMode = repeat})`.
|
||||
|
||||
`GpuTextureSource(gpu.Texture, {sampler})` adapts a raw texture. Barrel helpers:
|
||||
`gpuTextureFromImage`, `gpuTextureFromAsset` (mipless, aliases on materials), `imageFromAsset`,
|
||||
`imageFromBytes`. Cooked `.fstex`: `loadTexture(sourcePath, {package, bundle, sampling}) ->
|
||||
TextureSource`, `releaseTexture`, `clearTextureCache`.
|
||||
|
||||
Custom-shader GPU barrel (`package:flutter_scene/gpu.dart`): `Shader`, `ShaderLibrary`,
|
||||
`loadShaderLibraryAsync` (use this, not `ShaderLibrary.fromAsset` which throws on web),
|
||||
`resolveShaderBundleKey`, `Texture`, `SamplerOptions`, `MinMagFilter`, `MipFilter`,
|
||||
`SamplerAddressMode`, `IndexType`, `VertexFormat`, `VertexStepMode`.
|
||||
|
||||
---
|
||||
|
||||
## Lighting and environment
|
||||
|
||||
Lights (all in `light.dart`, all fields mutable):
|
||||
|
||||
- `DirectionalLight({direction /*(-0.3,-1,-0.2)*/, color, intensity = 3.0, priority = 0, castsShadow
|
||||
= false, cacheStaticShadows = true, shadowFadeRange = 2.0, shadowSoftness = 0.08, shadowCascadeCount
|
||||
= 4, shadowMaxDistance = 150.0, shadowCascadeSplitLambda = 0.6, shadowMapResolution = 1024,
|
||||
shadowDepthBias = 0.02, shadowNormalBias = 0.02, shadowAmbientStrength = 0.0, shadowFilter =
|
||||
rotatedPoisson, shadowCasterFaces = front, contactShadows = false, contactShadowDistance = 0.3,
|
||||
angularRadius = 0.005})`.
|
||||
- `PointLight({color, intensity = 1.0, range = 0.0, falloffExponent = 2.0})`. No shadows.
|
||||
- `SpotLight({color, intensity = 1.0, range = 0.0, falloffExponent = 2.0, direction /*(0,-1,0)*/,
|
||||
innerConeAngle = 0.0, outerConeAngle = pi/4, castsShadow = false, ...})`.
|
||||
- `RectAreaLight({color, intensity = 1.0, width = 1.0, height = 1.0, range = 0.0})`. Local XY plane,
|
||||
emits along +Z, no shadows.
|
||||
- `SunLight(SunSky source, {castsShadow = true, ...})` drives `Scene.directionalLight` from a sky.
|
||||
|
||||
`ShadowCasterFaces` = `front` | `back` | `both`. `DirectionalShadowFilter` = `rotatedPoisson` |
|
||||
`fixedPcf` | `pcss`. `ShadowCascade`, `Lighting` (per-draw state) are exported.
|
||||
|
||||
Scene lighting: `Scene.directionalLight` (`DirectionalLight?`, null = IBL only; honors `direction`;
|
||||
highest-priority one gets cascaded shadows), `Scene.sunLight`, `Scene.environment` (`EnvironmentMap?`,
|
||||
null falls back to `EnvironmentMap.studio()`; for genuinely no IBL use `EnvironmentMap.empty()`),
|
||||
`Scene.environmentIntensity` (1.0), `Scene.environmentTransform` (`Matrix3.identity()`),
|
||||
`Scene.skybox` (`Skybox?`, null = transparent), `Scene.skyEnvironment`.
|
||||
|
||||
### EnvironmentMap
|
||||
|
||||
Carries a prefiltered specular radiance atlas AND SH-9 diffuse coefficients (one texture path, no
|
||||
separate radiance/irradiance). Factories: `.empty()`, `.constantDiffuse(ambientRadiance)`,
|
||||
`.fromGpuTextures({required prefilteredRadiance, diffuseSphericalHarmonics, diffuseShTexture})` (the
|
||||
texture must already be prefiltered), `.fromUIImages({required radianceImage, ...})`,
|
||||
`.fromEquirectHdr({required Float32List linearPixels, w, h, ...})`,
|
||||
`.fromEquirectImageAsset({required assetPath, maxWidth = 4096, ...})` (auto-detects .hdr/.exr/LDR),
|
||||
`.fromEquirectImageBytes(...)`, `.fromSky(SkySource, {...})`, `.studio()` (zero-config default).
|
||||
Deprecated: `.fromAssets` (use `.fromEquirectImageAsset`). Env images must be equirect 2:1.
|
||||
`prefilterEquirectRadiance` is exported. `Scene.loadEnvironment(assetPath, {showSkybox = true,
|
||||
skyBlur = 0.0, intensity, exposure, rotationY, maxWidth = 4096, bundle})` is one-call setup.
|
||||
|
||||
### Skybox/sky sources
|
||||
|
||||
`Skybox(SkySource source, {intensity = 1.0})`. `SkySource` implementers: `EnvironmentSkySource({blurriness
|
||||
= 0.0})`, `ShaderSkySource({fragmentShader, fragmentShaderName, radianceCubeFragmentShader,
|
||||
useEnvironment = false})`, `GradientSkySource({zenithColor, horizonColor, groundColor, sunDirection,
|
||||
sunColor, sunSharpness = 400.0})`, `PhysicalSkySource({sunDirection, sunAngularRadius = 0.0175,
|
||||
rayleighCoefficient = 2.0, mieCoefficient = 0.005, turbidity = 10.0, energy = 1.0, ...})`.
|
||||
`SkyEnvironment(ShaderSkySource, {refresh = manual, interval, faceResolution = 128, equirectWidth =
|
||||
512})`; `SkyEnvironmentRefresh` = `manual` | `interval` | `everyFrame`.
|
||||
|
||||
### Exposure and tone mapping
|
||||
|
||||
`Scene.exposure` (1.0; not 2.0), `Scene.toneMapping` (`ToneMappingMode.pbrNeutral`; also `aces`,
|
||||
`reinhard`, `linear`, `agx`), `Scene.agxWhite` (16.29), `Scene.agxContrast` (1.25). Static
|
||||
`Scene.physicalCameraExposure({required aperture, shutterSpeed, iso})` returns a multiplier to assign
|
||||
to `exposure`.
|
||||
|
||||
---
|
||||
|
||||
## Post-processing
|
||||
|
||||
Every effect is a settings object on `Scene`, off by default, turned on with `enabled`. Environment
|
||||
looks blend via `EnvironmentSettings` (snapshot/lerp of the whole look) and `EnvironmentVolume` /
|
||||
`EnvironmentVolumeComponent` (spatial).
|
||||
|
||||
| Scene field | Type | Key fields (default) | Requires |
|
||||
| --- | --- | --- | --- |
|
||||
| `ambientOcclusion` | `AmbientOcclusionSettings` | `method` (obscurance/`groundTruth`), `radius` (0.33), `intensity` (1.0), `power` (1.5), `bentNormals` (false), `halfResolution` (true), `indirectLight` (0.0 = SSGI), `specularMode` | perspective camera |
|
||||
| `screenSpaceReflections` | `ScreenSpaceReflectionsSettings` | `intensity` (1.0), `maxDistance` (24.4), `thickness` (0.46), `stride` (9.0), `maxSteps` (90), `blur` (0.3), `debugView` | perspective camera |
|
||||
| `fog` | `Fog` | `mode` (`FogMode.exponential`; also none/linear/exponentialSquared), `color`, `density` (0.02), `start`/`end`, needs both `enabled` AND non-none `mode` | any camera |
|
||||
| `godRays` | `GodRaysSettings` | `intensity` (1.0), `density` (0.5), `anisotropy` (0.7), `stepCount` (24), `maxDistance` (200), `color` | shadow-casting DirectionalLight + perspective camera |
|
||||
| `depthOfField` | `DepthOfField` | `focusDistance` (10.0), `fStop` (2.8), `focalLength`, `sensorHeight` (0.024), `bladeCount`, `quality` (low/medium/high) | perspective camera |
|
||||
| `autoExposure` | `AutoExposureSettings` | `strength` (0.55), `compensation`, `minEv` (-4), `maxEv` (4), `speedUp` (3.0), `speedDown` (1.0); multiplies on top of `exposure` | none |
|
||||
| `postProcess` | `PostProcessSettings` | see below | none |
|
||||
|
||||
`AmbientOcclusionMethod` = `obscurance` (McGuire SAO) | `groundTruth` (GTAO). `SpecularAmbientOcclusionMode`
|
||||
= `none` | `simple` | `bentCone`. `SsrDebugView` = composite/reflectedUv/hitMask/normal/confidence/depth.
|
||||
|
||||
`PostProcessSettings` (all sub-settings off by default; mutate the nested objects):
|
||||
- `colorGrading` (`ColorGradingSettings`): `brightness` (1.0), `contrast` (1.0), `saturation` (1.0),
|
||||
`temperature`, `tint`, `lift`/`gamma`/`gain`, `lut` (`ColorLut?`, applies after tone mapping,
|
||||
independent of `enabled`), `lutBlend` (1.0).
|
||||
- `chromaticAberration` (`intensity` 0.2), `vignette` (`intensity` 0.5, `radius` 0.75, `smoothness`
|
||||
0.5), `filmGrain` (`intensity` 0.3), `bloom` (`threshold` 1.0, `intensity` 0.15, `scatter` 0.7,
|
||||
and `lensFlare`: `enabled` false, `intensity` 1.0, `ghostCount` 4, `ghostSpacing` 0.3, `haloRadius`
|
||||
0.35, `haloIntensity` 1.0, `chromaticAberration` 0.005; rides the bloom, needs bloom enabled).
|
||||
- `customEffects` (`List<PostEffect>`).
|
||||
|
||||
`ColorLut.fromCubeString`/`.fromCubeAsset` (Adobe `.cube`, edge 2..64).
|
||||
|
||||
Custom post: `PostEffect({gpu.Shader? fragmentShader, insertion = beforeTonemap, enabled = true,
|
||||
useFrameInfo = false})`, added to `scene.postProcess.customEffects`. Engine binds `uniform sampler2D
|
||||
input_color` at `in vec2 v_uv`. `PostInsertion` = `beforeTonemap` (linear HDR premultiplied) |
|
||||
`afterTonemap` (display-referred).
|
||||
|
||||
---
|
||||
|
||||
## Scene, render, and widgets
|
||||
|
||||
`Scene()` (no args; calls `initializeStaticResources()`, needs a live Flutter GPU context). Methods:
|
||||
`add`, `addAll`, `addMesh`, `remove`, `removeAll`, `update(dt)` (optional), `render(camera, canvas,
|
||||
{viewport, pixelRatio})`, `renderViews(views, canvas, {region, pixelRatio})`, `warmUp(views,
|
||||
{includeOffscreen})`, `raycast(ray, {maxDistance, layerMask, where, includeInvisible})`,
|
||||
`raycastAll(...)`, `addRenderPass`/`removeRenderPass`, `captureRenderGraph({viewIndex, request,
|
||||
timeout})`, `captureEnvironment({required position, faceResolution = 128, equirectWidth = 512,
|
||||
layerMask})` -> `EnvironmentMap` (one-shot static capture; use `ReflectionProbeComponent` for a
|
||||
node-anchored, parallax-corrected, auto-blended probe). Statics: `Scene.initializeStaticResources()`,
|
||||
`Scene.isReadyToRender`, `Scene.physicalCameraExposure`, `Scene.isAntiAliasingModeSupported`,
|
||||
`Scene.effectiveAntiAliasingMode`.
|
||||
|
||||
`Scene.antiAliasingMode` (`AntiAliasingMode.auto` -> msaa or fxaa; also `none`, `msaa`, `fxaa`, `smaa`),
|
||||
`Scene.renderScale` (1.0), `Scene.filterQuality` (`FilterQuality.medium`), `Scene.views`
|
||||
(`List<RenderView>` for RenderTexture targets).
|
||||
|
||||
`RenderView({required Camera camera, RenderTexture? target, Rect? viewport /*normalized 0..1,
|
||||
ignored when target set*/, int layerMask = kRenderLayerAll, order = 0, AntiAliasingMode?
|
||||
antiAliasingMode, double? renderScale, FilterQuality? filterQuality, List<Plane> cullingPlanes})`.
|
||||
`kRenderLayerDefault = 1`, `kRenderLayerAll = 0xFFFFFFFF`.
|
||||
|
||||
`RenderTexture`, `RenderTextureSampling`, `RenderTextureUpdate`, `RenderTextureView(renderTexture,
|
||||
{fit = contain, filterQuality = medium, followLayout = false})`.
|
||||
|
||||
Widgets:
|
||||
- `SceneView(Scene scene, {Camera? camera, SceneCameraBuilder? cameraBuilder, SceneViewsBuilder?
|
||||
viewsBuilder, autoTick = true, pixelRatio, onTick, loading, loadingBuilder, revealMinDuration,
|
||||
warmUp = false, children})`. App-owned scene; does not write scene properties. `camera`,
|
||||
`cameraBuilder`, `viewsBuilder` are mutually exclusive.
|
||||
- `SceneView.declarative({environment, environmentIntensity = 1.0, exposure = 1.0, toneMapping =
|
||||
pbrNeutral, camera, cameraBuilder, viewsBuilder, children, ...})`. View-owned scene.
|
||||
- `SceneViewsBuilder` is exported as of 0.22.0 (older docs list it as a trap; it is public now).
|
||||
- Declarative widgets: `SceneNode`, `SceneMesh`, `SceneModel`, `SceneSubtree`, `SceneNodeHost`,
|
||||
`SceneNodeController`, `SceneModelSource`, `AssetModelSource`, `MemoryModelSource`,
|
||||
`SceneAnimationSpec`. `WidgetTexture`, `WidgetTextureController`, `WidgetUpdatePolicy`. `SceneScope`.
|
||||
- Camera resolution precedence: `camera` -> `cameraBuilder(elapsed)` -> `scene.camera` (or first
|
||||
mounted `CameraComponent`) -> default `PerspectiveCamera()`.
|
||||
|
||||
`CustomRenderPass`, `RenderInput`, `RenderPassContext`, `RenderStage`, `TransientWriter`,
|
||||
`NodeFilter`, `HighlightStyle`, render-graph capture types (`CapturedPass`, `CapturedResource`,
|
||||
`RenderGraphCaptureRequest`, `RenderGraphCaptureResult`) are all exported.
|
||||
|
||||
---
|
||||
|
||||
## Assets and animation
|
||||
|
||||
Setup: `flutter pub add flutter_scene` then `dart run flutter_scene:init`. Enable Flutter GPU with
|
||||
`flutter run --enable-flutter-gpu` (native only; nothing for web). Requires Flutter 3.47 stable+, NOT
|
||||
master. Impeller is default; do not pass `--enable-impeller`. Never pass
|
||||
`--enable-experiment=native-assets` (breaks the build on Dart 3.10+).
|
||||
|
||||
Two model-loading paths, do not conflate:
|
||||
|
||||
- Pipeline (preferred, needs the `buildScenes` hook): `loadScene(sourcePath, {package, bundle,
|
||||
registry, onReload, applyStageTo}) -> Future<Node>`. `sourcePath` is the SOURCE path relative to
|
||||
the package root (e.g. `'assets/level.glb'`), NOT a generated name. Companions:
|
||||
`loadSceneSubtree`, `releaseScene`, `clearSceneTemplateCache`.
|
||||
- Runtime glTF (no hook, parses every load): `Node.fromGlbAsset(assetPath)`,
|
||||
`Node.fromGlbBytes(bytes)`, `Node.fromGltfBytes(gltfJson, {required resolveUri})`. Each synthesizes
|
||||
a root node.
|
||||
|
||||
Sibling loaders: `loadTexture` (`.fstex`), `loadFmatMaterial`/`loadFmatSky` (`.fmat`).
|
||||
|
||||
Build hooks (`package:flutter_scene/build_hooks.dart`): `buildScenes({buildInput, buildOutput,
|
||||
inputFilePaths, discoveryRoot = 'assets/', assetMode = generatedTree, compressTextures = false})`,
|
||||
`buildMaterials({...})`, `buildTextures({..., required textures, contents})`, `buildEngineAssets`,
|
||||
`buildTargetShaderBundleJson`. Outputs land in `flutter_scene_generated/` (never commit
|
||||
`.fsceneb`/`.shaderbundle`/`.fmat.json`/`.fstex`). Removed 0.21.0: `legacyOnly`,
|
||||
`dataAssetsIfAvailable`, `outputDirectory`.
|
||||
|
||||
Animation (`Animation`, `AnimationClip`, `AnimationPlayer` exported):
|
||||
|
||||
- Off a loaded model: `node.parsedAnimations`, `node.findAnimationByName(name)`,
|
||||
`node.createAnimationClip(animation)`, `node.removeAnimationClip(clip)`. Clips start paused at t=0;
|
||||
call `play()`.
|
||||
- `AnimationClip`: `playbackTime` (assignment is seek), `playbackTimeScale` (1; negative reverses),
|
||||
`weight` (0..1), `playing`, `loop`. `play()`, `pause()`, `stop()`, `replay()`, `gotoAndPlay(t)`,
|
||||
`seek(t)`, `advance(dt)`, `rebind(newTarget, {animation})`. Channels bind by node NAME; channels
|
||||
whose node is absent from the subtree are dropped (0.22.0 asserts in debug when ALL channels drop).
|
||||
- `AnimationPlayer`: `createAnimationClip(animation, bindTarget)` (a second call with the same
|
||||
`Animation.name` replaces), `getClipByName`, `rebind`, `update(dt)` (auto-driven per frame).
|
||||
- `Animation({name, channels})`, `AnimationChannel`, `BindKey({required nodeName, property =
|
||||
translation})`, `AnimationProperty` = `translation` | `rotation` | `scale`.
|
||||
- Declarative: `SceneModel(assetPath, animations: [SceneAnimationSpec(name, {playing = true, loop =
|
||||
true, weight = 1.0, speed = 1.0})])`. Note `SceneModel` loads via the runtime glTF path.
|
||||
|
||||
The engine-agnostic scene-document core is a separate package `scene` (0.2.0), re-exported through
|
||||
`package:flutter_scene/fscene.dart`. `flutter_scene_importer` and `flutter_gpu_shim` no longer exist
|
||||
(folded in). Physics and audio are separate barrels (`physics.dart`, `audio.dart`).
|
||||
Reference in New Issue
Block a user