この記事の内容
プログラム中でのOpenGLリソースの取り扱いを簡単化した。その方向性を軽く解説する。
問題点:GPU転送処理を書くのが手間
ゲームプログラムではGPUへの転送が必要なリソースを取り扱う。例えばテクスチャや頂点データ、シェーダーなど。スプライト1つ描画するのにも下記のようなデータ群を管理する必要がある。せめてGPU転送処理だけでも無くなればうれしい。
スプライト描画に必要なデータ群:
- 転送元画像(byte[])
- 転送元座標頂点データ(float[])
- 転送元UV頂点データ(float[])
- 転送元インデックス頂点データ(ushort[])
- 描画方法(enumなど)※事前にシェーダーを転送しておいてenumで指定する想定。
- マテリアル情報(マテリアルカラー、透明度など)
- 頂点データ組み合わせ情報(各種頂点データ)
- スプライト情報(画像、頂点データ組み合わせ情報、描画方法、マテリアル情報)
- スプライト描画先(座標と重ね合わせ順番)
- 転送済みテクスチャID(int)
- 転送済み座標のVBO ID(int)
- 転送済みUVのVBO ID(int)
- 転送済みインデックスのIBO ID(int)
- 転送済みVAO ID(int)
- (略)
多い。
対策:バッファ付きハンドルクラスを導入する
GPU転送が必要なリソースはかならずバッファを通すことにする。
リソース毎にバッファ付きハンドルクラスを用意しておき、クラス利用者にはバッファ操作だけで完結させる。GPU転送処理はクラスライブラリの裏側で自動でやる。
実装例
テクスチャのバッファ付きハンドルクラスの例。C#でOpenGLを想定。
public class TextureHandle
{
// リビジョン番号。更新有無判定に。
public readonly SingleBufferRevision Revisions;
// バッファ。
public Image32Buffer GameLogicBuffer;
// GPU転送済みデータ。
public VramLoadedTexImage2D GpuAccepted;
public TextureHandle()
{
this.Revisions = new SingleBufferRevision();
this.GameLogicBuffer = null;
this.GpuAccepted = default;
}
// バッファ更新。
public void UpdateTexture(Image32Buffer img)
{
this.GameLogicBuffer = img.DeepCopy();
this.Revisions.UpdateBuffer();
}
// バッファが更新されていたら再転送する。
public void GpuUpdateIfNeed()
{
if (this.Revisions.IsUpdated)
{
if (!this.GpuAccepted.IsValid)
{
this.GpuAccepted = TexImageUtil.LoadTextureToGL(this.GameLogicBuffer);
}
else
{
TexImageUtil.BlitTexture(this.GameLogicBuffer, this.GpuAccepted.texId);
}
this.Revisions.AcceptBuffer();
}
}
}
リビジョン管理はどのリソースでも同じなのでクラスにまとめた。
public class SingleBufferRevision
{
public bool IsDispositionRequested { get; private set; }
public long SenderBufferRevision;
public long ReceiverAcceptedRevision;
public SingleBufferRevision()
{
this.IsDispositionRequested = false;
this.SenderBufferRevision = 0;
this.ReceiverAcceptedRevision = 0;
}
public void ReturnResource()
{
this.IsDispositionRequested = true;
}
public void UpdateBuffer()
{
if (!this.IsUpdated)
{
this.SenderBufferRevision += 1;
}
}
public void AcceptBuffer()
{
this.ReceiverAcceptedRevision = this.SenderBufferRevision;
}
public bool IsUpdated
{
get { return this.SenderBufferRevision > this.ReceiverAcceptedRevision; }
}
}
バッファ付きハンドルクラスの種類
テクスチャ以外のリソースも同様にバッファ付きハンドルクラスを用意する。
例:
- テクスチャ。画像データ、テクスチャID。
- 頂点データ。float[]、VBO。
- 頂点インデックスデータ。ushort[]。
- マテリアル。色、テクスチャハンドル等。
- スプライトレンダラー。各種頂点データハンドル、頂点インデックスデータハンドル、VAO, マテリアルハンドル、描画方法、行列、描画位置、表示/非表示。
私の作成中ゲームは2Dなのでこのようなもので動いたが、3Dゲームなら他にも欲しくなるはず。例:カメラ行列、ディレクショナルライト、シャドウマップフレームバッファ、ボーンリスト、スキンメッシュレンダラー等。
使い方
- 1、バッファ付きハンドルクラスのインスタンスを作る。
- 2、ハンドルクラスのバッファを更新する。例:textureHandle.UpdateTexture()を呼ぶ。一緒にリビジョン番号も更新する。
- 3、レンダラークラスにリソースハンドルを設定する。
- 4、1フレームの終わりにリソースハンドルのバッファをGPU転送する。例:textureHandle.GpuUpdateIfNeed()を呼ぶ。
- 5、レンダラークラスで描画する。
- 6、描画完了後、リソースハンドルの破棄フラグを見て破棄する。例:textureHandle.Revisions.IsDispositionRequestedを見る。
ハンドル保持リスト
処理対象のハンドルがあちこちにちらばっていると管理しにくい。保持専用のリストを作ってまとめて管理するのが楽。
リソース破棄時はその場ではリスト要素削除はせず、handle.Revisions.ReturnResource()を呼ぶだけにとどめる。1フレームの終わりにフラグを見てまとめて解放およびリストから削除する。