flutter3d
Showcase Changelog 62 packages API reference

Pitfalls

None of these are project setup. They are conditions without which Flutter GPU either refuses to start or silently renders nothing, collected from hitting each one.

They are grouped by what you see, because that is what you have when you go looking.

It will not start #

Symptom Cause and fix
Failed to initialize ShaderLibrary: Flutter GPU must be enabled via the Flutter GPU manifest setting Flutter GPU is enabled per application, not just per channel. Set FLTEnableFlutterGPU to true in macos/Runner/Info.plist, or pass --enable-flutter-gpu. On Android the key is io.flutter.embedding.android.EnableFlutterGPU in AndroidManifest.xml
Failed to initialize ShaderLibrary: Flutter GPU requires the Impeller rendering backend Impeller is not the default renderer on macOS yet. Without FLTEnableImpeller set to true in Info.plist, the app only works through flutter run --enable-impeller and double-clicking the built .app fails
Shaders stop loading after flutter upgrade The shader bundle format is tied to the Flutter version. In a checkout, re-run both builders: flutter3d_impeller/tool/build_shaders.sh for the bundle every application links and flutter3d/example/tool/build_shaders.sh for the one the demo loads. Installed from pub.dev there is nothing of yours to rebuild: the bundle ships inside flutter3d_impeller, so upgrade that package to a version built against your SDK. An extension carrying shaders of its own runs dart run flutter3d_impeller:build_shaders from its own root
MACOSX_DEPLOYMENT_TARGET is set to 10.15, but the range of supported deployment target versions is 12.0 to 27.0.x flutter create generates 10.15 and current Xcode will not build it. Raise it to 12.0 in macos/Runner.xcodeproj/project.pbxproj

Nothing appears #

Symptom Cause and fix
A black viewport with no errors at all Viewport and Scissor default to a zero-sized rect and the API does not complain about drawing into one. Set both explicitly every frame
A draw is submitted, the counter goes up, nothing appears There is no non-indexed draw. draw() with only a vertex buffer bound succeeds and renders nothing. Bind an index buffer even when the indices are the identity 0, 1, 2, … sequence
The model is clipped against the near plane, or "half of it vanished" vector_math.makePerspectiveMatrix produces OpenGL depth [-1, 1] while Impeller follows the Metal/Vulkan convention [0, 1]. The engine's matrix is in PerspectiveProjection.toMatrix
Everything is culled away Y must not be flipped in the projection. Metal NDC has +Y up while the framebuffer origin is top-left, which already gives the right orientation. Flipping mirrors the image and therefore reverses on-screen winding, so culling discards exactly the visible faces
Frame targets fail to allocate, every frame, from the first A model was put in the scene before the renderer ever built its targets. Start with an empty Scene, load asynchronously and swap the node in

It crashes #

Symptom Cause and fix
SIGSEGV in AGXG15XFamilyRenderContext setFragmentBuffer:offset:atIndex: inside RenderPass::Draw() A shader declared a uniform block (via a shared #include) but never reads it. Reflection still reports the block with a non-zero size while the compiled Metal function binds no buffer for it, and binding that phantom block kills the process with no Dart stack trace. Checking sizeInBytes is not enough: the permutation needs explicit metadata (LightingModel.usesFragInfo), and a shader with no material inputs should not declare the block at all, which is why the header is split into lib/color.glsl and lib/surface.glsl
Bad state: The shader has no uniform block named "frame_info" Impeller reflects a uniform block under its struct type name, not the variable name. For uniform FrameInfo { … } frame_info; the key is FrameInfo. Textures are the opposite: bindTexture looks them up by variable name (base_color_texture)
A command encoder is already encoding to this command buffer Metal allows one open encoder per command buffer, and flutter_gpu has no way to end a RenderPass. A multi-pass frame needs a command buffer per pass, submitted in order. Buffers on the same queue execute in submission order, so that is also how the passes get sequenced
failed to bind texture on one lighting model and not the others The model's metadata claims a sampler the compiled shader does not have. LightingModel now asserts that a model sampling no material maps cannot sample the metallic-roughness one either
A texture is bound but the shader has no such slot The compiler drops a sampler whose result never reaches the output, exactly as it does an unused uniform block. A model that samples a map and then ignores the value (Lambert reading metallic-roughness) ends up without the slot. tool/build_shaders.sh prints the compiled binding table so the metadata can be checked against it
Binding has not yet been initialized when reading assets off the UI isolate BackgroundIsolateBinaryMessenger.ensureInitialized(token) grants a background isolate a working channel but creates no ServicesBinding, and rootBundle resolves through ServicesBinding.instance. Routing flutter/assets by hand fails deeper still: Flutter's own reply handler throws on a cast. Keep file reads on the UI isolate and request siblings over a port
PathAccessException … Operation not permitted when writing a file macOS Flutter apps are sandboxed. Anything outside ~/Library/Containers/<bundle id>/Data is refused, so frame captures resolve relative paths against the app's own temp directory

It looks wrong #

Symptom Cause and fix
Geometry and lighting flicker under load CommandBuffer.submit() is asynchronous. Calling HostBuffer.reset() right after it rewinds a bump allocator the GPU may still be reading, so the next frame overwrites live uniform data. Use a ring of ~3 host buffers, one per frame in flight
The scene is all ambient, as if the light shone away from the camera Getters shaped like readDirection([out]) ending in result.normalized() return a new vector and leave out holding the un-normalised, un-negated value. The renderer read its own variable, the direction was inverted, N·L went negative and clamped to zero. Normalise in place, and pin it with expect(returned, same(out))
The background washes out after moving to an HDR target The clear colour is authored display-referred, but the scene target holds linear light and the composite pass encodes on the way out. Convert the clear to linear or it goes through the encode twice
A normal-mapped surface lights from the wrong side, on half the model The bitangent sign. glTF's bitangent is cross(normal, tangent) * w, and it is minus dP/dv, because texture V grows downwards while a normal map's green channel points up. Deriving w from +dP/dv gives tangent directions that agree with an exporter to seven digits and signs that are backwards everywhere, which only shows up on mirrored UV islands
Textures are upside down on an OBJ import OBJ texture space has its origin at the bottom left, so V is flipped by default. This is the single most common cause
A teapot looks faceted and broken With no vn, OBJ needs smooth (area-weighted) normals generated. The format prescribes nothing, files routinely omit them, and the geometry they omit them for is curved
Additive particles occlude each other setDepthWrite(false) did nothing until Flutter 3.47. Fixed in the SDK; the software backend mirrored the bug on purpose so the two would stay comparable
A character's shadow is drawn as a blurred slab beside them A directional map fitted to the whole scene. On 120 m × 260 m that is fourteen centimetres of world per texel. Use ShadowSettings(cascades: 3, resolution: 1024), and remember the atlas is resolution × cascades wide
The view stops turning when the cursor reaches the edge of the window Flutter exposes no pointer lock on any desktop platform and does not surface the browser's. packages/flutter3d_pointer_lock supplies it on macOS by turning off the association between the physical mouse and the on-screen cursor, and in a browser through requestPointerLock
A web build turns the camera by dragging when the mouse should capture, or shows no on-screen controls on a phone Playing was asking kIsWeb, which is one answer to three different questions. It asks the capture backend whether there is a pointer to take and defaultTargetPlatform whether the player has fingers, since a mobile browser reports itself as android or iOS
A shadow has a straight edge in a scene with no straight edges A normal offset measured in metres. What it clears is the width of one texel of the shadow map, which grows with distance, so a fixed offset is right at one range and too small at every greater one. The surface then shadows itself out to wherever the map runs out of content, and that boundary is the straight line
No cursor anywhere after a hot restart A plugin holding the pointer outlives the Dart isolate, because the engine's registrar owns it. The Dart side comes back remembering nothing, so nothing asks for the cursor back. PointerLock issues a reset on construction for exactly this
Every edge in the frame goes jagged once a glass object is in the scene Since 0.8.0 a frame with a transmissive draw splits the scene around a copy of itself, and the multisampled targets keep nothing for the second pass, so the frame draws one sample a pixel. FrameResult.antiAliasing.msaaDeclined names the reason. The question is asked of the scene before culling, so a pane off screen does it too. Put glass on a layer the view does not draw when it is not needed, or turn on the temporal resolve, which is the anti-aliasing that survives a split frame
Glass shows a flat colour where it used to show the sky Glass now reads the scene behind it, and where nothing is drawn behind it that is the clear colour. 0.7.4 showed the environment there. Switch the sky on, or give the room a back wall
Every glTF model is lighter after upgrading to 0.8.0 baseColorFactor is read as linear, as the specification says; a factor of 0.5 used to draw as 0.21. The file is right and the old picture was wrong, so there is nothing to fix unless a value was hand-tuned against the old reading
The sun's shadow is a smear across the whole cascade after upgrading ShadowSettings.directionalLightRadius is in radians since 0.8.0. A radius tuned in the old unit is now an enormous sun, clamped at sixteen texels. The real sun's is about 0.0047
Embers, sparks and splats look dim and leave trails with temporal anti-aliasing on Nothing that blends writes a velocity, so the resolve reprojects the wall behind the ember and keeps nine tenths of it. Set TemporalSettings.reactive above nought, and a contributor of your own overrides PassContributor.encodeReactive
A single captured frame with temporal anti-aliasing on is aliased and shifted by a fraction of a pixel The resolve needs history, and the first frame has none. Render a few frames before capturing, or turn the resolve off for the capture
A splat cloud draws as speckle SplatComposite.hashed asked for by name with no temporal resolve to average it. Leave the composite on automatic, which draws the sorted blend when no resolve runs

It looks fine and is not #

The most expensive category, because nothing is obviously broken.

Symptom Cause and fix
Shadows look right but toggling them changes nothing Check the setting actually reaches RenderSettings. A control wired to a panel but not to the renderer looks completely convincing. Capture the same frame with the feature on and off and diff the two, a zero difference is the whole answer
Golden references that all pass and all look the same Five of six lighting goldens recorded byte-identical images: the scene's lighting model reached the UI field but never the materials, so every one rendered as PBR. Swap one reference for another's and confirm the comparison fails. A golden suite that cannot fail is worse than none, because it is believed
copyWith silently turns features off A copyWith that drops a field does exactly what was asked and something else, and the something else looks like the feature never worked. RenderSettings.copyWith was missing six fields, so changing the exposure switched reflections and fog back off. test/render_settings_test.dart round-trips every field through an argument-less call
Two runs of the same save diverge Monster thinking was staggered across steps by Object.hashCode, which is an address. It is an ordinal now. Anything using math.Random has the same problem, so use GameRandom, which has readable state
A lift will not move while anybody is on it A Mover refuses to move into any body, and a passenger standing on it overlaps where it is about to be on every step. Rider is what tells being carried from being in the way
A platform stops carrying its passenger clearKinematicDeltas() called before body.step. Note it is a sideways platform that shows this: a rising lift penetrates the capsule and the controller pushes it out upwards, delta or no delta, so the obvious test passes
Monsters walk straight at the player in exactly the corridors where a route matters The nav grid's cell size. A cell touching a wall has a clearance of one however far the wall is, so at cellSize: 0.5 a one-metre corridor refuses any body needing clearance two. Bake at 0.25
An agent looks stuck Two callers sharing one Navigation with different destinations. update re-targets every field it holds, so the second one's fields flow to the first one's goal without any error
The whole population of a level is standing on the roof One height per column, and the lowest surface wins, a ceiling's upper face is a perfectly good standing surface by every local test there is. Where that is genuinely wrong, the bake reports a LevelIssue warning
There are no enemies, and everything is wired up The ActorSystem was built and never stepped. "Wired up" and "running" are two different claims
The first frame after loading spends its whole budget catching up Loading blocked the ticker and all of that time is in the accumulator. Call loop.clock.reset() after a load, since none of that time happened in the game
Temporal anti-aliasing is on and the edges are no smoother The resolve needs a velocity buffer, the velocity needs the surface buffer, and a device that opens one colour attachment has none. There the engine does not even jitter, since a jittered picture nobody averages shakes. FrameResult.antiAliasing.temporal says whether it ran
An effect works on the desktop and silently does less in a browser WebGL2 answers four capabilities narrower than the other backends. supportsFloat32Filtering is true only where OES_texture_float_linear was granted, so the EVSM shadow filter falls back to the 3×3 kernel and FrameResult.skipped lists shadow moments as PassSkip.unsupported. supportsIndependentBlend is true only with OES_draw_buffers_indexed, so weighted blended transparency draws the list twice. hdrOutputFormats is empty, so OutputTransform.extendedSrgb draws the SDR frame. supportsGpuTimestamps and supportsCompute are false, so FramePass.gpuMicros is null. Ask the device, and read skipped, before comparing timings across backends
A material with a clear coat or transmission draws as plain metal-rough Only LightingModel.pbrLayered reads Material.extensions. A material built by hand with LightingModel.pbr is drawn without its layers. Pick the model with LightingModel.pbr.withLayers(layers), as the loaders do
Occlusion culling is on and FrameResult.culled does not move OcclusionMode.software rasterises only meshes marked MeshNode.occluder, and nothing is marked by default. OcclusionMode.hiZ needs nothing marked but answers "visible" until its first readback arrives, after a camera cut and with more than one view
spatialUpscale is on and the frame comes back small, or unchanged It runs only with a renderScale below one and the temporal resolve off, since the resolve already rebuilds the full size. SpatialUpscaleSettings.runsFor(settings) answers for a frame
Adaptive quality picks rows that do not match what the device can do The committed QualityTables were costed on the software rasteriser, and QualityTable.measured is false for every class. The controller corrects each row from what it measures, but the order of the rows by cost is a stand-in until the table is measured on the class's own GPU

The habit worth keeping #

When a setting looks like it does nothing, do not argue from the finished picture. Capture the same frame twice, once with it on and once with it off, and diff the two. Two explanations for a broken contact-hardening estimate were argued from a screenshot and both turned out wrong; the quantity that settled it never left the shader, and nothing displayed it, so the debugging was five runs of guessing where it should have been one run of looking.