效果图:
仓库地址:https://github.com/im-red/asciiart_gl
之前使用Qt实现过一个CPU版本的ASCII Art滤镜,流程大致如下:
- 根据字体选择确定单元方格大小。
- 将每个可用字符绘制到单元方格大小的空白图片上,计算每个可用字符的平均灰度值。
- 根据第二步计算出来的每个可用字符的平均灰度值,生成一个0~255灰度值到字符的映射表。
- 按单元方格大小对输入图片进行划分,灰度化后求每个方格的平均灰度值。
- 查询灰度值到字符的映射表,对输入图片的每个方格使用字符进行填充。
CPU版本的滤镜性能比较差,用来处理图片尚可使用,如果用来处理视频就不能胜任了,所以今天试着用OpenGL实现了一下。流程上与CPU版本的前3步是相同的,后面的流程修改为:
- 根据灰度值到字符的映射表,生成一个查找表图:将0~255灰度值对应的字符从左往右依次绘制到图片上,每个字符占据单元方格大小。
- 在shader中,将图片纹理划分成一个个的单元方格,将单元方格左上角的颜色作为本方格的颜色,对颜色进行灰度化后得到灰度值。
- 根据上一步得到的灰度值,计算灰度值对应字符在查找表纹理中的起点坐标。然后在根据当前像素在单元方格中的偏移量得到查找表纹理中我们要取的像素的坐标,取出其颜色进行显示。
fragment shader代码如下:
#version 450 core
out vec4 fragColor;
in vec2 texCoord;
uniform float charWidth;
uniform float charHeight;
uniform sampler2D imageTexture;
uniform sampler2D asciiTexture;
void main()
{
float gridWidth = charWidth;
float gridHeight = charHeight;
int xGrid = int(texCoord.x / gridWidth);
int yGrid = int(texCoord.y / gridHeight);
float xDelta = texCoord.x - xGrid * gridWidth;
float yDelta = texCoord.y - yGrid * gridHeight;
vec4 texColor = texture(imageTexture, vec2(xGrid * gridWidth, yGrid * gridHeight));
float gray = texColor.x * 0.299 + texColor.y * 0.587 + texColor.z * 0.114;
int index = int(gray / (1.0 / 255.0));
fragColor = texture(asciiTexture, vec2(1.0 / 256.0 * (index + xDelta / gridWidth), yDelta / gridHeight));
};
OpenGL版本滤镜的性能没有定量测试,但是拖动窗口大小时,图片重绘速度很快,没有卡顿感,应用到视频上问题应该不大。
OpenGL版本的不足之处在于无法将ASCII化的图片以字符形式输出到终端,似乎是把ASCII Art的精髓给丢掉了:-(