我希望在自制的游戏中实现一个简单的HUD(Head-up display 抬头显示器),使其看起来“像那么回事”;DCS游戏中的座舱制作的算比较精致的,鉴于财力问题,我们这次能够“抄作业“的最好对象就是JF-17”枭龙“战机的HUD了:

枭龙模组的HUD非常复杂,我们本次尝试只选择实现这些内容:

  • 反射式成像的效果:图像中心的光线始终与载机轴线平行,看起来始终保持在正前方;图像看起来在无限远处,成像大小仅和视角有关,前后移动,并不会有近大远小的效果
  • 速度、高度、航向指示器,且刻度有滚动效果
  • 姿态仪,能够正确显示俯仰、偏航、滚转角度
  • 速度矢量符号、机头指向
  • 其他纯文字内容:攻角、过载、历史最大过载、马赫数、HUD模式等

贴图绘制

实现HUD的整体思路是将其中动态的部分拆解成独立的图片,来进行移动、旋转操作。通过一些取巧的手段,本次实现的内容都不需要动态生成mesh就能够完成,这样工作量就少了很多。

HUD中的图像除了文字就是一些线条,虽然不太复杂但是对位置的精准性要求比较高,所以我使用了手写svg的方式来绘制HUD。这种方法后续调整参数比较方便,同时不需要专业的软件,直接用文本编辑器就可以了。我构造了一个简单的工作流:首先写出要绘制内容的jinja2模板,然后通过python脚本填入参数生成.svg文件,再使用inkscape将其渲染为.png文件。

以左侧的速度表的滚动刻度为例,它的jinja2模板长这个样子:

<?xml version="1.0" encoding="UTF-8"?>
{% set height = scale_height * (scales|length * line_widths|length) %}
{% set x_mid = width/2 %}
{% set y_mid = height %}
<svg width="{{width}}" height="{{height}}">
    {# background #}
    <g stroke="#00ff00" stroke-width="{{line_stroke_width}}">
        {% for i, _ in scales %}
            {% for j, line_width in line_widths %}
                {% set x = 0 if side=='left' else width-line_width %}
                {% set dy = (j + i * line_widths|length) * scale_height + line_stroke_width%}
                <line x1="{{x}}" x2="{{x + line_width}}" 
                    y1="{{height - dy}}" y2="{{height - dy}}" />
            {% endfor %}
        {% endfor %}
    </g>
</svg>

生成的svg图长这个样子

<?xml version="1.0" encoding="utf-8"?>
<svg height="125" width="20">
 <g stroke="#00ff00" stroke-width="1.5">
  <line x1="10" x2="20" y1="123.5" y2="123.5"/>
  <line x1="14" x2="20" y1="118.5" y2="118.5"/>
  <line x1="14" x2="20" y1="113.5" y2="113.5"/>
  <line x1="14" x2="20" y1="108.5" y2="108.5"/>
  <line x1="14" x2="20" y1="103.5" y2="103.5"/>
  <line x1="10" x2="20" y1="98.5" y2="98.5"/>
  <line x1="14" x2="20" y1="93.5" y2="93.5"/>
  <line x1="14" x2="20" y1="88.5" y2="88.5"/>
  <line x1="14" x2="20" y1="83.5" y2="83.5"/>
  <line x1="14" x2="20" y1="78.5" y2="78.5"/>
  <line x1="10" x2="20" y1="73.5" y2="73.5"/>
  <line x1="14" x2="20" y1="68.5" y2="68.5"/>
  <line x1="14" x2="20" y1="63.5" y2="63.5"/>
  <line x1="14" x2="20" y1="58.5" y2="58.5"/>
  <line x1="14" x2="20" y1="53.5" y2="53.5"/>
  <line x1="10" x2="20" y1="48.5" y2="48.5"/>
  <line x1="14" x2="20" y1="43.5" y2="43.5"/>
  <line x1="14" x2="20" y1="38.5" y2="38.5"/>
  <line x1="14" x2="20" y1="33.5" y2="33.5"/>
  <line x1="14" x2="20" y1="28.5" y2="28.5"/>
  <line x1="10" x2="20" y1="23.5" y2="23.5"/>
  <line x1="14" x2="20" y1="18.5" y2="18.5"/>
  <line x1="14" x2="20" y1="13.5" y2="13.5"/>
  <line x1="14" x2="20" y1="8.5" y2="8.5"/>
  <line x1="14" x2="20" y1="3.5" y2="3.5"/>
 </g>
</svg>

字体

我没有找到JF-17中使用的字体,但是找到了一个开源的航空字体:B612,据说是空客在使用的字体。在unity中,可以使用TextMeshPro来生成与渲染我们自己选择的字体;另外,还支持一些超文本标记,后续扩展HUD中的其他文字显示会比较方便,比如左右对齐、居中、删除线之类的。

SDF着色

在初期的测试中,我发现图像的显示质量十分感人,即使把分辨率拉满,线条边缘也和抠图没扣干净一样,有一些残留的白色。参考TextMeshPro的字体显示方案,解决这个问题的方法就是采用SDF(Signed Distance Field)贴图来进行着色,参考这个blog。使用SDF后,线条的边缘在各种缩放下都能一直保持清晰,不过在贴图分辨率不高的情况下,转角处会更圆滑。

SDF的原理很多地方都有了。简单来说问题的原因是图像渲染时的插值,在同一个色块的内部,插值工作的很好,但是在(字体/线条)的边缘处,颜色发生了突变,插值的结果就不科学了,会产生模糊的效果。对于字体、线条来说,这个问题就很严重,因此要采用一个适合插值的东西作为贴图的值输入着色器。由此产生了SDF的概念,它描述的是像素点与纹样边缘的距离,插值后也很科学(不完全对,比如字体转角处就会变圆滑),在着色器中,最后将这个距离输出为图像的alpha值,就可以获得一个清晰的边缘了,参考这个shader:Image-SDF

在github上,已经有工具能够将.png图像转化为SDF贴图了,我做了一点小改动:SDF-Generator;只写入alpha通道,不需要其他颜色信息了(基色直接在材质里设置)。这样在unity中导入贴图时可以选择alpha8格式,节省一点空间。

滚动刻度

滚动刻度的范围可以非常大,比如速度表,其展示的范围从0~1000+都有可能(我采用了公制单位,就是任性),直接输出到一张图片中就是又细又长,体积太大。所以需要输出可变的刻度数字,再通过上下移动较小尺寸的组合对象来实现滚动的效果,当偏移超过+-0.5时,改变数字内容,再重置偏移量;多余的部分使用遮罩去除即可。

姿态仪与速度矢量符号

姿态仪也通过一整张图片实现,目前使用了均匀的刻度分划,方便进行平移,在大角度时效果不是太好,不过问题不大。

观察JF-17的HUD,可以发现姿态仪是如何展示滚转、俯仰、偏航、攻角数据的,特别地,速度矢量符号总是处在姿态仪的中心垂线上:

滚转角和速度矢量符号的存在让姿态仪图像的平移稍微麻烦了亿点,需要整点三角函数才能计算出最终的平移量。

反射式成像效果

图像中心的光线始终与载机轴线平行:这个实现起来比较简单,只要让HUD的xy坐标随着观察者一起移动即可。

图像看起来在无限远处,成像大小仅和视角有关:可以让HUD始终保持在观察者前方的固定距离上,也可以让HUD随着观察者z坐标进行缩放。但是后者在观察者距离HUD平面很近的时候,缩放值会非常小,由于数值计算精度的原因,图像会抖动起来,效果非常糟糕。

当姿态仪图像的大小确定后,我们实际上已经固定了视角-HUD中距离的换算关系了,否则姿态仪的虚拟地平线就不能和真正的地平线重合了。假设1弧度对应于HUD中2单位的距离,我们将HUD平面固定在观察者前3单位的位置上,那么此时HUD的缩放大小应该是1*3/2=1.5,即放大1.5倍。


最后整出来的效果就是这样:【超级鱼窝】从零开始的飞行模拟游戏 - HUD测试,看起来海星