UE4で色域チェックしてみる

UE4で色域チェックをやってみる話です。
色に関しては現在進行中でまだまだ勉強中なので、突っ込みどころあれば是非お願いします。

![gamutout4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/0bd7fa31-0866-0604-a57b-fbc654e20383.gif)

## きっかけ
最近HDRなどが実装され始めた関係で、一つのアプリケーションの中で複数の色域を使うケースが出てきました。
動画やテクスチャ素材をSDR版とHDR版両方のアセットを別々に持つのは現実的ではなく、実行中の状態を見て、どちらかをどちらかへ変換する事が多いかと思います。
例えば、HDRを有効にすると色域がRec709からRec2020(もしくはDCI-P3)になりますが、特にSDR環境だけを想定して作られたアセットが多い現状の環境ではこの変換により、大きく違う色味になってしまう事が多々あります。
広色域への変換やその逆について、LUTなど工夫のしどころは沢山ありますが、まずは正しくない値を使ってしまい大きくずれた箇所を検出するところからかと思ったので、変換後にアーティストの意図した色にならなそうな所を検出したりそれらの補助のための機能をつくってみました。

## なぜ色味がかわるのか
図のように色域の違いによって白色点(D65)からRGB各頂点への角度の違いが出ている為で、補正するために補完カーブを工夫したり、秘伝のタレのようなLUTを当てたりするなど一筋縄ではいかない印象です。
特に色域のエッジに張り付いたような色を使っている時や、範囲外の色を使っていると、広い色域に変換された際に意図しない色になる事が多いです。
また、HDR動画をSDR環境で再生する場合など、広色域から狭い色域に変換した際もRGB値にマトリクスをかけて2020から709の変換を行うだけだと、色域外の値になってしまったり、エッジに張り付いた色になる事があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/1c443c86-8bfd-0c9a-2568-54cd78081ff7.png)

## 今回つくったチェッカー
今回作ったものは、それらの作業を補助するためのチェッカーです。
新しく作るアセットはともかくとして、以前作ったデータなどすべてちゃんとした色になっている所は少なく、過渡期であるこの時期はどうしても混在しがちです。
そこで、今現在アプリ内で動いているアセットの中で、実際やばい所はどこなのかチェックする機能を作ってみました。
具体的にはベースカラーの色域をポストマテリアルでチェックし、範囲外の部分の色を変えています。
せっかくなので4.26で作っています。

## どうやって範囲外を調べるか
ポストマテリアル上で、Gバッファを取得し、ベースカラーの色空間をXYZに変換、更にxyYへ変換して得られた座標と色域の三角形の内外判定を行っています。
### 色空間の変換
まずはRGBをxyYに変換するために、2つのカスタムノードを作ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/b2aae175-d3ca-55f1-6a1a-3fb5b23be8b8.png)


RGB(sRGB/709)からXYZへ
※このマトリクスは色域によって異なります。

	const float3x3 m = {
		0.4123907993,  0.3575843394,  0.1804807884,
		0.2126390059,  0.7151686788,  0.0721923154,
		0.0193308187,  0.1191947798,  0.9505321522,
	};
	return mul(m, rgb.xyz);
XYZからxyYへ

	float sum = XYZ.x + XYZ.y + XYZ.z;
	return float3(XYZ.x / sum, XYZ.y / sum, XYZ.y);


### 内外判定
次に、算出したxyY空間上の点が三角形内に入っているかをチェックするためのカスタムノードを作ります。返り値が1なら内側、0なら外側です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/219c40e7-0a27-1a20-5438-f380f6ba697a.png)


	float3 v1 = float3(P2 - P1, 0.0f);
	float3 v2 = float3(P3 - P2, 0.0f);
	float3 v3 = float3(P1 - P3, 0.0f);
	float3 up = float3(0.0f, 0.0f, 1.0f);
	float3 o1 = cross(v1, up);
	float3 o2 = cross(v2, up);
	float3 o3 = cross(v3, up);
	float d1 = dot(o1.xy, xy.xy - P1.xy) > 0.0f ? 0.0f : 1.0f;
	float d2 = dot(o2.xy, xy.xy - P2.xy) > 0.0f ? 0.0f : 1.0f;
	float d3 = dot(o3.xy, xy.xy - P3.xy) > 0.0f ? 0.0f : 1.0f;
	return d1 * d2 * d3;


作ったカスタムノードを組み合わせると以下のようなポストマテリアルが出来上がります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/f7ac1f32-bd77-96a3-4b9e-a3002711b3e6.png)

今回は709の色域かをチェックするので、三角形の各頂点の値はR(x=0.64, y= 0.33) G(x=0.3, y= 0.6) B(x=0.15, y= 0.06)とし、アルベドから得たxyY座標が中に入っているかを判定します。
結果をシーンカラーに掛けた値とシーンカラーそのままの値を1秒おきに交互に表示することで、色域外の場所だけが1秒おきに黒く点滅する仕組みです。

## 実際に動かしてみる。
作ったものを動かしてみましょう。
まずは、作ったものが正しく動いているか確認する必要があります。
###正しく判定できているかテストするには
テスト用のサーフェースマテリアルを作ってみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/0eaebe99-081a-d9d2-63fa-20a93d79d510.png)

これは、xyYから709のRGBに変換する関数を使って、メッシュのUVからRGBを出力するサーフェースマテリアルです。
これなら範囲外の色を意図して作れるため、検出結果もわかりやすいとおもいました。もし正しく検出できていれば色度図の709の三角形と同じ形が浮かび上がるはずです。

マテリアルの中身は以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/27e97812-74cb-9467-8798-f9ec807a2f14.png)

カスタムノードであるxyYToRec709は同名の関数が、以下のリンクにあるDirectX12のHDRサンプルの中にあるので、それを参考にしました。
https://github.com/microsoft/DirectX-Graphics-Samples/blob/master/Samples/Desktop/D3D12HDR/src/color.hlsli

	float Y = 1;
	// https://github.com/ampas/aces-dev/blob/v1.0.3/transforms/ctl/README-MATRIX.md
	static const float3x3 XYZtoRGB =
	{
		3.2409699419, -1.5373831776, -0.4986107603,
		-0.9692436363, 1.8759675015, 0.0415550574,
		0.0556300797, -0.2039769589, 1.0569715142
	};
	float3 XYZ = Y * float3(xy.x / xy.y, 1.0, (1.0 - xy.x - xy.y) / xy.y);
	float3 RGB = mul(XYZtoRGB, XYZ);
	float maxChannel = max(RGB.r, max(RGB.g, RGB.b));
	return RGB / max(maxChannel, 1.0);

### 初めての実行
出来上がったマテリアルをアサインしたキューブを、チェック用ポストエフェクト越しに見ると以下のような結果になりました。それっぽく検出されている所もありますが、本来検出されていないといけない場所が漏れている所があり、惜しい感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/918152e5-929c-0b51-1441-7803b6ad5250.png)
### 誤差対応をする
原因はおそらくエッジ上の判定の誤差だと思うので、チェックする三角形を白色点方向に0.01%だけ縮めてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/555cbcba-5072-d47b-7d76-a9c97215a792.png)
カスタムノードの中身は以下の通りです。今回白色点は固定で中に書いていますが、引数で渡せるようになると汎用性が上がるでしょう

	float2 wp = float2(0.3127f, 0.329f);
	outP2 = ((P2 - wp) * scale) + wp;
	outP3= ((P3 - wp) * scale) + wp;
	return ((P1 - wp) * scale) + wp;

以上を適用して再度試した所、以下のように無事意図した結果になりました。
![gamutchek.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/2b6cb532-3397-df4b-56f5-7b753133422a.gif)
### 視認性を上げる
しかしながらこれでは元から暗い場所などで分かりにくいので、単色で塗りつぶすのではなく、検出した場所をアニメーションさせます。
下記のようにノードを組んで、黄色と黒の縞模様をスクロールを表現します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/42a5a92d-4f6a-1c20-cee0-ec30ec4d208e.png)
単色塗りつぶしの時より視認性がよくなりました。
![gamutout2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/3238e5af-2c88-374e-fe52-8a003d1fb8a5.gif)

## 実践的なシーンで試してみる
最後に実践的なシーンで試してみました。これまでは素材の色だけをチェックしていましたが、ライティングなどが適用された後の色もチェックするようにし、表示するパターンを変えてみました。
### 3パターンを用意
・黄色　素材の色が色域外
・シアン　ライティング後の色が色域外
・グレーの網目　両方

![gamutout4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/0bd7fa31-0866-0604-a57b-fbc654e20383.gif)
![gamutout5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502600/cec75b74-3385-55f4-7016-745baa06f950.gif)
元の色と警告色が似ていても、アニメーションのおかげでチェックに引っかかている箇所が認識できました。
## 最後に
今回作ったものだけでは本来の問題を解決できるものではありませんが、まずはチェックで引っかかった所を範囲内の無難な色を選び直していければと思います。
今後は作ったカスタムノード群を随時増やしていって、機能を充実させていきたいです。
最初にも書きましたが、まだ勉強中の分野で、理解できていないところや勘違いしている所も多いと思うので、間違っていたりしたら是非ご指摘お願します。

次はUE4でレイトレと言ったらこの人、もんしょさんのRTXブランチについての記事ですね。楽しみです。