MORE RE-ORGANIZING

This commit is contained in:
2026-09-08 14:00:27 -07:00
parent 0c1ea70738
commit ad41f0e140
248 changed files with 1091 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
---
name: flutter_scene-looks
version: 4
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:
```dart
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:
```dart
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.
```dart
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.
```dart
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.
```dart
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.
```dart
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.
## Micro-surface metrics and anti-waxiness tuning
When tuning procedural materials or custom shaders, use surface metrics and exposure discipline to eliminate waxiness, plastic reads, or harsh aliasing:
- **Surface reads like smooth plastic.** Increase high-frequency micro-scale variation. Low 1-pixel luminance gradients `(|dL/dx| + |dL/dy|) / 2` indicate untextured or overly smooth surfaces.
- **Blotchy macro clouds.** Balance high-frequency and low-frequency energy ratios (`hf/lf`). High variance with low fine detail indicates large-scale noise patches without adequate surface grain.
- **Harsh normal-map glitter or torn noise.** Check terminator crossing behavior under low grazing light angles (sun low on the horizon). Over-amplified normal maps flip adjacent pixels between full light and full shadow.
- **Colors look washed out in bright areas.** Tone mapping curves compress channel differences near the shoulder. A surface reading high brightness with low saturation is often over-exposed rather than under-pigmented; reduce exposure to restore natural material saturation.
## 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.

View File

@@ -0,0 +1,290 @@
# The look stack, knob by knob
Every field here is a constructor argument on `EnvironmentSettings` (`lib/src/environment_settings.dart`). Assigning `scene.environmentSettings = EnvironmentSettings(...)` applies all of them at once. Defaults are the constructor defaults; a preset only names what it changes. Direct lights (`scene.directionalLight` and friends) are separate and not covered by `EnvironmentSettings`.
Color fields are `vm.Vector3` (import `package:vector_math/vector_math.dart as vm`).
---
## Base look
| Field | Default | What it does |
| --- | --- | --- |
| `toneMapping` | `ToneMappingMode.pbrNeutral` | HDR to display operator. `pbrNeutral` preserves hue/saturation, `aces` is contrasty and filmic, `agx` has the gentlest highlight rolloff, `reinhard`/`linear` are simpler references. |
| `exposure` | `1.0` | Linear scene exposure multiplier. The one knob for overall brightness. Do not use `2.0`; that was an old-renderer hack. |
| `environmentIntensity` | `1.0` | Scales image-based lighting (the environment map) contribution. |
| `agxWhite` | `16.29` | AgX white point, only meaningful with `ToneMappingMode.agx`. |
| `agxContrast` | `1.25` | AgX contrast, only with `agx`. |
`environment` (an `EnvironmentMap?`) and the sky fields are also on `EnvironmentSettings`, but building environment maps is the domain of the idioms skill; a null environment resolves to a default studio IBL.
## Auto exposure (`autoExposure*`)
Meters the frame and multiplies on top of `exposure`. Off by default.
| Field | Default | What it does |
| --- | --- | --- |
| `autoExposureEnabled` | `false` | Turn metering on. |
| `autoExposureStrength` | `0.55` | How fully it drives toward the metered target (0 = none, 1 = full). |
| `autoExposureCompensation` | `0.0` | EV bias applied after metering. |
| `autoExposureMinEv` / `autoExposureMaxEv` | `-4.0` / `4.0` | Clamp range for the adaptation. |
| `autoExposureSpeedUp` / `autoExposureSpeedDown` | `3.0` / `1.0` | Adaptation rate brightening vs darkening. |
## Bloom (`bloom*`)
Blooms bright pixels. Off by default. The cheapest way to make a scene feel lit rather than rendered.
| Field | Default | What it does |
| --- | --- | --- |
| `bloomEnabled` | `false` | Turn bloom on. |
| `bloomThreshold` | `1.0` | Brightness above which a pixel blooms. Lower for more glow. |
| `bloomIntensity` | `0.15` | Strength of the added glow. |
| `bloomScatter` | `0.7` | Spread of the glow, wider values feel dreamier. |
### Lens flares (`lensFlare*`)
Ghost chains and a halo ring off the bloom pyramid, for bright emissive sources and sun disks. Rides the bloom chain, so `bloomEnabled` must be on and the flare scales with `bloomIntensity`. Off by default. A little goes a long way; a strong source with high intensity/halo washes the frame.
| Field | Default | What it does |
| --- | --- | --- |
| `lensFlareEnabled` | `false` | Turn flares on. Needs `bloomEnabled`. |
| `lensFlareIntensity` | `1.0` | Strength of the flare features relative to the bloom. |
| `lensFlareGhostCount` | `4` | Internal-reflection ghosts along the line through the screen center (clamped to 8 at render). |
| `lensFlareGhostSpacing` | `0.3` | Spacing between ghosts, as a fraction of the distance to the center. |
| `lensFlareHaloRadius` | `0.35` | Halo ring radius in screen UV units. |
| `lensFlareHaloIntensity` | `1.0` | Halo strength relative to the ghosts. `0` disables the halo. The halo is what washes the whole frame, so tame it first. |
| `lensFlareChromaticAberration` | `0.005` | Radial color dispersion of the flare features. |
## Color grading (`colorGrading*`)
Post-tone-map color shaping. Off by default. Reach here to change the *mood* of the color rather than the exposure.
| Field | Default | What it does |
| --- | --- | --- |
| `colorGradingEnabled` | `false` | Turn grading on. |
| `brightness` | `1.0` | Multiplicative brightness. |
| `contrast` | `1.0` | Contrast around mid-gray. |
| `saturation` | `1.0` | Color saturation. Above 1 is vivid, below 1 desaturates toward gray. |
| `temperature` | `0.0` | Warm (positive) to cool (negative) white balance. |
| `tint` | `0.0` | Green to magenta balance. |
| `lift` / `gamma` / `gain` | `Vector3(0)` / `Vector3(1)` / `Vector3(1)` | Per-channel shadow/mid/highlight color control (lift-gamma-gain). |
| `colorGradingLut` | `null` | A `ColorLut` (from a `.cube` file) applied after tone mapping. Independent of `colorGradingEnabled`. |
| `colorGradingLutBlend` | `1.0` | LUT mix amount. |
## Vignette (`vignette*`)
Darkens the frame edges. Off by default. Small amounts read as cinematic; large amounts as a peephole.
| Field | Default | What it does |
| --- | --- | --- |
| `vignetteEnabled` | `false` | Turn vignette on. |
| `vignetteIntensity` | `0.5` | Darkening strength at the edge. |
| `vignetteRadius` | `0.75` | How far in the darkening starts (smaller = tighter, more closed-in). |
| `vignetteSmoothness` | `0.5` | Falloff softness of the edge. |
## Chromatic aberration (`chromaticAberration*`)
Splits color channels toward the edges. Off by default. A touch adds a lens feel; too much looks broken.
| Field | Default | What it does |
| --- | --- | --- |
| `chromaticAberrationEnabled` | `false` | Turn it on. |
| `chromaticAberrationIntensity` | `0.2` | Channel-separation strength. |
## Film grain (`filmGrain*`)
Adds animated grain. Off by default. Sells a moody or analog look and hides banding in dark gradients.
| Field | Default | What it does |
| --- | --- | --- |
| `filmGrainEnabled` | `false` | Turn it on. |
| `filmGrainIntensity` | `0.3` | Grain strength. |
## Ambient occlusion (`ambientOcclusion*`)
Screen-space contact darkening. Off by default. Requires a `PerspectiveCamera`. The single biggest upgrade for "grounded" vs "floating". Half-resolution by default to stay affordable.
| Field | Default | What it does |
| --- | --- | --- |
| `ambientOcclusionEnabled` | `false` | Turn AO on. |
| `ambientOcclusionMethod` | `AmbientOcclusionMethod.obscurance` | `obscurance` is the cheap default; `groundTruth` (GTAO) is higher quality and needed for bent normals. |
| `ambientOcclusionIntensity` | `1.0` | Darkening strength. |
| `ambientOcclusionRadius` | `0.33` | World-space sampling radius. |
| `ambientOcclusionPower` | `1.5` | Contrast of the occlusion curve. |
| `ambientOcclusionBias` | `0.07` | Self-occlusion rejection. |
| `ambientOcclusionBentNormals` | `false` | Compute bent normals (needs `groundTruth`); improves indirect lighting direction and enables `bentCone` specular AO. |
| `ambientOcclusionSpecularMode` | `SpecularAmbientOcclusionMode.none` | `simple` occludes reflections cheaply; `bentCone` is directional and needs bent normals. |
| `ambientOcclusionHalfResolution` | `true` | Compute at half res. Keep on unless AO edges look too coarse. |
| `ambientOcclusionIndirectLight` | `0.0` | Above 0 turns on screen-space global illumination (SSGI) bounce; expensive. Its radiance history reprojects, so the bounce stays put under camera motion (object motion still lags). |
| `ambientOcclusionMultiBounce` | `0.0` | Approximate multi-bounce darkening recovery. |
| `ambientOcclusionSampleCount` | `16` | Samples for the `obscurance` method. |
| `ambientOcclusionSliceCount` / `ambientOcclusionStepsPerSlice` | `3` / `3` | GTAO slice sampling. |
| `ambientOcclusionDetail` | `0.5` | Fine-detail term weight (obscurance). |
| `ambientOcclusionHorizonAngle` | `0.06` | Horizon rejection angle. |
| `ambientOcclusionThickness` / `ambientOcclusionThicknessHeuristic` | `0.5` / `0.004` | Depth thickness assumptions for occlusion. |
| `ambientOcclusionDirectLightAffect` | `0.0` | How much AO also dims direct light. |
| `ambientOcclusionVisibilityBitmask` | `false` | Bitmask visibility estimator. |
| `ambientOcclusionDepthMipChain` | `false` | Build a depth mip chain for wide-radius sampling. |
## Screen-space reflections (`screenSpaceReflections*`)
Reflects on-screen geometry. Off by default. Requires a `PerspectiveCamera`. Adds realism to floors, water, and glossy surfaces, but only reflects what is on screen.
| Field | Default | What it does |
| --- | --- | --- |
| `screenSpaceReflectionsEnabled` | `false` | Turn SSR on. |
| `screenSpaceReflectionsIntensity` | `1.0` | Reflection strength. |
| `screenSpaceReflectionsMaxDistance` | `24.4` | Max world-space ray distance. |
| `screenSpaceReflectionsThickness` | `0.46` | Assumed surface thickness for hit tests. |
| `screenSpaceReflectionsStride` | `9.0` | March step size (larger = faster, coarser). |
| `screenSpaceReflectionsMaxSteps` | `90` | Ray-march step budget. |
| `screenSpaceReflectionsBlur` | `0.3` | Roughness-based blur of the reflection. |
| `screenSpaceReflectionsDistanceFadeStart` | `0.0` | Where reflections start fading with distance. |
| `screenSpaceReflectionsResolutionScale` | `1.0` | Compute resolution scale; drop below 1 to save cost. |
## Fog (`fog*`)
Distance and height fog, evaluated in linear HDR before tone mapping. Off by default. Needs both `fogEnabled` and a non-`none` `fogMode`. Applies to lit and unlit materials; the skybox is left unfogged, so set `fogColor` to your horizon color for distant blending.
| Field | Default | What it does |
| --- | --- | --- |
| `fogEnabled` | `false` | Turn fog on. |
| `fogMode` | `FogMode.exponential` | `none`, `linear`, `exponential`, `exponentialSquared`. Must be non-`none` to render. |
| `fogColor` | `Vector3(0.6, 0.7, 0.8)` | Fog tint. Match your sky/horizon. |
| `fogDensity` | `0.02` | Density for the exponential modes. |
| `fogStart` / `fogEnd` | `0.0` / `200.0` | Near/far bounds for `linear` mode. |
| `fogSkyColorInfluence` | `0.0` | Blend fog color toward the sky color. |
| `fogMaxOpacity` | `1.0` | Cap on how opaque fog gets. |
| `fogHeight` / `fogHeightFalloff` | `0.0` / `0.0` | Height-fog band and falloff. |
| `fogSunInScatter` / `fogSunInScatterExponent` | `0.0` / `8.0` | Sun in-scatter glow through the fog. |
| `fogCutoffDistance` | `0.0` | Distance beyond which fog stops accumulating. |
## God rays (`godRays*`)
Volumetric light shafts. Off by default. Requires a shadow-casting `DirectionalLight` and a `PerspectiveCamera`; without a shadow-casting sun there is nothing to shaft.
| Field | Default | What it does |
| --- | --- | --- |
| `godRaysEnabled` | `false` | Turn shafts on. |
| `godRaysIntensity` | `1.0` | Shaft strength. |
| `godRaysDensity` | `0.5` | Medium density the light scatters through. |
| `godRaysAnisotropy` | `0.7` | Forward-scatter bias (higher = tighter shafts toward the sun). |
| `godRaysStepCount` | `24` | March steps; higher is smoother and costlier. |
| `godRaysMaxDistance` | `200.0` | Max shaft distance. |
| `godRaysJitter` | `1.0` | Dither to hide banding. |
| `godRaysColor` | `Vector3(1)` | Shaft tint. |
## Depth of field (`depthOfField*`)
Physically parameterized lens blur. Off by default. Requires a `PerspectiveCamera`. Great for a hero shot, wasteful for a full interactive scene.
| Field | Default | What it does |
| --- | --- | --- |
| `depthOfFieldEnabled` | `false` | Turn DoF on. |
| `depthOfFieldFocusDistance` | `10.0` | World distance in sharp focus. |
| `depthOfFieldFStop` | `2.8` | Aperture; lower = shallower focus, more blur. |
| `depthOfFieldFocalLength` | `0.0` | Lens focal length (0 derives from FOV). |
| `depthOfFieldSensorHeight` | `0.024` | Sensor height in meters (35mm-ish). |
| `depthOfFieldBlurScale` | `1.0` | Overall blur multiplier. |
| `depthOfFieldMaxForegroundBlur` / `depthOfFieldMaxBackgroundBlur` | `24.0` / `32.0` | Blur radius caps. |
| `depthOfFieldBladeCount` | `0` | Aperture blades for bokeh shape (0 = round). |
| `depthOfFieldBladeRotation` / `depthOfFieldBladeCurvature` | `0.0` / `0.0` | Bokeh blade shaping. |
| `depthOfFieldQuality` | `DepthOfFieldQuality.medium` | `low` (16 taps, mobile/web), `medium` (32 taps + postfilter), `high` (48 taps). |
---
## Not on `EnvironmentSettings`
Two look-affecting settings live outside the `EnvironmentSettings` snapshot: anti-aliasing is a `Scene` field, and reflection probes are a scene-graph component. Set them directly.
### Anti-aliasing (`Scene.antiAliasingMode`)
Edge anti-aliasing. `AntiAliasingMode.auto` is the default: it picks `msaa` where the backend supports it and `fxaa` otherwise. Set it directly, not through `EnvironmentSettings`.
```dart
scene.antiAliasingMode = AntiAliasingMode.smaa;
```
| Mode | What it does |
| --- | --- |
| `none` | No anti-aliasing; native-resolution edges. |
| `msaa` | 4x MSAA on the scene pass, the best geometry-edge quality and cheap on mobile GPUs, but not supported on every Flutter GPU backend (falls back to `fxaa`). Check `Scene.isAntiAliasingModeSupported` and read `Scene.effectiveAntiAliasingMode`. |
| `fxaa` | One post pass over the tone-mapped image, supported everywhere, but softens all high-contrast edges including texture detail. |
| `smaa` | SMAA 1x, three post passes, supported everywhere. Reconstructs edge shapes so edges are cleaner than `fxaa` with far less texture blurring, at roughly 3x the `fxaa` cost. Reach for it when `fxaa` looks mushy and `msaa` is unavailable. |
| `auto` | `msaa` where supported, else `fxaa`. |
### Reflection probes (`ReflectionProbeComponent`)
SSR only reflects what is currently on screen. A reflection probe captures the surroundings from a point into a local, parallax-corrected environment, so off-screen geometry reflects correctly inside a bounded box (a mirror ball, a glossy floor in a room). It is a `Component` attached to a `Node`, not an `EnvironmentSettings` field.
```dart
final probe = Node()
..localTransform = vm.Matrix4.translation(vm.Vector3(0, 1, 0)); // reflective spot
probe.addComponent(ReflectionProbeComponent(
extents: vm.Vector3(4, 3, 4), // box half-extents (the influence + parallax volume)
));
scene.add(probe);
```
| Constructor arg | Default | What it does |
| --- | --- | --- |
| `extents` | `Vector3.all(5.0)` | Half-extents of the world-axis-aligned box that is both the influence volume and the parallax proxy. |
| `blendDistance` | `1.0` | Distance over which the probe cross-fades with the environment at the box edge. |
| `priority` | `10.0` | Which probe wins where several overlap. |
| `weight` | `1.0` | Contribution scale in the blend. |
| `faceResolution` | `128` | Cubemap face resolution of the capture. |
| `captureOnActivate` | `true` | Capture once when the probe joins the scene. Call `requestCapture()` to re-capture after the scene changes; the capture is a static snapshot otherwise. |
For a one-shot environment capture with no node or parallax (e.g. to hand a captured `EnvironmentMap` to another material), `Scene.captureEnvironment(position: ...)` returns an `EnvironmentMap` directly.
### Planar reflectors (`PlanarReflectorComponent`)
A true mirror for one flat surface, re-rendered every frame the surface is visible: the engine renders the scene from the view camera reflected across the surface's plane (near plane clamped to the mirror, so nothing behind it leaks in) and hands the capture to the surface's material. Use it for mirrors and glossy floors where SSR's on-screen-only reflections or a probe's static capture are not enough.
Two pieces pair up. The component goes on the mirror node (the plane is the node's local `+Y` through its transform, or an explicit `localNormal`):
```dart
final mirror = Node(mesh: Mesh(PlaneGeometry(width: 10, depth: 10), mirrorMaterial))
..addComponent(PlanarReflectorComponent());
scene.add(mirror);
```
And the surface's material is a `.fmat` that declares the `planar_reflection` engine input and samples `GetPlanarReflection()` (mirrored scene color in rgb, `a` 1 while a capture is bound; fall back to the environment reflection at `a == 0`). A worked mirror lives at `examples/flutter_app/assets/planar_mirror.fmat`.
| Constructor arg | Default | What it does |
| --- | --- | --- |
| `resolutionScale` | `0.5` | Capture resolution relative to the view (clamped `0.1..1.0`). The fragment-cost lever. |
| `layerMask` | all layers | What renders into the capture. The draw-cost lever. |
| `reflectionGroupId` | `-1` | Co-planar surfaces sharing a non-negative id share one capture per frame; `-1` means an own capture. |
| `clipBias` | `1e-3` | World-space offset of the clip plane in front of the mirror, keeping the surface itself out of the capture. |
| `localNormal` | local `+Y` | The mirror plane's facing direction in node space. |
The capture is a second scene submission per reflection group per frame: its CPU and draw-call cost scales with scene complexity, not just resolution. It reuses the frame's shadow atlas and runs without screen-space post; reflectors seen inside a capture draw their base look, so captures never recurse.
---
## Cost and budget
Post effects are screen-space passes; they cost per output pixel, not per triangle. Rough order, cheapest first:
- **Nearly free.** Tone mapping, exposure, color grading, vignette, chromatic aberration, film grain, bloom (with lens flares). The `clean` and `stylized` looks live here.
- **Moderate.** Ambient occlusion (keep `ambientOcclusionHalfResolution: true`), fog, `fxaa`, `smaa` (~3x `fxaa`). `msaa` is nearly free on mobile GPUs but costs more elsewhere.
- **Expensive.** SSR, god rays, depth of field, and especially SSGI (`ambientOcclusionIndirectLight > 0`). Each adds ray-marching or gather passes. A reflection probe's capture renders the scene six times, so capture on activate or an occasional `requestCapture()`, never per frame.
Budget guidance:
- **Mobile and web are the ceiling.** A look that is smooth on desktop can tank a phone. Profile the target, do not assume.
- **Keep AO at half resolution** unless the coarse edges actually show. Full-res AO rarely earns its cost.
- **Do not stack the expensive effects blindly.** SSR plus god rays plus depth of field together on a low-end device needs profiling; drop one or lower its resolution/step budget (`screenSpaceReflectionsResolutionScale`, `godRaysStepCount`, `DepthOfFieldQuality.low`).
- **Depth of field is a hero-shot tool.** It reads as intentional on a framed still and as a smear on a free-moving interactive camera. Prefer it where the camera is controlled.
- **Bloom and grading buy the most look per cost.** When you need "flashier" cheaply, reach for these before the screen-space passes.
## Why each look is built the way it is
**showcase** aims for a clean, believable, flattering render, the default for showing a model off. `aces` tone mapping gives contrast and pop; soft bloom (threshold just above 1.0) lights the highlights without haze; GTAO with bent normals and `bentCone` specular AO grounds the object and tightens reflections in its cavities; SSR adds real floor and surface reflections; a light vignette focuses the eye. Every choice supports "look at this object", nothing calls attention to itself.
**stylized** trades realism for graphic punch. It leans on color (saturation up, a warm push, contrast up) and glow (a lower bloom threshold so more of the frame blooms) and deliberately *omits* AO and SSR, because heavy occlusion and reflection read as realism and fight the flatter, poppier intent. Cheap to run, since it is all near-free passes.
**moody** is the atmospheric, cinematic end. Lower exposure and cool grading set a somber base; exponential fog with a dark horizon color adds depth and hides the far plane; strong AO deepens the shadows; a tight heavy vignette closes the frame; film grain and a touch of chromatic aberration add texture and a lens feel; god rays (given a shadow-casting sun) add drama. It is the most expensive look; on a tight budget, drop god rays first, then SSR if present.
**clean** is the honest look, for when the render must show the *actual* material and lighting without editorializing: an editor viewport, an inspector, a UI-embedded preview. Default `pbrNeutral` tone mapping preserves hue and saturation, and the only effect on is gentle half-res AO for grounding. No bloom, grading, or vignette, because each of those changes what the color and brightness actually are, which is exactly what an accurate preview must not do. Also the cheapest look by far.