Pull to refresh

Использование шейдерных эффектов в Delphi XE3 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:

Полезные ссылки:
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.