続きは土日に書くといいましたが、いざ続きを書き始めるとノリノリで書ききってしまいました。
↓前回はこちら。
さて、前回は「シェーダで色合成を行う利点と欠点」と「0からスクラッチの合成を踏まえたシェーダを用意する流れ」を記載しました。
前回の記事の内容までだと、単純な「アルファマップ別持ち」処理に留まっているので、せっかくなので境界線への色付け方法も紹介します。
個人的にはこの辺りが一番シェーダ開発で楽しいポイントです。
更に1ステップアップで、境目に色を付けたい場合
前回のコーディングでスクラッチ表現の見た目を実装したい、というおおよその目的は達成されました。
ただ、それだけだとちょっと面白くない。
という事で「簡単に境目に色を付ける方法」も手順を紹介しておきます
境目とは何ぞや?を考える
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のカーブを考えてみましょう
シンプルな直線 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()は整数部を切り取り、少数部のみを繰り返す関数です。
つまり
をy=frac(x)とすると
というカーブに加工できます。
この時、xが1.0の時に少数部のみとなるので0。
つまり、xの値が0.9999999…まではyが0.999999…と比例していき、1の時に0になるギザ刃が出来上がるわけです。
で言うならば
こんなカーブ。
早くも、xが0~0.99999…までは同値、1の時に0のカーブが出来上がりました。
ceil()での加工
次に使う ceil()関数は、小数点を繰り上げする関数です。
0は0のまま、0.0000001でも値が入ると1に繰り上げられます。
にy=ceil(x)とすると
というカーブになります。
もう察しがついたでしょう、という事で目的の関数は y=ceil(frac(x)) となります
をfrac(x)として
を更にceil()でくくって
となるわけです。
という事で
- 0、1は0
- 0、1以外は1
となる関数が出来ました!
早速y=ceil(frac(x))をシェーダに組み込みましょう!
シェーダに組み込む
前回のシェーダ
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)(赤)が入るような計算ですね。
さて、どういった見た目になるか…
きったなっっ!なんと汚い境界線でしょう。
なぜこんなことになってしまったかというと、Pattern.pngに圧縮が掛かっているからです。
ceil(frac(x))はどんな微少の色変化も検知して繰り上げる計算なので圧縮の際のデータのチリが反応してしまっているのですね。
検証のためにPattern.pngの圧縮を非圧縮にしてみましょう。
ProjectツリービューからPattern.pngを選択し、FormatをRGBA32bitに変更
綺麗な境界線になりました!ただ、よく見ると、少しゴミが見えます。
これは、マッピング外(uv0~1以外)の繰り返し部分が侵食している場合に発生しているゴミですね。
という事でループ設定も切っておきます。
Pattern.pngのループ設定をRepeatからClampへ。
で、表示を見ると
ゴミも無くなって、完璧に境界線が表示できました!
仕上げに
最後の仕上げとして、境界線の色をマテリアルから指定できるようにしておきましょう。
前回同様に、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)));
で、出来上がったコードの完成形はこちら
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
}
}
}
マテリアルから境界線の色を変更する事が可能になりました! お疲れさまでした!
※BorderがEdgeという名前になっているのはスクショを撮ったタイミングの記載が違うからなのでお気になさらず。
最後に
という事で、スクラッチ合成処理を行いつつ、くっきりとした境界線を描くシェーダでした。
個人的にはカーブを色々加工する辺りがとても好きなので、シンパシーを感じたならば是非色々な計算を実験してみて欲しいです。
「数学って美しい」という言葉をよく聞きますが、シェーダの演算についてはまさにそれを体現しているように思います。
さて、今回はパッキリした境界線を描く所で完了としましたが、
せっかく土台があるので、次回は圧縮が効いたままでも境界線をなじませるには?という記事が書けたら書こうかな、と思います。