Lighting and environment
Flutter Scene lights surfaces in two complementary ways. An image-based environment wraps the scene in light coming from every direction, and an optional directional light adds a single hard source like the sun, which can cast shadows. A separate exposure and tone-mapping stage then turns the high-dynamic-range result into the final image.
In the demo below the sun drives all of it at once. Moving it repaints the sky, re-lights the sphere through the environment, and swings the cast shadow across the ground.
Image-based lighting
Section titled “Image-based lighting”The environment is an EnvironmentMap. It carries prefiltered lighting derived from an equirectangular panorama and lights every material in the scene, including the diffuse fill and the specular reflections you saw in Materials.
A new Scene starts with a procedural studio environment, so materials are lit out of the box. To use your own, load a panorama.
scene.environment = await EnvironmentMap.fromAssets( radianceImagePath: 'assets/studio_panorama.png',);Two knobs adjust how it contributes.
scene.environmentIntensity = 1.5; // scale the environment's brightnessscene.environmentTransform = Matrix3.rotationY(0.8); // rotate itDrawing a sky
Section titled “Drawing a sky”Setting an environment lights the scene but does not draw anything behind it. A Skybox fills the background. The simplest source draws the active environment itself, so the background matches the reflections.
scene.skybox = Skybox(EnvironmentSkySource());You can also draw a procedural sky instead of a panorama. GradientSkySource and PhysicalSkySource render a sky with a sun you position.
final sky = GradientSkySource()..sunDirection = vm.Vector3(0.3, 0.6, 0.4);scene.skybox = Skybox(sky);Lighting from a sky
Section titled “Lighting from a sky”A procedural sky can also become the light source. SkyEnvironment bakes the sky into the image-based environment, so the ambient light matches the sky you are drawing. Choose how often it re-bakes with the refresh mode.
scene.skyEnvironment = SkyEnvironment(sky, refresh: SkyEnvironmentRefresh.manual);// After moving the sun, re-bake:scene.skyEnvironment!.invalidate();Use SkyEnvironmentRefresh.manual and invalidate() when the sky changes occasionally, interval for a slow time-of-day cycle, or everyFrame when it changes constantly.
A custom sky
Section titled “A custom sky”The built-in sources cover common cases, but you can also author your own procedural sky as a .fmat. Instead of the fragment { } block a material uses, a sky .fmat has a sky { vec3 Sky(vec3 direction) } block that returns the sky’s radiance for a view direction. The engine draws it full screen and supplies the direction, and the parameters are typed and hot-reloadable like any .fmat.
material { name: "GradientSky", parameters: [ { type: vec3, name: zenith_color, default: [0.05, 0.18, 0.55] }, { type: vec3, name: horizon_color, default: [0.45, 0.62, 0.90] }, { type: vec3, name: sun_direction, default: [0.4, 0.5, 0.6] }, { type: float, name: sun_sharpness, hint: range(16, 2000, 1), default: 400.0 }, ],}
sky { vec3 Sky(vec3 direction) { vec3 color = mix(material_params.horizon_color, material_params.zenith_color, sqrt(max(direction.y, 0.0))); // A bright (HDR) sun disk toward sun_direction. float s = max(dot(direction, normalize(material_params.sun_direction)), 0.0); color += vec3(3.0, 2.7, 2.2) * pow(s, material_params.sun_sharpness); return color; }}loadFmatSky returns a PreprocessedSky, a sky source you use exactly like the built-in ones, both as the skybox and as the light through a SkyEnvironment.
final sky = await loadFmatSky('assets/gradient_sky.fmat');scene.skybox = Skybox(sky);scene.skyEnvironment = SkyEnvironment(sky, refresh: SkyEnvironmentRefresh.everyFrame);
// Drive its typed parameters like any .fmat material.sky.parameters.setVec3('sun_direction', vm.Vector3(0.3, 0.6, 0.4));Authoring a sky uses the same build hook and workflow as materials, covered in Custom materials.
Directional light and shadows
Section titled “Directional light and shadows”A DirectionalLight adds a single analytic source, layered on top of the image-based lighting. Set it with castsShadow to get a depth-mapped shadow.
scene.directionalLight = DirectionalLight( direction: vm.Vector3(-0.3, -1.0, -0.2), // the way the light travels intensity: 3.0, castsShadow: true,);direction is the direction the light travels, so it points away from the sun. The demo keeps the sky’s sun and the directional light in sync by pointing the light opposite the sky’s sunDirection.
Shadows over image-based lighting
Section titled “Shadows over image-based lighting”A cast shadow physically only removes the direct light, leaving the image-based ambient untouched. So when the environment does the lighting, especially a sky-baked environment that already contains the sun’s energy, the ambient alone keeps a shadowed area looking fully lit and the shadow barely reads. That is exactly the case where you want image-based lighting to do the work but still want the illusion of a sun casting shadows.
shadowAmbientStrength handles this. It lets the cast shadow darken the image-based ambient too, from 0.0 (physically correct, ambient untouched) to 1.0 (the shadow darkens the ambient as much as it does the direct light). Set the direct intensity low, or to 0, so the environment lights the scene and the light serves only as a shadow caster.
scene.directionalLight = DirectionalLight( direction: vm.Vector3(-0.3, -1.0, -0.2), intensity: 0.0, // no direct light; the environment lights the scene castsShadow: true, shadowAmbientStrength: 1.0, // let the shadow darken the ambient);It is a deliberately non-physical, artistic control for image-based-lit scenes that want shadows to read as shadows. To tie such a shadow to a procedural sky’s sun automatically (aim the light opposite the sun and recolor it each frame), assign a SunLight to scene.sunLight instead of managing directionalLight by hand.
Exposure and tone mapping
Section titled “Exposure and tone mapping”Materials output linear high-dynamic-range color. A final stage applies exposure, a tone-mapping operator, and the display transform.
scene.exposure = 2.5; // linear multiplier before tone mappingscene.toneMapping = ToneMappingMode.pbrNeutral; // the default operatorBright outdoor panoramas usually need a higher exposure than the 1.0 default; the demo and the Materials sphere both use 2.5. To drive exposure from camera settings instead, Scene.physicalCameraExposure derives a multiplier from aperture, shutter speed, and ISO.
Key API
Section titled “Key API”EnvironmentMap, Skybox, GradientSkySource, PhysicalSkySource, SkyEnvironment, DirectionalLight, and ToneMappingMode.