前回の記事で TEffect の実体は TFilter のラッパーのような役目であることを紹介しました。
実際に効果を付与しているのは Filter です。
そこで、今回は Filter そのものの作り方を紹介します。
とはいえ解説が煩雑になったので、ソースを見た方が良いかもしれません。
まず、Filter で使われている仕組みについて解説します。
シェーダー
TFilter は、画像に対してさまざまな視覚効果(フィルター)を適用するための仕組みですが、その内部ではシェーダーと呼ばれる GPU プログラムが使われています。
したがって、TFilter を理解・活用するためには、シェーダーの基本的な知識が不可欠です。
では、その「シェーダー」とは何なのでしょうか?
シェーダーとは?
GPU 上で直接実行される小さなプログラム
のことです。
主に描画処理の各段階で実行され、頂点の変換やピクセルの色計算、さらには物理演算まで、GPU の並列処理能力を活かして多様な処理を高速に行います。
GPU
GPU(Graphics Processing Unit)は、画像処理に特化した演算装置です。特に多数の処理を同時に並列実行できる能力に優れており、シェーダーを使って処理を GPU に任せることで、CPU による逐次実行と比べてはるかに高速に処理できます。
たとえば、4K 画像(3840×2160)のピクセル数は 8,294,400 にのぼり、各ピクセルには ARGB の 32bit(4バイト) の色情報が格納されています。このすべてのピクセルに対して色変換やフィルタ処理を行うと、単純に 829 万回以上の繰り返し処理が必要になります。
このような処理を CPU で直列に行う場合、各ピクセルの処理にたとえ 100ナノ秒しかかからなくても、全体で 約0.8秒かかってしまいます(100ns × 8,294,400 ≒ 0.829秒)。
一方、GPU は数千個ものコアを備えており、これらのピクセル処理を同時に分散実行できます。たとえば 2048 コアの GPU を使えば、理論上は 400 倍近い高速化が見込め、数ミリ秒程度で完了することもあります(実際の速度はメモリ転送やアルゴリズムによって異なります)。
このように、画像や映像のような大量のデータを一括処理する場面では、GPU の並列性能が圧倒的な威力を発揮します。
シェーダーの種類
シェーダーには様々な種類があります。
ここでは代表的なシェーダーを紹介します。
シェーダー名 | 役割 | 解説 |
---|---|---|
Vertex Shader | 頂点の位置を計算 | モデルの各頂点をワールド座標やスクリーン座標に変換します。頂点カラーやテクスチャ座標もここで計算されます。 |
Pixel Shader | ピクセルの色を計算 | 画面に表示される最終的な色を決定します。光や影、テクスチャなどを使って計算します。 |
Geometry Shader | 図形の追加・変形 | 三角形や線などの図形(プリミティブ)を追加・変形できます。必要に応じて新しい頂点を生成できます。 |
Compute Shader | 汎用的な並列計算 | 描画とは無関係に、物理演算やAIなどの大量データをGPUで高速に処理します。 |
Tessellation Shader | 曲面の細分化 | モデルの表面を滑らかにするために、ポリゴンを細かく分割して高精度に描画します。 |
Delphi の Filter システム
Delphi の TFilter システムでは、以下のシェーダーが作れます。
- Vertex Shader 頂点の位置を計算するシェーダー
- Pixel Shader ピクセルの色を計算するシェーダー
Vertex Shader を使うフィルターは、アフィン変換などの変形・回転・拡大縮小といった幾何的な処理に用いられます。
一方、前回紹介したガウシアンブラーのように色や輝度に変化を加えるフィルターは、Pixel Shader が担っています。
Pixel Shader は、Fragment Shader とも言われ、ライブラリによって呼称が異なります。
この記事では「Pixel Shader」で統一します。
Shader Language
さて、Pixel Shader を自作したいと考えたとき、「そもそもどうやって作るのか?」という疑問が湧いてくるかもしれません。
すでに説明した通り、シェーダーとは GPU 上で実行されるプログラムです。つまり、GPU が理解できる形式(バイナリ)に変換する必要があります。
しかし、バイナリコードを人間が直接書くのは現実的ではありません。これはちょうど、CPU の機械語を人間が直接書かず、高水準言語(C や Object Pascal など)を使うのと同じです。
GPU にも同様に、人間が記述できるプログラミング言語が用意されており、これを Shader Language (シェーダー言語) と呼びます。
プログラミングに様々な言語(C、Object Pascal, Python など)があるように、Shader Language にも複数の種類があります。
どの Shader Language を使うかは、主に利用する グラフィックス API によって決まります。
以下の表は、主要なグラフィックス API と、それぞれが使用する Sharer Language の対応を示したものです。
API | Shader Language | 備考 |
---|---|---|
DirectX | HLSL(High-Level Shader Language) | Microsoft が開発した Direct3D 用のシェーダー言語 |
OpenGL / OpenGL ES | GLSL(OpenGL Shading Language) | GLSL ES はモバイル向けの簡易版 |
Metal | Metal Shading Language | Apple 独自の GPU 言語 |
Skia | SkSL(Skia Shading Language) | Skia 専用のシェーダー言語 |
Vulkan | SPIR-V(バイナリ中間表現) | GLSL や HLSL から変換して使う中間バイトコード形式。直接記述も可能 |
それぞれの API によって使うシェーダー言語は異なりますが、どれも GPU の並列演算能力を最大限に活用するための手段である点は共通しています。
Vulkan は他の API と異なり、GLSL や HLSL を中間言語(SPIR-V)にコンパイルしてから使用します。
SPIR-V 自体は低レベルなバイナリ形式ですが、GLSL や HLSL を記述して glslang や DXC で SPIR-V に変換するのが一般的です。
Vulkan は今回使いませんが、 Skia を利用したアプリでは UseGlobalVulkan フラグで Vulkan の使用を強制できます。
さて、ここからが本題です。
この記事ではピクセルの色を変化させる TColorTransformFilter を作成します。
TColorTransformFilter は、行列(マトリクス)を各ピクセルの色(ARGB形式の float4)に掛け合わせて色を変換するフィルターです。
色変換行列の設定には SetMatrix メソッドを使用します。
また、ColorSpaceMode プロパティを使うことで、色空間として RGB(sRGB)または Linear(線形)を選択できます。
Linear 色空間:人間の視覚に近い色再現が可能で、見た目の調整に適しています。
RGB 色空間:計算の正確性が求められる場面(たとえばエンコードや分析)に適しています。
このフィルターの実装は、以下の4つのステップで進めます:
- Pixel Shader の作成
- TColorTransformFilter クラスの実装
- TColorTransformEffect クラスの実装
- FireMonkey へのフィルター登録
なお今回は、色の変換のみを目的としているため、Vertex Shader はデフォルトのものを使用し、Pixel Shader のみを自作します。
デフォルトの Vertex Shader は、頂点情報に対して何も変更を加えないシェーダーです。
1. Pixel Shader の作成
Delphi は、Windows / macOS / iOS / Android / Linux に対応しているのはご存知だと思います。
各プラットフォームは使っているグラフィックス API が違うため必然的に、HLSL / MSL / GLSL / SKSL の4つのシェーダーを書く必要があります。
さらに DirectX 9 系をサポートする場合は 9 系の HLSL も書く必要があります。
具体的なコードは長大になるのでソースでご覧頂くとして、Delphi TFilter で利用する際の注意点です。
シェーダー言語 | 対応プラットフォーム | 提供形式 | Delphi における注意点 |
---|---|---|---|
HLSL | Windows(DirectX)用 | バイナリ | Delphi では コンパイル済みの .fxo バイナリをバイト配列として埋め込んで使用します。 |
GLSL | Android(OpenGL ES)用 | テキスト | Delphi の仕様により、uniform 変数名には 先頭にアンダースコア(例:_color ) が必要です。 |
MSL | macOS / iOS(Metal)用 | テキスト | macOS / iOS 向けに使用される Metal 専用のシェーダー言語です。 |
SkSL | Skia 使用時(各プラットフォーム) | テキスト | Skia を有効化した場合は必ず SkSL が使用されます。 |
HLSL 以外は実行時にコンパイルされて利用されるのですが、HLSL だけは事前にコンパイルし、そのバイナリを渡す必要があります。
また、Delphi の仕様として、GLSL だけ後述の uniform 変数の先頭にアンダースコアが必要です。
GLSL側にアンダースコア
呼び出し側はアンダースコア無し
TContextShaderVariable.Create('foo', TContextShaderVariableKind.Float, 0, 0),
特殊な uniform 変数
GPU と CPU の間で値を受け渡すために使用されるのが uniform 変数です。これは、描画処理中に 全ピクセルや全頂点で共通の値を保持するために使われます。
値を受け渡す変数を表す「uniform」という用語は、Shader Language ごとに異なります(例:HLSL では cbuffer)。
この記事では「uniform」で統一します。
■ Input
すべてのシェーダー言語で共通して使用される uniform 変数です。
Input は描画対象となるテクスチャ(画像データ)を表しており、フィルター処理の対象となる画像を GPU に渡すために使われます。
前の記事で ValusAsBitmap['Input']
で指定した画像の事です。
■ Resolution と InputResolution
これらは SkSL 専用の uniform 変数です。
- Resolution: 出力先のサイズ
- InputResolution: 入力画像のサイズ
これらの特殊な uniform 変数は自動的に値がセットされるため、何もせず登録コードを書くだけで大丈夫です。
TColorTransformFilter 独自の uniform 変数
TColorTransformFilter では、色変換に必要な以下の uniform 変数を独自に定義・使用します。
■ ColorMatrix
色変換行列を渡すための uniform 変数です。
4×4 の float4 配列として渡しますが、実際に使用されるのは上位 3×3 の成分のみです。
ではなぜ 4×4 なのかというと、シェーダーによってメモリアラインメントが 4 の倍数であることが求められるためです。
異なるプラットフォームでも同一コードで扱えるよう、汎用性を確保する目的で 4×4 を使用しています。
■ ColorSpaceMode
使用する色空間を指定するための変数です。
現在の実装では以下の 2種類がサポートされています:
- RGB(通常の RGB 色空間)
- Linear(線形色空間:視覚的に自然な色表現が可能)
用途に応じて切り替えることで、視覚的な演出と正確な色処理を両立できます。
uniform 変数の登録
これらの uniform 変数は TFilter に登録する必要があります。
一般的に uniform 変数はコンストラクタで登録します。
後述のコンストラクタのコードをご覧ください。
具体的には次のようなコードをコンストラクタで呼びだします。
function GetContextShaderSource(
const AArch: TContextShaderArch;
const AShader: TBytes): TContextShaderSource;
begin
var Variables: TArrayTContextShaderVariable> := [
TContextShaderVariable.Create(
'Input',
TContextShaderVariableKind.Texture,
0,
0)
];
if AArch = TContextShaderArch.SKSL then
begin
Variables := Variables + [
TContextShaderVariable.Create(
'Resolution',
TContextShaderVariableKind.Float2,
0,
8),
TContextShaderVariable.Create(
'InputResolution',
TContextShaderVariableKind.Float2,
1,
8)
];
end;
Variables := Variables + [
TContextShaderVariable.Create(
'ColorMatrix',
TContextShaderVariableKind.Matrix,
COLORMATRIX_INDEXES[AArch],
COLORMATRIX_SIZES[AArch]),
TContextShaderVariable.Create(
'ColorSpaceMode',
TContextShaderVariableKind.Vector,
COLORSPACE_INDEXES[AArch],
COLORSPACE_SIZES[AArch])
];
Result := TContextShaderSource.Create(AArch, AShader, Variables);
end;
TContextShaderSource を使って Filter 情報を登録します。
第1引数:対象となるシェーダーアーキテクチャ(HLSL / GLSL / MSL / SKSLなど)
第2引数:バイト配列化されたシェーダーコード
※テキスト形式のシェーダー(GLSL, MSL, SkSL)の場合は、TEncoding.UTF8.GetBytes を使ってバイト配列に変換して渡します
第3引数:TContextShaderVariable の配列(uniform 変数の情報)
TContextShaderVariable は変数の情報を表します。
以下の様な引数を取ります。
TContextShaderVariable.Create(
変数の名前,
変数の種類,
変数の開始アドレス,
変数のサイズ,
ここで、開始アドレスとサイズは、各シェーダーで異なるため定数として定義しています。
HLSL
// インデックスの始まりは0
DX11_COLORMATRIX_INDEX = 0;
DX11_COLORMATRIX_SIZE = 64; // 占有バイト数
// 次のインデックスは前の変数のサイズ分進んだ所から
DX11_COLORSPACE_INDEX = 64;
DX11_COLORSPACE_SIZE = 4;
METAL
// インデックスの始まりは0
METAL_COLORMATRIX_INDEX = 0;
METAL_COLORMATRIX_SIZE = 1;
// 次のインデックスの始まりは 1
METAL_COLORSPACE_INDEX = 1;
METAL_COLORSPACE_SIZE = 1;
// Size は、使う場合は 1 、無効な場合は 0
GLSL
// Filter 内部で自動設定されるので何でも良い
OPENGL_COLORMATRIX_INDEX = 0;
OPENGL_COLORMATRIX_SIZE = 0;
OPENGL_COLORSPACE_INDEX = 0;
OPENGL_COLORSPACE_SIZE = 0;
SKSL
// インデックスは連番
// Resolution と InputResolution 分インデックスが進んで 2 になる
SKIA_COLORMATRIX_INDEX = 2;
SKIA_COLORMATRIX_SIZE = 4; // 変数の数 (配列として考えた場合の要素数)
SKIA_COLORSPACE_INDEX = 3;
SKIA_COLORSPACE_SIZE = 1;
2. TColorTanshformFilter クラスの作成
これらを踏まえた上で TColorTransformFilter の実装です。
クラス定義
type
TColorSpaceMode = (sRGB, Linear);
TColorTransformFilter = class(TFilter)
private var
FColorSpaceMode: TColorSpaceMode;
FMatrix: TMatrix;
protected
procedure LoadShaders; override;
// Matrix の内容を一括設定
procedure SetMatrix(
const
Am11, Am12, Am13,
Am21, Am22, Am23,
Am31, Am32, Am33: Single
); overload; virtual;
procedure SetMatrix(const AMatrix: TMatrix); overload; virtual;
public
class function FilterAttr: TFilterRec; override;
constructor Create; override;
// Matrix
property Matrix: TMatrix read FMatrix;
published
// 色空間を選択
property ColorSpaceMode: TColorSpaceMode
read FColorSpaceMode write FColorSpaceMode default TColorSpaceMode.Linear;
end;
ここで、重要なメソッドを解説します。
Create コンストラクタ
通常の Delphi のクラスと同じで Create で初期設定をします。
TShaderManager.RegisterShaderFromData メソッドを使って Shader Language と uniform 変数を登録しています。
constructor TColorTransformFilter.Create;
{ 中略 }
begin
inherited;
FColorSpaceMode := TColorSpaceMode.Linear;
// FShaders は array of TContextShader として定義されています。
// TFilter.Create の中で SetLength(FShaders, 10) としてデフォルトで 10 個分確保されています。
// この仕組みによって、1つのフィルター内で複数のシェーダーを利用できます。
// 今回は1つしか使用しないので 0 番目にシェーダーを登録しています。
FShaders[0] :=
TShaderManager.RegisterShaderFromData(
// ファイル名。値は何でもよいですが、一部のシェーダーはこのファイル名で保存してからコンパイルされます
'colortrans.fps',
// シェーダーの種類。今回は PixelShader です
TContextShaderKind.PixelShader,
// Shader のオリジナルファイル名。特に使われません。
'',
// 各シェーダーの配列
// SKSL, DX9, DX11L9, METAL, GLSL は各シェーダーのテキスト表現・バイナリ表現です
[
GetContextShaderSourceStr(TContextShaderArch.SKSL, SKSL),
GetContextShaderSource(TContextShaderArch.DX9, DX9),
GetContextShaderSource(TContextShaderArch.DX11_level_9, DX11L9),
GetContextShaderSourceStr(TContextShaderArch.Metal, METAL),
GetContextShaderSourceStr(TContextShaderArch.GLSL, GLSL)
]
);
end;
LoadShaders メソッド
Shader がロードされた時に呼ばれるメソッドです。
ここで FilterContext プロパティの SetSharedVariable メソッドを使って uniform 変数に値を設定します。
procedure TColorTransformFilter.LoadShaders;
begin
// デフォルトの Vertex Shader と Constructor で設定した Pixel Shader を設定
FilterContext.SetShaders(FVertexShader, FShaders[0]);
// uniform 変数の値をセット
FilterContext.SetShaderVariable(
'ColorMatrix',
[
Vector3D(FMatrix.m11, FMatrix.m21, FMatrix.m31, 0),
Vector3D(FMatrix.m12, FMatrix.m22, FMatrix.m32, 0),
Vector3D(FMatrix.m13, FMatrix.m23, FMatrix.m33, 0),
Vector3D(0, 0, 0, 0)
]
);
FilterContext.SetShaderVariable(
'ColorSpaceMode',
[Vector3D(Ord(FColorSpaceMode), 0, 0, 0)]
);
end;
FilterAttr メソッド
ここでは、TFilterRec を使って、フィルターの情報を返します。
TFilterRec の引数は下記の通りです。
第1引数:名前
第2引数:説明
第3引数:前の記事で解説した ValueAsXXXX で設定できる変数の配列です
第3引数が設定できると、自動的に uniform 変数の内容に適用されるので非常に便利なのですが、float4 (double 相当) や行列を指定できないため、今回は使用していません。
class function TColorTransformFilter.FilterAttr: TFilterRec;
begin
Result := TFilterRec.Create('ColorTransform', 'Transform colors', []);
end;
参考までにガウシアンブラーの設定値はこうなっています。
第3引数に ‘BlurAmount’ を指定しています。
class function TGaussianBlurFilter.FilterAttr: TFilterRec;
begin
Result :=
TFilterRec.Create(
'GaussianBlur',
'An effect that GaussianBlurs.',
[
// 値の情報を返す構造体
TFilterValueRec.Create(
// 変数名
'BlurAmount',
// 説明
'The GaussianBlur factor.',
// 型
TFilterValueType.Float,
1, // デフォルト値
0.01, // 最小値
10) // 最大値
]
);
end;
3. TColorTransformEffect クラスの作成
コントロールにフィルターを簡単に設定するために Effect も生成しておくのが良いでしょう。
type
TColorTransformEffect = class(TFilterEffect)
private
FMatrix: TMatrix;
FColorSpaceMode: TColorSpaceMode;
protected
function CreateFilter: TFilter; override;
public
constructor Create(AOwner: TComponent); override;
published
property Matrix: TMatrix read FMatrix write FMatrix;
property ColorSpaceMode: TColorSpaceMode
read FColorSpaceMode write FColorSpaceMode default TColorSpaceMode.Linear;
property Enabled;
end;
ここで重要なメソッドは CreateFilter です。
// フィルタが必要になったとき呼ばれます。
// フィルタを生成して返します。
function TColorTransformEffect.CreateFilter: TFilter;
var
CTF: TColorTransformFilter absolute Result;
begin
CTF := TColorTransformFilter(TFilterManager.FilterByName('ColorTransform'));
// 必要なパラメータを設定
CTF.SetMatrix(FMatrix);
CTF.ColorSpaceMode := FColorSpaceMode;
end;
4. FireMonkey へのフィルタの登録
最後に TFilterManager.RegisterFilter を使って作成したフィルタを登録します。
第1引数:グループ名。何でも大丈夫ですが、他の色を使用するフィルタと合せて Color としました。
第2引数:作成したクラスを指定します。
initialization
GlobalUseMetal := True; // macOS / iOS では、Metal を使うように指示
TFilterManager.RegisterFilter('Color', TColorTransformFilter);
全ソース
TColorTransformFilter の全ソースです。
非常に長いので、gist に置きました。
説明できていない各 Shader Language 等はソースを参考にしてください。
PK.Graphic.Filter.ColorTransform.pas
左が元画像で、右がフィルタ適用後の画像です。
マトリクスの m11 を 0 にして、赤成分を 0 にしてみました。
フィルタの適用
procedure TForm1.Button1Click(Sender: TObject);
begin
var F := TColorTransformFilter.Create;
try
F.ColorSpaceMode := TColorSpaceMode.Linear;
F.SetMatrix(
m11.Text.ToSingle, m12.Text.ToSingle, m13.Text.ToSingle,
m21.Text.ToSingle, m22.Text.ToSingle, m23.Text.ToSingle,
m31.Text.ToSingle, m32.Text.ToSingle, m33.Text.ToSingle
);
F.ValuesAsBitmap['Input'] := Image1.Bitmap;
F.ApplyWithoutCopyToOutput;
F.FilterContext.CopyToBitmap(Image2.Bitmap, Image2.Bitmap.Bounds);
finally
F.Free;
end;
end;
フィルターを作成するためには各種シェーダーを書けないといけないためハードルが高いです。
ですが、AI に聞きながらやればできなくはないです!
実際、今回 Metal や SkSL については聞きながら作成しました。
興味があったらチャレンジしてみてください!
HLSL のソースです。
GLSL / MSL / SKSL はソースコードに載っています。
DirectX 11 用
Texture2D inputTex : register(t0);
SamplerState inputSampler : register(s0);
cbuffer ColorTransformBuffer : register(b0)
{
float4x4 ColorMatrix;
float ColorSpaceMode; // 0 = sRGB, 1 = LinearRGB
};
struct PSInput
{
float2 TexCoord : TEXCOORD0;
};
// sRGB ⇔ LinearRGB 変換
float3 SRGBToLinear(float3 c)
{
float3 low = c / 12.92;
float3 high = pow((c + 0.055) / 1.055, 2.4);
return lerp(low, high, step(0.04045, c));
}
// LinearRGB ⇔ sRGB 変換
float3 LinearToSRGB(float3 c)
{
float3 low = c * 12.92;
float3 high = 1.055 * pow(c, 1.0 / 2.4) - 0.055;
return lerp(low, high, step(0.0031308, c));
}
// Pixel Shader 本体
float4 main(PSInput IN) : SV_Target
{
float4 color = inputTex.Sample(inputSampler, IN.TexCoord);
float3 rgb = color.rgb;
if (ColorSpaceMode == 1)
{
rgb = SRGBToLinear(rgb);
rgb = mul((float3x3)ColorMatrix, rgb);
rgb = LinearToSRGB(rgb);
}
else
{
rgb = mul((float3x3)ColorMatrix, rgb);
}
return float4(rgb, color.a);
}
DirectX 9 用
sampler2D inputTex : register(s0);
float4x4 ColorMatrix : register(c0); // c0-c3
float ColorSpaceMode : register(c4); // c4.x
struct PSInput
{
float2 TexCoord : TEXCOORD0;
};
struct PSOutput
{
float4 Color : COLOR0;
};
float3 SRGBToLinear(float3 c)
{
float3 low = c / 12.92;
float3 high = pow((c + 0.055) / 1.055, 2.4);
return lerp(low, high, step(0.04045, c));
}
float3 LinearToSRGB(float3 c)
{
float3 low = c * 12.92;
float3 high = 1.055 * pow(c, 1.0 / 2.4) - 0.055;
return lerp(low, high, step(0.0031308, c));
}
PSOutput main(PSInput IN)
{
PSOutput result;
float4 color = tex2D(inputTex, IN.TexCoord);
float3 rgb = color.rgb;
float3x3 mat3 = float3x3(
ColorMatrix._11, ColorMatrix._12, ColorMatrix._13,
ColorMatrix._21, ColorMatrix._22, ColorMatrix._23,
ColorMatrix._31, ColorMatrix._32, ColorMatrix._33
);
if (ColorSpaceMode == 1.0)
{
rgb = SRGBToLinear(rgb);
rgb = mul(mat3, rgb);
rgb = LinearToSRGB(rgb);
}
else
{
rgb = mul(mat3, rgb);
}
result.Color = float4(rgb, color.a);
return result;
}
Views: 0