| name | flutter_scene-looks |
| version | 3 |
| description | Give a flutter_scene render a deliberate, polished look. Use this whenever a scene looks flat, dull, or washed out, or whenever the ask is to make it look good, because a good look is lighting plus post-processing, not geometry. Ships copy-paste EnvironmentSettings presets that configure the whole stack coherently. |
Making flutter_scene look good
The single most common reason a flutter_scene render looks amateur is that the post-processing stack was left at defaults. flutter_scene has a deep lit-and-post pipeline (image-based lighting, tone mapping, bloom, lens flares, ambient occlusion, screen-space reflections, fog, god rays, depth of field, color grading, vignette, film grain). Out of the box almost all of it is off, so a bare scene is technically correct and visually flat.
The insight: a polished look is lighting and post-processing, not geometry. Better meshes will not fix a flat render. Do not spend effort on modeling detail when the scene reads dull; spend it on the look. And a look is a coherent set of choices, not twelve knobs turned independently. Turning bloom, AO, SSR, fog, grain, and grading up one at a time, each by feel, lands in muddy incoherent territory. Pick one deliberate preset and paste it whole.
Apply a look in one line
Every scene-wide look field lives on one aggregate value, EnvironmentSettings, assigned through a single setter:
scene.environmentSettings = EnvironmentSettings(/* fields below */);
That one assignment configures tone mapping, exposure, image-based-lighting intensity, and the entire post stack, including fog, god rays, depth of field, and auto exposure. You do not need to touch scene.fog, scene.godRays, scene.depthOfField, or scene.autoExposure separately; those are the live per-effect objects, but EnvironmentSettings carries all of their fields and applies them for you. Every effect is off by default, so a preset only names the fields it turns on.
EnvironmentSettings is also a blendable snapshot. Read the current look with scene.environmentSettings, and cross-fade two looks with EnvironmentSettings.lerp(a, b, t) driven from an animation. That is how you transition day to night or ramp an effect in.
Lights are separate, and still matter
EnvironmentSettings covers image-based lighting (the environment map) and the whole post stack, but direct lights are not part of it. Shadows, god rays, and any strong key light come from the scene's lights, set separately:
scene.directionalLight = DirectionalLight(
direction: vm.Vector3(-0.4, -1.0, -0.3),
intensity: 4.0,
castsShadow: true, // shadows are off until you ask
);
An unset scene.environment still resolves to a default studio IBL, so a PhysicallyBasedMaterial is always lit. But a scene with no direct light is soft and shadowless. God rays require a shadow-casting DirectionalLight; shadows require castsShadow: true on the light.
The four looks
Paste one whole. Each is a real EnvironmentSettings literal; import package:vector_math/vector_math.dart as vm for the Vector3 color fields. AO, SSR, god rays, and depth of field require a PerspectiveCamera (the only built-in camera).
showcase
Clean, bright product-viz beauty. Punchy tone mapping, soft bloom on highlights, grounded contact occlusion, and real reflections. The default reach-for-it look.
scene.environmentSettings = EnvironmentSettings(
toneMapping: ToneMappingMode.aces,
exposure: 1.0,
bloomEnabled: true,
bloomThreshold: 1.1,
bloomIntensity: 0.2,
bloomScatter: 0.7,
ambientOcclusionEnabled: true,
ambientOcclusionMethod: AmbientOcclusionMethod.groundTruth,
ambientOcclusionBentNormals: true,
ambientOcclusionSpecularMode: SpecularAmbientOcclusionMode.bentCone,
ambientOcclusionIntensity: 1.0,
screenSpaceReflectionsEnabled: true,
screenSpaceReflectionsIntensity: 1.0,
vignetteEnabled: true,
vignetteIntensity: 0.25,
);
stylized
Vivid and graphic. Saturated, slightly warm, glowing, flatter shading (no heavy occlusion or reflections). For playful or illustrative scenes.
scene.environmentSettings = EnvironmentSettings(
toneMapping: ToneMappingMode.aces,
colorGradingEnabled: true,
saturation: 1.25,
contrast: 1.1,
brightness: 1.05,
temperature: 0.1,
bloomEnabled: true,
bloomThreshold: 0.9,
bloomIntensity: 0.28,
bloomScatter: 0.8,
vignetteEnabled: true,
vignetteIntensity: 0.2,
);
moody
Dark, cinematic, atmospheric. Lower exposure, cool graded, foggy, heavy vignette, subtle grain and aberration, deep occlusion. God rays if the scene has a shadow-casting sun. Use a cool horizon-colored fog.
scene.environmentSettings = EnvironmentSettings(
toneMapping: ToneMappingMode.aces,
exposure: 0.8,
colorGradingEnabled: true,
contrast: 1.15,
saturation: 0.9,
temperature: -0.1,
fogEnabled: true,
fogMode: FogMode.exponential,
fogColor: vm.Vector3(0.05, 0.06, 0.09),
fogDensity: 0.03,
ambientOcclusionEnabled: true,
ambientOcclusionMethod: AmbientOcclusionMethod.groundTruth,
ambientOcclusionIntensity: 1.2,
ambientOcclusionPower: 1.8,
vignetteEnabled: true,
vignetteIntensity: 0.6,
vignetteRadius: 0.6,
filmGrainEnabled: true,
filmGrainIntensity: 0.25,
chromaticAberrationEnabled: true,
chromaticAberrationIntensity: 0.15,
godRaysEnabled: true, // needs scene.directionalLight with castsShadow: true
godRaysIntensity: 1.0,
godRaysDensity: 0.6,
godRaysColor: vm.Vector3(1.0, 0.95, 0.85),
);
clean
Neutral and honest. Minimal post, no grading, no bloom, no vignette, just correct tone mapping and gentle grounding occlusion. The right look for an editor, an inspector, a UI-embedded viewer, or anywhere you want an accurate read of the actual material.
scene.environmentSettings = EnvironmentSettings(
toneMapping: ToneMappingMode.pbrNeutral, // the engine default
exposure: 1.0,
ambientOcclusionEnabled: true,
ambientOcclusionIntensity: 0.8,
ambientOcclusionHalfResolution: true,
);
Tuning from a preset
Start from the nearest look, then move one field at a time.
- Too dim or too bright overall. Change
exposure (default 1.0), not per-light intensity. Or turn on autoExposureEnabled: true to let the scene meter itself.
- Highlights not glowing. Lower
bloomThreshold toward 1.0 or below, or raise bloomIntensity. bloomScatter widens the glow.
- Want a lens flare off a bright source. Turn on
lensFlareEnabled (needs bloomEnabled). Keep lensFlareIntensity modest and drop lensFlareHaloIntensity first if the flare washes the frame; the halo is the broad wash, the ghosts are the crisp chain.
- Reads flat and ungrounded.
ambientOcclusionEnabled: true. Use AmbientOcclusionMethod.groundTruth for quality, keep ambientOcclusionHalfResolution: true for cost.
- Colors feel wrong. Turn on
colorGradingEnabled and reach for saturation, contrast, temperature, tint before anything else.
- Wrong tone-map feel.
ToneMappingMode.aces is contrasty and filmic, pbrNeutral (default) preserves hue and saturation, agx is the most neutral highlight rolloff. Do not reintroduce an exposure: 2.0 hack; that was an artifact of an older renderer.
Look tools that are not on EnvironmentSettings
Anti-aliasing, reflection probes, and planar reflectors affect the look but are set outside EnvironmentSettings, so a preset does not turn them on.
- Anti-aliasing is a
Scene field. The default AntiAliasingMode.auto already picks msaa where the backend supports it and fxaa otherwise, so edges are handled. Reach for scene.antiAliasingMode = AntiAliasingMode.smaa when fxaa looks mushy (it blurs texture detail) and msaa is unavailable; SMAA keeps edges clean without the blur, at ~3x fxaa cost.
- Reflection probes capture true local reflections that SSR cannot, because SSR only reflects what is on screen. Attach a
ReflectionProbeComponent to a node placed at the reflective spot (a room, a mirror ball); it captures the surroundings into a parallax-corrected box and blends with the environment. The capture renders the scene six times, so it happens once on activate (or on an explicit requestCapture()), never per frame.
- Planar reflectors render a true per-frame mirror for one flat surface (a mirror, a glossy floor), which neither SSR nor a probe can produce. Attach a
PlanarReflectorComponent to the mirror node and give the surface a .fmat material declaring engine_inputs: [ planar_reflection ]; the component renders the scene once more per frame from the reflected camera and the material samples it with GetPlanarReflection(). That extra scene render is the cost, so bound it with resolutionScale (default 0.5) and layerMask. See references/looks.md for all three.
Cost
The post stack is not free. AO, SSR, depth of field, and god rays each add screen-space passes, and mobile and web are the budget. Keep ambientOcclusionHalfResolution: true, prefer DepthOfFieldQuality.low on mobile, and do not stack SSR plus god rays plus depth of field on a low-end target without profiling. The clean look is nearly free; moody is the heaviest. See references/looks.md for the full per-effect knob reference, defaults, and cost notes.