
В последнее время ни для кого не секрет, что все больше применяются эффекты в играх, обычных программах и различных графических интерфейсах: анимация, плавное появление, свечение и т.д. и все это реализовано при использовании шейдеров и обрабатывается на видео чипах, что по производительности на порядок превосходит обычные CPU. Разберемся как это реализовано в FMX 2.0
Написание шейдерных эффектов в FireMonkey 2.0, представляет собой фильтр — это класс, в котором создается, храниться и регистрируется сам шейдер. Фильтр использует уже скомпилированный байт код шейдера и записывается как массив данных.
Для создания фильтра нам потребуется создать сам шейдер, будем реализовывать его на C-подобном языке высокого уровня HLSL с использованием программы AMD RenderMonkey, она предоставляет все нужные инструменты для работы с шейдерными эффектами. Программа проста в использовании даже начинающему, но для профессионалов лучше конечно использовать Nvidia Fx-Composer – это более продвинутая IDE для разработки и отладки шейдеров.
Реализация шейдера в программе RenderMonkey
И так приступим к разработке самого шейдерного эффекта.
Создадим новый проект и добавим в него стандартный шаблон DirectX эффекта Screen-AlignedQuad

Добавим uniform переменные: time(Float), radius(Float2), clr1(Color), clr2(Color)

У переменной time выставим Time0_X, т.е. в переменную будут передаваться значения времени

Вершинный шейдер оставим без изменений, т.к. нам он не потребуется.
В пиксельном укажем настройки: Target: ps_2_a и Entry Point: main
После этого отредактируем пиксельный шейдер и для примера запишем в него следующий код:
uniform float time: register(c0);
uniform float2 radius: register(c1);
uniform float4 clr1: register(c2);
uniform float4 clr2: register(c3);
float4 main(float2 uv: TEXCOORD0): COLOR
{
//the centre point for each blob
float2 move1;
move1.x = cos(time)*0.4;
move1.y = sin(time*1.5)*0.8;
float2 move2;
move2.x = cos(time*2.0)*0.4;
move2.y = sin(time*3.0)*0.4;
float2 p = (uv-0.5)*2;
//radius for each blob
float r1 =(dot(p+move1,p+move1))*8.0;
float r2 =(dot(p+move2,p+move2))*16.0;
float r3 =(dot(p-(radius-0.5)*2,p+(radius-0.5)*2))*10.0;
//sum the meatballs
float metaball =(1.0/r1+1.0/r2);
//alter the cut-off power
float col = pow(metaball,8.0);
//set the output color
float4 res = float4(clr1.r,clr1.g-col,clr1.b,clr1.a);
col = pow(metaball+1.0/r3,2.0);
res -= float4(clr2.r,clr2.g-col,clr2.b-col,clr2.a);
return res;
}
Если все сделано правильно, то компилируем шейдер нажатием F6 или кнопкой на панели меню

В окне просмотра будет примерно следующее

Если все работает, то сохраняем эффект в файл HLSL

После всего этого нам потребуется скомпилировать наш эффект через утилиту fxc (effect-compiler) от Microsoft, более подробно с описанием команд можно ознакомиться тут
Компиляция шейдера происходит командой: fxc /T ps_2_a /E main /Fo SampleShader.fxo SampleShader.hlsl
Теперь наш шейдер скомпилирован, приступим к написанию фильтра в среде DelphiXE3
Создание фильтра и эффекта в Delphi FireMonkey 2.0
Для создания фильтра добавим в uses следующие модули: FMX.Filter, FMX.Types3D
Создадим новый класс наследуемый от TFilter, структура и реализация будет следующая
Исходный код
TSampleFilter = class(TFilter)
public
constructor Create; override;
class function FilterAttr: TFilterRec; override;
end;
{ TSampleFilter }
constructor TSampleFilter.Create;
begin
inherited;
FShaders[0] := TShaderManager.RegisterShaderFromData('SampleFilter.fps', TContextShaderKind.skPixelShader,'',[
TContextShaderSource.Create(TContextShaderArch.saDX9, [
$01,$02,$FF,$FF,$FE,$FF,$3D,$00,$43,$54,$41,$42,$1C,$00,$00,$00,$BF,$00,$00,$00,
$01,$02,$FF,$FF,$04,$00,$00,$00,$1C,$00,$00,$00,$00,$01,$00,$00,$B8,$00,$00,$00,
$6C,$00,$00,$00,$02,$00,$02,$00,$01,$00,$00,$00,$74,$00,$00,$00,$00,$00,$00,$00,
$84,$00,$00,$00,$02,$00,$03,$00,$01,$00,$00,$00,$74,$00,$00,$00,$00,$00,$00,$00,
$89,$00,$00,$00,$02,$00,$01,$00,$01,$00,$00,$00,$90,$00,$00,$00,$00,$00,$00,$00,
$A0,$00,$00,$00,$02,$00,$00,$00,$01,$00,$00,$00,$A8,$00,$00,$00,$00,$00,$00,$00,
$63,$6C,$72,$31,$00,$AB,$AB,$AB,$01,$00,$03,$00,$01,$00,$04,$00,$01,$00,$00,$00,
$00,$00,$00,$00,$63,$6C,$72,$32,$00,$72,$61,$64,$69,$75,$73,$00,$01,$00,$03,$00,
$01,$00,$02,$00,$01,$00,$00,$00,$00,$00,$00,$00,$74,$69,$6D,$65,$00,$AB,$AB,$AB,
$00,$00,$03,$00,$01,$00,$01,$00,$01,$00,$00,$00,$00,$00,$00,$00,$70,$73,$5F,$32,
$5F,$61,$00,$4D,$69,$63,$72,$6F,$73,$6F,$66,$74,$20,$28,$52,$29,$20,$48,$4C,$53,
$4C,$20,$53,$68,$61,$64,$65,$72,$20,$43,$6F,$6D,$70,$69,$6C,$65,$72,$20,$39,$2E,
$32,$37,$2E,$39,$35,$32,$2E,$33,$30,$32,$32,$00,$51,$00,$00,$05,$04,$00,$0F,$A0,
$83,$F9,$22,$3E,$00,$00,$00,$3F,$DB,$0F,$C9,$40,$DB,$0F,$49,$C0,$51,$00,$00,$05,
$05,$00,$0F,$A0,$CD,$CC,$CC,$3E,$CD,$CC,$4C,$3F,$00,$00,$00,$40,$00,$00,$00,$00,
$51,$00,$00,$05,$06,$00,$0F,$A0,$00,$00,$00,$41,$00,$00,$80,$41,$00,$00,$20,$41,
$00,$00,$00,$00,$51,$00,$00,$05,$07,$00,$0F,$A0,$45,$76,$74,$3E,$83,$F9,$A2,$3E,
$45,$76,$F4,$3E,$00,$00,$00,$3F,$51,$00,$00,$05,$08,$00,$0F,$A0,$01,$0D,$D0,$B5,
$61,$0B,$B6,$B7,$AB,$AA,$2A,$3B,$89,$88,$88,$39,$51,$00,$00,$05,$09,$00,$0F,$A0,
$AB,$AA,$AA,$BC,$00,$00,$00,$BE,$00,$00,$80,$3F,$00,$00,$00,$3F,$1F,$00,$00,$02,
$00,$00,$00,$80,$00,$00,$03,$B0,$01,$00,$00,$02,$00,$00,$03,$80,$04,$00,$E4,$A0,
$04,$00,$00,$04,$00,$00,$01,$80,$00,$00,$00,$A0,$00,$00,$00,$80,$00,$00,$55,$80,
$13,$00,$00,$02,$00,$00,$01,$80,$00,$00,$00,$80,$04,$00,$00,$04,$00,$00,$01,$80,
$00,$00,$00,$80,$04,$00,$AA,$A0,$04,$00,$FF,$A0,$25,$00,$00,$04,$01,$00,$01,$80,
$00,$00,$00,$80,$08,$00,$E4,$A0,$09,$00,$E4,$A0,$05,$00,$00,$03,$01,$00,$01,$80,
$01,$00,$00,$80,$05,$00,$00,$A0,$01,$00,$00,$02,$02,$00,$0F,$80,$07,$00,$E4,$A0,
$04,$00,$00,$04,$00,$00,$0D,$80,$00,$00,$00,$A0,$02,$00,$94,$80,$02,$00,$FF,$80,
$13,$00,$00,$02,$00,$00,$0D,$80,$00,$00,$E4,$80,$04,$00,$00,$04,$00,$00,$0D,$80,
$00,$00,$E4,$80,$04,$00,$AA,$A0,$04,$00,$FF,$A0,$25,$00,$00,$04,$02,$00,$02,$80,
$00,$00,$00,$80,$08,$00,$E4,$A0,$09,$00,$E4,$A0,$05,$00,$00,$03,$01,$00,$02,$80,
$02,$00,$55,$80,$05,$00,$55,$A0,$02,$00,$00,$03,$01,$00,$0C,$80,$00,$00,$44,$B0,
$04,$00,$55,$A1,$04,$00,$00,$04,$01,$00,$03,$80,$01,$00,$EE,$80,$05,$00,$AA,$A0,
$01,$00,$E4,$80,$5A,$00,$00,$04,$00,$00,$01,$80,$01,$00,$E4,$80,$01,$00,$E4,$80,
$05,$00,$FF,$A0,$05,$00,$00,$03,$00,$00,$01,$80,$00,$00,$00,$80,$06,$00,$00,$A0,
$06,$00,$00,$02,$00,$00,$01,$80,$00,$00,$00,$80,$25,$00,$00,$04,$02,$00,$01,$80,
$00,$00,$AA,$80,$08,$00,$E4,$A0,$09,$00,$E4,$A0,$25,$00,$00,$04,$03,$00,$02,$80,
$00,$00,$FF,$80,$08,$00,$E4,$A0,$09,$00,$E4,$A0,$05,$00,$00,$03,$01,$00,$02,$80,
$03,$00,$55,$80,$05,$00,$00,$A0,$05,$00,$00,$03,$01,$00,$01,$80,$02,$00,$00,$80,
$05,$00,$00,$A0,$04,$00,$00,$04,$00,$00,$0C,$80,$01,$00,$E4,$80,$05,$00,$AA,$A0,
$01,$00,$44,$80,$5A,$00,$00,$04,$00,$00,$04,$80,$00,$00,$EE,$80,$00,$00,$EE,$80,
$05,$00,$FF,$A0,$05,$00,$00,$03,$00,$00,$04,$80,$00,$00,$AA,$80,$06,$00,$55,$A0,
$06,$00,$00,$02,$00,$00,$04,$80,$00,$00,$AA,$80,$02,$00,$00,$03,$00,$00,$01,$80,
$00,$00,$AA,$80,$00,$00,$00,$80,$02,$00,$00,$03,$00,$00,$06,$80,$00,$00,$55,$81,
$01,$00,$D0,$A0,$02,$00,$00,$03,$00,$00,$06,$80,$00,$00,$E4,$80,$00,$00,$E4,$80,
$04,$00,$00,$04,$01,$00,$03,$80,$01,$00,$EE,$80,$05,$00,$AA,$A0,$00,$00,$E9,$81,
$04,$00,$00,$04,$00,$00,$06,$80,$01,$00,$F8,$80,$05,$00,$AA,$A0,$00,$00,$E4,$80,
$5A,$00,$00,$04,$00,$00,$02,$80,$01,$00,$E4,$80,$00,$00,$E9,$80,$05,$00,$FF,$A0,
$05,$00,$00,$03,$00,$00,$02,$80,$00,$00,$55,$80,$06,$00,$AA,$A0,$06,$00,$00,$02,
$00,$00,$02,$80,$00,$00,$55,$80,$02,$00,$00,$03,$00,$00,$02,$80,$00,$00,$55,$80,
$00,$00,$00,$80,$05,$00,$00,$03,$00,$00,$01,$80,$00,$00,$00,$80,$00,$00,$00,$80,
$05,$00,$00,$03,$00,$00,$01,$80,$00,$00,$00,$80,$00,$00,$00,$80,$04,$00,$00,$04,
$00,$00,$01,$80,$00,$00,$00,$80,$00,$00,$00,$81,$02,$00,$55,$A0,$04,$00,$00,$04,
$00,$00,$06,$80,$00,$00,$55,$80,$00,$00,$55,$81,$03,$00,$E4,$A0,$01,$00,$00,$02,
$01,$00,$06,$80,$00,$00,$E4,$81,$02,$00,$00,$03,$00,$00,$02,$80,$00,$00,$00,$80,
$01,$00,$55,$80,$01,$00,$00,$02,$01,$00,$09,$80,$03,$00,$E4,$A1,$02,$00,$00,$03,
$00,$00,$0D,$80,$01,$00,$E4,$80,$02,$00,$E4,$A0,$01,$00,$00,$02,$00,$08,$0F,$80,
$00,$00,$E4,$80,$FF,$FF,$00,$00
],[
TContextShaderVariable.Create('Time', TContextShaderVariableKind.vkFloat,0,1),
TContextShaderVariable.Create('Radius', TContextShaderVariableKind.vkFloat2,1,1),
TContextShaderVariable.Create('Color1', TContextShaderVariableKind.vkVector,2,1),
TContextShaderVariable.Create('Color2', TContextShaderVariableKind.vkVector,3,1)
]
)]);
end;
class function TSampleFilter.FilterAttr: TFilterRec;
begin
Result := TFilterRec.Create('SampleFilter', '', [
TFilterValueRec.Create('Time', 'SetTime', 0, 0, MaxSingle),
TFilterValueRec.Create('Radius', '', TPointF.Create(0,0), TPointF.Create(0,0), TPointF.Create(65535,65535)),
TFilterValueRec.Create('Color1', 'SetColor 1', TFilterValueType.vtColor, $FFFFFFFF, 0, 0),
TFilterValueRec.Create('Color2', 'SetColor 2', TFilterValueType.vtColor, $00FFFFFF, 0, 0)
]);
end;
Для получения байт кода будем использовать следующую процедуру:
procedure FillStringsFromBinFile(const FileName: string; List: TStrings);
var
i, bytesRead: Integer;
byteArray: array [1 .. 40] of byte;
S : string;
F: TFileStream;
begin
F := TFileStream.Create(FileName, fmOpenRead);
try
List.Clear;
while F.Position <> F.Size do
begin
bytesRead := F.Read(byteArray,40);
S := '$' + IntToHex(byteArray[1],2);
for i := 2 to bytesRead do
S := S + ',$' + IntToHex(byteArray[i],2);
if F.Size - F.Position > 0 then
S := S + ',';
List.Add(S);
end;
finally
F.Free
end;
end;
Зарегистрируем фильтр в разделе инициализации
initialization
TFilterManager.RegisterFilter('SampleFilter', TSampleFilter);
После этого фильтр уже можно использовать в программе, но для этого лучше создать собственный эффект использующий этот фильтр, и передает параметры шейдеру
Создадим эффект наследуемый от TEffect
TSampleFXEffect = class(TEffect)
private
FFilter: TFilter;
FTime: Single;
FRadius: TPosition;
procedure SetTime(const AValue: Single);
procedure SetRadius(const AValue: TPosition);
function GetColor1: TAlphaColor;
procedure SetColor1(const AValue: TAlphaColor);
function GetColor2: TAlphaColor;
procedure SetColor2(const AValue: TAlphaColor);
procedure RadiusChange(Sender: TObject);
protected
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure ProcessEffect(Canvas: TCanvas; const Visual: TBitmap; const Data: single); override;
published
property Time: Single read FTime write SetTime;
property Radius: TPosition read FRadius write SetRadius;
property Color1: TAlphaColor read GetColor1 write SetColor1;
property Color2: TAlphaColor read GetColor2 write SetColor2;
property Trigger;
property Enabled;
end;
и его полная реализация
Исходный код
{ TSampleFXEffect }
constructor TSampleFXEffect.Create(AOwner: TComponent);
begin
inherited;
FFilter := TSampleFilter.Create;
FEffectStyle := [esDisablePaint];
FRadius := TPosition.Create(PointF(0,0));
FRadius.OnChange := RadiusChange;
end;
destructor TSampleFXEffect.Destroy;
begin
FreeAndNil(FFilter);
FreeAndNil(FRadius);
inherited;
end;
procedure TSampleFXEffect.ProcessEffect(Canvas: TCanvas; const Visual: TBitmap;
const Data: single);
begin
FFilter.ValuesAsBitmap['Input'] := Visual;
Visual.Assign(FFilter.ValuesAsBitmap['Output']);
end;
procedure TSampleFXEffect.RadiusChange(Sender: TObject);
begin
FFilter.ValuesAsPoint['Radius'] := FRadius.Point;
UpdateParentEffects;
end;
function TSampleFXEffect.GetColor1: TAlphaColor;
begin
if FFilter <> nil then
Result := FFilter.ValuesAsColor['Color1']
else
Result := $FFFFFF00;
end;
procedure TSampleFXEffect.SetColor1(const AValue: TAlphaColor);
begin
if FFilter.ValuesAsColor['Color1'] <> AValue then
begin
FFilter.ValuesAsColor['Color1'] := AValue;
UpdateParentEffects;
end;
end;
function TSampleFXEffect.GetColor2: TAlphaColor;
begin
if FFilter <> nil then
Result := FFilter.ValuesAsColor['Color2']
else
Result := $FFFFFF00;
end;
procedure TSampleFXEffect.SetColor2(const AValue: TAlphaColor);
begin
if FFilter.ValuesAsColor['Color2'] <> AValue then
begin
FFilter.ValuesAsColor['Color2'] := AValue;
UpdateParentEffects;
end;
end;
procedure TSampleFXEffect.SetRadius(const AValue: TPosition);
begin
FRadius.Assign(AValue);
end;
procedure TSampleFXEffect.SetTime(const AValue: Single);
begin
if FTime <> AValue then
begin
FTime := AValue;
FFilter.ValuesAsFloat['Time'] := AValue;
UpdateParentEffects;
end;
end;
И не забываем регистрировать наш эффект
initialization
TFilterManager.RegisterFilter('SampleFilter', TSampleFilter);
RegisterFmxClasses([TSampleFXEffect]);
После этого эффект можно использовать в программе с регулированием всех настроек:

И конечно же все в сборе для Delphi XE3:
- Исходный код примера и шейдера: SampleShaderSource.7z
- Скомпилированный: SampleShaderCompiled.7z
Полезные ссылки:
- Книга: Шейдеры для программистов игр и художников – в этой книге подробно описаны принципы создания Shader effects и работа в программе RenderMonkey
- Песочница GLSL эффектов: http://glsl.heroku.com/
- Библиотека готовых эффектов: NVIDIA Shader Library
- Компоненты эффектов для FireMonkey2.0