给 Minecraft 编写 Font Shader!使用 GB2312 字符集像素字,渲染游戏画面
仓库地址
GitHub
Gitee
两个仓库同步维护,如果你可以访问 GitHub 的话请访问 GitHub 下载;如果你不可以访问 GitHub 的话可以从 Gitee 下载。
想法来由
这个项目其实最初是在我的 FontVideo 项目之前诞生。我当时设想的是:假设我想要直接在服务器的终端界面上,不使用 X Server 转发的方式(因为卡顿和延迟严重),运行游戏 客户端,登录管理员客户端账号来 视察 服务器内玩家的游玩情况(或者给玩家帮忙,针对作弊玩家采集证据并依规干掉作弊玩家等)的话,有没有办法直接在服务器后台的 终端界面 上显示游戏的画面呢?
既然是终端界面,那肯定只能使用纯文本了。但是文字它也是有形状的呀,而且 SSH 终端是支持彩色文字的。既然如此,那似乎可以用文字来拟合图像的画面,并设置文字的颜色。每个文字的字体看着都像个 卷积核,用这个卷积核去匹配画面,然后筛选出最匹配的文字,那不就能……大致 整 出个画面了么?
一开始我是这样想的,但是其实有个问题就是服务器其实没有 GPU,因此也没有能够达到跑 Shader 的环境,说不定用 CPU Rasterize 到 XVFB 渲染画面的效率极低(毕竟 Minecraft 游戏的场景里的三角面数量肯定是上万上十万不止的了,还有分辨率要求),这个项目并不能落地。但是我可以先试试呀,用客户端跑 Shader 看看效果。我当时非常好奇如果使用汉字去拟合游戏的画面图像,会怎么样。
效果图



甚至还是非常有辨识度的(尤其是动态画面的情况下),只是无法在远距离的情况下区分僵尸和草地、树叶。

开发过程
使用 Sublime-text 开发,按照 OptiFine 的 shader 接口文档来开发。
在整个渲染流程中,我们不需要针对地形、实体、实体方块、云、静态光源、动态光源等进行区分渲染。只需要使用默认的渲染流程,然后在最终合成的时候,进行字体的匹配。因此只需要实现 composite
的流程就可以了。因为我需要多个 Pass 来完成渲染,所以我实现了 composite
和 composite1
、final
的 vsh
和 fsh
。
为了避免特殊符号的干扰(特殊符号容易导致过拟合,在卷积计算中的分数过高,会淘汰掉其它的字体),在字体的采用上只采用纯 GB2312 汉字部分。
GB2312 有 6762 个汉字 ,而如果在 Fragment Shader 里,采用了 不太对 的渲染策略(比如,使用一个分辨率为横竖字符个数的 Render Buffer,再对每个像素扩大采样到原图,遍历 6762 个汉字)的话,就无法把 GPU 的并行性优势充分发挥出来,从而导致严重的卡顿。
我的做法是将上述大小的 Render Buffer 再横竖扩展 X 倍,然后分区域分别匹配不同范围的汉字,最终再折叠这些区域,取分数最高的汉字用于显示。这样既可以遍历所有的汉字,又可以充分发挥 GPU 的并行计算优势。
首先需要设置配置文件,让 Optifine 的渲染引擎为我们加载字体文件(采用旧光照方式以简化开发,另外就是我讨厌 MC 里面的云,所以我关闭了云)
修改 shader.properties
clouds = off
oldLighting = true
texture.composite.gaux1=textures/gb2312.png
顶点着色器
顶点着色器(Vertex Shader)的部分没什么好说的。Optifine 提供了太多的常量接口,根本不需要传递什么信息给 Fragment Shader,只需计算好顶点投影坐标即可:
#version 130
out vec2 texCoord;
void main()
{
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
texCoord = gl_MultiTexCoord0.xy;
}
片段(像素)着色器
片段(像素)着色器(Fragment Shader)的部分就需要仔细设计了。
Pass1:composite.fsh 用于预处理画面,写出三个渲染目标,分别对应颜色、灰度、字体纹理采样。
#version 130
uniform sampler2D colortex0;
uniform sampler2D colortex4;
/* RENDERTARGETS: 0, 1, 2 */
uniform float viewWidth;
uniform float viewHeight;
in vec2 texCoord;
vec2 Resolution = vec2(viewWidth, viewHeight);
vec2 fragCoord = texCoord * Resolution;
void main()
{
vec4 color = texture(colortex0, texCoord);
float gray = pow(length(color.rgb), 0.8) * 0.7 + 0.3;
gl_FragData[0] = color;
gl_FragData[1] = vec4(gray);
gl_FragData[2] = vec4(texelFetch(colortex4, ivec2(fragCoord), 0).rgb, 1.0);
}
Pass2:composite1.fsh 用于字符匹配。根据自己的像素位置,判断自己是要给哪个格子干活,要计算什么样的字符范围的卷积,以及分数存储到哪里。这块的代码很长,我就不粘出来了,有条件的朋友们请自行查阅 Repo。
Pass3:final.fsh 用于折叠上一个 Pass 生成的结果,合并所有的匹配结果取分数最高,然后从字体库里采样,显示字体。
用户指导:如何安装着色器
确保已安装 optifine,然后打开 Minecraft 根目录(通常位于 C:\Users\你的用户名\AppData\Roaming.minecraft
)。进入 shaderpacks
文件夹(如果没有,则需要手动创建),新建名为 font-shader-1.0.0
的子文件夹,将 shaders
文件夹及 LICENSE
文件拷贝进去。请务必阅读 LICENSE
文件。
Optifine 安装说明
需先安装 Java。 前往 optifine 官网下载对应的《我的世界》的 optifine 版本。
安装Java后,双击 optifine.jar
文件即可完成安装。