#include "rogueviz.h" // This module allows creating complex animations with smooth camera movement // enable with -smoothcam option, or the new "smooth camera" option in the Animations dialog // it still seems a bit buggy // to add: insert positions? split/merge segments? improved join? namespace hr { using pcell = cell*; inline void hread(hstream& hs, transmatrix& h) { for(int i=0; i(mapstream::cellids[c]); } inline void hread(hstream& hs, pcell& c) { int32_t at = hs.get(); c = mapstream::cellbyid[at]; } namespace smoothcam { string smooth_camera_help = "This feature lets you create animations with complex but smooth camera movement.\n\n" "An animation is composed from a number of segments.\n\n" "In each segment, you can provide a number of positions, and times for them. " "For example, if you add a camera position A at time 0 and a camera position B at time 1, " "we will move linearly from A to B. Polynomial approximation is used inside a segment, " "while separate segments are animated independently.\n\n" "The 'interval' values are the interval between the current and next position. " "The total sum of 'interval' values is made equal to the 'animation period'. " "If you place two positions X and Y with interval 0 between them, X will be used" "as the actual position, while Y-X will be the first derivative. Thus, for example, " "placing two equal positions with interval 0 will force the camera to smoothly stop."; struct frame { string title; cell *where; transmatrix sView; transmatrix V; transmatrix ori; ld front_distance, up_distance; ld interval; }; struct animation { cell *start_cell; transmatrix start; ld start_interval; vector frames; }; map > labels; map > > traces; vector anims; transmatrix last_view, current_position, last_view_comp; cell *last_centerover; // during the animation, transform original coordinates to the current view coordinates transmatrix last_computed; ld last_time; void analyze_view_pre() { current_position = current_position * last_view * inverse(View); } void analyze_view_post() { last_view = View; } animation *current_segment; void start_segment() { anims.emplace_back(); auto& anim = anims.back(); anim.start_cell = centerover; anim.start = Id; last_view = Id; current_position = Id; current_segment = &anim; } /** does not work correctly -- should adjust to the current cell */ void join_segment() { int n = anims.back().frames.size(); if(n < 2) return; auto s1 = anims.back().frames[n-2]; auto s2 = anims.back().frames[n-1]; start_segment(); auto& l = anims[anims.size()-2]; anims.back().frames.push_back(l.frames[n-2]); anims.back().frames.push_back(l.frames[n-1]); anims.back().start_cell = l.start_cell; anims.back().start = l.start; anims.back().start_interval = 0; } map indices; string gentitle() { return lalign(0, centerover, ":", indices[centerover]++); } bool animate_on; bool view_labels, view_trace; void edit_interval(ld& v) { dialog::add_action([&v] { dialog::editNumber(v, -10, 10, 1, 0, "interval", ""); }); } transmatrix try_harder_relative_matrix(cell *at, cell *from) { transmatrix U = Id; int d = celldistance(at, from); again: while(d > 0) { forCellIdEx(c1, i, at) { int d1 = celldistance(c1, from); if(d1 < d) { U = currentmap->iadj(at, i) * U; d = d1; at = c1; goto again; } } println(hlog, "still failed"); return Id; } println(hlog, "got U = ", U); return U; } void edit_segment(int aid) { cmode = sm::PANNING; gamescreen(); dialog::init(XLAT("animation segment"), 0xFFFFFFFF, 150, 0); dialog::addSelItem("interval", fts(anims[aid].start_interval), 'i'); edit_interval(anims[aid].start_interval); dialog::addItem("delete", 'd'); dialog::add_action([aid] { anims.erase(anims.begin()+aid); if(anims.empty()) start_segment(); popScreen(); }); dialog::addItem("mirror", 'm'); dialog::add_action([aid] { auto a = anims[aid]; reverse(a.frames.begin(), a.frames.end()); ld* last = &a.start_interval; for(auto& f: a.frames) { swap(*last, f.interval); last = &f.interval; } anims.push_back(std::move(a)); popScreen(); }); dialog::addItem("copy before", 'c'); dialog::add_action([aid] { auto a = anims[aid]; anims.insert(anims.begin() + aid, a); current_segment = nullptr; popScreen(); }); dialog::addItem("swap with the last segment", 'x'); dialog::add_action([aid] { swap(anims.back(), anims[aid]); current_segment = nullptr; popScreen(); }); dialog::addBack(); dialog::display(); } void generate_trace(); void edit_step(animation& anim, int id) { cmode = 0; gamescreen(); dialog::init(XLAT("animation step"), 0xFFFFFFFF, 150, 0); auto& f = anim.frames[id]; dialog::addSelItem("title", f.title, 't'); dialog::addSelItem("interval", fts(f.interval), 'i'); edit_interval(f.interval); dialog::addSelItem("front distance", fts(f.front_distance), 'f'); dialog::add_action([&f] { dialog::editNumber(f.front_distance, -5, 5, .1, 1, "front distance", ""); }); dialog::addSelItem("up distance", fts(f.up_distance), 'u'); dialog::add_action([&f] { dialog::editNumber(f.up_distance, -5, 5, .1, 1, "up distance", ""); }); dialog::addItem("delete", 'd'); dialog::add_action([&anim, id] { anim.frames.erase(anim.frames.begin()+id); popScreen(); }); if(&anim == current_segment) { dialog::addItem("change to current camera location", 'e'); dialog::add_action([&f] { f.where = centerover; f.sView = View; f.V = current_position; popScreen(); }); } dialog::addItem("move the camera here", 'r'); dialog::add_action([&f] { transmatrix Rel = calc_relative_matrix(centerover, f.where, inverse(View) * C0); println(hlog, "Rel = ", Rel); if(eqmatrix(Rel, Id) && centerover != f.where) Rel = try_harder_relative_matrix(centerover, f.where); View = f.sView * Rel; NLP = ortho_inverse(f.ori); playermoved = false; current_display->which_copy = nonisotropic ? gpushxto0(tC0(view_inverse(View))) : View; popScreen(); }); dialog::addItem("edit this segment and move the camera here", 'p'); dialog::add_action([&anim, &f] { last_view = View = f.sView; NLP = ortho_inverse(f.ori); centerover = f.where; current_position = f.V; playermoved = false; current_display->which_copy = nonisotropic ? gpushxto0(tC0(view_inverse(View))) : View; current_segment = &anim; popScreen(); }); dialog::addItem("start a new segment from here", 'n'); dialog::add_action([&f] { View = f.sView; centerover = f.where; playermoved = false; NLP = ortho_inverse(f.ori); current_display->which_copy = nonisotropic ? gpushxto0(tC0(view_inverse(View))) : View; start_segment(); popScreen(); }); if(&anim == current_segment) { dialog::addItem("insert the current position before this", 'j'); dialog::add_action([&anim, id] { anim.frames.insert(anim.frames.begin() + id, frame{gentitle(), centerover, View, current_position, ortho_inverse(NLP), 1, 1, 0}); popScreen(); }); } dialog::addBack(); dialog::display(); } int last_segment; ld test_t = 0; ld c_front_dist = 0, c_up_dist = 0; void handle_animation(ld t); bool side = true; void snap_to_center() { cmode = side ? sm::SIDE : 0; gamescreen(); draw_crosshair(); dialog::init(XLAT("snap to center"), 0xFFFFFFFF, 150, 0); dialog::addItem("center on mouse", ' '); dialog::add_action([] { View = gpushxto0(unshift(mapeditor::mouse_snap())) * View; }); dialog::addItem("mouse up", 'w'); dialog::add_action([] { View = spin90() * spintox(unshift(mapeditor::mouse_snap())) * View; }); dialog::addItem("mouse down", 's'); dialog::add_action([] { View = spin270() * spintox(unshift(mapeditor::mouse_snap())) * View; }); dialog::addItem("mouse left", 'a'); dialog::add_action([] { View = spin180() * spintox(unshift(mapeditor::mouse_snap())) * View; }); dialog::addItem("mouse left", 'd'); dialog::add_action([] { View = Id * spintox(unshift(mapeditor::mouse_snap())) * View; }); dialog::addBack(); dialog::display(); keyhandler = [] (int sym, int uni) { handlePanning(sym, uni); dialog::handleNavigation(sym, uni); if(doexiton(sym, uni)) popScreen(); }; } void show() { cmode = side ? sm::SIDE : 0; gamescreen(); draw_crosshair(); dialog::init(XLAT("smooth camera"), 0xFFFFFFFF, 150, 0); int aid = 0; labels.clear(); dialog::start_list(2000, 2000, 'A'); for(auto& anim: anims) { dialog::addSelItem("segment #" + its(aid) + (&anim == current_segment ? "*" : ""), fts(anim.start_interval), dialog::list_fake_key++); dialog::add_action_push([aid] { edit_segment(aid); }); int id = 0; for(auto& f: anim.frames) { labels[f.where][inverse(f.sView) * C0] = f.title; string dist; if(f.where != centerover) dist = its(celldistance(f.where, centerover)) + " cells"; else { hyperpoint h1 = tC0(iso_inverse(View)); hyperpoint h2 = tC0(iso_inverse(f.sView)); ld d = hdist(h1, h2); if(d > 1e-3) dist = fts(d) + "au"; else { transmatrix T = f.sView * iso_inverse(View); dist = fts(kz(acos_clamp(T[2][2]))) + "°/" + fts(kz(acos_clamp(T[1][1]))) + "°"; } } dialog::addSelItem(f.title + " [" + dist + "]", fts(f.interval), dialog::list_fake_key++); dialog::add_action_push([&anim, id] { edit_step(anim, id); }); id++; } aid++; } dialog::end_list(); if(current_segment) { dialog::addItem("create a new position", 'a'); dialog::add_action([] { current_segment->frames.push_back(frame{gentitle(), centerover, View, current_position, ortho_inverse(NLP), 1, 1, 0}); }); } dialog::addItem("create a new segment", 'b'); dialog::add_action(start_segment); if(current_segment) { dialog::addItem("increase interval by 1", 's'); dialog::add_key_action('s', [] { if(!current_segment->frames.empty()) current_segment->frames.back().interval += 1; else current_segment->start_interval+=1; }); } /* dialog::addItem("join a new segment", 'j'); dialog::add_action(join_segment); */ dialog::addBoolItem_action("view the labels", view_labels, 'l'); dialog::addBoolItem("view the trace", view_trace, 't'); dialog::add_action([] { view_trace = !view_trace; if(view_trace) generate_trace(); }); dialog::addBoolItem_action("side display", side, 'M'); dialog::addItem("test the animation", 't'); dialog::add_action([] { animate_on = false; last_time = HUGE_VAL; last_segment = -1; test_t = 0; dialog::editNumber(test_t, 0, 100, 0.1, 0, "enter the percentage", ""); dialog::reaction = [] { handle_animation(test_t / 100); }; dialog::extra_options = [] { dialog::addSelItem("current segment", its(last_segment), 'C'); dialog::addSelItem("current front", fts(c_front_dist), 'F'); dialog::addSelItem("current up", fts(c_up_dist), 'U'); }; }); dialog::addBoolItem("view the crosshair", crosshair_size, 'x'); dialog::add_action([] { crosshair_size = crosshair_size ? 0 : 10; }); dialog::addBoolItem("run the animation", animate_on, 'r'); dialog::add_action([] { animate_on = !animate_on; last_time = HUGE_VAL; }); if(GDIM == 2) dialog::addItem("centering", 'X'); dialog::add_action_push(snap_to_center); dialog::addHelp(); dialog::add_action([] { gotoHelp(smooth_camera_help); }); dialog::addBack(); dialog::display(); keyhandler = [] (int sym, int uni) { handlePanning(sym, uni); dialog::handleNavigation(sym, uni); if(doexiton(sym, uni)) popScreen(); }; } void prepare_for_interpolation(hyperpoint& h) { if(prod) { h[3] = zlevel(h); ld t = exp(h[3]); h[0] /= t; h[1] /= t; h[2] /= t; } } void after_interpolation(hyperpoint& h) { if(prod) { ld v = exp(h[3]) / sqrt(abs(intval(h, Hypc))); h[0] *= v; h[1] *= v; h[2] *= v; } else h = normalize(h); } void handle_animation(ld t) { ld total_total = 0; vector totals; for(auto& anim: anims) { ld total = anim.start_interval; for(auto& f: anim.frames) total += f.interval; totals.push_back(total); total_total += total; } if(total_total == 0) return; t = frac(t); t *= total_total; int segment = 0; while(totals[segment] < t && segment < isize(totals)-1) t -= totals[segment++]; auto& anim = anims[segment]; if(t < last_time || segment != last_segment) { last_time = 0; last_segment = segment; current_segment = &anim; View = anim.start; last_view_comp = View; centerover = anim.start_cell; current_position = Id; last_view = View; last_centerover = centerover; } ld total = anim.start_interval; vector times; for(auto& f: anim.frames) { times.push_back(total); total += f.interval; } hyperpoint pts[3]; for(int j=0; j<3; j++) { for(int i=0; i<4; i++) { vector values; for(auto& f: anim.frames) { hyperpoint h; if(j == 0) h = tC0(f.V); if(j == 1) { h = tC0(parallel_transport(f.V, f.ori, ztangent(f.front_distance))); } if(j == 2) { h = tC0(parallel_transport(f.V, f.ori, ctangent(1, -f.up_distance))); } prepare_for_interpolation(h); values.push_back(h[i]); } int n = isize(values); for(int ss=1; ss<=n-1; ss++) { for(int a=0; a tN(NLP, NLP); dynamicval tV(View, View); dynamicval tC(current_display->which_copy, current_display->which_copy); dynamicval tc(centerover, centerover); cell* cview = nullptr; vector at; traces.clear(); auto send = [&] { if(cview && !at.empty()) traces[cview].push_back(at); cview = centerover; at.clear(); }; for(ld t=0; t<=1024; t ++) { handle_animation(t / 1024); if(cview != centerover) send(); at.push_back(inverse(View) * C0); optimizeview(); if(cview != centerover) { send(); at.push_back(inverse(View) * C0); } } send(); } void hwrite(hstream& hs, const animation& anim) { hwrite(hs, anim.start_cell, anim.start, anim.start_interval, anim.frames); } void hread(hstream& hs, animation& anim) { hread(hs, anim.start_cell, anim.start, anim.start_interval, anim.frames); } void hwrite(hstream& hs, const frame& frame) { hwrite(hs, frame.title, frame.where, frame.sView, frame.V, frame.ori, frame.front_distance, frame.up_distance, frame.interval); } void hread(hstream& hs, frame& frame) { hread(hs, frame.title, frame.where, frame.sView, frame.V, frame.ori, frame.front_distance, frame.up_distance, frame.interval); } bool draw_labels(cell *c, const shiftmatrix& V) { if(view_labels) for(auto& p: labels[c]) queuestr(V * rgpushxto0(p.first), .1, p.second, 0xFFFFFFFF, 1); if(view_trace) for(auto& v: traces[c]) { for(auto p: v) curvepoint(p); queuecurve(V, 0xFFD500FF, 0, PPR::FLOOR); for(auto p: v) curvepoint(p); queuecurve(V, 0x80000080, 0, PPR::SUPERLINE); } return false; } bool enabled; void enable() { if(enabled) return; enabled = true; rogueviz::cleanup.push_back([] { enabled = false; }); rogueviz::rv_hook(hooks_preoptimize, 75, analyze_view_pre); rogueviz::rv_hook(hooks_postoptimize, 75, analyze_view_post); rogueviz::rv_hook(anims::hooks_anim, 100, handle_animation0); rogueviz::rv_hook(hooks_drawcell, 100, draw_labels); rogueviz::rv_hook(hooks_o_key, 190, [] (o_funcs& v) { v.push_back(named_dialog("smoothcam", show)); }); rogueviz::rv_hook(mapstream::hooks_savemap, 100, [] (hstream& f) { f.write(17); hwrite(f, anims); }); anims.clear(); start_segment(); } void enable_and_show() { showstartmenu = false; start_game(); enable(); pushScreen(show); } auto hooks = arg::add3("-smoothcam", enable_and_show) + arg::add3("-smoothcam-on", [] { enable_and_show(); animate_on = true; last_time = HUGE_VAL; }) + addHook(dialog::hooks_display_dialog, 100, [] () { if(current_screen_cfunction() == anims::show) { dialog::addItem(XLAT("smooth camera"), 'C'); dialog::add_action(enable_and_show); } }) + + addHook(mapstream::hooks_loadmap, 100, [] (hstream& f, int id) { if(id == 17) { enable(); hread(f, anims); current_segment = &anims.back(); } }); }}