Procedural sky: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: sun (line 50), dome (line 52), sky (line 100).
1/// A sky that needs no image: a gradient, a glow and a sun disc, drawn by the
2/// renderer from a few numbers.
3///
4/// Quoted by `procedural_sky.md` and shown whole in the Source tab.
5library;
7import 'dart:math' as math;
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:vector_math/vector_math.dart';
13final class ProceduralSkyDemo extends ShowcaseDemo {
14 double sunHeight = 14.0;
15 double sunDisc = 6.0;
16 double glow = 0.4;
17 bool dome = false;
19 late final LightNode _sun;
20 late final MeshNode _dome;
23 void configureView(DemoContext context) {
24 context.orbit
25 ..distance = 7.0
26 ..pitch = 0.1
27 ..yaw = 0.9;
28 context.orbit.target.setValues(0.0, 0.8, 0.0);
29 }
31 // The way to the sun for the current height, at a fixed compass bearing.
32 Vector3 get _towardsSun {
33 final double up = sunHeight * math.pi / 180.0;
34 return Vector3(-math.cos(up) * 0.6, math.sin(up), -math.cos(up) * 0.8);
35 }
38 Scene build(DemoContext context) {
39 final Material ground = Material(
40 name: 'ground',
41 baseColor: Vector4(0.42, 0.4, 0.36, 1.0),
42 roughness: 0.9,
43 doubleSided: true,
44 );
45 final Material ball = Material(
46 name: 'ball',
47 baseColor: Vector4(0.8, 0.8, 0.82, 1.0),
48 roughness: 0.35,
49 );
50 _sun = LightNode(name: 'sun', intensity: 3.0);
52 final MeshData shell = const SkyDome().build();
53 paintSky(
54 shell,
55 SkyGradient(
56 zenith: Vector3(0.08, 0.2, 0.5),
57 horizon: Vector3(0.5, 0.58, 0.68),
58 nadir: Vector3(0.06, 0.06, 0.07),
59 ).colour,
60 );
61 _dome = skyNode(DeviceMesh.upload(context.device, shell))..visible = false;
63 return Scene()
64 ..ambientIntensity = 0.4
65 ..add(
66 MeshNode(
67 DeviceMesh.upload(
68 context.device,
69 const PlaneShape(width: 30, depth: 30).build(),
70 ),
71 ground,
72 name: 'ground',
73 ),
74 )
75 ..add(
76 MeshNode(
77 DeviceMesh.upload(
78 context.device,
79 SphereShape(radius: 0.8, segments: 32, rings: 16).build(),
80 ),
81 ball,
82 name: 'ball',
83 )..setPosition(0.0, 0.8, 0.0),
84 )
85 ..add(_sun)
86 ..add(_dome);
87 }
90 void update(DemoContext context, double dt) {
91 _sun.setLocalForward(-_towardsSun);
92 _dome.visible = dome;
93 // The dome is small and has to follow the eye, or it is a ball on the
94 // ground.
95 followCamera(_dome, context.camera);
96 }
99 RenderSettings settings(DemoContext context) => RenderSettings(
100 sky: SkySettings(
101 enabled: !dome,
102 directionToSun: _towardsSun,
103 sunIntensity: sunDisc,
104 glowStrength: glow,
105 ),
106 );
109 List<DemoControl> controls(DemoContext context) => <DemoControl>[
110 SliderControl(
111 'Sun height',
112 min: 2,
113 max: 85,
114 value: () => sunHeight,
115 onChanged: (double v) => sunHeight = v,
116 format: (double v) => '${v.round()} deg',
117 ),
118 SliderControl(
119 'Sun disc',
120 min: 0,
121 max: 20,
122 value: () => sunDisc,
123 onChanged: (double v) => sunDisc = v,
124 ),
125 SliderControl(
126 'Glow',
127 min: 0,
128 max: 1,
129 value: () => glow,
130 onChanged: (double v) => glow = v,
131 ),
132 ToggleControl(
133 'Painted dome instead',
134 value: () => dome,
135 onChanged: (bool v) => dome = v,
136 ),
137 ];
140 void verify(Scene scene, FrameResult frame) {
141 final FramePass draw = frame.passes.firstWhere(
142 (FramePass p) => p.name == 'scene',
143 );
144 final int meshes = scene.meshes.where((MeshNode m) => m.visible).length;
145 // The sky is one full-screen triangle inside the scene pass, so it is the
146 // draw that comes on top of the meshes.
147 if (draw.drawCalls != meshes + 1) {
148 throw StateError(
149 'expected $meshes mesh draws and one for the sky, '
150 'got ${draw.drawCalls}',
151 );
152 }
153 }
154}