ShaderTips

シェーダーTips

主にUnityシェーダーについての記事を書いています。

【Unity】ディザリングを実装してみた

ディザリングとは

一定間隔で穴を空けることで、不透明だけど半透明に見せることができます。 通常の半透明で発生する不具合や、処理負荷の問題が発生しないのが利点です。 youtu.be

穴を開けるパターン

現在描画するピクセルがを開けるか判定する方法の一つとして、しきい値マップという数値の配列を使用します。 ピクセルの位置に対応する閾値マップの値と設定した閾値を比較して、ピクセルの破棄を行うか判定します。

https://wikimedia.org/api/rest_v1/media/math/render/svg/3c62838dbbd378c058444a60b9c803b9bb4ee09c

配列ディザリング - Wikipedia引用

実装

Shader "Dither"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _DitherLevel("DitherLevel", Range(0, 16)) = 1
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 positionCS : SV_POSITION;
                float4 positionSS : TEXCOORD1;
            };
            
            // しきい値マップ
            static const float4x4 pattern =
            {
                0,8,2,10,
                12,4,14,6,
                3,11,1,9,
                15,7,13,565
            };
            static const int PATTERN_ROW_SIZE = 4;

            sampler2D _MainTex;
            sampler2D _DitherTex;
            float _Alpha;
            half _DitherLevel;

            v2f vert (appdata v)
            {
                v2f o;
                o.positionCS = UnityObjectToClipPos(v.positionOS);
                o.positionSS = ComputeScreenPos(o.positionCS);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // ①
                // スクリーン座標
                float2 screenPos = i.positionSS.xy / i.positionSS.w;
                // 画面サイズを乗算して、ピクセル単位に
                float2 screenPosInPixel = screenPos.xy * _ScreenParams.xy;

                // ②
                // ディザリングテクスチャ用のUVを作成
                int ditherUV_x = (int)fmod(screenPosInPixel.x, PATTERN_ROW_SIZE);
                int ditherUV_y = (int)fmod(screenPosInPixel.y, PATTERN_ROW_SIZE);
                float dither = pattern[ditherUV_x, ditherUV_y];
                  
                 // ③
                 // 閾値が0以下なら描画しない
                clip(dither - _DitherLevel);

                //メインテクスチャからサンプリング`
                float4 color = tex2D(_MainTex, i.uv); 
                return color;
            }
            ENDCG
        }
    }
}

ComputeScreenPosで求めた値を正規化すると0〜1のスクリーン座標になります。

そこに_ScreenParams.xy=スクリーンの幅と高さ(ピクセル単位)を乗算することで、0〜スクリーンの幅、高さの値にしています。

詳しくはこちら ny-program.hatenablog.com

しきい値マップは4x4の行列なので①で計算したスクリーン座標を4で除算した余りを求め、しきい値マップの成分を取り出し、それを閾値とします。 これにより、スクリーン上を縦横4x4分割し、分割された単位ごとにしきい値マップの成分が割り当てられることになります。

しきい値マップから取得した閾値よりマテリアルプロパティから設定した閾値が大きければそのピクセルの描画を破棄して穴を開けています。

参考

3dcg-school.pro