2
3

More than 1 year has passed since last update.

スクラッチの境界線をシェーダで色付けしてみる

Last updated at Posted at 2022-02-23

続きは土日に書くといいましたが、いざ続きを書き始めるとノリノリで書ききってしまいました。

↓前回はこちら。

さて、前回は「シェーダで色合成を行う利点と欠点」と「0からスクラッチの合成を踏まえたシェーダを用意する流れ」を記載しました。
前回の記事の内容までだと、単純な「アルファマップ別持ち」処理に留まっているので、せっかくなので境界線への色付け方法も紹介します。
個人的にはこの辺りが一番シェーダ開発で楽しいポイントです。

更に1ステップアップで、境目に色を付けたい場合

前回のコーディングでスクラッチ表現の見た目を実装したい、というおおよその目的は達成されました。
ただ、それだけだとちょっと面白くない。
という事で「簡単に境目に色を付ける方法」も手順を紹介しておきます

境目とは何ぞや?を考える

Pattern.pngを見直してみましょう
Pattern.png
よく見るとわかるのですが、こちらの画像、実は白黒2色ではなく、白と黒の間に微妙にグラデーションが入っているのですね。
つまり

  • 黒(0) … 削った部分
  • 黒でも白でもない部分(0 < color < 1) … 境目
  • 白(1) … 削ってない部分

という考え方が出来るわけです。

つまり「0でも1でもない部分に色を流し込めば境目に色を塗れる!」という事です。

0でも1でもない部分を抽出する

簡単に思いつくならばif文です。
先ほどlerpで使ったのpatternCol.rの色を使って

fixed4 col;
if ( 0.0 < patternCol.r && patternCol.r < 1.0 )
{
    col = 境界線の色;
}
else
{
    col = lerp(baseCol, overlayCol, patternCol.r);
}

とC#よろしく、分岐してしまえば簡単に対応できるでしょう。
ただ、前述した通りFragentシェーダはピクセル数分計算が走ってしまう = 512x512x1=262,144回のif文が発生してしまい(厳密にはコンパイラ挙動で違ったりもしますが)避けるべき事象となります。
※ただ、最近はif文も結構禁忌ではなくなってきている。
※今回のケースでは避けたいですが、SRPBatcher用に1シェーダで複数の見た目を制御する際には、下手にシェーダを分けるよりも分岐した方が良いケースもある。

という事で計算のみで0<color<1を抽出する計算の1例を紹介します

カーブで考える

0~1のカーブを考えてみましょう
20220223_4.png
シンプルな直線 y=x (縦y横x)です。
yを抽出後の値、xを元の値、と考えると y=xは前回のPattern.pngの色取得を引っ張って来ると、

    fixed finalValue = patternCol.r;

となります。
今回欲しいのは 0<color<1なので、patternCol.rが0の時に0。0超過かつ1未満の時に1、1の時に0という値が取得出来たら目的が達成できそうです。

カーブを加工する

正直ココからの計算は十人十色なのですが、自分がfrac()関数が好き!なので、frac()を使用する計算方法を紹介してみます。

frac()での加工

frac()は整数部を切り取り、少数部のみを繰り返す関数です。

つまり
20220223_5.pngをy=frac(x)とすると20220223_6.png
というカーブに加工できます。
この時、xが1.0の時に少数部のみとなるので0。
つまり、xの値が0.9999999…まではyが0.999999…と比例していき、1の時に0になるギザ刃が出来上がるわけです。
20220223_4.pngで言うならば20220223_7.png
こんなカーブ。
早くも、xが0~0.99999…までは同値、1の時に0のカーブが出来上がりました。

ceil()での加工

次に使う ceil()関数は、小数点を繰り上げする関数です。

0は0のまま、0.0000001でも値が入ると1に繰り上げられます。
20220223_4.pngにy=ceil(x)とすると20220223_8.png
というカーブになります。
もう察しがついたでしょう、という事で目的の関数は y=ceil(frac(x)) となります

20220223_4.pngをfrac(x)として20220223_7.pngを更にceil()でくくって20220223_9.pngとなるわけです。

という事で

  • 0、1は0
  • 0、1以外は1

となる関数が出来ました!
早速y=ceil(frac(x))をシェーダに組み込みましょう!

シェーダに組み込む

前回のシェーダ

Scratch.shader
Shader "Unlit/Scratch"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _PatternTex ("Pattern", 2D) = "white" {}
        _OverlayTex ("Overlay", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _PatternTex;
            float4 _PatternTex_ST;
            sampler2D _OverlayTex;
            float4 _OverlayTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 baseCol = tex2D(_MainTex, i.uv);
                fixed4 patternCol = tex2D(_PatternTex, i.uv);
                fixed4 overlayCol = tex2D(_OverlayTex, i.uv);
                fixed4 col = lerp(baseCol, overlayCol, patternCol.r);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

の、最終的に仕上がった色計算の後に、境界線の色を流し込んでみましょう

fixed4 col = lerp(baseCol, overlayCol, patternCol.r);

に、同じくlerp()で境界線の色を合成してみます

fixed4 col = lerp(baseCol, overlayCol, patternCol.r);
col = lerp(col, fixed4(1,0,0,1), ceil(frac(patternCol.r)));

lerp()は前回の記事で紹介した通り lerp(valueが0の時の色、valueが1の時の色, value) なので
ceil(frac(patternCol.r))が0ならcol(そのままの色)、1ならfixed(1,0,0,1)(赤)が入るような計算ですね。

さて、どういった見た目になるか…
image.png
きったなっっ!なんと汚い境界線でしょう。
なぜこんなことになってしまったかというと、Pattern.pngに圧縮が掛かっているからです。
ceil(frac(x))はどんな微少の色変化も検知して繰り上げる計算なので圧縮の際のデータのチリが反応してしまっているのですね。
検証のためにPattern.pngの圧縮を非圧縮にしてみましょう。
20220223_10.png
ProjectツリービューからPattern.pngを選択し、FormatをRGBA32bitに変更
image.png
綺麗な境界線になりました!ただ、よく見ると、少しゴミが見えます。
スクリーンショット 2022-02-24 005311.png
これは、マッピング外(uv0~1以外)の繰り返し部分が侵食している場合に発生しているゴミですね。
という事でループ設定も切っておきます。
Pattern.pngのループ設定をRepeatからClampへ。
20220223_11.png
で、表示を見ると
スクリーンショット 2022-02-24 005904.png
ゴミも無くなって、完璧に境界線が表示できました!

仕上げに

最後の仕上げとして、境界線の色をマテリアルから指定できるようにしておきましょう。

前回同様に、Propertiesと定義部を修正します

    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _PatternTex ("Pattern", 2D) = "white" {}
        _OverlayTex ("Overlay", 2D) = "white" {}
        _BorderColor ("Border", Color) = (1,1,1,1)
    }

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _PatternTex;
            float4 _PatternTex_ST;
            sampler2D _OverlayTex;
            float4 _OverlayTex_ST;
            fixed4 _BorderColor;//←追加

そして、先ほど赤色固定していたfixed4(1,0,0,1)を_BorderColorに置き換えて

col = lerp(col, _BorderColor, ceil(frac(patternCol.r)));

で、出来上がったコードの完成形はこちら

Scratch.shader
Shader "Unlit/Scratch"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _PatternTex ("Pattern", 2D) = "white" {}
        _OverlayTex ("Overlay", 2D) = "white" {}
        _BorderColor("Border", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _PatternTex;
            float4 _PatternTex_ST;
            sampler2D _OverlayTex;
            float4 _OverlayTex_ST;
            fixed4 _BorderColor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 baseCol = tex2D(_MainTex, i.uv);
                fixed4 patternCol = tex2D(_PatternTex, i.uv);
                fixed4 overlayCol = tex2D(_OverlayTex, i.uv);
                fixed4 col = lerp(baseCol, overlayCol, patternCol.r);
                col = lerp(col, _BorderColor, ceil(frac(patternCol.r)));
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

20220223_12.png

マテリアルから境界線の色を変更する事が可能になりました! お疲れさまでした!
※BorderがEdgeという名前になっているのはスクショを撮ったタイミングの記載が違うからなのでお気になさらず。

最後に

という事で、スクラッチ合成処理を行いつつ、くっきりとした境界線を描くシェーダでした。
個人的にはカーブを色々加工する辺りがとても好きなので、シンパシーを感じたならば是非色々な計算を実験してみて欲しいです。
「数学って美しい」という言葉をよく聞きますが、シェーダの演算についてはまさにそれを体現しているように思います。

さて、今回はパッキリした境界線を描く所で完了としましたが、
せっかく土台があるので、次回は圧縮が効いたままでも境界線をなじませるには?という記事が書けたら書こうかな、と思います。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3