@@ -364,6 +364,328 @@ def anim_func(global_frame):
364
364
else :
365
365
plt .show ()
366
366
367
+ def make_custom_anim (res , filename , viewsize = 1000 , viewsize_z = 1000 , f16_scale = 30 , trail_pts = 60 ,
368
+ elev = 30 , azim = 45 , skip_frames = None , chase = False , fixed_floor = False ,
369
+ init_extra = None , update_extra = None ):
370
+ '''
371
+ make a 3d plot of the GCAS maneuver.
372
+
373
+ see examples/anim3d folder for examples on usage
374
+ '''
375
+
376
+ plot .init_plot ()
377
+ start = time .time ()
378
+
379
+ if not isinstance (res , list ):
380
+ res = [res ]
381
+
382
+ if not isinstance (viewsize , list ):
383
+ viewsize = [viewsize ]
384
+
385
+ if not isinstance (viewsize_z , list ):
386
+ viewsize_z = [viewsize_z ]
387
+
388
+ if not isinstance (f16_scale , list ):
389
+ f16_scale = [f16_scale ]
390
+
391
+ if not isinstance (trail_pts , list ):
392
+ trail_pts = [trail_pts ]
393
+
394
+ if not isinstance (elev , list ):
395
+ elev = [elev ]
396
+
397
+ if not isinstance (azim , list ):
398
+ azim = [azim ]
399
+
400
+ if not isinstance (skip_frames , list ):
401
+ skip_frames = [skip_frames ]
402
+
403
+ if not isinstance (chase , list ):
404
+ chase = [chase ]
405
+
406
+ if not isinstance (fixed_floor , list ):
407
+ fixed_floor = [fixed_floor ]
408
+
409
+ if not isinstance (init_extra , list ):
410
+ init_extra = [init_extra ]
411
+
412
+ if not isinstance (update_extra , list ):
413
+ update_extra = [update_extra ]
414
+
415
+ #####
416
+ # fill in defaults
417
+ if filename == '' :
418
+ full_plot = False
419
+ else :
420
+ full_plot = True
421
+
422
+ for i , skip in enumerate (skip_frames ):
423
+ if skip is not None :
424
+ continue
425
+
426
+ if filename == '' : # plot to the screen
427
+ skip_frames [i ] = 5
428
+ elif filename .endswith ('.gif' ):
429
+ skip_frames [i ] = 2
430
+ else :
431
+ skip_frames [i ] = 1 # plot every frame
432
+
433
+ if filename == '' :
434
+ filename = None
435
+
436
+ ##
437
+ all_times = []
438
+ all_states = []
439
+ all_modes = []
440
+ all_ps_list = []
441
+ all_Nz_list = []
442
+
443
+ for r , skip in zip (res , skip_frames ):
444
+ t = r ['times' ]
445
+ s = r ['states' ]
446
+ m = r ['modes' ]
447
+ ps = r ['ps_list' ]
448
+ Nz = r ['Nz_list' ]
449
+
450
+ t = t [0 ::skip ]
451
+ s = s [0 ::skip ]
452
+ m = m [0 ::skip ]
453
+ ps = ps [0 ::skip ]
454
+ Nz = Nz [0 ::skip ]
455
+
456
+ all_times .append (t )
457
+ all_states .append (s )
458
+ all_modes .append (m )
459
+ all_ps_list .append (ps )
460
+ all_Nz_list .append (Nz )
461
+
462
+ ##
463
+
464
+ fig = plt .figure (figsize = (8 , 7 ))
465
+ ax = fig .add_subplot (111 , projection = '3d' )
466
+
467
+ # Set dark mode
468
+ ax .set_facecolor ('black' )
469
+ fig .patch .set_facecolor ('black' )
470
+ ax .xaxis .set_pane_color ((0 , 0 , 0 , 1 ))
471
+ ax .yaxis .set_pane_color ((0 , 0 , 0 , 1 ))
472
+ ax .zaxis .set_pane_color ((0 , 0 , 0 , 1 ))
473
+
474
+ ax .xaxis .label .set_color ('white' )
475
+ ax .yaxis .label .set_color ('white' )
476
+ ax .zaxis .label .set_color ('white' )
477
+ ax .tick_params (axis = 'x' , colors = 'white' )
478
+ ax .tick_params (axis = 'y' , colors = 'white' )
479
+ ax .tick_params (axis = 'z' , colors = 'white' )
480
+
481
+ ##
482
+
483
+ parent = get_script_path ()
484
+ plane_point_data = os .path .join (parent , 'f-16.mat' )
485
+
486
+ data = loadmat (plane_point_data )
487
+ f16_pts = data ['V' ]
488
+ f16_faces = data ['F' ]
489
+
490
+ plane_polys = Poly3DCollection ([], color = 'white' if full_plot else 'k' )
491
+ ax .add_collection3d (plane_polys )
492
+
493
+ ax .set_xlabel ('X [ft]' , fontsize = 14 , color = 'white' )
494
+ ax .set_ylabel ('Y [ft]' , fontsize = 14 , color = 'white' )
495
+ ax .set_zlabel ('Altitude [ft]' , fontsize = 14 , color = 'white' )
496
+
497
+ trail_line , = ax .plot ([], [], [], color = 'white' , lw = 2 , zorder = 50 )
498
+
499
+ extra_lines = []
500
+
501
+ for func in init_extra :
502
+ if func is not None :
503
+ extra_lines .append (func (ax ))
504
+ else :
505
+ extra_lines .append ([])
506
+
507
+ first_frames = []
508
+ frames = 0
509
+
510
+ for t in all_times :
511
+ first_frames .append (frames )
512
+ frames += len (t )
513
+
514
+ def anim_func (global_frame ):
515
+ 'updates for the animation frame'
516
+
517
+ index = 0
518
+ first_frame = False
519
+
520
+ for i , f in enumerate (first_frames ):
521
+ if global_frame >= f :
522
+ index = i
523
+
524
+ if global_frame == f :
525
+ first_frame = True
526
+ break
527
+
528
+ frame = global_frame - first_frames [index ]
529
+ states = all_states [index ]
530
+ times = all_times [index ]
531
+ modes = all_modes [index ]
532
+ Nz_list = all_Nz_list [index ]
533
+ ps_list = all_ps_list [index ]
534
+
535
+ print (f"Frame: { global_frame } /{ frames } - Index { index } frame { frame } /{ len (times )} " )
536
+
537
+ speed = states [frame ][0 ]
538
+ alpha = states [frame ][1 ]
539
+ beta = states [frame ][2 ]
540
+ alt = states [frame ][11 ]
541
+
542
+ phi = states [frame ][StateIndex .PHI ]
543
+ theta = states [frame ][StateIndex .THETA ]
544
+ psi = states [frame ][StateIndex .PSI ]
545
+
546
+ dx = states [frame ][StateIndex .POS_E ]
547
+ dy = states [frame ][StateIndex .POS_N ]
548
+ dz = states [frame ][StateIndex .ALT ]
549
+
550
+ if first_frame :
551
+ ax .view_init (elev [index ], azim [index ])
552
+
553
+ for i , lines in enumerate (extra_lines ):
554
+ for line in lines :
555
+ line .set_visible (i == index )
556
+
557
+ if chase [index ]:
558
+ ax .view_init (elev [index ], rad2deg (- psi ) - 90.0 )
559
+
560
+ colors = ['red' , 'blue' , 'green' , 'magenta' ]
561
+
562
+ mode_names = []
563
+
564
+ for mode in modes :
565
+ if not mode in mode_names :
566
+ mode_names .append (mode )
567
+
568
+ mode = modes [frame ]
569
+ mode_index = modes .index (mode )
570
+ col = colors [mode_index % len (colors )]
571
+
572
+ s = f16_scale [index ]
573
+ s = 25 if s is None else s
574
+ pts = scale3d (f16_pts , [- s , s , s ])
575
+
576
+ pts = rotate3d (pts , theta , psi - math .pi / 2 , - phi )
577
+
578
+ size = viewsize [index ]
579
+ size = 1000 if size is None else size
580
+ minx = dx - size
581
+ maxx = dx + size
582
+ miny = dy - size
583
+ maxy = dy + size
584
+
585
+ vz = viewsize_z [index ]
586
+ vz = 1000 if vz is None else vz
587
+
588
+ if fixed_floor [index ]:
589
+ minz = 0
590
+ maxz = vz
591
+ else :
592
+ minz = dz - vz
593
+ maxz = dz + vz
594
+
595
+ ax .set_xlim ([minx , maxx ])
596
+ ax .set_ylim ([miny , maxy ])
597
+ ax .set_zlim ([minz , maxz ])
598
+
599
+ verts = []
600
+ fc = []
601
+ ec = []
602
+ count = 0
603
+
604
+ # draw ground
605
+ if minz <= 0 <= maxz :
606
+ z = 0
607
+ verts .append ([(minx , miny , z ), (maxx , miny , z ), (maxx , maxy , z ), (minx , maxy , z )])
608
+ fc .append ('0.8' )
609
+ ec .append ('0.8' )
610
+
611
+ # draw f16
612
+ for face in f16_faces :
613
+ face_pts = []
614
+
615
+ count = count + 1
616
+
617
+ if not full_plot and count % 10 != 0 :
618
+ continue
619
+
620
+ for findex in face :
621
+ face_pts .append ((pts [findex - 1 ][0 ] + dx , \
622
+ pts [findex - 1 ][1 ] + dy , \
623
+ pts [findex - 1 ][2 ] + dz ))
624
+
625
+ verts .append (face_pts )
626
+ fc .append ('white' )
627
+ ec .append ('white' )
628
+
629
+ plane_polys .set_verts (verts )
630
+ plane_polys .set_facecolor (fc )
631
+ plane_polys .set_edgecolor (ec )
632
+
633
+ # do trail
634
+ t = trail_pts [index ]
635
+ t = 200 if t is None else t
636
+ trail_len = t // skip_frames [index ]
637
+ start_index = max (0 , frame - trail_len )
638
+
639
+ pos_xs = [pt [StateIndex .POS_E ] for pt in states ]
640
+ pos_ys = [pt [StateIndex .POS_N ] for pt in states ]
641
+ pos_zs = [pt [StateIndex .ALT ] for pt in states ]
642
+
643
+ trail_line .set_data (np .asarray (pos_xs [start_index :frame ]), np .asarray (pos_ys [start_index :frame ]))
644
+ trail_line .set_3d_properties (np .asarray (pos_zs [start_index :frame ]))
645
+
646
+ if update_extra [index ] is not None :
647
+ update_extra [index ](frame )
648
+
649
+ plt .tight_layout ()
650
+
651
+ interval = 30
652
+
653
+ if filename .endswith ('.gif' ):
654
+ interval = 60
655
+
656
+ anim_obj = animation .FuncAnimation (fig , anim_func , frames , interval = interval , \
657
+ blit = False , repeat = True )
658
+
659
+ if filename is not None :
660
+
661
+ if filename .endswith ('.gif' ):
662
+ print ("\n Saving animation to '{}' using 'imagemagick'..." .format (filename ))
663
+ anim_obj .save (filename , dpi = 180 , writer = 'imagemagick' ) # dpi was 80
664
+ print ("Finished saving to {} in {:.1f} sec" .format (filename , time .time () - start ))
665
+ else :
666
+ fps = 40
667
+ codec = 'libx264'
668
+
669
+ print ("\n Saving '{}' at {:.2f} fps using ffmpeg with codec '{}'." .format (filename , fps , codec ))
670
+
671
+ # if this fails do: 'sudo apt-get install ffmpeg'
672
+ try :
673
+ extra_args = []
674
+
675
+ if codec is not None :
676
+ extra_args += ['-vcodec' , str (codec )]
677
+
678
+ anim_obj .save (filename , fps = fps , extra_args = extra_args )
679
+ print ("Finished saving to {} in {:.1f} sec" .format (filename , time .time () - start ))
680
+ except AttributeError :
681
+ traceback .print_exc ()
682
+ print ("\n Saving video file failed! Is ffmpeg installed? Can you run 'ffmpeg' in the terminal?" )
683
+ else :
684
+ plt .show ()
685
+
686
+
687
+
688
+
367
689
def scale3d (pts , scale_list ):
368
690
'scale a 3d ndarray of points, and return the new ndarray'
369
691
0 commit comments