はじめに
エンジニアの小林です。以前、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を使用して、改善した事例をお話しました。
最後まで読んで頂いた方、ありがとうございました。