Использование шейдерных эффектов в FireMonkey 2.0

В последнее время ни для кого не секрет, что все больше применяются эффекты в играх, обычных программах и различных графических интерфейсах: анимация, плавное появление, свечение и т.д. и все это реализовано при использовании шейдеров и обрабатывается на видео чипах, что по производительности на порядок превосходит обычные 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:

Полезные ссылки: