【Unity】ポケモン剣盾のディゾルブ処理を再現してみる【ディザリング】
はじめに
ポケモンの剣盾をやっていて、最初に気になったのがカメラとプレイヤーの間の遮断物が半透明でなくディザリング処理のように消えていくところです。
昔の半透明が使えない時代はよく多用されてましたが、最近はお目にかかってなかったので「お?」と思いました。
半透明は色々干渉が大きいので良い判断だと思います。
では実際に試して見た結果がこちらです。
ディザリングする
ディザリング用のテクスチャを用意して、アルファ値を見ながらディザリングしていきます。
今回用意したテクスチャはこちら

32段階のタイルパターンでグラデーションをつけてます。
例えば Alpha = 0.5 であれば

このパターンを並べて 中間色にし、黒い部分は後ろの色、白い部分は自分の色を出すようなアルゴリズムです。

Alpha = 0.25 であれば 8個目のタイルで

このタイルを引き詰めて

25%のアルファの完成です。
シェーダー
頂点シェーダー
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
struct appdata
{
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
UNITY_VPOS_TYPE vpos : VPOS;
};
v2f vert( appdata v, out float4 vertex : SV_POSITION)
{
v2f o = (v2f)0;
vertex = UnityObjectToClipPos( v.vertex);
o.texcoord = float2( v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw);
return o;
}
頂点シェーダーで注意する部分は、ピクセルシェーダーでスクリーン座標 VPOSを設定するため、構造体 v2f に SV_POSITION を設定できません。でも出力しないとならないので、 引数で
out float4 vertex : SV_POSITION
を追加して頂点情報を出力しています。
ピクセルシェーダー
sampler2D _GradationMap;
sampler2D _ToonMap;
float4 _GradationMap_ST;
float4 _GradationMap_TexelSize;
float _Alpha;
half4 frag( v2f i) : SV_Target
{
if( _Alpha == 0)
{
discard; // 中止
}
float _CellSize = _GradationMap_TexelSize.w;
float2 localuv = fmod( i.vpos.xy, _CellSize) / _GradationMap_TexelSize.zw;
float alpha = floor(_Alpha * 32.0) / 32.0;
localuv.x += alpha;
float3 gradation = tex2D( _GradationMap, localuv).rgb;
if( gradation.r < 0.5)
{
discard; // 中止
}
float3 sample = tex2D( _MainTex, i.texcoord).rgb;
return float4( sample, 1);
}
実際にはトゥーンとライトの計算が入ってますが省いてます。
まず _Alpha が 0 であれば完全な透明なので即刻終了。
sampler2D _GradationMap;
には先程の グラデーションのテクスチャが設定されてます。
_CellSize は 8 が入り 1タイル分の大きさが入ります。
float2 localuv = fmod( i.vpos.xy, _CellSize) / _GradationMap_TexelSize.zw;
float alpha = floor(_Alpha * 32.0) / 32.0;
localuv.x += alpha;
localuv には スクリーン座標を 8 割った余りを使うことで 8のループが連続で続きます。これを テクスチャーのサイズで割ってやれば グラデーションの UV 値を計算できます。
あとは 32等分された UV が来るので グラデーションテクスチャーを参照し、黒ければ描画終了。白ければ実際の物の色を出せば完成です。
シェーダーコード
ライトやトゥーンを省いた、最小コードになります。
Shader "Neko/Dissolve"
{
Properties
{
_MainTex("Main Texture", 2D) = "white" {}
[NoScaleOffset]_GradationMap( "Gradation Map", 2D) = "white" {}
_Alpha( "Alpha", Range(0.0, 1.0)) = 1.0
}//Properties
CGINCLUDE
#include "UnityCG.cginc"
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
struct appdata
{
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
UNITY_VPOS_TYPE vpos : VPOS;
};
v2f vert( appdata v, out float4 vertex : SV_POSITION)
{
v2f o = (v2f)0;
vertex = UnityObjectToClipPos( v.vertex);
o.texcoord = float2( v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw);
return o;
}
sampler2D _GradationMap;
sampler2D _ToonMap;
float4 _GradationMap_ST;
float4 _GradationMap_TexelSize;
float _Alpha;
half4 frag( v2f i) : SV_Target
{
if( _Alpha == 0)
{
discard; // 中止
}
float _CellSize = _GradationMap_TexelSize.w;
float2 localuv = fmod( i.vpos.xy, _CellSize) / _GradationMap_TexelSize.zw;
float alpha = floor(_Alpha * 32.0) / 32.0;
localuv.x += alpha;
float3 gradation = tex2D( _GradationMap, localuv).rgb;
if( gradation.r < 0.5)
{
discard; // 中止
}
float3 sample = tex2D( _MainTex, i.texcoord).rgb;
return float4( sample, 1);
}
ENDCG
}//Pass
}//SubShader
FallBack "Diffuse"
}//Shader
プロパティに設定
_Alpha をプロパティとして設定すれば、あたかもアルファブレンドのような振る舞いが可能となります。

あとはプレイヤーとカメラの間に トリガーなコリジョンをつけて、遮蔽物のコリジョンに触ったときに OnTriggerStay() で受け取って、対象のアルファを変更すれば完成です。
自分はカメラのコリジョンに以下のようなコードをつけて対象をフェードアウトしてもらうようにしています。
public class DissolveCameraBehaviour : MonoBehaviour
{
//----------------------------------------------------------------------------
private void OnTriggerStay( Collider other)
{
if( other.gameObject.layer == LayerMask.NameToLayer( "Dissolve"))
{
other.gameObject.GetComponent<DissolveBehaviour>()?.FadeOut();
}
}
}// class DissolveCameraBehaviour
最後に
不透明のレンダリングなのでアルファエラーも起きず、そこそこ高速なので、Switch のような非力なGPU でも比較的軽く使えるのではないでしょうか?
