Unityでシェーダーを作成します。 GrabPass、PerRendererData

こんにちは Unityでシェーダーを作成した経験を共有したいと思います。 2DのDisplacement / Refractionシェーダーから始めて、それを記述するために使用される機能(GrabPass、PerRendererData)を検討し、必然的に発生する問題にも注意を払いましょう。

この情報は、シェーダーの一般的な概念を持ち、それらを作成しようとしたが、Unityが提供する機能に精通しておらず、アプローチする側がわからない場合に役立ちます。 ご覧ください。私の経験があなたの理解に役立つかもしれません。



これが私たちが達成したい結果です。

画像

準備する


まず、指定されたスプライトを単純に描画するシェーダーを作成します。 彼はさらなる操作の基礎になります。 それに何かが追加され、逆に何かが削除されます。 標準の「Sprites-Default」とは、結果に影響を与えないタグとアクションがいくつかないため異なります。

スプライトをレンダリングするためのシェーダーコード
Shader "Displacement/Displacement_Wave" { Properties { [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} _Color ("Color" , Color) = (1,1,1,1) } SubShader { Tags { "RenderType" = "Transparent" "Queue" = "Transparent" } Cull Off Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; fixed4 _Color; sampler2D _MainTex; v2f vert (appdata v) { v2f o; o.uv = v.uv; o.color = v.color; o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 texColor = tex2D(_MainTex, i.uv)*i.color; return texColor; } ENDCG } } } 

表示するスプライト
背景は実際には透明で、意図的に暗くなっています。

画像

結果のワーク。

画像

グラブパス


ここでのタスクは、画面上の現在の画像に変更を加えることです。そのためには、画像を取得する必要があります。 そして、 GrabPassの一節はこれに役立ちます。 このパッセージは、 _GrabTextureテクスチャの画面イメージをキャプチャします。 テクスチャには、このシェーダーを使用するオブジェクトがレンダリングする前に描画されたもののみが含まれます。

テクスチャ自体に加えて、スキャンの座標からピクセルカラーを取得する必要があります。 これを行うには、追加のテクスチャ座標をフラグメントシェーダーデータに追加します。 これらの座標は正規化されておらず(値は0〜1の範囲ではありません)、カメラの空間内のポイントの位置(投影)を表します。

 struct v2f { float4 vertex : SV_POSITION; float2 uv : float4 color : COLOR; float4 grabPos : TEXCOORD1; }; 

そして、頂点シェーダーでそれらを塗りつぶします。

 o.grabPos = ComputeGrabScreenPos (o.vertex); 

_GrabTextureから色を取得するために、正規化されていない座標を使用する場合、次のメソッドを使用できます

 tex2Dproj(_GrabTexture, i.grabPos) 

ただし、別の方法を使用して、遠近法の分割を使用して、自分で座標を正規化します。 他のすべてをwコンポーネントに分割します。

 tex2D(_GrabTexture, i.grabPos.xy/i.grabPos.w) 

wコンポーネント
wコンポーネントへの分割は、透視投影を使用する場合にのみ必要です。正投影では常に1になります。実際、 wはカメラへの距離の値を格納します。 ただし、depth- zではありません。その値は0〜1の範囲である必要があります。depthの操作は別のトピックに値するため、シェーダーに戻ります。

パースペクティブ分割は頂点シェーダーでも実行でき、既に準備されたデータをフラグメントシェーダーに転送できます。

 v2f vert (appdata v) { v2f o; o.uv = v.uv; o.color = v.color; o.vertex = UnityObjectToClipPos(v.vertex); o.grabPos = ComputeScreenPos (o.vertex); o.grabPos /= o.grabPos.w; return o; } 

フラグメントシェーダーをそれぞれ追加します。

 fixed4 frag (v2f i) : SV_Target { fixed4 = grabColor = tex2d(_GrabTexture, i.grabPos.xy); fixed4 texColor = tex2D(_MainTex, i.uv)*i.color; return grabColor; } 

指定された混合モードをオフにします フラグメントシェーダー内にブレンドモードを実装しています。

 //Blend SrcAlpha OneMinusSrcAlpha Blend Off 

そして、 GrabPassの結果を見てください

画像

何も起きていないようですが、そうではありません。 わかりやすくするために、わずかなシフトを導入します。このため、変数の値をテクスチャ座標に追加します。 変数を変更できるように、新しい_DisplacementPowerプロパティを追加します。

 Properties { [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} _Color ("Color" , Color) = (1,1,1,1) _DisplacementPower ("Displacement Power" , Float) = 0 } SubShader { Pass { ... float _DisplacementPower; ... } } 

また、フラグメントシェーダーに変更を加えます。

 fixed4 grabColor = tex2d(_GrabTexture, i.grabPos.xy + _DisplaccementPower); 

ホップと結果! シフトのある画像。



シフトが成功すると、より複雑な歪みに進むことができます。 指定されたポイントで変位力を保存する事前に準備されたテクスチャを使用します。 X軸のオフセット値の赤色、Y軸の緑色。

歪みに使用されるテクスチャ



始めましょう。 テクスチャを保存する新しいプロパティを追加します。

 _DisplacementTex ("Displacement Texture", 2D) = "white" {} 

そして変数。

 sampler2D _DisplacementTex; 

フラグメントシェーダーでは、テクスチャからオフセット値を取得し、テクスチャ座標に追加します。

 fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); 

ここで、 _DisplacementPowerパラメーターの値を変更することにより、元の画像をシフトするだけでなく、歪ませています。



オーバーレイ


これで、画面上には空間のゆがみのみが表示され、最初に示したスプライトはありません。 それを元の場所に戻します。 これを行うには、色の難しい組み合わせを使用します。 オーバーレイブレンディングモードなど、他のものを使用します。 その式は次のとおりです。



ここで、Sは元の画像、Cは修正、つまり私たちのスプライト、Rは結果です。

この数式をシェーダーに転送します。

 fixed4 color = grabColor < 0.5 ? 2*grabColor*texColor : 1-2*(1-texColor)*(1-grabColor); 

シェーダーでの条件演算子の使用は、やや紛らわしいトピックです。 プラットフォームと使用するグラフィックスAPIに大きく依存します。 場合によっては、条件文はパフォーマンスに影響しません。 しかし、常にフォールバックする価値があります。 数学と利用可能なメソッドの助けを借りて、条件演算子を置き換えることができます。 次の構造を使用します

 c = step ( y, x); r = c * a + (1 - c) * b; 

ステップ機能
xが y以上の場合、step関数は1を返します。 xが yより小さい場合 0。

たとえば、 x = 1、 y = 0.5の場合、 cの結果は1になります。次の式は次のようになります
r = 1 * a + 0 * b
なぜなら 0を掛けると0になり、結果はaの値になります。
それ以外で、 cが0の場合、
r = 0 * a + 1 * b
最終結果はbになります。

オーバーレイモードの色を書き換えます。

 fixed s = step(grabColor, 0.5); fixed4 color = s * (2 * grabColor * texColor) + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); 

スプライトの透明度を考慮してください。 これを行うには、2色間の線形補間を使用します。

 color = lerp(grabColor, color ,texColor.a); 

完全なフラグメントシェーダーコード。

 fixed4 frag (v2f i) : SV_Target { fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; fixed4 texColor = tex2D(_MainTex, i.uv + offset)*i.color; fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); fixed s = step(grabColor, 0.5); fixed4 color = s * (2 * grabColor * texColor) + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); color = lerp(grabColor, color ,texColor.a); return color; } 

そして、私たちの仕事の結果。



GrabPass機能


上記のように、 GrabPassパス{}は画面のコンテンツを_GrabTextureテクスチャにキャプチャします。 さらに、このパッセージが呼び出されるたびに、テクスチャのコンテンツが更新されます。
画面の内容がキャプチャされるテクスチャの名前を指定することにより、継続的な更新を回避できます。
 GrabPass{"_DisplacementGrabTexture"} 

これで、テクスチャのコンテンツは、フレームごとのGrabPassパスの最初の呼び出しでのみ更新されます。 これにより、 GrabPass {}を使用するオブジェクトが多数ある場合にリソースが節約されます。 ただし、2つのオブジェクトが重なると、両方のオブジェクトが同じ画像を使用するため、アーティファクトが顕著になります。

GrabPass {"_ DisplacementGrabTexture"}を使用します。



GrabPass {}を使用します。



アニメーション


次に、エフェクトをアニメートします。 爆風が成長するにつれて歪みの力をスムーズに減らし、その消滅をシミュレートしたいと思います。 これを行うには、マテリアルのプロパティを変更する必要があります。

アニメーション用のスクリプト
 public class Wave : MonoBehaviour { private float _elapsedTime; private SpriteRenderer _renderer; public float Duration; [Space] public AnimationCurve ScaleProgress; public Vector3 ScalePower; [Space] public AnimationCurve PropertyProgress; public float PropertyPower; [Space] public AnimationCurve AlphaProgress; private void Start() { _renderer = GetComponent<SpriteRenderer>(); } private void OnEnable() { _elapsedTime = 0f; } void Update() { if (_elapsedTime < Duration) { var progress = _elapsedTime / Duration; var scale = ScaleProgress.Evaluate(progress) * ScalePower; var property = PropertyProgress.Evaluate(progress) * PropertyPower; var alpha = AlphaProgress.Evaluate(progress); transform.localScale = scale; _renderer.material.SetFloat("_DisplacementPower", property); var color = _renderer.color; color.a = alpha; _renderer.color = color; _elapsedTime += Time.deltaTime; } else { _elapsedTime = 0; } } } 

そしてその設定


アニメーションの結果。



Perrendererdata


以下の行に注意してください。

 _renderer.material.SetFloat("_DisplacementPower", property); 

ここでは、マテリアルのプロパティの1つを変更するだけでなく、ソースマテリアルのコピーを作成し(このメソッドが最初に呼び出されたときのみ)、それを既に使用しています。 これは非常に有効なオプションですが、ステージ上に複数のオブジェクト(たとえば1000)がある場合、非常に多くのコピーを作成しても何の効果もありません。 より良いオプションがあります-これはシェーダーで[PerRendererData]属性を使用し、スクリプトでMaterialPropertyBlockオブジェクトを使用しています。

これを行うには、シェーダーの_DisplacementPowerプロパティに属性を追加します。

 [PerRendererData] _DisplacementPower ("Displacement Power" , Range(-.1,.1)) = 0 

その後、プロパティはインスペクターに表示されなくなります。 これで、オブジェクトごとに個別になり、値が設定されます。



スクリプトに戻り、変更を加えます。

 private MaterialPropertyBlock _propertyBlock; private void Start() { _renderer = GetComponent<SpriteRenderer>(); _propertyBlock = new MaterialPropertyBlock(); } void Update() { ... //_renderer.material.SetFloat("_DisplacementPower", property); _renderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetFloat("_DisplacementPower", property); _renderer.SetPropertyBlock(_propertyBlock); ... } 

ここで、プロパティを変更するために、 マテリアルのコピーを作成せずにオブジェクトのMaterialPropertyBlockを更新します。

SpriteRendererについて
シェーダーでこの行を見てみましょう。

 [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} 

SpriteRendererは 、スプライトでも同様に機能します。 彼はMaterialPropertyBlockを使用して_MainTexプロパティを自分で設定します。 したがって、 _MainTexプロパティはマテリアルのインスペクターに表示されず、 SpriteRendererコンポーネントで必要なテクスチャを指定します。 同時に、ステージ上にはさまざまなスプライトが存在する可能性がありますが、レンダリングに使用されるマテリアルは1つだけです(自分で変更しない限り)。

PerRendererData機能


MaterialPropertyBlockは、レンダリングに関連するほぼすべてのコンポーネントから取得できます。 たとえば、 SpriteRendererParticleRendererMeshRendererおよびその他のRendererコンポーネント。 ただし、常に例外があります 。これはCanvasRendererです。 このメソッドを使用してプロパティを取得および変更することはできません。 したがって、UIコンポーネントを使用して2Dゲームを作成している場合、シェーダーを作成するときにこの問題が発生します。

回転


画像が回転すると不快な効果が発生します。 ラウンドウェーブの例では、これは特に顕著です。

回すときの正しい波(90度)は、別の歪みを与えます。



赤は、テクスチャ内の同じポイントから取得されたベクトルですが、このテクスチャの回転は異なります。 オフセット値は同じままで、回転を考慮しません。

この問題を解決するために、 unity_ObjectToWorld変換行列を使用します。 ローカル座標からワールド座標へのベクトルの再計算に役立ちます。

 float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; offset = mul( unity_ObjectToWorld, offset); 

ただし、マトリックスにはオブジェクトのスケールに関するデータも含まれているため、歪みの強さを示すときは、オブジェクト自体のスケールを考慮する必要があります。

 _propertyBlock.SetFloat("_DisplacementPower", property/transform.localScale.x); 

右の波も90度回転しますが、歪みは正しく計算されるようになりました。



クリップ


テクスチャには十分な透明ピクセルがあります(特にRectメッシュタイプを使用する場合)。 シェーダーはそれらを処理しますが、この場合は意味がありません。 したがって、不必要な計算の数を減らすようにします。 clip(x)メソッドを使用して、透明ピクセルの処理を中断できます 。 渡されたパラメーターがゼロより小さい場合、シェーダーは終了します。 ただし、アルファ値は0未満にできないため、小さな値を減算します。 プロパティ( Cutout )に入れて、画像の透明部分をトリミングするために使用することもできます。 この場合、個別のパラメーターは不要なので、数値0.01を使用します。

完全なフラグメントシェーダーコード。

 fixed4 frag (v2f i) : SV_Target { fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy * 2 - 1) * _DisplacementPower * displPos.a; offset = mul( unity_ObjectToWorld,offset); fixed4 texColor = tex2D(_MainTex, i.uv + offset)*i.color; clip(texColor.a - 0.01); fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); fixed s = step(grabColor, 0.5); fixed4 color = s * 2 * grabColor * texColor + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); color = lerp(grabColor, color ,texColor.a); return color; } 

PS:シェーダーとスクリプトのソースコードはgitへリンクです。 プロジェクトには、歪み用の小さなテクスチャジェネレータもあります。 台座付きのクリスタルは、資産-2D Game Kitから取りました。

Source: https://habr.com/ru/post/J427357/


All Articles