Unity Blendモード

多くの人は、画像やビデオを操作するための最も一般的なプログラムに存在するブレンドモード( ブレンドモード )について聞いたことがあるでしょう。 そこで、それはコンテンツを作成するための重要なツールであり、長い間それらの不可欠な部分になりました。



そして、ゲームはどうですか?



パーティクルシステムにColor Dodgeブレンドを使用する必要がある場合、またはUIアーティストがゲームインターフェース用に美しいグラフィックを作成したが、その要素のいくつかが何らかの種類のソフトライトを使用しているとします。 それとも、リンチの映画から直接効果を得るために、3次元オブジェクトを分割ミキシングにかける必要があったのでしょうか?







この記事では、一般的なミキシングモードの動作原理を検討し、Unityゲームエンジンでの効果を可能な限り正確に再現しようとします。



混合アルゴリズム



まず、何をする必要があるかを正確に把握しましょう。 たとえば、2つのグラフィック要素を取り、一方が他方と重なるように配置します。







通常(通常)ブレンドモードでは、下層( a )の各ピクセルの色は、それを「オーバーラップ」する層( b )のピクセルの色に完全に置き換えられます。 ここではすべてが簡単です。このようにして、ゲームのグラフィックオブジェクトの大部分が「混合」されます。







スクリーンモードでは、両方のレイヤーのピクセルカラーが反転され、乗算され、再び反転されます。 このアルゴリズムをCgで実装します:



fixed4 Screen (fixed4 a, fixed4 b) { fixed4 r = 1.0 - (1.0 - a) * (1.0 - b); ra = ba; return r; }
      
      





結果の色のアルファ成分( ra )では、最上層のアルファ値( ba )を渡して、マテリアルの透明度レベルを独立して制御する機能を維持することに注意してください。







オーバーレイアルゴリズムは条件付きで機能します。「暗い」領域では色が乗算され、「明るい」領域では画面モードのアナログが使用されます。



 fixed4 Overlay (fixed4 a, fixed4 b) { fixed4 r = a < .5 ? 2.0 * a * b : 1.0 - 2.0 * (1.0 - a) * (1.0 - b); ra = ba; return r; }
      
      









ブレンドモードを暗くすると、2つのレイヤーの3つのカラーチャネルのそれぞれの値が比較され、「暗い」レイヤーが残ります。



 fixed4 Darken (fixed4 a, fixed4 b) { fixed4 r = min(a, b); ra = ba; return r; }
      
      





他のモードのほとんどは、同様のスキームに従って機能します。 興味がある場合は、Cgでの別の18のミキシングアルゴリズムの実装については、 gist.github.com / Elringus / d21c8b0f87616ede9014を参照してください。



したがって、一般的な用語での問題は次のように定式化できます。オブジェクトの素材の各ピクセル( b )に対して、「下」にあるピクセルを見つけ( a )、選択したアルゴリズムを使用して、それらを「混合」します。



GrabPassを使用した実装



必要なすべてのミキシングアルゴリズムを受け取ったので、問題は小さいように思えるかもしれません。オブジェクトを「下に」配置するピクセルの色を取得するだけです。 ただし、実際の実装で最も問題となるのはこの段階でした。



実際、レンダリングパイプラインのロジックにより、フラグメントシェーダーの実行中に同じ「バックレイヤー」が配置されているフレームバッファーのコンテンツにアクセスすることは不可能です。







最終イメージ(最終イメージ)は、フラグメントシェーダーの実行後にそれぞれ形成されます。Cgプログラムの実行中に直接取得することはできません。 そのため、回避策を探す必要があります。



実際、フラグメントシェーダーの一部としての最終画像上のデータの必要性は、非常に頻繁に発生します。 たとえば、ほとんどのポストエフェクト(ポストプロセッシングエフェクト)の実装は、「最終画像」へのアクセスなしでは考えられません。 そのような場合、いわゆるテクスチャーへのレンダリングがあります。フレームバッファーからのデータは特別なテクスチャーにコピーされ、次にフラグメントシェーダーが実行されるときにそこから読み込まれます。







UnityでUnityを使用するには、いくつかの方法があります。 この場合、最も適切なのはGabPassの使用です。これは、画面の内容をオブジェクトが描画されるテクスチャにキャプチャする特別なタイプの「パス」(パス)です。 まさに必要なもの!



UIグラフィックス用のシンプルなシェーダーを作成し、それにGrabPassを追加し、フラグメント関数からDarkenアルゴリズムを使用して色混合の結果を返します。



Grabdarken.shader
 Shader "Custom/GrabDarken" { Properties { _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } Blend SrcAlpha OneMinusSrcAlpha GrabPass { } Pass { CGPROGRAM #include "UnityCG.cginc" #pragma vertex ComputeVertex #pragma fragment ComputeFragment sampler2D _MainTex; sampler2D _GrabTexture; fixed4 _Color; struct VertexInput { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; }; struct VertexOutput { float4 vertex : SV_POSITION; fixed4 color : COLOR; half2 texcoord : TEXCOORD0; float4 screenPos : TEXCOORD1; }; VertexOutput ComputeVertex (VertexInput vertexInput) { VertexOutput vertexOutput; vertexOutput.vertex = mul(UNITY_MATRIX_MVP, vertexInput.vertex); vertexOutput.screenPos = vertexOutput.vertex; vertexOutput.texcoord = vertexInput.texcoord; vertexOutput.color = vertexInput.color * _Color; return vertexOutput; } fixed4 Darken (fixed4 a, fixed4 b) { fixed4 r = min(a, b); ra = ba; return r; } fixed4 ComputeFragment (VertexOutput vertexOutput) : SV_Target { half4 color = tex2D(_MainTex, vertexOutput.texcoord) * vertexOutput.color; //      , //   ""    float2 grabTexcoord = vertexOutput.screenPos.xy / vertexOutput.screenPos.w; grabTexcoord.x = (grabTexcoord.x + 1.0) * .5; grabTexcoord.y = (grabTexcoord.y + 1.0) * .5; //    , //  V      . #if UNITY_UV_STARTS_AT_TOP grabTexcoord.y = 1.0 - grabTexcoord.y; #endif fixed4 grabColor = tex2D(_GrabTexture, grabTexcoord); return Darken(grabColor, color); } ENDCG } } Fallback "UI/Default" }
      
      







結果を評価するために、ブレンドモードのデモ中にグラフィックエディタで使用したものと同じテクスチャを使用してみましょう。







図からわかるように、UnityでのUIグラフィックスのレンダリングの結果とPhotoshopでのドキュメントの結果は同じです。



1つの「ではない」場合はここで停止できます。テクスチャへのレンダリングはかなり時間のかかる操作です。 ミッドレンジのPCでさえ、これらの操作を100個以上同時に使用すると、フレームレートが著しく低下します。 GrabPassの速度がディスプレイの解像度に反比例するという事実により、状況は悪化します。 超高解像度ディスプレイを搭載したiPadで同様の手順を実行すると、パフォーマンスがどうなるか想像してみてください。 私の場合、空のシーンで「非伝統的な」ミキシングを行ったUIオブジェクトのペアでさえ、FPSが20未満になりました。



統合グラブでの実装



1つの最適化自体が示唆しています。単一のGrabPassを使用しないのはなぜですか。 フレーム内の元の画像は変更されません。つまり、一度「取得」して、その後のすべてのミキシング操作に使用できます。



Unityは、計画を実行する便利な方法を提供します。 変数の名前を含む行をGrabPassコンストラクトに渡すだけで、「一般的な」レンダリングテクスチャを保存できます。



 GrabPass { "_SharedGrabTexture" }
      
      





これで、このシェーダーを使用するマテリアルインスタンスは、一般的なテクスチャレンダラーから情報を受け取り、インスタンスの1つによって既に実行されている場合、高価なGrabPassを実行しません。 したがって、深刻なパフォーマンスの問題なしに、一度に多数のミキシング操作を使用する機会が得られます。



残念ながら、このソリューションには1つの重大な欠点があります。異なるオブジェクトは「バックレイヤー」の画像に関する同じ情報を使用するため、この同じレイヤーはそれらに対して同一になります。 つまり、このようなオブジェクトは互いに「見えない」ため、ミキシング時にこの情報を考慮しません。



この問題は、ミキシングを使用する2つのオブジェクトを重ねて「重ね合わせる」場合に明らかになります。







さらに、1つのGrabPassでさえ、ほとんどのモバイルデバイスにとって「高価」すぎる可能性があるため、代替アプローチを探す必要があります。



BlendOpを使用した実装



どの形式でもGrabPassを使用するとコストがかかりすぎるため、GrabPassを使用しないようにしましょう。 オプションの1つ:フラグメントシェーダーの後に実行されるブレンディングモードを変更してみてください(Unityレンダリングパイプラインの一部として):







このステージは主に半透明オブジェクトの処理に使用され、その変更の可能性は非常に限られています-Cg命令をそこに挿入することはできません。 一連のキーフレーズの助けを借りてのみ、フラグメントシェーダーから取得した色がその背後にある色とどのように相互作用するべきか(まったくそうすべきか)を構成することができます。



操作は、次の構成によって定義されます。



 Blend SrcFactor DstFactor
      
      





ロジックは、ソースカラー(フラグメントシェーダーから取得)に第1オペランドが返す値( SrcFactor )を乗算し、ターゲットカラー(「バック」レイヤーの色)に第2オペランド( DstFactor )を乗算し、結果の値を加算します。 一方、オペランドのリストはかなり制限されています。単位、ゼロ、ソースとターゲットの色、およびそれらの反転の結果を操作できます。



BlendOpオプションコマンドは、機能を多少拡張し、2つのオペランドの結果の加算を、最小値または最大値を取る減算で置き換えることができます。



少しの想像力で、次のミキシングアルゴリズムを実装することができました。





BlendenOpを使用するために、シェーダーを変更してUIグラフィックをDarkenモードで混合します。



BlendOpDarken.shader
 Shader "Custom/BlendOpDarken" { Properties { _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } BlendOp Min Blend One One Pass { CGPROGRAM #include "UnityCG.cginc" #pragma vertex ComputeVertex #pragma fragment ComputeFragment sampler2D _MainTex; fixed4 _Color; struct VertexInput { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; }; struct VertexOutput { float4 vertex : SV_POSITION; fixed4 color : COLOR; half2 texcoord : TEXCOORD0; }; VertexOutput ComputeVertex (VertexInput vertexInput) { VertexOutput vertexOutput; vertexOutput.vertex = mul(UNITY_MATRIX_MVP, vertexInput.vertex); vertexOutput.texcoord = vertexInput.texcoord; vertexOutput.color = vertexInput.color * _Color; return vertexOutput; } fixed4 ComputeFragment (VertexOutput vertexOutput) : SV_Target { return tex2D(_MainTex, vertexOutput.texcoord) * vertexOutput.color; } ENDCG } } Fallback "UI/Default" }
      
      







デモンストレーションでは、同じテクスチャを使用します。







問題は明らかです。「ニーズに合わせて」ブレンドフェーズを使用しているため、アルファブレンドを行う場所はなく、オブジェクトの透明度は単純に無視されます。 一方、不透明なオブジェクトは、パフォーマンスを損なうことなく正しく混合されます。 したがって、Blendコンストラクトを使用して再作成できるモードのいずれかを使用する必要があり、オブジェクトに透明な領域がない場合、これがおそらく最適なオプションです。



フレームバッファフェッチを使用した実装



フラグメントシェーダーからフレームバッファーにアクセスすることは不可能であると先に述べました。 実際、これは完全に真実ではありません。



2013年に、 EXT_shader_framebuffer_fetch関数がOpenGL ES 2.0 仕様に追加され、フラグメントシェーダーからフレームバッファーデータにアクセスできるようになりました。 そして数か月前、Unity 4.6.3リリースで、Cgからのこの機能のサポートが発表されました。



Framebuffer Fetchを使用するようにシェーダーを変更します。



FrameBufferFetchDarken.shader
 Shader "Custom/FrameBufferFetchDarken" { Properties { _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #include "UnityCG.cginc" #pragma vertex ComputeVertex #pragma fragment ComputeFragment sampler2D _MainTex; fixed4 _Color; struct VertexInput { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; }; struct VertexOutput { float4 vertex : SV_POSITION; fixed4 color : COLOR; half2 texcoord : TEXCOORD0; }; VertexOutput ComputeVertex (VertexInput vertexInput) { VertexOutput vertexOutput; vertexOutput.vertex = mul(UNITY_MATRIX_MVP, vertexInput.vertex); vertexOutput.texcoord = vertexInput.texcoord; vertexOutput.color = vertexInput.color * _Color; return vertexOutput; } fixed4 Darken (fixed4 a, fixed4 b) { fixed4 r = min(a, b); ra = ba; return r; } fixed4 ComputeFragment (VertexOutput vertexOutput #ifdef UNITY_FRAMEBUFFER_FETCH_AVAILABLE , inout fixed4 fetchColor : COLOR0 #endif ) : SV_Target { half4 color = tex2D(_MainTex, vertexOutput.texcoord) * vertexOutput.color; #ifdef UNITY_FRAMEBUFFER_FETCH_AVAILABLE fixed4 grabColor = fetchColor; #else fixed4 grabColor = fixed4(1, 1, 1, 1); #endif return Darken(grabColor, color); } ENDCG } } Fallback "UI/Default" }
      
      













パーフェクト。 他に何が必要なのでしょうか? 不要な操作、最高のパフォーマンス、ミキシングロジックを実装することはできません...上記の図は、iPad Airから取得したスクリーンショットの断片です。 しかし、たとえば、Unityエディターでは、シェーダーは単に動作を拒否します。



問題は、OpenGL ES仕様のサポートがiOSデバイスでのみ完全に実装されていることです。 他のプラットフォームでは(グラフィックサブシステムがOpenGL ES APIを使用している場合でも)、この関数は機能しない可能性があるため、クロスプラットフォーム機能に依存することはできません。



おわりに



Unityゲームエンジンでのブレンドモードの4つの実装を検討しました。





単一のユニバーサルでクロスプラットフォームなソリューションを見つけることはできませんでしたが、提示されたオプションを組み合わせることで、ほとんどの場合「混合」を使用できます。



結論として、Unityのパーティクルエフェクト、GUI要素、3次元オブジェクト、およびスプライトに適用されるいくつかのブレンドモードを示すビデオを提供したいと思います。







また、私はチャンスを取っています(AppEngine-我慢できます!)WebGLアセンブリへのリンクを公開し、 さまざまなブレンドモードをインタラクティブに試すことができます



ご清聴ありがとうございました!



All Articles