java 实现字符动画《BadApple!!》

有屏幕的地方就会有《BadApple!!》。最近看了各种版本的《BadApple!!》字符动画,觉得有点神奇,所以尝试实现一下,把踩坑的过程记录下来。

遗憾的是,因为cmd机能的限制,虽然实现了动画效果,但是画面会“抖动”,有机会换个播放载体进行优化(比如Jframe)。

参考:http://tieba.baidu.com/p/2850192041?pn=1

 

一、原理介绍

动画的原理,就是大量的帧以一定的频率进行播放,让静态画面实现动态效果。

同理,字符动画的实现原理,就是提取原版视频的帧,将其转换为字符帧,然后使用一定的频率进行播放。在播放字符帧的的同时,可以开启另一条线程播放音频,最终可以实现动画效果。

讲到这里,有基础的同学已经可以自己尝试做做看了。下面会给出详细的解决方案。

二、准备素材

(1)找到视频

首先你需要《BadApple!!》的视频。我从网上找到视频属性为:

2

下载地址:http://pan.baidu.com/s/1jHXsBfo

(2)使用Maven构建Java项目

因为涉及很多依赖,所以使用Maven构建Java项目,选择quick start即可。

需要引入如下依赖:

pom.xml

jlayer负责处理音频,负责播放mp3。jna和jna-platform负责调用本地dll,实现字符的输出(可以不引入)。

为什么可以不引入jna依赖?这个问题和字符的输出方式有关,直接影响视频帧的播放效果。(这个问题将在下面讲到)

二、提取视频帧

本来我想要自己实现视频帧的提取,但是需要解决的问题太多了(解析视频、截图、转成图片),担心会把事情搞得很复杂,所以选择了使用ffmpeg。

具体思路是:使用Java脚本操作ffmpeg,实现视频帧的提取。

(1)下载ffmeg

首先你需要下载ffmpeg工具。http://ffmpeg.org/

我个人下载的是shared版本,虽然没有完全编译,但是已经是可使用状态。

下载完后应该是这样的:

2

我们需要使用的是ffmpeg.exe。需要此程序的绝对路径。

(2)编写java脚本

其实我们完全可以使用命令操作ffmpeg,直接截取视频帧。但是为了重用性,最好写成脚本。(因为本项目使用java实现,所以脚本我也用java写。你也可以用别的脚本语言实现,比如Python)

SetImages.java

运行效果为:

2

(3)视频帧参数的调整

个人认为,在所有参数中,最重要的是视频帧的尺寸:

直接影响字符帧的播放效果。

1.视频帧的尺寸问题

原版视频帧的尺寸是512 * 384,为什么要缩放为90 * 30?

字符帧是由视频帧转换而来的,每一个有效像素都会转换为一个字符。如果视频帧的尺寸很大,有效像素就会变多,导致字符帧的字符量增多。在输出字符时,如果字符量很大,输出时间就会变长,造成输出延迟,导致动画“抖动”。

所以,为了字符帧的播放更加流畅,视频帧的尺寸必须缩小,让字符帧的字符量减少。当然,缩小视频帧的代价是精度会丢失,这是必要的妥协。

2.视频帧的宽高比问题

原版视频帧的尺寸是512 * 384,宽高比为4:3,缩放之后为90 * 30,宽高比为3:1。为什么要改变宽高比?字符帧的比例会不会有问题(变扁)?

之所以需要改变宽高比,是因为cmd字号的长和宽像素不相等。

6

如图所示,cmd中字符的宽高比为9:21。如果视频帧沿用原来的4:3宽高比,最终在cmd中显示的比例将会变成4:7(4 * 9 : 3 * 21 = 36:63 = 4:7)。比起原视频,字符帧会“变窄”,而不是“变扁”。

为了尽量保持视频的原比例,如果我们把视频帧的宽高比调整为3:1,最终在cmd中显示的比例将会约等于4:3(3 * 9 : 1 * 21 = 27:21 约等于 4:3),更接近原视频比例。

具体调整可以参考第⑨节。

(4)按比例缩放视频帧(不使用ffmpeg)

一开始我不知道ffmpeg截取视频帧可以指定尺寸,所以写了一个按比例缩放的脚本。

Zoom.java

该脚本可以按比例正确缩放图片。

刚写完,我就发现了ffmpeg修改视频帧尺寸的功能,导致该脚本完全没有派上用场…

三、生成字符帧

(1)实现

遍历所有视频帧,每一张视频帧对应生成一张字符帧。遍历每一张视频帧中的所有像素,如果像素为“黑”,则写入一个空格(在cmd中表现为黑色背景)。如果像素为“白”,则写入一个字符“M”(在cmd中表现为白色字符,之所以选择M,是因为M占有效面积大,字符更加饱满,效果更好)。

SetTxts.java

需要注意两个细节:

1.换行符问题

txt中的换行符是\r\n,\r\n的顺序不能乱,不能是\n(要区分cmd中的换行符)。否则txt中的字符将会一团乱,完全看不出图像。

2.flush()方法

每次写入txt完成时,必须调用flush()方法,把缓冲区中的数据全部输出。否则txt中的字符可能不全,图像会残缺。

实现效果为:

7

(2)为什么选择《BadApple!!》?

因为《BadApple!!》本身只有黑白二色,容易生成字符帧。如果选择别的五颜六色的视频,在生成字符帧时就要分辨更多的像素,比《BadApple!!》复杂很多。

如果要处理彩色图片,在提取视频帧后,要把每一帧转成灰度图(灰白图),让像素更容易分辨,方便生成字符帧。

TestGreyImage.java

这是原图:

9

转换之后变成了(有一种冷场的感觉…):

grey

四、获取视频中的音频(Mp3)

写一个java脚本,使用ffmpeg获取音频。

SetMp3.java

五、把txt中的字符信息加载于内存中

在播放字符帧时,如果每次都从txt文件中读取字符,会有一定延迟,导致每秒帧数减少,播放效果“不连贯”。

因为要播放的字符帧并不是很多,占用的内存不大,所以在播放之前要把所有的字符帧加载到内存中,避免加载txt时的延迟。

GetTxts.java

getTxts()方法返回一个数组String[],播放字符帧时可以遍历数组,读取要播放的字符。

六、编写播放器

写一个播放器,周期播放字符帧,另开一条线程播放音频。

Main.java

在理想情况下最好能达到每秒30帧以上,人物的动作会更加流畅。这里我设定每秒播放15帧,是性能上的妥协。如果每秒输出太多帧,上一帧还没有输出完,下一帧就接踵而至,又会表现为画面“抖动”。

至此,编码部分已经基本完成。

七、尝试优化“抖动”现象(失败)

(需要在pom.xml中加入jna依赖。这是一个失败的优化方案,有兴趣可以尝试一下)

之所以会发生“抖动”现象,是因为System.out.print()方法需要一个个输出所有字符,字符是随着数据流逐渐显示的 ,无法一次显示完整的一段文字。

输出字符时cmd界面会往下滚动,上一帧的字符还没有完全离开cmd界面,下一帧的字符已经开始输出了。这就导致了同一时间内我们会看到两帧画面。就出现了画面的“抖动”。

解决方法也很简单,就是在下一帧播放时,必须保证上一帧已经完全离开cmd界面。所以,每次开始播放字符帧,必须把cmd的光标至于(0,0)处,保证上一帧完全离开,下一帧是“单独播放的”。

现在问题就变成了“如何把cmd的光标至于(0,0)处”。查询之后,发现了一个SetConsoleCursorPosition()方法,可以设置光标的位置。

问题是,SetConsoleCursorPosition是一个windows本地函数,在Java中是不能直接调用的,这时候jna就派上用场了。

思路大致如下:我们使用c++自己实现一个print()方法,每次输出完所有字符后,都调用SetConsoleCursorPosition()方法重置光标到(0,0),然后使用jna调用print()方法,代替System.out.print()方法,就能解决“抖动”问题。

(1)创建dll工程

666

1.代码部分

写一个头文件main.h

main.h

写出具体实现:

main.cpp

2.编译

在vs2010中,dll工程模式默认编译为Win32(32位),你可以在vs的设置中改成64位。

如果你使用的jdk、Eclipse、jna都是64位,在调用32位的dll时,会报出“不是有效的 win32应用程序”错误(同理,如果jdk、Eclipse、jna都是32位,调用64位dll也会报错)。

因此,在编译dll之前,一定要该清楚本机、工具、环境的位数。编译成dll之后,我们需要其绝对地址。

(2)使用jna调用dll

Printer.java

写一个接口Printer,定义一个Print()方法(要和dll中的方法同名),之后就可以使用这个Print()方法直接代替System.out.print()方法了。

直接调用Printer类中INSTANCE对象(返回一个Printer对象)的Print()方法即可。

(3)此优化方案存在的问题

现在我们可以使用jna实现字符打印了,但是问题又来了:这个方法打印字符太慢了,无法迅速输出所有字符,动画变成了幻灯片…

个人认为,问题的原因是“jna调用本地方法效率很差”,导致字符无法迅速输出。此问题暂时无法解决(也许电脑之间存在差异,如果有兴趣可以试试)。

至此,优化方案失败了…如果该方案行不通,就不需要加入jna依赖了,简单地syso输出字符即可。

八、打包jar文件

使用Maven打包项目需要一定的技巧。个人推荐使用以下插件:

pom.xml

maven-surefire-plugin在打包时可以跳过测试阶段。

maven-jar-plugin设置jar包的入口文件(java -jar x.jar时首先执行的class)。

maven-dependency-plugin负责把依赖的jar包(jlayer、jna)打包到${project.build.directory}/lib下,即构建路径下的lib文件夹中。如果你使用Eclipse,一般情况下就是在target/lib中。

⑨、设置播放用的cmd

(1)创建快捷方式

快捷方式的目标路径指向“C:\Windows\SysWOW64\cmd.exe”(支持64位的cmd)或者“C:\Windows\System32\cmd.exe”(支持32位的cmd)。

(2)修改“目标”和“起始位置”

666

目标改成“C:\Windows\SysWOW64\cmd.exe /k “java -Xmx1024m -jar myBadApple-0.0.1-SNAPSHOT.jar””(因为要加载所有txt中的字符,所以把jvm的运行内存扩充至1g,防止内存不够用)

起始位置置空(在哪里执行起始位置就在哪里)

(3)调整“字体”和“布局”

我使用的是18号,9 * 21(宽高比)的新宋体。布局的宽高比均为150 * 75。

因电脑而异,可以自己调整达到最好的效果。我最后实现的效果为:

666

我为了减少动画“抖动”,主动降低了帧数,导致精度丢失比较严重。如果电脑性能够好,理论上可以让动画播放流畅,细节清晰。

十、总结

这属于一个娱乐性质的小程序吧。

踩了很多坑,学会了jna等一系列工具的用法,感觉有不少的收获。

发表评论

电子邮件地址不会被公开。 必填项已用*标注