Files
Sanctification/flutter-scene-spike/.claude/skills/flutter_scene-looks/SKILL.md
2026-09-08 13:39:03 -07:00

10 KiB

name, version, description
name version description
flutter_scene-looks 4 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.

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.