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