Qiita Teams that are logged in
You are not logged in to any team

Community
Service
Qiita JobsQiita ZineQiita Blog
12
Help us understand the problem. What is going on with this article?
@Shinoda_Naoki

経緯

デメリットとしてはパレット置換に比べると色相の異なる４色以上の変換ができないので自由度が減る場合がありますが、現実的には３系統の明暗グラデーション置換ができれば事足りる場合がほとんどだと思います。

```  赤系:(x,0,0) => （x*A.r, x*A.g, x*A.b）;
緑系:(0,y,0) => （y*B.r, y*B.g, y*B.b）;
青系:(0,0,z) => （z*C.r, z*C.g, z*C.b）;
それ以外:(x,y,z) => (x,y,z); // 変化なし！
```

シェーダー実装（基本編）

```Shader "Custom/HueTrans"
{
Properties
{
[NoScaleOffset] _MainTex ("Day Texture", 2D) = "white" {}

_RedTransfar ("RED Transfer Color", Color) = (1,0,0,1)
_GreenTransfar ("GREEN Transfer Color", Color) = (0,1,0,1)
_BlueTransfar ("BLUE Transfer Color", Color) = (0,0,1,1)
}
{
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;

fixed3 _RedTransfar;
fixed3 _GreenTransfar;
fixed3 _BlueTransfar;

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;
}

inline fixed4 transferColor(in fixed4 srcCol) {
// test srcCol if the two of r,g,b are 0.
float d1 = (1 - srcCol.r) * (1 - srcCol.g) * (1 - srcCol.b);
float d2 = srcCol.r + srcCol.g + srcCol.b;
float flag = step(d1 + d2, 1);

fixed3 rgb = lerp(
srcCol.rgb,
_RedTransfar * srcCol.r + _GreenTransfar * srcCol.g + _BlueTransfar * srcCol.b,
flag );

return fixed4(rgb,1);
}

fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);

// stop rendering if it is transpalent.

// apply hue transform.
col =  transferColor(col);

// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
```

```    inline fixed4 transferColor(in fixed4 srcCol) {
// test srcCol if the two of r,g,b are 0.
float d1 = (1 - srcCol.r) * (1 - srcCol.g) * (1 - srcCol.b);
float d2 = srcCol.r + srcCol.g + srcCol.b;
float flag = step(d1 + d2, 1);

fixed3 rgb = lerp(
srcCol.rgb,
_RedTransfar * srcCol.r + _GreenTransfar * srcCol.g + _BlueTransfar * srcCol.b,
flag );

return fixed4(rgb,1);
}
```

ここだけで以下の変換を全て行っていることになります。意外とコンパクトに纏まってますよね（自画自賛）！？

```  赤系:(x,0,0) => （x*A.r, x*A.g, x*A.b）;
緑系:(0,y,0) => （y*B.r, y*B.g, y*B.b）;
青系:(0,0,z) => （z*C.r, z*C.g, z*C.b）;
それ以外:(x,y,z) => (x,y,z); // 変化なし！
```

シェーダーで使用している計算式は、（私が知らないだけでどこかの偉い人が発明済みかもしれませんが）自力で捻りだしたものですので参考資料とかはありません。

rgb の同時計算自体はわりとすぐに思い付いたので、それを利用できるような flag の算出方法を何とか捻りだそうと試行錯誤していて、偶然見つけたようなものです。そんな経緯なので、直感的にわかりやすい説明はなく、なぜこれでいいのか後付けで証明してみました。

`d1 + d2 > 1` を展開すると `rg + gb + br > rgb` という不等式ができますが、もし r,g,b のうち二つ以上が0であれば両辺共に0になってこれは成り立ちません

ゆえに、r,g,b のうち少なくとも二つが0であるは `d1 + d2 == 1` であり、それ以外の場合は `d1 + d2 > 1` である、と言えます。これを 0,1 値に変換したのが `float flag = step(d1 + d2, 1);` の行です。

その次の lerp 関数を使った式は、シェーダーでおなじみの三項演算子的な使い方をしてます。
`flag == 0` の時、rgb には元のピクセル値 (`srcCol.rgb`) が入り、 `flag == 1` の時は rgb には `_RedTransfar * srcCol.r + _GreenTransfar * srcCol.g + _BlueTransfar * srcCol.b` が入ります。
しかし `flag == 1` の時は r,g,b のうち少なくとも二つは0であるので、結局のところ `_RedTransfar * srcCol.r``_GreenTransfar * srcCol.g``_BlueTransfar * srcCol.b` のどれか1つのうち0でないものが入ります（※厳密には r,g,b 全て0の場合もこっちの計算が適用されるので、全て0になることもあるが、黒が書き出されるだけなので問題ない）。

光沢も出したい（白混色対応）

このシェーダーの弱点は黒との混色だけで白との混色には対応できていません。
どういうことかと言うと、下記のような光沢のある絵で使われる RGB と白との混色（ハイライト）が置換できないということです。

シェーダー実装（拡張編）

```    inline fixed4 transferColor(in fixed4 srcCol) {
// test srcCol if the two of r,g,b are 0.
float d1 = (1 - srcCol.r) * (1 - srcCol.g) * (1 - srcCol.b);
float d2 = srcCol.r + srcCol.g + srcCol.b;
float flag = step(d1 + d2, 1);

fixed3 rgb = lerp(
srcCol.rgb,
_RedTransfar * srcCol.r + _GreenTransfar * srcCol.g + _BlueTransfar * srcCol.b,
flag );

fixed3 white = fixed3(1,1,1);
flag = (1 - flag) * step(1, srcCol.r) * step(1, 1- abs(srcCol.g - srcCol.b));
rgb = lerp(
rgb,
lerp(_RedTransfar, white, srcCol.g),
flag);

flag = (1 - flag) * step(1, srcCol.g) * step(1, 1- abs(srcCol.b - srcCol.r));
rgb = lerp(
rgb,
lerp(_GreenTransfar, white, srcCol.b),
flag);

flag = (1 - flag) * step(1, srcCol.b) * step(1, 1- abs(srcCol.r - srcCol.g));
rgb = lerp(
rgb,
lerp(_BlueTransfar, white, srcCol.r),
flag);

return fixed4(rgb,1);
}
```

flag は `(1-flag)` で直前の式の else 条件とし、r, g, b のどれか一つが1で、他の二つが等しい場合に 1 になるようになっています。例えば赤系のハイライトなら (r,g,b) == (1,x,x) になるような色が対象です。この時の書き出し色は lerp 関数を使って `x * white + (1-x) * _RedTransfar` となります。

```  赤系+黒:(x,0,0) => （x*A.r, x*A.g, x*A.b）;
赤系+白:(1,x,x) => （x + (1-x)*A.r, x + (1-x)*A.g, x + (1-x)*A.b）;
緑系+黒:(0,y,0) => （y*B.r, y*B.g, y*B.b）;
緑系+白:(y,1,y) => （y + (1-y)*B.r, y + (1-y)*B.g, y + (1-y)*B.b）;
青系+黒:(0,0,z) => （z*C.r, z*C.g, z*C.b）;
青系+白:(z,z,1) => （z + (1-z)*C.r, z + (1-z)*C.g, z + (1-z)*C.b）;
それ以外:(x,y,z) => (x,y,z); // 変化なし！
```

結果

ご覧の通り、ハイライト部分も色置換できるようになりました！

更なるコード効率化（改良編）

そして思いのほか上手く行ったので下記に紹介します。

```    inline fixed4 transferColor(in fixed4 srcCol) {
float lowest = min(srcCol.r, min(srcCol.g, srcCol.b));
fixed3 hueCol = srcCol.rgb - lowest;
fixed3 hilight = fixed3(lowest,lowest,lowest);

// test srcCol if the two of r,g,b are 0.
float d1 = (1 - hueCol.r) * (1 - hueCol.g) * (1 - hueCol.b);
float d2 = hueCol.r + hueCol.g + hueCol.b;
float flag = step(d1 + d2, 1);

fixed3 rgb = lerp(
srcCol.rgb,
_RedTransfar * hueCol.r + _GreenTransfar * hueCol.g + _BlueTransfar * hueCol.b + hilight,
flag );

return fixed4(rgb,1);
}
```

まず、追加された冒頭の三行について。
lowest は r,g,b のうちで最小値を求めています。最小値ですが、黒混色も白混色も r,g,b のうち二つが最小値になるはずです。
hueCol は元の色から最小値を引いた色です。この操作により、白混色の対象カラーも黒混色と同じ (x,0,0),(0,y,0),(0,0,z) のどれかに変わりますね！
highlight はスカラー値である lowest を fixed3 に変えただけです。RGBが等しいのでグレー系ですね。これは色変換後に足すべき白混色の度合いを意味します。

さて、その次の三行は黒混色の時と同じ条件判定用の計算ですね。先ほど述べたように、hueCol は黒変換と白変換で同じ条件式が使えるようになっているはずです。
メインの rgb 計算式は、 lerp の二つ目の引数式の最後に + highlight が追加されていることに注目してください。これで白混色の場合は元色と同じ割合だけ白成分が追加されます。黒混色の場合 highlight は黒(0,0,0)なので何も影響はありません。

ご使用上の注意

お勧めは以下のように、FilterMode を Pointにして Format を無圧縮にすることです。

感想

ただそれだけだと、半年もすればどうしてこんな式で動くのか自分でも忘れてしまいそうなので、こうやって記事に残すのは備忘録としても役立ちそうです。

12
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
1. We will deliver articles that match you
By following users and tags, you can catch up information on technical fields that you are interested in as a whole
2. you can read useful information later efficiently
By "stocking" the articles you like, you can search right away