简单,优雅,有极强的涌现性,又发人深省。这就是能令我盯着它发呆的"生命游戏"。今天练习在 GPU 里运行"生命游戏",文末有项目地址。

生命游戏的规则

生命游戏(Game of Life)是一类二维的元胞自动机,由 J.Conway 在1970年代设计。规则如下:

  1. 有一个二维网格,每个格子代表一个元胞。
  2. 格子有0和1两种状态,对应元胞的"死"和"生"。
  3. 每个元胞有8个相邻的元胞,元胞和其8个邻居的当前时刻状态决定了它下一时刻的状态。
  • 如果元胞当前为"生",则仅当8个邻居中有2个或3个为"生"时,该元胞保持"生",否则变为"死"
  • 如果元胞当前为"死",则仅当8个邻居中有3个为"生"时,该元胞变为"生",否则保持"死"

csrss占用大量gpu csgo占用gpu_3d

在GPU里运算

生命游戏的规则很简单,并且对并行计算非常友好,非常适合通过 GPU 进行运算和展现。本文使用 Cocos Creator 2.4.0,通过编写shader实现基于 GPU 的生命游戏运算。

流程

整个过程涉及到3张纹理:表示网格初始状态的纹理T和两个 RenderTexture (分别命名为RTARTB)。 RTARTB会在运行时交替地计算自身的下一时刻状态到对方纹理中。大致流程如下:

csrss占用大量gpu csgo占用gpu_qml_02

设置纹理状态

// 禁用纹理动态合图
texture.packable = false;

// 采样坐标周期循环,可以比较方便地实现元胞自动机的周期型边界条件,不过要求纹理大小必须是2的幂
texture.setWrapMode(cc.Texture2D.WrapMode.REPEAT, cc.Texture2D.WrapMode.REPEAT);

// 使用最近距离采样,避免出现插值
texture.setFilters(cc.Texture2D.Filter.NEAREST, cc.Texture2D.Filter.NEAREST);

状态迭代

根据规则,需要对当前位置和周围8个邻居的状态进行采样。对邻近位置进行采样需要根据纹素大小进行偏移,这里命名为dxdy,通过 uniform 变量传入 shader。 dx=1.0/widthdy=1.0/height

// 统计8个邻居的状态,计算结束后sum的范围是[0.0, 8.0]
vec4 sum = vec4(0.);
vec4 d = vec4(dx, dy, -dy, 0.);
sum += texture(texture, uv-d.xy);   // 即uv + (-dx, -dy)处采样
sum += texture(texture, uv-d.xw);   // 即uv + (-dx, 0.)处采样
sum += texture(texture, uv-d.xz);   // 即uv + (-dx, +dy)处采样
sum += texture(texture, uv+d.wz);   // ...以此类推
sum += texture(texture, uv+d.wy);
sum += texture(texture, uv+d.xz);
sum += texture(texture, uv+d.xw);
sum += texture(texture, uv+d.xy);

判断sum是否在某个区间内,可以将两个step()函数叠加进行判断。

// 如果元胞当前为"生",则仅当8个邻居中有2个或3个为"生"时,该元胞保持"生",否则变为"死"
// 只有当sum = 2或者3时,oneCase的值是1.0
vec4 oneCase = step(vec4(1.9), sum) * step(sum, vec4(3.1));

// 如果元胞当前为"死",则仅当8个邻居中有3个为"生"时,该元胞变为"生",否则保持"死"
// 只有当sum = 3时,zeroCase的值是1.0
vec4 zeroCase = step(vec4(2.9), sum) * step(sum, vec4(3.1));

根据当前元胞自身状态进行分支选择

vec4 col = texture(texture, uv);
col = mix(zeroCase, oneCase, col);

初始状态

不同的初始状态决定了生命游戏的后续发展,通过精心设计初始状态,可以产生状态循环、整体平移等神奇的表现。
这里直接搬运网络上的现成的模型翻译成RenderTexture作为初始状态,参考了 https://funnyjs.com/jspages/game-of-life.html

并行的生命游戏

表示元胞的"生"、"死"不需要用完一个vec4变量,只需要一个分量即可。所以当我们把初始状态纹理的 RGB 通道解耦,让它们独立取0或1后,我们就得到了3个生命游戏的初始状态纹理,shader 代码不需要改动就可以支持3个生命游戏的并行演算(A通道也利用上就是4个)。

csrss占用大量gpu csgo占用gpu_3d_03