【Unity HLSL】暗号みたいなHSV→RGB変換の数式を「グラフ」で完全に理解する
Unityでシェーダーを書いていると、色を扱う際に「HSVからRGBへの変換」が必要になることがよくあります。
ネットで検索すると、以下のような最適化された変換コードがよく出てきます。
float3 hsv2rgb(float3 c) {
float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * lerp(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
普段シェーダーを書かない人からすると、
「謎の数字(2.0 / 3.0など)がいっぱいある」「abs?frac?何をしているか全く分からない」と、まるで暗号のように見えるかもしれません。
しかし、この数式は「グラフの線をパキパキと折り曲げて、特定の色を作るための波を作っているだけ」なのです。
この記事では、複雑な数式がどのような計算をして色を作っているのか、グラフを使って1ステップずつ視覚的に解説します!
そもそもHSVとは?なぜ使うの?
私たちが普段モニターで見ている色は「赤(R)、緑(G)、青(B)」の光の組み合わせ(RGB)で表現されています。モニターなどの機械にとっては都合が良いのですが、人間にとっては「もう少し暗いオレンジにしたい」と思った時に、RGBそれぞれの数値をどう調整すればいいか直感的に分かりづらいという欠点があります。
そこで人間にとって分かりやすいように作られたのがHSVです。
Hue(色相):
赤、青、緑といった「色味」(0〜360度の円で表現されることが多い)
Saturation(彩度):
色の「鮮やかさ」(低いと白黒に近づく)
Value(明度):
色の「明るさ」(低いと黒に近づく)
HSVを使えば「色を変えたい、鮮やかにしたい、暗くしたい」といった人間の感覚そのままに数値をいじれるため、ゲーム内のエフェクトの色を時間で変化させたり、ユーザーに色を選ばせたりする処理では、HSVが非常に重宝されます。
謎の変数「K」に入っている数字の正体
数式の最初に出てくる変数 K。ここには一見すると適当な数字が入っているように見えます。
float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
実はこれ、「色相環(カラーホイール)の角度」を0.0〜1.0の範囲に置き換えたものです。
RGBの3色は、色相環の上でちょうど「120度ずつ」離れています。
赤(R):0度(基準) = 1.0
緑(G):120度ズレ = 360度の1/3 = 2.0 / 3.0 (※計算の都合上のオフセット)
青(B):240度ズレ = 360度の2/3 = 1.0 / 3.0 (※計算の都合上のオフセット)
これらの数字は、「赤、緑、青の波が一番強くなるタイミングを、それぞれ1/3ずつズラす」ための準備(オフセット値)なのです。最後の 3.0 については、すぐ後のグラフ解説で登場します。
数式を分解!グラフで見る波の作り方
それでは、メインとなる以下の数式が何をしているのか、1つの色(今回は赤)に注目してグラフの変化を見ていきましょう。
abs(frac(Hue + Offset) * 6.0 - 3.0) - 1.0
(※分かりやすくするため、元の数式を少し展開しています)
ステップ1 : fracで「繰り返す波」を作る
まずは frac(Hue + Offset) の部分です。frac は小数点以下の数字だけを取り出す関数です。
Hue(色相)の値が0から1へ増えていくと、以下のような「のこぎり」のような波ができます。

(0から1に向かって上がり、1になると0にストンと落ちる)
ステップ2 : 6倍に引き伸ばす
次に、先ほどの波に * 6.0 を掛けます。
なぜ6倍するのか?それは色相環が「赤→黄→緑→水色→青→紫→赤」という6つの主要な色の区間に分かれているからです。

(波が縦に大きく、0〜6の範囲に引き伸ばされました)
ステップ3 : 「3」を引いて下にズラす
先ほどの状態から - 3.0(ここで変数Kの 3.0 が使われます!)を引き算します。

(波全体が下がり、-3から+3の範囲を行き来するようになりました)
ステップ4 : abs(絶対値)でV字に折り曲げる
ここが魔法のポイントです。abs(...) を使って、マイナスの値をすべてプラスに反転(絶対値に)させます。

(マイナスだった部分が上に折り返され、きれいな「V字型(ジグザグ)」の波になりました)
本来、GPU(グラフィックボード)は if 文(条件分岐)の計算が苦手です。この abs を使うテクニックは、if文を使わずに波を折り曲げるための賢い最適化なのです。
ステップ5 : さらに1を引いて下にズラす
(全体が下がり、-1から+2の範囲になりました)
ステップ6 : clampで上下を切り取る(完成!)
最後に clamp(値, 0.0, 1.0) という関数を使って、「0より小さい部分は0に、1より大きい部分は1にする」という切り取り(平坦化)を行います。

(上がはみ出た部分は真っ平らな1に、下がはみ出た部分は真っ平らな0になりました)
これで完成です!
この「台形」のようなグラフこそが、HSVのHueを変化させたときに「赤色(R)」がどれくらい光るかを表す波形になります。
緑(G)や青(B)も、最初の変数 K でスタート位置をズラしているだけで、全く同じ台形の波を作っています。
以下参考画像
赤 黄 緑 シアン 青 マゼンタ 赤
↓ ↓ ↓ ↓ ↓ ↓ ↓


あとは、この完成した波に対して、Saturation(彩度)で白を混ぜ、Value(明度)で全体の暗さを掛け算しているだけです。
具体的には何に使えるの??
例えばHueの値に時間を入れれば虹色を表現することができます。
hsv2rgb(float3(frac(_Time.y),1,1);
まとめ
一見すると複雑怪奇なシェーダーの数式も、分解してみると以下のようなシンプルな加工の連続です。
線を引く (frac)
引き伸ばしてズラす (* 6.0 - 3.0)
絶対値で折り曲げる (abs)
はみ出た部分をハサミで切る (clamp)
シェーダーの数式は「数式というより、図形工作」に近い感覚で作られています。このグラフのイメージを持っておくと、他のシェーダーコードを読むときも少し苦手意識が減るかもしれません!
次回はRGBからHSVに変換するコードについて解説したいと思います!!
これからもUnity、シェーダー関連の記事を投稿していくのでいいねとフォローよろしくお願いします!
