1 module scene;
2 
3 import gfx.math;
4 
5 interface LightAnim
6 {
7     void anim(in float dt);
8     @property FVec3 color();
9     @property bool on();
10 }
11 
12 /// constant light without animation
13 class ConstantLight : LightAnim
14 {
15     FVec3 _color;
16 
17     this(FVec3 color)
18     {
19         _color = color;
20     }
21 
22     override void anim(in float) {}
23     override @property FVec3 color()
24     {
25         return _color;
26     }
27     override @property bool on()
28     {
29         return true;
30     }
31 }
32 
33 private float smoothstep(float edge0, float edge1, float x) {
34   // Scale, bias and saturate x to 0..1 range
35   x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
36   // Evaluate polynomial
37   return x * x * (3 - 2 * x);
38 }
39 
40 private float clamp(float x, float lowerlimit, float upperlimit) {
41   if (x < lowerlimit)
42     x = lowerlimit;
43   if (x > upperlimit)
44     x = upperlimit;
45   return x;
46 }
47 
48 /// blinking light
49 class BlinkLight : LightAnim
50 {
51     LightAnim _anim;
52     float _on;
53     float _period;
54     float _step;
55     float _phase;
56     float _level;
57 
58     this(LightAnim anim, in float on, in float off, in float step, in float phase)
59     in (on > step && off > step, "Blink light on or off is smaller than step")
60     in (phase >= 0f && phase < (on + off), "Blink light initial phase out of period")
61     {
62         _anim = anim;
63         _on = on;
64         _period = on + off;
65         _step = step;
66         _phase = phase;
67         computeLevel();
68     }
69 
70     final void computeLevel()
71     {
72         if (_phase < (_on-_step)) {
73             _level = 1f;
74         }
75         else if (_phase < _on) {
76             // step down
77             _level = 1f - smoothstep(_on - _step, _on, _phase);
78         }
79         else if (_phase < (_period-_step)) {
80             _level = 0f;
81         }
82         else {
83             // step up
84             _level = smoothstep(_period - _step, _period, _phase);
85         }
86     }
87 
88     override void anim(in float dt)
89     {
90         _anim.anim(dt);
91         _phase += dt;
92         while (_phase >= _period) _phase -= _period;
93         computeLevel();
94     }
95 
96     override @property FVec3 color()
97     {
98         return _anim.color * _level;
99     }
100 
101     override @property bool on()
102     {
103         return _level > 0f && _anim.on;
104     }
105 }
106 
107 struct MovingObj
108 {
109     /// position of the object relative to its parent
110     FVec3 position = fvec(0, 0, 0);
111     /// rotation speed of the object around itself
112     /// using Euler angles, in rad/s
113     FVec3 spin = fvec(0, 0, 0);
114     /// current rotation of the object
115     FMat3 rotation = FMat3.identity;
116 
117     /// rotate the rotation matrix with the spin euler vector
118     /// over dt secs
119     void rotate(in float dt)
120     {
121         const euler = spin * dt;
122         rotation *= eulerAngles(euler);
123     }
124 
125     /// Compute the full transform
126     FMat4 transform()
127     {
128         return translate(
129             mat(
130                 vec(rotation[0], 0),
131                 vec(rotation[1], 0),
132                 vec(rotation[2], 0),
133                 vec(0, 0, 0, 1),
134             ),
135             position,
136         );
137     }
138 }
139 
140 struct SaucerBody
141 {
142     FMat4 transform = FMat4.identity;
143     FVec3 color = fvec(0, 0, 0);
144     float shininess = 1f;
145 }
146 
147 struct Saucer
148 {
149     MovingObj mov;
150     size_t saucerIdx;
151     SaucerBody[] bodies;
152 
153     FVec3 lightPos;
154     float lightLuminosity;
155     LightAnim lightAnim;
156 
157     void anim(in float dt)
158     {
159         mov.rotate(dt);
160         lightAnim.anim(dt);
161     }
162 }
163 
164 struct SaucerSubStruct
165 {
166     MovingObj mov;
167     Saucer[] saucers;
168 }
169 
170 struct DeferredScene
171 {
172     MovingObj mov;
173     SaucerSubStruct[] subStructs;
174 
175     /// prepare scene and return approximative bounding radius
176     float prepare()
177     {
178         /// Build a structure of 9 sub structures, each containing 9 saucers.
179         /// Saucers have a fixed position relative to their sub-structure,
180         /// which have fixed position relative to the super structure.
181         /// Each object (structure, sub-structure and saucer spin around their center)
182         /// is setup such as to not have collisions.
183 
184         import std.algorithm : map;
185         import std.array : array;
186         import std.math : PI, sqrt;
187         import std.random : choice, Random, uniform;
188 
189         const margin = 1f;
190         const saucerSz = 3f;
191         const saucerBound = saucerSz + 2 * margin;          // 5
192         const subStructSz = 3 * saucerBound;                // 15
193         const subStructBound = subStructSz + 2 * margin;    // 17
194         const sceneSz = subStructBound * 3;                 // 51
195         const numSubStructs = 9;
196         const numSaucers = 9;
197 
198         const subDist = (sceneSz / 2f) - (subStructSz / 2f);
199         const saucerDist = (subStructBound / 2f) - (saucerSz / 2f);
200 
201         auto rnd = Random(43);
202         size_t saucerIdx = 0;
203 
204         FVec3 spin(in float minRpm, in float maxRpm)
205         {
206             const float[2] sign = [-1f, 1f];
207             const signs = fvec(
208                 choice(sign[], rnd),
209                 choice(sign[], rnd),
210                 choice(sign[], rnd),
211             );
212 
213             enum RPM = 2*PI / 60;
214             return signs * fvec(
215                 uniform(minRpm * RPM, maxRpm * RPM, rnd),
216                 uniform(minRpm * RPM, maxRpm * RPM, rnd),
217                 uniform(minRpm * RPM, maxRpm * RPM, rnd),
218             );
219         }
220 
221         MovingObj[] buildMovingObjs(in uint num, in float dist, in float minRpm, in float maxRpm)
222         {
223             MovingObj[] objs = new MovingObj[num];
224 
225             const coord = dist * sqrt(2f) / 2;
226 
227             foreach(i; 0 .. num) {
228                 FVec3 pos = fvec(0, 0, 0);
229                 if (i > 0) {
230                     const mask = i - 1;
231                     pos.x = (mask & 0b001) ? coord : -coord;
232                     pos.y = (mask & 0b010) ? coord : -coord;
233                     pos.z = (mask & 0b100) ? coord : -coord;
234                 }
235                 objs[i] = MovingObj(pos, spin(minRpm, maxRpm), FMat3.identity);
236             }
237             return objs;
238         }
239 
240         FVec3 color() {
241             return normalize(
242                 fvec(
243                     uniform(0.1, 1.0, rnd),
244                     uniform(0.1, 1.0, rnd),
245                     uniform(0.1, 1.0, rnd),
246                 )
247             );
248         }
249 
250         LightAnim makeAnim()
251         {
252             import std.algorithm : max;
253 
254             auto base = new ConstantLight(color());
255             const float period = uniform(1.0, 10.0, rnd);
256             const float onOff = uniform(0.1, 0.6, rnd); // on 10% to 60%
257             const float on = max(0.5f, period * onOff);
258             const float off = period - on;
259             const float step = 0.3f;
260             const float phase = uniform(0f, period, rnd);
261             return new BlinkLight(base, on, off, step, phase);
262         }
263 
264 
265         Saucer makeSaucer(MovingObj mov) {
266 
267             SaucerBody body;
268             SaucerBody cockpit;
269             SaucerBody bulb;
270             const bodyCol = color();
271             const bulbCol = color();
272             FVec3 bulbPos;
273             {
274                 const xy = uniform(2.5, 3, rnd);
275                 const z = uniform(0.8, 1, rnd);
276                 body.transform = scale(fvec(xy, xy, z));
277                 body.color = bodyCol * 0.8;
278                 body.shininess = uniform(6.0, 16.0, rnd);
279             }
280             {
281                 const s = uniform(0.5, 1, rnd);
282                 const x = uniform(-0.5, 0, rnd);
283                 const z = uniform(0.8, 1, rnd);
284                 cockpit.transform = translation(fvec(x, 0, z))
285                         * scale(FVec3(s));
286                 cockpit.color = bodyCol;
287                 cockpit.shininess = uniform(20.0, 32.0, rnd);
288             }
289             {
290                 const s = uniform(0.2, 0.3, rnd);
291                 const x = uniform(1.0, 2.0, rnd);
292                 const z = uniform(1.5, 2.5, rnd);
293                 bulbPos = fvec(x, 0, z);
294                 bulb.transform = translation(bulbPos)
295                         * scale(FVec3(s));
296                 bulb.color = bulbCol;
297                 bulb.shininess = uniform(20.0, 32.0, rnd);
298             }
299 
300             return Saucer(
301                 mov, saucerIdx++, [body, cockpit, bulb],
302                 bulbPos,
303                 uniform(2f, 12f, rnd), // light luminosity
304                 makeAnim(),
305             );
306         }
307 
308         SaucerSubStruct makeSubStruct(MovingObj mov) {
309             return SaucerSubStruct(
310                 mov,
311                 buildMovingObjs(numSaucers, saucerDist, 3f, 7f)
312                     .map!(mov => makeSaucer(mov))
313                     .array
314             );
315         }
316 
317         mov.spin = spin(2f, 3f);
318         subStructs = buildMovingObjs(numSubStructs, subDist, 1f, 5f)
319             .map!(mov => makeSubStruct(mov))
320             .array;
321 
322         return sceneSz / 2f;
323     }
324 
325     @property size_t saucerCount()
326     {
327         import std.algorithm : map, sum;
328 
329         return subStructs
330             .map!(ss => ss.saucers.length)
331             .sum();
332     }
333 }