Files
Sanctification/spikes/tech-spikes/flutter-scene-spike/.claude/skills/flutter_scene-idioms/references/what-exists.md

31 KiB

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:

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.

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).