傻孩子(GorgonMeducer) · 2023年06月13日

【Arm-2D】如何简单高效的播放GIF

以下文章来源于嵌入式小书虫 ,作者FledgingSu 支离苏

image.png

【说在前面的话】

上一篇我们利用脏矩阵在m0的单片机上制作了酷炫的汽车仪表盘界面,今天我们继续压榨M0的性能,看看播放动画的效果怎么样。

什么?播放动画你有点不信?

那先看看下面的视频。

image.png

视频中的小孩走来走去,你知道这个是怎么实现的吗?

哈哈,下面我们就从原理开始讲起,一步一步实现这个小孩运动的效果(* ̄︶ ̄)

当然,我们还是先简单说一下这个板子的软硬件配置,如下

image.png

配置讲完接下来就看看这个动画效果是怎么实现的。

【动画原理】

首先,视频中的动画效果其实是一张gif图转换而来的。

image.png

网上有工具可以把 GIF图 转化为 平铺开的PNG图片(叫 sprites),平铺工具连接如下

https://ezgif.com/gif-to-sprite

打开 此网站如下

image.png

上传完成,点击Upload按钮,如下

image.png

在跳转到的新页面选择to sprite,如下

image.png

然后点击蓝色按钮就可以生成图片,如下

image.png

最后,点击鼠标右键选择图片另存为即可。

把GIF展开后平铺的效果如下图所示

image.png

你是不是瞬间就明白了,原来播放动画其实就是不停地切换图片而已(* ̄︶ ̄)

【Arm-2D中的film】

播放动画的原理很简单,那我们接着看看Arm-2D中专门用来处理图片类动画的arm\_2d\_helper\_film\_t是怎么使用的。

首先,要把GIF转化后的PNG图片取模生成tile(制作tile之前的文章有讲,这里就不展开讲了)

extern const arm_2d_tile_t c_tileqiuRGB565;

接着用impl\_film宏生成arm\_2d\_helper\_film\_t,如下

static arm_2d_helper_film_t s_tileqiuRGB565Film = 
    impl_film(c_tileqiuRGB565, 80, 80, 6, 12, 100); 
  • 第1个参数为上面的平铺 tile
  • 第2个参数为每帧的 width(每张图片为80*80像素)
  • 第3个参数为每帧的height
  • 第4个参数为平铺 tile里的一行有几个帧
  • 第5个参数为总帧数
  • 第6个参数为一个参考的帧持续时间,(这里是100,代表100ms切换一帧)

好,有了这个arm\_2d\_helper\_film\_t类型的变量s\_tileqiuRGB565Film ,我们就可以用它来显示图片了,它的使用和普通的tile是一样的,也就是tile能出现的地方,这个s\_tileqiuRGB565Film 就可以出现在那里

接着把这个tile显示出来就可以了,如下

 arm_2d_rgb16_tile_copy_with_colour_keying_only(
      (arm_2d_tile_t *)&s_tileqiuRGB565Film,         
      ptTile,         
      &myRegion,
      GLCD_COLOR_BLACK
 ) ;

对了,这个只是显示一帧图片,那要显示下一帧该怎么办呢?

image.png

其实,这个也简单,在 frame\_start 事件处理程序里,调用 arm\_2d\_helper\_file\_next\_frame() 来换帧就行了,程序如下


static void __on_scene1_frame_start(arm_2d_scene_t *ptScene)
{      
    if (arm_2d_helper_is_time_out(  s_tileqiuRGB565Film.hwPeriodPerFrame, 
                                    &this.lTimestamp[3])) {
                                   
        arm_2d_helper_film_next_frame(&s_tileqiuRGB565Film);
    }
}
  • 这个s\_tileqiuRGB565Film.hwPeriodPerFrame就是刚才我们设置的第6个参数,超过100ms就切换成下一帧
  • 这里就是用 arm\_2d\_helper\_is\_timeout 来实现的锁帧换帧的

当然,这里只是按顺序切换到下一帧,那能不能跳到指定的帧去显示呢?

答案是肯定的,

使用 arm\_2d\_helper\_film\_set\_frame()函数就可以了,他的使用如下

 arm_2d_helper_film_set_frame(&s_tileqiuRGB565Film, 3);
  • 第2个参数就是要跳到第几帧,当然,这个值也可以是-1(负1代表最后一帧哦

    到这里arm\_2d\_helper\_film\_t的使用我们就讲完了,程序运行效果如下

    image.png

聪明的你是不是也发现,开始视频里的小孩是左右移动的,而现在我们只是切换图片并没有左右移动。别急,接下来我们就讲讲小孩的移动。

【小孩的左右移动】

其实,左右移动也很简单。因为在显示每一帧图片时,我们会传入一个arm\_2d\_region\_t类型的变量,只要修改它的值,小孩的位置就不一样了,程序如下

arm_2d_region_t myRegion = {
    .tLocation = {.iX = 104,.iY = 101,},
    .tSize = {.iWidth = 80,.iHeight = 80,},
};                    
//修改x方向的偏移量                
myRegion.tLocation.iX = iX_offset;
                    
arm_2d_rgb16_tile_copy_with_colour_keying_only(
      (arm_2d_tile_t *)&s_tileqiuRGB565Film,
              ptTile,
               &myRegion,
              GLCD_COLOR_BLACK
      ) ;
  • 第6行,左右移动就是修改X方向的坐标值,那iX\_offset是怎么确定的呢?

这个值也是在frame\_start 事件处理程序里设置的,如下

static void __on_scene1_frame_start(arm_2d_scene_t *ptScene)
{
 if (arm_2d_helper_is_time_out(  s_tileqiuRGB565Film.hwPeriodPerFrame, 
                                    &this.lTimestamp[3])) {
        int32_t iResult;                                
        arm_2d_helper_time_cos_slider(10, 150, 1200, 0, &iResult, &this.lTimestamp[1]);
        iX_offset = iResult;                              
        arm_2d_helper_film_next_frame(&s_tileqiuRGB565Film);                                           
    }
}
  • 这次我们使用了arm\_2d\_helper\_time\_cos\_slider函数,这样我们的运动轨迹就符合cos函数,使得移动看起来明显有加减速的感觉
  • 他的用法也很简单,cos slider是以cos函数的形式让一个值在 from 和to之间随着时间进行变化。例如
    arm\_2d\_helper\_time\_cos\_slider(10, 150, 1200, 0, &iResult, &this.lTimestamp\[1\]);

就是让你以1200ms为周期,在10~150之间以cos函数为参考进行变化,并把计算好的值传给iResult,是不是很简单。

好了,到这里视频中的动画效果就实现了,大家可以赶快动手试试了,此程序参考了官方的代码,地址如下

https://github.com/ARM-software/Arm-2D/blob/main/examples/[template][cmsis-rtos2][pfb]/RTE/Acceleration/arm\_2d\_scene\_1.c

当然,如果你之前还没有移植过Arm-2D,那也可以看这篇最新的移植教程哦,如下

【玩转Arm-2D】入门和移植从未如此简单

等等,不是说好的要对帧率进行优化吗?怎么就结束了呢?

接下来我们就看看怎么对帧率进行优化。

【帧率优化】

说到帧率优化,那肯定还是得用脏矩阵(局部刷新)了。上一篇也讲了一下脏矩阵,不过脏矩阵的计算官方已经帮我们计算好了,我们只是调用了一下,带着对脏矩阵计算的好奇心,这次我们手动对脏矩阵进行修改,看看里面到底是什么(* ̄︶ ̄)

首先,我们这次还是需要两块脏矩阵区域,如下图所示

image.png

当小球和小孩从位置1移动到位置2时,我们在位置1的区域擦除掉小孩并在位置2的区域重新绘制一帧图片就可以了,所以需要两块脏矩阵区域。那我们就先定义两块脏矩阵区域,如下

IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static)
/* a dirty region to be specified at runtime*/ 
//添加两个脏矩阵区域      
ADD_REGION_TO_LIST(s_tDirtyRegions,
   .tLocation = {.iX = 0,.iY = 101,},
   .tSize = {.iWidth = 80,.iHeight = 80,},   
),                

ADD_REGION_TO_LIST(s_tDirtyRegions),
  • 我们在\_\_arm\_2d\_scene1\_init函数中添加两个脏矩阵区域,第一个区域我们赋了初值,因为小球最开始的位置为最左边。

那问题又来了,怎么在运行时修改脏矩阵的区域呢?

哈哈,这个贴心的官方也为我们提供了修改脏矩阵的方法哦。其实脏矩阵区域说白了就是一个变量,我们只要获取到这个变量然后修改它的值就可以了,获取到脏矩阵变量也很简单,如下

this.use_as__arm_2d_scene_t.ptDirtyRegion[0].tRegion
  • 注意,这里ptDirtyRegion[0]数组下标为0,代表我们第一个添加的脏矩阵元素(即赋初值的那个),以此类推,下标为1就是第二个添加的脏矩阵元素,我们今天就修改这两个脏矩阵区域就可以了。

接下来,我们还是在frame\_start 事件处理程序里修改脏矩阵区域,程序如下

if (arm_2d_helper_is_time_out(  s_tileqiuRGB565Film.hwPeriodPerFrame, 
                                    &this.lTimestamp[3])) {
    int32_t iResult;                                
    arm_2d_helper_time_cos_slider(10, 150, 1200, 0, &iResult, &this.lTimestamp[1]);
    iX_num =   iResult;                              
    arm_2d_helper_film_next_frame(&s_tileqiuRGB565Film);
    //更新脏矩阵区域
    DirtyRegionUpdata( ptScene, iResult);                                       
}
  • 这次我们定义了一个函数来更新脏矩阵区域,DirtyRegionUpdata函数如下
void DirtyRegionUpdata2(arm_2d_scene_t *ptScene,int32_t iResult){
    user_scene_1_t *ptThis = (user_scene_1_t *)ptScene;
   //定义两个区域
    static arm_2d_region_t myRegion_old = {
        .tLocation = {.iX = 0,.iY = 101,},
        .tSize = {.iWidth = 80,.iHeight = 80,},    
    };                
            
    static arm_2d_region_t myRegion_new = {
        .tLocation = {.iX = 0,.iY = 101,},
        .tSize = {.iWidth = 80,.iHeight = 80,},  
     };
    //保存需要擦除的区域
    myRegion_old.tLocation.iX =  myRegion_new.tLocation.iX;
    //保存需要绘制的区域
    myRegion_new.tLocation.iX =  iResult;
    //更新脏矩阵  
    this.use_as__arm_2d_scene_t.ptDirtyRegion[0].tRegion  = myRegion_old;  
    this.use_as__arm_2d_scene_t.ptDirtyRegion[1].tRegion  = myRegion_new;  
}
  • 首先我们定义两个区域,一个用来保存需要擦除的区域一个用来保存需要绘制图片的区域,然后更新到脏矩阵区域就可以了,程序运行效果如下

image.png

FPS为55,到这里,你以为我们的帧率就优化完了吗?

no!no!no!

我们还可以继续优化

image.png

我们再来看看视频中小孩的运动,由于我们一帧图片为80*80,而小孩每帧横向移动的距离不会超过80,如下图所示

image.png

小孩由位置1(蓝色区域)移动到位置2(红色区域),我们设置了两块脏矩阵区域,而这两块区域是有重合的,也就是我们的程序刷了重复的内容,所以我们就可以把重复的区域优化掉

image.png

其实这个也简单,我们把两块区域合并成一块就可以了,如下图所示

image.png

我们把脏矩阵区域设置成绿色的区域就可以了,修改后的程序如下

void DirtyRegionUpdata(arm_2d_scene_t *ptScene,int32_t iResult){
    user_scene_1_t *ptThis = (user_scene_1_t *)ptScene;                   
   //定义两个区域
    static arm_2d_region_t myRegion_old = {
        .tLocation = {.iX = 0,.iY = 101,},
        .tSize = {.iWidth = 80,.iHeight = 80,},    
    };                
            
    static arm_2d_region_t myRegion_new = {
        .tLocation = {.iX = 0,.iY = 101,},
        .tSize = {.iWidth = 80,.iHeight = 80,},  
     };
    //保存需要擦除的区域
    myRegion_old.tLocation.iX =  myRegion_new.tLocation.iX;
    //保存需要绘制的区域
    myRegion_new.tLocation.iX =  iResult;     
    //把两个区域合并成一个区域
    if( myRegion_new.tLocation.iX > myRegion_old.tLocation.iX){
        myRegion_old.tSize.iWidth = 80 + myRegion_new.tLocation.iX - myRegion_old.tLocation.iX;
        this.use_as__arm_2d_scene_t.ptDirtyRegion[0].tRegion  = myRegion_old;   
    }else{
        myRegion_new.tSize.iWidth = 80 + myRegion_old.tLocation.iX - myRegion_new.tLocation.iX;
        this.use_as__arm_2d_scene_t.ptDirtyRegion[0].tRegion  = myRegion_new;  
    }         
}
  • 只需要在第17~24行,把两个区域合并成一个就可以了(* ̄︶ ̄)

修改后的程序运行效果如下

image.png

优化效果还可以,FPS达到了60(增加了5)。这个就是开始视频里显示的效果。怎么样,大家get到脏矩阵的用法了吧。这里只是起到一个抛砖引玉的效果,大家也可以用脏矩阵的思路优化帧率,比如只局部刷新gif变化的部分,这样也可以有效提升帧率哦。期待大家动手尝试脏矩阵,和我一起玩转Arm-2D,大家一起玩才更好玩。


原文:裸机思维
作者:GorgonMeducer 傻孩子

专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
1466
内容数
108
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息