高橋啓二朗(@_kzr)氏のKino Bloomフィルターの中身をみてみる。
- keijiro/KinoBloom
ソースはここ。
Bloom
Bloom.csとshaderを行ったり来たりしながら見ていく。
void OnRenderImage(RenderTexture source, RenderTexture destination)
MonoBehaviour.OnRenderImage(RenderTexture,RenderTexture)
メインとなる関数。
第一引数がこれまでの結果。第二引数にこの関数で変更した内容を適用してあげなければならない。
適用はGraphics.Blitで行う。
Graphics.Blit( source , dest );
この関数はあくまで第一引数の内容を第二引数に書き込むだけなので、今回のOnRenderImage内の場合は、OnRenderImage()の第二引数をこの Graphics.Blit()
の第二引数に入れて書き込んで上げる必要がある。
- OnRenderImage(RenderTexture,RenderTexture) は全てのレンダリングが終わった時に呼ばれるカメラ用イベント。
- なのでコンポーネントには
[RequireComponent( typeof(Camera ))]
が付いてる
動作
if (_material == null) { _material = new Material(_shader); _material.hideFlags = HideFlags.DontSave; }
- マテリアルがnull だったら指定のシェーダのマテリアルを作成する
- マテリアルのHideFlagに
DontSave
を当てる - Object.hideFlag
- オブジェクトは非表示、シーンに保存、ユーザーが編集可能かどうか
- HideFlags
- ヒエラルキーでの表示可否、インスペクタでの表示可否
- シーン上での保存可否
- 今回は
DontSave
なのでHideFlags.DontSaveInBuild | HideFlags.DontSaveInEditor | HideFlags.DontUnloadUnusedAsset
と同義 - DontUnloadUnusedAsset 扱いも入っているので実機でいれるなら DestroyImmediate()をかます必要がある。
var logh = Mathf.Log(source.height, 2) + _radius - 5; var logh_i = (int)logh; var iteration = Mathf.Max(2, logh_i);
- Math.Log() で logarythm RenderTexture souce の 高さの対数を2を底としてとっている。
- そこに半径となる値(Editorで5が最大値)から-5した値を加算。つまり( -5 ~ 0 )を加算。
- iterationは後で使う繰り返し回数。最低でも2回行う。
// update the shader properties _material.SetFloat("_SampleScale", 0.5f + logh - logh_i); _material.SetFloat("_Intensity", _intensity); var pf = -Mathf.Log(_exposure * 0.998f + 0.001f); _material.SetFloat("_Prefilter", pf); if (_antiFlicker) _material.EnableKeyword("PREFILTER_MEDIAN"); else _material.DisableKeyword("PREFILTER_MEDIAN");
shader側にパラメータを付与。
– Propertyで定義してなくてもパラメータを渡せることを初めて知った…。
– Bloom.shader に定義してある パラメータ “PREFILTER_MEDIAN” をスイッチさせている!!!!
// allocate temporary buffers var rt1 = new RenderTexture[iteration + 1]; var rt2 = new RenderTexture[iteration + 1]; var tx = source.width; var ty = source.height; for (var i = 0; i < iteration + 1; i++) { rt1[i] = GetTempBuffer(tx, ty); if (i > 0 && i < iteration) rt2[i] = GetTempBuffer(tx, ty); tx /= 2; ty /= 2; }
仮バッファを用意。
ループが回る都度、解像度を半分にしている。後の処理のコメントにもかかれてるけどMipMap用。
rt2
は最後と最初だけスキップ。
このローカル関数で一時的なレンダリングテクスチャを割当。
フォーマットは HDR でのプラットフォームによるデフォルトのフォーマット
。
RenderTexture GetTempBuffer(int width, int height) { return RenderTexture.GetTemporary( width, height, 0, RenderTextureFormat.DefaultHDR); }
// apply the prefilter Graphics.Blit(source, rt1[0], _material, 0);
- Sourceを rt1[0] の RenderTexture に書き込む。
- _material の pass 0 を使って描画。
ちょっとお試し。
rt1[0]
最初のPass
次の時点だけで中断。
// apply the prefilter Graphics.Blit(source, rt1[0], _material, 0); Graphics.Blit( rt1[0], destination ); return; // 処理を強制終了して描画状況を確認する。
Shader側では、
half4 frag_prefilter(v2f_img i) : SV_Target { #if PREFILTER_MEDIAN float3 d = _MainTex_TexelSize.xyx * float3(1, 1, 0); half4 s0 = tex2D(_MainTex, i.uv); half3 s1 = tex2D(_MainTex, i.uv - d.xz).rgb; half3 s2 = tex2D(_MainTex, i.uv + d.xz).rgb; half3 s3 = tex2D(_MainTex, i.uv - d.zy).rgb; half3 s4 = tex2D(_MainTex, i.uv + d.zy).rgb; half3 m = median(median(s0.rgb, s1, s2), s3, s4); #else half4 s0 = tex2D(_MainTex, i.uv); half3 m = s0.rgb; #endif m *= smoothstep(0, _Prefilter, Luminance(m)); return half4(m, s0.a); }
PREFILTER_MEDIAN
は先のパラメータでスイッチされている。_Prefilter
として受け取ったパラメータ_exposure
のパラメータをつかって smoothstep() 関数ででなだらかに。- Mathf.SmoothStep()と同じと思われる。ちゃんと読んでない。
UnityCG.cginc
を要確認。
half3 median(half3 a, half3 b, half3 c) { return a + b + c - min(min(a, b), c) - max(max(a, b), c); }
この関数で突出しすぎる値を抑えてるのかな?
rt1[3]
2つ目。
// create a mip pyramid for (var i = 0; i < iteration; i++) Graphics.Blit(rt1[i], rt1[i + 1], _material, 1); Graphics.Blit( rt1[3], destination ); return; // 処理を強制終了して描画状況を確認する。
ループで、テクスチャ解像度の低いバッファに書き込んでいっている。
ボカシ具合2。
Shader側の処理は以下のとおり。
half4 frag_box_reduce(v2f_img i) : SV_Target { float4 d = _MainTex_TexelSize.xyxy * float4(-1, -1, +1, +1); float3 s; s = tex2D(_MainTex, i.uv + d.xy).rgb; s += tex2D(_MainTex, i.uv + d.zy).rgb; s += tex2D(_MainTex, i.uv + d.xw).rgb; s += tex2D(_MainTex, i.uv + d.zw).rgb; return half4(s * 0.25, 0); }
rt1[最後]
// blur and combine loop _material.SetTexture("_BaseTex", rt1[iteration - 1]); Graphics.Blit(rt1[iteration], rt2[iteration - 1], _material, 2); Graphics.Blit( rt2[iteration - 1], destination ); return; // 処理を強制終了して描画状況を確認する。
ボカシがかなり強く入ってる。解像度が一番低いバッファなので当然ではあるのだけど…。
_material.SetTexture("_BaseTex", rt1[iteration - 1]);
ここで、最終的に解像度の一番低いテクスチャをマテリアルに渡す。
rt2[x]
for (var i = iteration - 1; i > 1; i--) { _material.SetTexture("_BaseTex", rt1[i - 1]); Graphics.Blit(rt2[i], rt2[i - 1], _material, 2); } Graphics.Blit( rt2[1], destination ); return; // 処理を強制終了して描画状況を確認する。
解像度の高いバッファに順次書き込み。
最終的に…
まで到達。
shader側。
half4 frag_tent_expand(v2f_img i) : SV_Target { float4 d = _MainTex_TexelSize.xyxy * float4(1, 1, -1, 0) * _SampleScale; float4 base = tex2D(_BaseTex, i.uv); float3 s; s = tex2D(_MainTex, i.uv - d.xy).rgb; s += tex2D(_MainTex, i.uv - d.wy).rgb * 2; s += tex2D(_MainTex, i.uv - d.zy).rgb; s += tex2D(_MainTex, i.uv + d.zw).rgb * 2; s += tex2D(_MainTex, i.uv ).rgb * 4; s += tex2D(_MainTex, i.uv + d.xw).rgb * 2; s += tex2D(_MainTex, i.uv + d.zy).rgb; s += tex2D(_MainTex, i.uv + d.wy).rgb * 2; s += tex2D(_MainTex, i.uv + d.xy).rgb; return half4(base.rgb + s * (1.0 / 16), base.a); }
RenderTextureのUVをずらしてボカシ処理。
最後の描画
一番最後にボカしまくったRenderTextureともともとの source
を合体させ、それを distination
に書き込む。
// finish process _material.SetTexture("_BaseTex", source); Graphics.Blit(rt2[1], destination, _material, 3);
shader 側
half4 frag_combine(v2f_img i) : SV_Target { half4 base = tex2D(_BaseTex, i.uv); half3 blur = tex2D(_MainTex, i.uv).rgb; return half4(base.rgb + blur * _Intensity, base.a); }
_Intensity
で増幅させたボカシ画像と合成してる。
RenderTextureの開放
生成したしたRenderTextureを開放。
// release the temporary buffers for (var i = 0; i < iteration + 1; i++) { ReleaseTempBuffer(rt1[i]); ReleaseTempBuffer(rt2[i]); }
まとめ
- レンダリング結果の画像を取得し解像度のちがうRenderTextureを用意
- RenderTextureにボカシ処理をShader経由で行う。
- ボカシた絵とオリジナルを合成して最終出力。
感想とか学んだこと
- カメラから送られる
OnRenderImage()
で最終描画内容を取得できる。 - イベント中に受け取る
RenderTexture
をMaterialとアタッチしたShaderを経由して加工できる。 - ShaderのPass別に処理を複数に分けることができ、必要に応じて適用する関数を変更できる。
- 人のコードやプロジェクトをみるとむちゃくちゃ勉強になる。