1 #include "rogueviz.h"
2 
3 // This module allows creating complex animations with smooth camera movement
4 
5 // enable with -smoothcam option, or the new "smooth camera" option in the Animations dialog
6 
7 // it still seems a bit buggy
8 
9 // to add: insert positions? split/merge segments? improved join?
10 
11 namespace hr {
12 
13 using pcell = cell*;
14 
hread(hstream & hs,transmatrix & h)15 inline void hread(hstream& hs, transmatrix& h) { for(int i=0; i<MDIM; i++) hread(hs, h[i]); }
hwrite(hstream & hs,transmatrix h)16 inline void hwrite(hstream& hs, transmatrix h) { for(int i=0; i<MDIM; i++) hwrite(hs, h[i]); }
17 
hwrite(hstream & hs,const pcell & c)18 inline void hwrite(hstream& hs, const pcell& c) {
19   hs.write<int>(mapstream::cellids[c]);
20   }
21 
hread(hstream & hs,pcell & c)22 inline void hread(hstream& hs, pcell& c) {
23   int32_t at = hs.get<int>();
24   c = mapstream::cellbyid[at];
25   }
26 
27 namespace smoothcam {
28 
29 string smooth_camera_help =
30   "This feature lets you create animations with complex but smooth camera movement.\n\n"
31   "An animation is composed from a number of segments.\n\n"
32   "In each segment, you can provide a number of positions, and times for them. "
33   "For example, if you add a camera position A at time 0 and a camera position B at time 1, "
34   "we will move linearly from A to B. Polynomial approximation is used inside a segment, "
35   "while separate segments are animated independently.\n\n"
36   "The 'interval' values are the interval between the current and next position. "
37   "The total sum of 'interval' values is made equal to the 'animation period'. "
38   "If you place two positions X and Y with interval 0 between them, X will be used"
39   "as the actual position, while Y-X will be the first derivative. Thus, for example, "
40   "placing two equal positions with interval 0 will force the camera to smoothly stop.";
41 
42 struct frame {
43   string title;
44   cell *where;
45   transmatrix sView;
46   transmatrix V;
47   transmatrix ori;
48   ld front_distance, up_distance;
49   ld interval;
50   };
51 
52 struct animation {
53   cell *start_cell;
54   transmatrix start;
55   ld start_interval;
56   vector<frame> frames;
57   };
58 
59 map<cell*, map<hyperpoint, string> > labels;
60 map<cell*, vector<vector<hyperpoint> > > traces;
61 
62 vector<animation> anims;
63 
64 transmatrix last_view, current_position, last_view_comp;
65 cell *last_centerover;
66 
67 // during the animation, transform original coordinates to the current view coordinates
68 transmatrix last_computed;
69 ld last_time;
70 
analyze_view_pre()71 void analyze_view_pre() {
72   current_position = current_position * last_view * inverse(View);
73   }
74 
analyze_view_post()75 void analyze_view_post() {
76   last_view = View;
77   }
78 
79 animation *current_segment;
80 
start_segment()81 void start_segment() {
82   anims.emplace_back();
83   auto& anim = anims.back();
84   anim.start_cell = centerover;
85   anim.start = Id;
86   last_view = Id;
87   current_position = Id;
88   current_segment = &anim;
89   }
90 
91 /** does not work correctly -- should adjust to the current cell */
join_segment()92 void join_segment() {
93   int n = anims.back().frames.size();
94   if(n < 2) return;
95   auto s1 = anims.back().frames[n-2];
96   auto s2 = anims.back().frames[n-1];
97   start_segment();
98   auto& l = anims[anims.size()-2];
99   anims.back().frames.push_back(l.frames[n-2]);
100   anims.back().frames.push_back(l.frames[n-1]);
101   anims.back().start_cell = l.start_cell;
102   anims.back().start = l.start;
103   anims.back().start_interval = 0;
104   }
105 
106 map<cell*, int> indices;
107 
gentitle()108 string gentitle() {
109   return lalign(0, centerover, ":", indices[centerover]++);
110   }
111 
112 bool animate_on;
113 bool view_labels, view_trace;
114 
edit_interval(ld & v)115 void edit_interval(ld& v) {
116   dialog::add_action([&v] {
117     dialog::editNumber(v, -10, 10, 1, 0, "interval", "");
118     });
119   }
120 
try_harder_relative_matrix(cell * at,cell * from)121 transmatrix try_harder_relative_matrix(cell *at, cell *from) {
122   transmatrix U = Id;
123   int d = celldistance(at, from);
124   again:
125   while(d > 0) {
126     forCellIdEx(c1, i, at) {
127       int d1 = celldistance(c1, from);
128       if(d1 < d) {
129         U = currentmap->iadj(at, i) * U;
130         d = d1;
131         at = c1;
132         goto again;
133         }
134       }
135     println(hlog, "still failed");
136     return Id;
137     }
138   println(hlog, "got U = ", U);
139   return U;
140   }
141 
edit_segment(int aid)142 void edit_segment(int aid) {
143   cmode = sm::SIDE;
144   gamescreen(0);
145   dialog::init(XLAT("animation segment"), 0xFFFFFFFF, 150, 0);
146   dialog::addSelItem("interval", fts(anims[aid].start_interval), 'i');
147   edit_interval(anims[aid].start_interval);
148   dialog::addItem("delete", 'd');
149   dialog::add_action([aid] {
150     anims.erase(anims.begin()+aid);
151     if(anims.empty()) start_segment();
152     popScreen();
153     });
154   dialog::addItem("mirror", 'm');
155   dialog::add_action([aid] {
156     auto a = anims[aid];
157     reverse(a.frames.begin(), a.frames.end());
158     ld* last = &a.start_interval;
159     for(auto& f: a.frames) { swap(*last, f.interval); last = &f.interval; }
160     anims.push_back(std::move(a));
161     popScreen();
162     });
163   dialog::addItem("copy before", 'c');
164   dialog::add_action([aid] {
165     auto a = anims[aid];
166     anims.insert(anims.begin() + aid, a);
167     current_segment = nullptr;
168     popScreen();
169     });
170   dialog::addItem("swap with the last segment", 'x');
171   dialog::add_action([aid] {
172     swap(anims.back(), anims[aid]);
173     current_segment = nullptr;
174     popScreen();
175     });
176   dialog::addBack();
177   dialog::display();
178   }
179 
180 void generate_trace();
181 
edit_step(animation & anim,int id)182 void edit_step(animation& anim, int id) {
183   cmode = sm::SIDE;
184   gamescreen(0);
185   dialog::init(XLAT("animation step"), 0xFFFFFFFF, 150, 0);
186   auto& f = anim.frames[id];
187   dialog::addSelItem("title", f.title, 't');
188   dialog::addSelItem("interval", fts(f.interval), 'i');
189   edit_interval(f.interval);
190   dialog::addSelItem("front distance", fts(f.front_distance), 'f');
191   dialog::add_action([&f] {
192     dialog::editNumber(f.front_distance, -5, 5, .1, 1, "front distance", "");
193     });
194   dialog::addSelItem("up distance", fts(f.up_distance), 'u');
195   dialog::add_action([&f] {
196     dialog::editNumber(f.up_distance, -5, 5, .1, 1, "up distance", "");
197     });
198   dialog::addItem("delete", 'd');
199   dialog::add_action([&anim, id] {
200     anim.frames.erase(anim.frames.begin()+id);
201     popScreen();
202     });
203   if(&anim == current_segment) {
204     dialog::addItem("change to current camera location", 'e');
205     dialog::add_action([&f] {
206       f.where = centerover;
207       f.sView = View;
208       f.V = current_position;
209       popScreen();
210       });
211     }
212   dialog::addItem("move the camera here", 'r');
213   dialog::add_action([&f] {
214     transmatrix Rel = calc_relative_matrix(centerover, f.where, inverse(View) * C0);
215     println(hlog, "Rel = ", Rel);
216     if(eqmatrix(Rel, Id) && centerover != f.where)
217       Rel = try_harder_relative_matrix(centerover, f.where);
218     View = f.sView * Rel;
219     NLP = ortho_inverse(f.ori);
220     playermoved = false;
221     current_display->which_copy =
222       nonisotropic ? gpushxto0(tC0(view_inverse(View))) :
223       View;
224     popScreen();
225     });
226   dialog::addItem("edit this segment and move the camera here", 'p');
227   dialog::add_action([&anim, &f] {
228     last_view = View = f.sView;
229     NLP = ortho_inverse(f.ori);
230     centerover = f.where;
231     current_position = f.V;
232     playermoved = false;
233     current_display->which_copy =
234       nonisotropic ? gpushxto0(tC0(view_inverse(View))) :
235       View;
236     current_segment = &anim;
237     popScreen();
238     });
239   dialog::addItem("start a new segment from here", 'n');
240   dialog::add_action([&f] {
241     View = f.sView;
242     centerover = f.where;
243     playermoved = false;
244     NLP = ortho_inverse(f.ori);
245     current_display->which_copy =
246       nonisotropic ? gpushxto0(tC0(view_inverse(View))) :
247       View;
248     start_segment();
249     popScreen();
250     });
251   if(&anim == current_segment) {
252     dialog::addItem("insert the current position before this", 'j');
253     dialog::add_action([&anim, id] {
254       anim.frames.insert(anim.frames.begin() + id, frame{gentitle(), centerover, View, current_position, ortho_inverse(NLP), 1, 1, 0});
255       popScreen();
256       });
257     }
258 
259   dialog::addBack();
260   dialog::display();
261   }
262 
263 int last_segment;
264 
265 ld test_t = 0;
266 ld c_front_dist = 0, c_up_dist = 0;
267 void handle_animation(ld t);
268 
show()269 void show() {
270   cmode = sm::SIDE;
271   gamescreen(0);
272   draw_crosshair();
273   dialog::init(XLAT("smooth camera"), 0xFFFFFFFF, 150, 0);
274   char key = 'A';
275   int aid = 0;
276 
277   labels.clear();
278 
279   for(auto& anim: anims) {
280     if(key == 'Z'+1) key = 1;
281     dialog::addSelItem("segment #" + its(aid) + (&anim == current_segment ? "*" : ""), fts(anim.start_interval), key++);
282     dialog::add_action_push([aid] { edit_segment(aid); });
283     int id = 0;
284     for(auto& f: anim.frames) {
285       labels[f.where][inverse(f.sView) * C0] = f.title;
286       string dist;
287 
288       if(f.where != centerover)
289         dist = its(celldistance(f.where, centerover)) + " cells";
290       else {
291         hyperpoint h1 = tC0(iso_inverse(View));
292         hyperpoint h2 = tC0(iso_inverse(f.sView));
293         ld d = hdist(h1, h2);
294         if(d > 1e-3)
295           dist = fts(d) + "au";
296         else {
297           transmatrix T = f.sView * iso_inverse(View);
298           dist = fts(kz(acos_clamp(T[2][2]))) + "°/" + fts(kz(acos_clamp(T[1][1]))) + "°";
299           }
300         }
301 
302       if(key == 'Z'+1) key = 1;
303       dialog::addSelItem(f.title + " [" + dist + "]", fts(f.interval), key++);
304       dialog::add_action_push([&anim, id] { edit_step(anim, id); });
305       id++;
306       }
307     aid++;
308     }
309 
310   if(current_segment) {
311     dialog::addItem("create a new position", 'a');
312     dialog::add_action([] {
313       current_segment->frames.push_back(frame{gentitle(), centerover, View, current_position, ortho_inverse(NLP), 1, 1, 0});
314       });
315     }
316 
317   dialog::addItem("create a new segment", 'b');
318   dialog::add_action(start_segment);
319 
320   if(current_segment) {
321     dialog::addItem("increase interval by 1", 's');
322     dialog::add_key_action('s', [] {
323       if(!current_segment->frames.empty())
324         current_segment->frames.back().interval += 1;
325       else
326         current_segment->start_interval+=1;
327       });
328     }
329 
330   /* dialog::addItem("join a new segment", 'j');
331   dialog::add_action(join_segment); */
332 
333   dialog::addBoolItem_action("view the labels", view_labels, 'l');
334   dialog::addBoolItem("view the trace", view_trace, 't');
335   dialog::add_action([] {
336     view_trace = !view_trace;
337     if(view_trace) generate_trace();
338     });
339 
340   dialog::addItem("test the animation", 't');
341   dialog::add_action([] {
342     animate_on = false;
343     last_time = HUGE_VAL;
344     last_segment = -1;
345     test_t = 0;
346     dialog::editNumber(test_t, 0, 100, 0.1, 0, "enter the percentage", "");
347     dialog::reaction = [] {
348       handle_animation(test_t / 100);
349       };
350     dialog::extra_options = [] {
351       dialog::addSelItem("current segment", its(last_segment), 'C');
352       dialog::addSelItem("current front", fts(c_front_dist), 'F');
353       dialog::addSelItem("current up", fts(c_up_dist), 'U');
354       };
355     });
356 
357   dialog::addBoolItem("view the crosshair", crosshair_size, 'x');
358   dialog::add_action([] { crosshair_size = crosshair_size ? 0 : 10; });
359 
360   dialog::addBoolItem("run the animation", animate_on, 'r');
361   dialog::add_action([] {
362     animate_on = !animate_on;
363     last_time = HUGE_VAL;
364     });
365 
366   dialog::addHelp();
367   dialog::add_action([] { gotoHelp(smooth_camera_help); });
368   dialog::addBack();
369   dialog::display();
370 
371   keyhandler = [] (int sym, int uni) {
372     handlePanning(sym, uni);
373     dialog::handleNavigation(sym, uni);
374     if(doexiton(sym, uni)) popScreen();
375     };
376   }
377 
prepare_for_interpolation(hyperpoint & h)378 void prepare_for_interpolation(hyperpoint& h) {
379   if(prod) {
380     h[3] = zlevel(h);
381     ld t = exp(h[3]);
382     h[0] /= t;
383     h[1] /= t;
384     h[2] /= t;
385     }
386   }
387 
after_interpolation(hyperpoint & h)388 void after_interpolation(hyperpoint& h) {
389   if(prod) {
390     ld v = exp(h[3]) / sqrt(abs(intval(h, Hypc)));
391     h[0] *= v;
392     h[1] *= v;
393     h[2] *= v;
394     }
395   else
396     h = normalize(h);
397   }
398 
handle_animation(ld t)399 void handle_animation(ld t) {
400   ld total_total = 0;
401 
402   vector<ld> totals;
403   for(auto& anim: anims) {
404     ld total = anim.start_interval;
405     for(auto& f: anim.frames)
406       total += f.interval;
407     totals.push_back(total);
408     total_total += total;
409     }
410 
411   if(total_total == 0) return;
412 
413   t = frac(t);
414   t *= total_total;
415   int segment = 0;
416   while(totals[segment] < t && segment < isize(totals)-1) t -= totals[segment++];
417 
418   auto& anim = anims[segment];
419 
420   if(t < last_time || segment != last_segment) {
421     last_time = 0;
422     last_segment = segment;
423     current_segment = &anim;
424     View = anim.start;
425     last_view_comp = View;
426     centerover = anim.start_cell;
427     current_position = Id;
428     last_view = View;
429     last_centerover = centerover;
430     }
431 
432   ld total = anim.start_interval;
433   vector<ld> times;
434   for(auto& f: anim.frames) {
435     times.push_back(total);
436     total += f.interval;
437     }
438 
439   hyperpoint pts[3];
440 
441   for(int j=0; j<3; j++) {
442     for(int i=0; i<4; i++) {
443       vector<ld> values;
444       for(auto& f: anim.frames) {
445         hyperpoint h;
446         if(j == 0)
447           h = tC0(f.V);
448         if(j == 1) {
449           h = tC0(parallel_transport(f.V, f.ori, ztangent(f.front_distance)));
450           }
451         if(j == 2) {
452           h = tC0(parallel_transport(f.V, f.ori, ctangent(1, -f.up_distance)));
453           }
454         prepare_for_interpolation(h);
455         values.push_back(h[i]);
456         }
457 
458       int n = isize(values);
459 
460       for(int ss=1; ss<=n-1; ss++) {
461         for(int a=0; a<n-ss; a++) {
462           // combining [a..a+(ss-1)] and [a+1..a+ss]
463           if(times[a+ss] == times[a])
464             values[a] = values[a] + (values[a+ss] - values[a]) * (t-times[a]);
465           else
466             values[a] = (values[a] * (times[a+ss] - t) + values[a+1] * (t - times[a])) / (times[a+ss] - times[a]);
467           }
468         values.pop_back();
469         }
470 
471       pts[j][i] = values[0];
472       }
473     after_interpolation(pts[j]);
474     }
475 
476   transmatrix V = View;
477   set_view(pts[0], pts[1], pts[2]);
478   c_front_dist = geo_dist(pts[0], pts[1]);
479   c_up_dist = geo_dist(pts[0], pts[2]);
480 
481   transmatrix T = View * inverse(last_view_comp);
482   last_view_comp = View;
483 
484   View = T * V;
485   fixmatrix(View);
486 
487   if(invalid_matrix(View)) {
488     println(hlog, "invalid_matrix ", View);
489     println(hlog, pts[0]);
490     println(hlog, pts[1]);
491     println(hlog, pts[2]);
492     println(hlog, "t = ", t);
493     exit(1);
494     }
495   last_time = t;
496   }
497 
handle_animation0()498 void handle_animation0() {
499   if(!animate_on) return;
500   handle_animation(ticks / anims::period);
501   anims::moved();
502   println(hlog, "at ", cview());
503   }
504 
generate_trace()505 void generate_trace() {
506   last_time = HUGE_VAL;
507   dynamicval<transmatrix> tN(NLP, NLP);
508   dynamicval<transmatrix> tV(View, View);
509   dynamicval<transmatrix> tC(current_display->which_copy, current_display->which_copy);
510   dynamicval<cell*> tc(centerover, centerover);
511   cell* cview = nullptr;
512   vector<hyperpoint> at;
513   traces.clear();
514   auto send = [&] {
515     if(cview && !at.empty()) traces[cview].push_back(at);
516     cview = centerover;
517     at.clear();
518     };
519   for(ld t=0; t<=1024; t ++) {
520     handle_animation(t / 1024);
521     if(cview != centerover) send();
522     at.push_back(inverse(View) * C0);
523     optimizeview();
524     if(cview != centerover) {
525       send();
526       at.push_back(inverse(View) * C0);
527       }
528     }
529   send();
530   }
531 
hwrite(hstream & hs,const animation & anim)532 void hwrite(hstream& hs, const animation& anim) {
533   hwrite(hs, anim.start_cell, anim.start, anim.start_interval, anim.frames);
534   }
535 
hread(hstream & hs,animation & anim)536 void hread(hstream& hs, animation& anim) {
537   hread(hs, anim.start_cell, anim.start, anim.start_interval, anim.frames);
538   }
539 
hwrite(hstream & hs,const frame & frame)540 void hwrite(hstream& hs, const frame& frame) {
541   hwrite(hs, frame.title, frame.where, frame.sView, frame.V, frame.ori, frame.front_distance, frame.up_distance, frame.interval);
542   }
543 
hread(hstream & hs,frame & frame)544 void hread(hstream& hs, frame& frame) {
545   hread(hs, frame.title, frame.where, frame.sView, frame.V, frame.ori, frame.front_distance, frame.up_distance, frame.interval);
546   }
547 
draw_labels(cell * c,const shiftmatrix & V)548 bool draw_labels(cell *c, const shiftmatrix& V) {
549   if(view_labels) for(auto& p: labels[c])
550     queuestr(V * rgpushxto0(p.first), .1, p.second, 0xFFFFFFFF, 1);
551   if(view_trace)
552     for(auto& v: traces[c]) {
553       for(auto p: v)
554         curvepoint(p);
555       queuecurve(V, 0xFFD500FF, 0, PPR::FLOOR);
556       for(auto p: v)
557         curvepoint(p);
558       queuecurve(V, 0x80000080, 0, PPR::SUPERLINE);
559       }
560   return false;
561   }
562 
563 bool enabled;
564 
enable()565 void enable() {
566   if(enabled) return;
567   enabled = true;
568   rogueviz::cleanup.push_back([] { enabled = false; });
569   rogueviz::rv_hook(hooks_preoptimize, 75, analyze_view_pre);
570   rogueviz::rv_hook(hooks_postoptimize, 75, analyze_view_post);
571   rogueviz::rv_hook(anims::hooks_anim, 100, handle_animation0);
572   rogueviz::rv_hook(hooks_drawcell, 100, draw_labels);
573   rogueviz::rv_hook(hooks_o_key, 190, [] (o_funcs& v) { v.push_back(named_dialog("smoothcam", show)); });
574   rogueviz::rv_hook(mapstream::hooks_savemap, 100, [] (fhstream& f) {
575     f.write<int>(17);
576     hwrite(f, anims);
577     });
578   anims.clear();
579   start_segment();
580   }
581 
enable_and_show()582 void enable_and_show() {
583   showstartmenu = false;
584   start_game();
585   enable();
586   pushScreen(show);
587   }
588 
589 auto hooks = arg::add3("-smoothcam", enable_and_show)
__anonc747c5f21e02null590   + arg::add3("-smoothcam-on", [] {
591     enable_and_show();
592     animate_on = true;
593     last_time = HUGE_VAL;
594     })
__anonc747c5f21f02() 595   + addHook(dialog::hooks_display_dialog, 100, [] () {
596     if(current_screen_cfunction() == anims::show) {
597       dialog::addItem(XLAT("smooth camera"), 'C');
598       dialog::add_action(enable_and_show);
599       }
600     }) +
__anonc747c5f22002(fhstream& f, int id) 601   + addHook(mapstream::hooks_loadmap, 100, [] (fhstream& f, int id) {
602     if(id == 17) {
603       enable();
604       hread(f, anims);
605       current_segment = &anims.back();
606       }
607     });
608 
609 }}
610