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 arefovNear/fovFar, NOTnear/far.PerspectiveCamera.framing(Aabb3 bounds, {direction, fovRadiansY, up, margin = 1.1}).PerspectiveProjection({fovRadiansY, near = 0.1, far = 1000.0})and abstractCameraProjection,Camera. Camera helpers:screenPointToRay,worldToScreen,getViewMatrix,getFrustum.- There is NO
OrthographicCamera. ImplementCameraProjection/Camerafor other projections. - Node-driven:
CameraComponent({CameraProjection? projection, activateOnMount = false})->toCamera()gives aNodeCamera. Camera node must not be scaled. - Interactive cameras:
CameraControllercomponents attached to the camera node.OrbitCameraController(turntable aroundtarget;orbitBy/dollyBy/panBy/frame),FlyCameraController(WASD + drag free flight;moveVertical: false= grounded first-person;look),FollowCameraController(third-person easing behindfollowTargetnode;orbitBy/dollyBy). All ease with frame-rate independentsmoothing(settle seconds), clamp pitch short of vertical, and write the node vialookAtFrom. Wire input with theCameraControls({required controller, enabled, autofocus, child})widget (Focus + gestures + wheel);SceneViewhas 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 untilupdateForCamera(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, ...})drivesScene.directionalLightfrom 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 ofenabled),lutBlend(1.0).chromaticAberration(intensity0.2),vignette(intensity0.5,radius0.75,smoothness0.5),filmGrain(intensity0.3),bloom(threshold1.0,intensity0.15,scatter0.7, andlensFlare:enabledfalse,intensity1.0,ghostCount4,ghostSpacing0.3,haloRadius0.35,haloIntensity1.0,chromaticAberration0.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,viewsBuilderare mutually exclusive.SceneView.declarative({environment, environmentIntensity = 1.0, exposure = 1.0, toneMapping = pbrNeutral, camera, cameraBuilder, viewsBuilder, children, ...}). View-owned scene.SceneViewsBuilderis 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 mountedCameraComponent) -> defaultPerspectiveCamera().
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
buildSceneshook):loadScene(sourcePath, {package, bundle, registry, onReload, applyStageTo}) -> Future<Node>.sourcePathis 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; callplay(). 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 sameAnimation.namereplaces),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})]). NoteSceneModelloads 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).