はじめに
エンジニアの小林です。以前、Forward+レンダリングなどで、Compute Shaderを使い、
ライトカリングなど処理しましたが、今回、ある作業で、Compute Shaderを使用した時のお話になります。
ある作業
ゲーム開発が進行してきますと、高解像度のキャプチャ画像がほしい、ですとか、
ゲーム画面をそのまま動画にしたいので、フレーム毎にFullHD解像度でキャプチャしてほしい、といった案件があったりします。
そんなに、複雑なことでもないですし、まあ、時間かからず完了できるでしょうと、実装を開始しました。
まずは実装
担当したプロジェクトのゲーム画面のフレームバッファは、R11G11B10といったフォーマットです。そして、出力するファイルフォーマットは、RGB8のbitmapになります。
テクスチャフォーマットが違いますし、また、GPU上で扱われるテクスチャはリニアに並んでいません。なので、CPUからアクセス可能なバッファへコピーします(このコピー時にピクセルがリニアにアクセスできる状態になります)
この状態から、1Pixel毎に変換をかけて、別バッファに格納するというシンプルな処理をします。そして、ファイルに保存し、毎フレーム繰り返せば、完了です。
が、実行してみると、おっそろしく遅く、1フレーム毎にキャプチャするのに、3, 4秒待たされるような状態です。
尺が、1分、2分もあるようなデモを60FPSでキャプチャしたら…、と考えると、とても、「これでキャプチャをお願いします。」とは言えないものでした。
1920 x 1080 ですから、200万回ものループ処理をしていることになります。コードを組みつつ、薄々想像していましたが、それを超える遅さでした…。
改善
CPUでメインスレッドのみで変換していたので、それは確かに重いですが、ワーカースレッド等を使わずに、Compute Shaderで、RGB8フォーマットにして出力してもいいのでは、と思い、実装してみました。
ComputeShaderは、テクスチャもフェッチできるので、読み取ったtexelをリニア空間から、sRGBに変換、念のため、0.0 から 1.0 でクランプし、RGB8に変換、別バッファに格納する、といたって、シンプルなコードです。
これをゲームの描画コマンド実行が完了した後に、上記のCompute Shaderを実行させ、出来たメモリバッファをファイルに保存するようにしました。
すると、劇的な速度改善が出来ました。体感できるほど、速くなりました。ただ、CPUで処理していた時は、シングルスレッドで処理していたので、フェアではないですが…。
(今となっては、FragmentShaderで、RGB8のレンダーターゲットに変更して、フレームバッファをテクスチャにして描画しても良かったかも、、とは思います)
Windowsで再現
以前、作成したFoward+レンダリングのデモに疑似環境として、実装してみました。
ComputeShader Code
uniform sampler2D s_colorSampler;
layout(std430, binding=0) buffer OutBuffer
{
uint8_t b_buffer[];
};
layout( local_size_x = 32, local_size_y = 32, local_size_z = 1 ) in;
// メイン
void main()
{
ivec2 texSize = textureSize(s_colorSampler, 0);
if (gl_GlobalInvocationID.x >= texSize.x || gl_GlobalInvocationID.y >= texSize.y)
return;
// テクスチャフェッチ
vec4 color = texelFetch(s_colorSampler, ivec2(gl_GlobalInvocationID.xy), 0);
// クランプ
color.rgb = clamp(color.rgb, 0.0, 1.0);
// 0.0 - 1.0 を 0 - 255へ
color.rgb *= vec3(255.0);
// 格納先を求める
uint offset = gl_GlobalInvocationID.y * (gl_WorkGroupSize.x * gl_NumWorkGroups.x) +
(gl_WorkGroupID.x * gl_WorkGroupSize.x) + gl_LocalInvocationID.x;
offset *= 3;
b_buffer[offset + 0] = uint8_t(color.b);
b_buffer[offset + 1] = uint8_t(color.g);
b_buffer[offset + 2] = uint8_t(color.r);
}
Cpu側ComputeShader Dispatch
glUseProgram( compShaderId );
unsigned int samplerLoc = toRgb8Shader_->GetUniformLocation("s_colorSampler");
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, colorBuf_);
glUniform1i(samplerLoc, 0);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, mOutBufferId);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, mOutBufferId);
glDispatchCompute(WIDTH/32, std::ceil(HEIGHT/32), 1);
void* outBuffer = glMapBuffer(GL_SHADER_STORAGE_BUFFER, GL_READ_ONLY);
memcpy(dst, outBuffer, 3 * WIDTH * HEIGHT);
glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
ComputeShaderでの変換は、Cpuの負荷には計測されませんし、Dispathまでの処理が、非常に速く、効果が期待できました。
計測結果は以下の通りです。
| CPUで変換 | GPU(ComputeShader)で変換 |
|---|---|
| 8、9ms | 38、39ms |
ComputeShaderでの変換の方が、かなり遅い結果に…。
OpenGLで組んでますが、ComputeShaderで変換し出力したShaderStorageBufferを読み取るため、glMapBuffer()を使用したら、これがかなり遅く(30数msかかる…)、望んだ結果になりませんでした。
glReadPixels()は、ここまで遅くならないので、Fragment Shaderで、変換した方が結果、速くなるかもしれません。
さいごに
以上となります。
疑似環境での結果が、何とも締まらない結果になってしまいました…。
ちょっとしたことでも、ComputeShaderを使用して、改善した事例をお話しました。
最後まで読んで頂いた方、ありがとうございました。