はじめに
カラーグレーディングとは
一言で言うと
色合いを調整するポストプロセス
コントラストや彩度などのパラメータ調整をして扱うケースも多いですが
今回はシンプルに Lookup Table (LUT) (もしくはLookup Textureといったりする) を使って色補正を行う実装をしてみる
LUTとは
色を別の色に変換する3次元テーブルのプリセット
概念的に3次元テクスチャを扱う
「概念的には」という言い方をしたのは
実際には2次元テクスチャで持つ場合も多いからである
.cubeなどのフォーマットで3次元データとして扱うことも可能だが
少なくとも、Siv3DではTexture3Dを扱えないので、.pngなどの画像フォーマットで2次元データで持つ
lut.png (32 * 32, 32)

↑こんな感じで、32x32の画像を32個横にならべることで32x32x32の3次元データを表現している
2次元テクスチャのデータは、縦横(xy)の2次元座標でピクセルの色が決まるのに対して
3次元テクスチャのデータは、縦横奥(xyz)の3次元座標でピクセルの色が決まる
LUTを使った色変換について
具体的にどのように色変換を行うかというと、とても単純である。
入力色として (r, g, b) の3パラメータがある
LUTは3次元データなので、上記のrgbをLUTのuvw(xyz)座標に割り当てることで、ピクセル座標の色が決定し
それがそのまま出力色になる
シェーダーコードにすると以下のようになります
// inColorのrgbをuvwにして色をサンプリング;
float3 outColor = lut3D.Sample(g_sampler0, inColor.rgb).rgb;
LUTを作るには?
Photshopなどで作れるらしい。
サンプルのLUTはSiv3Dで作った。
Siv3D用のシェーダーを書く
Texture3Dの場合何も考えなくてもいいが
2Dでやる場合、奥行を横に並べて表現した関係で
リニアサンプリングの補間処理などを考慮すると、こまごま対応がいるのは注意
(w方向の補間対応やz座標が変わる境界など)
Texture2D g_texture0 : register(t0);
Texture2D g_lut : register(t1);
SamplerState g_sampler0 : register(s0);
namespace s3d
{
struct PSInput
{
float4 position : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
}
cbuffer PSConstants2D : register(b0)
{
float4 g_colorAdd;
float4 g_sdfParam;
float4 g_sdfOutlineColor;
float4 g_sdfShadowColor;
float4 g_internal;
}
// LUTを使ったカラーグレーディング
float3 ColorGradingLUT(float3 color)
{
// 3Dテクスチャを使える場合これだけの事
// tex3D.Sample(g_sampler0, color.rgb);
// LUTのサイズを取得
uint width, height;
g_lut.GetDimensions(width, height);
float size = height;
float lutMaxIndex = size - 1;
// w方向のスライス
float wSlice = color.b * lutMaxIndex;
// w方向にLinear補間をかけるために2ピクセルのサンプルをする
float wSlice0 = floor(wSlice);
float wSlice1 = min(wSlice0 + 1, lutMaxIndex);
float wF = wSlice - wSlice0;
// ピクセル中心で計算
float u0 = (color.r * lutMaxIndex + 0.5 + wSlice0 * size) / width;
float u1 = (color.r * lutMaxIndex + 0.5 + wSlice1 * size) / width;
float v = (color.g * lutMaxIndex + 0.5) / height;
float3 lut0 = g_lut.SampleLevel(g_sampler0, float2(u0, v), 0).rgb;
float3 lut1 = g_lut.SampleLevel(g_sampler0, float2(u1, v), 0).rgb;
// w方向にLinear補間
return lerp(lut0, lut1, wF);
}
float4 PS(s3d::PSInput input) : SV_TARGET
{
// 元の色
float4 texColor = g_texture0.Sample(g_sampler0, input.uv);
// 色補正
texColor.rgb = ColorGradingLUT(texColor.rgb);
return (texColor * input.color) + g_colorAdd;
}
C++コードの実装
# include <Siv3D.hpp>
void Main()
{
const Texture windmill{ U"example/windmill.png" };
const Texture lut{ U"lut.png" };
const PixelShader ps = HLSL{ U"color_grading.hlsl", U"PS" };
Window::Resize(Size{windmill.width() * 2, windmill.height()});
while (System::Update()) {
{
ScopedCustomShader2D scopedPs(ps);
Graphics2D::SetPSTexture(1, lut);
windmill.draw();
}
{
windmill.draw(windmill.width(), 0);
}
}
}
左:LUTで色補正後
右:元画像
色補正にわざわざLUTを使わなくてもよい
さて、LUTを使ってカラーグレーディングの色補正をしてみましたが、
そもそも色をちょっと補正するだけなら、シェーダーで直接色の補正コードを書いてしまうのも手です。
プログラムで書くには複雑な色補正をしたい場合や、プリセット化することで処理を共通化したい場合などはLUTを使うのも選択肢でしょう。
3次元テクスチャはサイズが大きくなるとそれなりにメモリも使うので注意がいります。
まとめ
- LUTを使ったカラーグレーディングをSiv3Dでやってみた
- カラーグレーディングは色味の調整するポストプロセス
- LUTは色の変換を行う3次元テーブル
- 元の色のrgbを入力値としてテーブル内のピクセルを特定して、出力色とする
- フォーマットとしては2Dテクスチャで扱う事も多い
- シェーダーでサンプリングする際はw周りに注意
- そもそも簡易的な色補正はLUTなど使わなくてよい

