[iOS]MetalでGPUコンピューティング(10) Metal Shading Languageで記述されたライフゲームのロジック

More than 1 year has passed since last update.

この記事は、Metal Advent Calendar2016の14日目です。

これまで、MetalのGPUコンピューティングについて解説記事を書いてきました。
[iOS] MetalでGPUコンピューティング (1) 最小限のコードの記述と特性の把握
[iOS] MetalでGPUコンピューティング (2) 群知能
[iOS] MetalでGPUコンピューティング (3) MTLDevice
[iOS] MetalでGPUコンピューティング (4) MTKView
[iOS] MetalでGPUコンピューティング (5) MTLLibrary
[iOS] MetalでGPUコンピューティング (6) MTLCommandQueue
[iOS] MetalでGPUコンピューティング (7) MTLCommandBuffer
[iOS] MetalでGPUコンピューティング (8) MTLComputeCommandEncoder
[iOS] MetalでGPUコンピューティング (9) MTLComputePipelineState

本記事では、前回に引き続きAppleが提供するサンプルコードの解説を行います。

扱うサンプルコードは、前回と同じライフゲームのアプリ、MetalGameOfLifeです。
MetalGameOfLife

IMG_5933.PNG
(実行画面)

今回は、サンプルコード内のに記述されたライフゲームのロジックについて解説を行います。
ライフゲームそのものに関しては、YouTubeなどで動画を検索していただければ概観がつかめるかとおもます。
要約すると、周囲のマス目の個体数の疎密により、個体の生死や存続が決定されるプログラムです。

このライフゲームのロジックは、シェーダ内にコンピューティング用の関数で記述されています。

繰り返しになるのですが、このサンプルコードは、主に以下のファイルで構成されています。

AAPLRender.h
AAPLRender.m
AAPLViewController.h
AAPLViewController.m
Sharder.metal

このうち、AAPLRender.mには並列コンピューティング及び描画のCPU側のロジックが、Shader.metalには頂点シェーダー、フラグメントシェーダー、GPUコンピューティング用のシェーダーが書かれています。

予めテクスチャの各点に対応するスレッドが割り当てられているので、各点に対応する処理は並列で行われます。

ここからは、Shader.metal内におけるライフゲームのロジックの箇所を解説していきます。
Shader.mに以下の記述があります。

Shader
kernel void game_of_life(texture2d<uint, access::sample> readTexture [[texture(0)]],
                         texture2d<uint, access::write> writeTexture [[texture(1)]],
                         sampler wrapSampler [[sampler(0)]],
                         ushort2 gridPosition [[thread_position_in_grid]])
{
    ushort width = readTexture.get_width();
    ushort height = readTexture.get_height();
    float2 bounds(width, height);
    float2 position = float2(gridPosition);
...                                                                    

game_of_life関数が記述されています。kernerlと書かれているのでこれはコンピューティング用の関数になります。
引数として、入力用のテクスチャ、出力用のテクスチャ、サンプラー、グリッド内におけるこのスレッドの位置(gridPosition)が渡されています。
flot2型はfloat型の二次元ベクトルです。

この関数外に、周囲のセルの生死判定用の構造体が記述されています。Metal Shading LanguageはC++ベースなので構造体を使用することもできます。

Shader
constant float2 kNeighborDirections[] =
{
    float2(-1, -1), float2(-1, 0), float2(-1, 1),
    float2( 0, -1), /*  center  */ float2( 0, 1),
    float2( 1, -1), float2( 1, 0), float2( 1, 1),
};

また、セルの生死を判定する定数を設定しています。テクスチャのrgbを利用しているので生を0、死を255としています。

Shader
constant int kCellValueAlive = 0;
constant int kCellValueDead = 255;

これらを用いて、以下のロジックで周囲の生きているセルの数がカウントされます。

Shader
ushort neighbors = 0;
for (int i = 0; i < 8; ++i)
{
    // Sample from the current game state texture, wrapping around edges if necessary
    float2 coords = (position + kNeighborDirections[i] + float2(0.5)) / bounds;
    ushort cellValue = readTexture.sample(wrapSampler, coords).r;
    neighbors += (cellValue == kCellValueAlive) ? 1 : 0;
}

そして、以下のロジックでセルの生死を判定し、結果をテクスチャに書き出しています。

Shader
ushort deadFrames = readTexture.read(uint2(position)).r;
...        
bool alive = (deadFrames == 0 && (neighbors == 2 || neighbors == 3)) || (deadFrames > 0 && (neighbors == 3));
...
ushort cellValue = alive ? kCellValueAlive : deadFrames + 1;
...        
writeTexture.write(cellValue, uint2(position));

cellValueが、
alive ? kCellValueAlive : kCellValueDeadではなく、
alive ? kCellValueAlive : deadFrames + 1;なのは、
死亡したセルの色を徐々に変化させるためです。

このように、ライフゲームのような並列処理に適したコードは、Metal Shading Languageで記述可能なわけなのです。

今回はライフゲームのサンプルコード内におけるライフゲームのロジックの解説を行いました。
次回以降、さらに他の箇所についての解説を行なっていきます。