字幕君

字幕君(字幕君只是一个代号,其窗口名字还是「Fluid Demo」)是用于在舞台上显示歌词烘托演出气氛的一个小程序,最近的一次亮相是……在俺们的「胶囊乐队」第一次演出时登台,用于显示歌名和歌词。

mmexport1416941092981–>Screenshot - 11292014 - 05:02:09 AM

 

(对的,在现场那天,连FPS Counter都没有拿掉。因为俺感觉跳动的示数看起来就会让人觉得兴奋哈哈)

字幕君是以[1]为蓝本修改的。原先的程序模拟了一个热源、一个密度源作用下带有障碍物的网格中流体流动的现象。字幕君以[1]为蓝本修改的过程中,增加了以下内容:

  1. 将文字输出到密度场中参与流体相关计算、以及配套的非常非常简陋的设置歌词、颜色、转场时间的脚本。
  2. 经由boost::shared_memory_object来使得通过ssh遥控字幕君成为可能。

这两项功能是独立的、不互相依赖的。同时,在制作字幕君的过程中,还略微对Gauss-Seidel法和Jacobi法有了一些了解。

【Gauss-Seidel、Jacobi法和共轭梯度法】

这三种方法的用处都是为了解以下方程,亦即Navier-Stokes方程中的Diffuse项:

Diffuse项的连续形式:
Diffuse Term Continuous Form
将其转换成网格上的离散形式,就能看出 Jacobi 法和 Gauss-Seidel 法的端倪:
Diffuse Term Discrete Form
式子右边的是上一时刻的u场中的值,左边的是欲求的本时刻的u场中的值。经过多次迭代以后,上一时刻的u场就变成了本时刻的u场。

 

这个程序中解线性方程使用的是Jacobi法,[2]中用的是Gauss-Seidel法。Jacobi法与Gauss-Seidel法的区别在于在一次迭代中,是否使用已经在这次迭代中覆写过的元素。对应到程序中就是在最内层循环中的赋值语句的左侧写入到哪里: Jacobi法是在每一次循环结束时才统一将x场覆写一次,但是 Gauss-Seidel 法在循环过程中就将 x 覆写一次:

// Gauss-Seidel
for(int i=1; i<=w; i++) {
    for(int j=1; j<=h; j++) {         
       x[IX(i, j)] = (a*(x[IX(i-1,j)] + 
                         x[IX(i+1,j)] +
                         x[IX(i,j-1)] + 
                         x[IX(i,j+1)]) +                                x0[IX(i,j)]) / c;
   }
}
// Jacobian 
for(int i=1; i<=w; i++) {
    for(int j=1; j<=h; j++) {
      aux[IX(i, j)] = (a*(x[IX(i-1,j)] + 
                          x[IX(i+1,j)] +
                          x[IX(i,j-1)] + 
                          x[IX(i,j+1)]) +
                       x0[IX(i,j)]) / c;
    } 
} 
memcpy(x, aux, sizeof(float)*(w+2)*(h+2));

至共轭梯度法的实现则和这两者不太相同。[4]在讲述共轭梯度法时将问题描述为了一个线性系统Ax=b。一开始费了一番力气才明白其中的A、x和b分别是什么。虽然花了一番力气,但是有了这样的一般化描述,以后面对类似问题时就更容易明白了吧。在具体实现过程中,矩阵A是以一个函数FluidSolver::multiplyASolver存在的(对于N*N的网格来说,A矩阵的大小是N^2 * N^2,但是它是一个稀疏矩阵,大量的元素是0),x是下一时时刻的x场,b是这一时刻的x场。将展开后的离散形式中的下一时刻的x全部移到左边,这一时间的x全部移到右边,即可得到A的撰写方法。

// Conjugate Gradient (来自[4])
0. 设置A和b
1. r <- b - Ax
2. p = r
3. rho = dot(r, r)
4. rho0 = rho
5. for(iter=0; iter

Jacobi法的好处是每个格子之间没有相互依赖,适合GPU实现。Gauss-Seidel法的好处是收敛速度快一些。共轭梯度法的收敛速度更快,如下图所示:

收敛速度

以我自身的使用情况看来,[1]所提供的GPU实现可以在320x320的格子上达到60fps的更新速度,但是[2]的CPU实现只要超过300x90就会迟滞了。不过字幕君一开始的想法是在[2]上试验的,因为[2]中流体参与运算的所有过程都是在CPU上进行,不涉及帧缓冲对象和贴图对象之类的OpenGL Clinet和Host之间的数据交换,比较简单。

[4]说,共轭梯度法在GPU上的表现比红+黑的Gauss-Seidel法和Jacobi法都慢。至于为何尚不可知,但是若是在CPU上,凭借其高速收敛的特性,共轭梯度法应当是最快的。

【将文字内容与流体模拟连起来】

将文字输出到密度场中借用了freetype-gl的范例。freeglut-gl完成了读入TTF字形、将矢量字形转为顶点缓冲并将文字输出到一张贴图的工作。在这之后只需将含有文字的贴图以Shader写入[1]中的循环,既可做成一个以字形为形状的密度源。

目前的脚本支持三个不同的贴图同时在屏幕上出现,而这套脚本就是用于管理这三个不同的贴图,使其能在{不可见、淡入、可见、淡出}这四种状态间转换,另外控制每个状态中所对应的参数({淡入/出时间、处于可见状态时每帧向密度场添加的流量})。与以上截图相对应的脚本如下所示:

CapsuleBegin
Pen Color(1,1,1) Delay(1000)
Silo0 Text Pos(35,190) Delay(0) Weights(0.01,0.01) " 啦啦啦 啦啦啦 啦"
Brush0 Rect Velocity Pos(480,0,460,270) Value(-30,0,0,1) Frames(200000)
Silo0 FadeIn Duration(1000)
Silo1 Text Pos(35,90) Delay(1000) Weights(0.01,0.01) " 在生命中\n 最美丽的一天 "
Silo1 FadeIn Duration(1000)
CapsuleEnd

CapsuleBegin
Silo0 FadeOut Duration(1000) Impulse(0)
Silo1 FadeOut Duration(1000) Impulse(0)
Brush0 Stop
CapsuleEnd

从中可见在脚本是由一些位于CapsuleBegin和CapsuleEnd之间的命令组成的,若干连续执行的命令构成一个动作,在实际上台演出时,按一下空格键就会执行一个动作,即是包含在此动作中的所有命令。CapsuleBegin命令可以有标签,若有的话,则可通过B与N键来定位。在这回使用的脚本中,标签全部位于17首曲子各自的开始处。这样在遇到意外需要重来一曲或是字幕放映出错时,就可以及时调回到正确的位置了。

【远程控制】

若想在表演场地显示字幕君,必须将参与计算的电脑连接至调音台上的VGA Port上。由于调音台离舞台很远,所以人在舞台上时就不能直接操作参与计算的电脑。由于表演场地有Wi-Fi覆盖,所以通过Wi-Fi进行远程操控就成为一种可行方案。所以,将另外一台电脑相连之后,就有了以下的拓扑图:

逸珑8089D  XPS M1330 舞台上的银幕

这台逸珑8089D是去年团购时买的,重装了Debian 7。鉴于龙芯笔记本图形性能有限且无线网络带宽可能有限的问题,带图形的远程桌面不是好选择,所以只考虑了以文字界面的SSH登录这种方式。

SSH登录以后,还剩下的问题就是如何将正在运行中的程序的状态告知用户和如何响应用户的输入。这两个问题都可以通过为字幕君再编写一个配套的控制程序来解决。通过参照[3]的范例,达成了字幕君与字幕君的控制程序之间以boost::shared_memory_object通信的功能。在以上的画面呈现时,字幕君控制器所在的控制台中会显示如下内容:

Received: Heart Beat
Silo 0: [  KEEP      ]  啦啦啦 啦啦啦 啦
Silo 1: [  KEEP      ]  在生命中\n 最美丽的一天 
Silo 2: [  KEEP      ] 10.
Action -1/215, Line1005/1455

这样字幕君的功能就差不多齐全了。

【杂项】

※ 因为歌词有中文,所以在程序中有很多地方都需要使用wchar_t*,所以相应的字串处理函数也需要使用宽字符的对应版,如strcmp的对应即是wcscmp(Wide Character String Comparison)。有一个例,即是在printf时,可以不像其它与char*对应的函数那样使用wchar_t的对应版wprintf,而是只需使用printf("%ls")。其原因是wprintf与printf是不推荐混用的。另外,读取中文文本目前还不清楚除了使用C++ STL中的以外的方法,因此也只能将CPP文件宣告为extern "C"以后在C文件中使用。

※ 表演场地的舞台的VGA接口支持的分辨率有很多,当时选定的是960x540,可是实际测量之后,发现只有正中间的大小约为720x480的区域是可见的,周围全被裁剪掉了。为了照顾不同的屏幕大小,字幕君可将整个窗口的内容缩放,而相应的渲染流程也从直接渲染到窗口的帧缓存修改为先渲染到某贴图,然后将各个贴图贴到窗口上。这样做的原因是有些Shader依赖窗口位置和大小,而若直接从Clip Space的坐标(-1, 1),就可避免绝对窗口大小带来的问题。

【接下来的打算】

接下来我还打算将字幕君用作自己想翻唱的一首歌里,可是目前的状态是我唱功很不行,大概还需要多爬爬关于唱歌方法的文章和播客内容才能知晓如何突破吧。

【参考】

[1] Simple Fluid Simulation at The Little Grasshopper
[2] Stable Fluids at Caltech Multi-Res Modelling Group - Stam's Stable Fluids
[3] Create two C++ programms using the Boost library for a shared memory example
[4] Goncalo Amador, A. G. (2009). Linear Solvers for Stable Fluids: GPU vs CPU. In 17th Encontro Portugues de Computacao Grafica (EPCG’09).

此条目发表在Programming, Uncategorized分类目录。将固定链接加入收藏夹。