臥槽!GPU里的生命游戲,居然還能這樣玩!
簡單,優(yōu)雅,有極強的涌現(xiàn)性,又發(fā)人深省。這就是能令我盯著它發(fā)呆的"生命游戲"。今天練習在 GPU 里運行"生命游戲",文末有項目地址。
生命游戲的規(guī)則
生命游戲(Game of Life)是一類二維的元胞自動機,由 J.Conway 在1970年代設計。規(guī)則如下:
有一個二維網(wǎng)格,每個格子代表一個元胞。 格子有0和1兩種狀態(tài),對應元胞的"死"和"生"。 每個元胞有8個相鄰的元胞,元胞和其8個鄰居的當前時刻狀態(tài)決定了它下一時刻的狀態(tài)。 如果元胞當前為"生",則僅當8個鄰居中有2個或3個為"生"時,該元胞保持"生",否則變?yōu)?死" 如果元胞當前為"死",則僅當8個鄰居中有3個為"生"時,該元胞變?yōu)?生",否則保持"死"

在GPU里運算
生命游戲的規(guī)則很簡單,并且對并行計算非常友好,非常適合通過 GPU 進行運算和展現(xiàn)。本文使用 Cocos Creator 2.4.0,通過編寫shader實現(xiàn)基于 GPU 的生命游戲運算。
流程
整個過程涉及到3張紋理:表示網(wǎng)格初始狀態(tài)的紋理T和兩個 RenderTexture (分別命名為RTA、RTB)。 RTA和RTB會在運行時交替地計算自身的下一時刻狀態(tài)到對方紋理中。大致流程如下:

設置紋理狀態(tài)
// 禁用紋理動態(tài)合圖
texture.packable = false;
// 采樣坐標周期循環(huán),可以比較方便地實現(xiàn)元胞自動機的周期型邊界條件,不過要求紋理大小必須是2的冪
texture.setWrapMode(cc.Texture2D.WrapMode.REPEAT, cc.Texture2D.WrapMode.REPEAT);
// 使用最近距離采樣,避免出現(xiàn)插值
texture.setFilters(cc.Texture2D.Filter.NEAREST, cc.Texture2D.Filter.NEAREST);
狀態(tài)迭代
根據(jù)規(guī)則,需要對當前位置和周圍8個鄰居的狀態(tài)進行采樣。對鄰近位置進行采樣需要根據(jù)紋素大小進行偏移,這里命名為dx和dy,通過 uniform 變量傳入 shader。 dx=1.0/width,dy=1.0/height。
// 統(tǒng)計8個鄰居的狀態(tài),計算結束后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是否在某個區(qū)間內,可以將兩個step()函數(shù)疊加進行判斷。
// 如果元胞當前為"生",則僅當8個鄰居中有2個或3個為"生"時,該元胞保持"生",否則變?yōu)?死"
// 只有當sum = 2或者3時,oneCase的值是1.0
vec4 oneCase = step(vec4(1.9), sum) * step(sum, vec4(3.1));
// 如果元胞當前為"死",則僅當8個鄰居中有3個為"生"時,該元胞變?yōu)?生",否則保持"死"
// 只有當sum = 3時,zeroCase的值是1.0
vec4 zeroCase = step(vec4(2.9), sum) * step(sum, vec4(3.1));
根據(jù)當前元胞自身狀態(tài)進行分支選擇
vec4 col = texture(texture, uv);
col = mix(zeroCase, oneCase, col);
初始狀態(tài)
不同的初始狀態(tài)決定了生命游戲的后續(xù)發(fā)展,通過精心設計初始狀態(tài),可以產生狀態(tài)循環(huán)、整體平移等神奇的表現(xiàn)。
這里直接搬運網(wǎng)絡上的現(xiàn)成的模型翻譯成RenderTexture作為初始狀態(tài),參考了 https://funnyjs.com/jspages/game-of-life.html
并行的生命游戲
表示元胞的"生"、"死"不需要用完一個vec4變量,只需要一個分量即可。所以當我們把初始狀態(tài)紋理的 RGB 通道解耦,讓它們獨立取0或1后,我們就得到了3個生命游戲的初始狀態(tài)紋理,shader 代碼不需要改動就可以支持3個生命游戲的并行演算(A通道也利用上就是4個)。
代碼 & Demo

源碼地址:
http://store.cocos.com/app/detail/2895 (點擊閱讀原文可跳轉)
參考
https://zhuanlan.zhihu.com/p/39451507
http://micro.ustc.edu.cn/CompPhy/lecturenote/comp_sun_3_4.pdf
https://funnyjs.com/jspages/game-of-life.html

