How to Create Your Own FireMonkey Image Effect

Before I start, I would like to mention that my knowledge on pixel shaders is pretty limited, but I’ll share what I have done in last few days after some research on the internet and after playing around with the FireMonkey GPU powered image effects.

The idea of this post is to explain the steps to add a new shader effect to FireMonkey and not to make a deep dive into the world of shader languages.

FireMonkey Image Processing

Delphi XE2 introduced FireMonkey, the first native CPU and GPU powered platform for rich business applications. It includes the ImageFX engine, an image processing component library that leverages programmable graphics hardware whenever possible to provide near real-time processing. This library use only hardware acceleration for image processing, it abstracts the pixel-level manipulation process required when applying a filter to an image, making it simple for applications to implement image transformation capabilities without extensive coding. Each filter specifies a single transformation or effect.
This GPU-based image processing library uses DirectX Pixel Shader effects on Windows platform, OpenGL fragment shaders on Mac OS X and OpenGL/ES fragment shaders on iOS.

To use those effects on Windows you will need a PC with a low-end video card that supports Pixel Shader 2.0. Any Mac running OS X 10.6 Snow Leopard or the latest OS X 10.7 Lion has a qualified GPU. And for iOS, any device running iOS 4.2 or later also has a qualified GPU. For more details see the FireMonkey prerequisites.

Pixel Shaders

Pixel shaders, also known as fragment shaders, compute color and other attributes of each pixel. Pixel shaders range from always outputting the same color, to applying a lighting value, to doing bump mapping, shadows, specular highlights, translucency and other phenomena. They can alter the depth of the pixel (for Z-buffering), or output more than one color if multiple render targets are active. A pixel shader alone cannot produce very complex effects, because it operates only on a single pixel, without knowledge of a scene’s geometry (Source: Wikipedia)

Pixel shader effects allow you to add effects, such as grayscale, blur, and brightness, to rendered objects. Pixel shader effects use an algorithm to alter how pixels are displayed. For example, the following image shows the FireMonkey application ShaderFilters running on Mac OS X with an image and the radial blur effect applied.

Figure 1. FireMonkey ShaderFilters – Radial Blur

HLSL, GLSL and GLSL/ES

High Level Shading Language (HLSL) is a language for DirectX developed by Microsoft that allows you to create C-like programmable shaders for the Direct3D pipeline. Its OpenGL equivalent is GLSL. OpenGL Shading Language (GLSL) is a high-level shading language based on the syntax of the C programming language. OpenGL for Embedded Systems (OpenGL ES) is a subset of the OpenGL 3D graphics application programming interface (API) designed for embedded systems such as mobile phones, PDAs, and video game consoles.

The DirectX SDK provides a compiler to compile the HLSL down into GPU assembly code. Looking at one FireMonkey filters source file (e.g. FMX.FilterCatStyle.pas), the first constant at the constructor part of each filter is the compiled assembly code of a HLSL filter/effect. The second constant is usually the GLSL code (or ARB – OpenGL Assembly Language a low-level shading language) used at OS X and the third is the GLSL/ES code used at iOS. I wrote usually, because some effects like GaussianBlur (see file FMX.FilterCatBlur.pas) use multiple passes (2) and each pass has a different code.

Shader programs come in three forms: vertex shaders, geometry shaders, and pixel shaders. Since we are talking about the image effects from FireMonkey, I will write only about pixel shaders. Please note that limitations are placed upon HLSL programs based upon the pixel shader model version (i.e: Pixel Shader v2.0 used by FireMonkey is instruction limited to 96 total instructions, 32 texture and 64 arithmetic instructions). For more information on HLSL, the MSDN Library entry serves as a good reference tool.

Creating a Custom Pixel Shader Effect

Everyone can create a custom pixel shader effects for FireMonkey. This process is quite simple and systematic:

  1. Code your pixel shader effect using HLSL
  2. Compile it into byte-code
  3. Convert the HLSL shader to GLSL and GLSL/ES
  4. Include the shader in a unit

Before you get started, look below for what you will need:

  Windows Environment

  • Windows XP/Vista/7/Server 2003/Server 2008;
  • Shazzam + Microsoft .NET Framework 4 or Microsoft DirectX Software Development Kit
  • NVIDIA Cg Toolkit

  OS X Environment

  • Xcode 4.2 (To create an iOS App)

1. Code Your Pixel Shader Effect Using HLSL

To develop an effect, code the HLSL using an editor of your preference and save the code with a .fx extension. I highly suggest you to use the Shazzam software for the reasons:

  • You will be able to edit and compile the HLSL code in a convenient UI
  • You will not need to install the 900MB DirectX SDK to compile the effect code
  • It comes with many sample shaders 🙂

The sample effect I choose for this post is called Frosty Outline created by Fakhruddin Faizal found at Shazzam software. See below for the code for the file FrostyOuline.fx.

float Width : register(C0);

///
<summary>The height of the frost.</summary>
/// 150
/// 400
/// 300
float Height : register(C1);

//--------------------------------------------------------------------------------------
// Sampler Inputs (Brushes, including Texture1)
//--------------------------------------------------------------------------------------

sampler2D input : register(S0);

//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------

float4 main(float2 middle : TEXCOORD) : COLOR

{
	float2 topLeft;
	float2 left;
	float2 bottomLeft;
	float2 top;
	float2 bottom;
	float2 topRight;
	float2 right;
	float2 bottomRight;

	topLeft.x = middle.x - 1/Width;
	topLeft.y = middle.y - 1/Height;
	top.x = middle.x;
	top.y = middle.y - 1/Height;
	topRight.x = middle.x + 1/Width;
	topRight.y = middle.y - 1/Height;
	left.x = middle.x - 1/Width;
	left.y = middle.y;
	right.x = middle.x + 1/Width;
	right.y = middle.y;
	bottomLeft.x = middle.x - 1/Width;
	bottomLeft.y = middle.y + 1/Height;
	bottom.x = middle.x;
	bottom.y = middle.y + 1/Height;
	bottomRight.x = middle.x + 1/Width;
	bottomRight.y = middle.y + 1/Height;

	float4 m = tex2D (input , middle);
	float4 tl = tex2D (input, topLeft);
	float4 l = tex2D (input, left);
	float4 bl = tex2D (input, bottomLeft);
	float4 t = tex2D (input, top);
	float4 b = tex2D (input, bottom);
	float4 tr = tex2D (input, topRight);
	float4 r = tex2D (input, right);
	float4 br = tex2D (input, bottomRight);

	float4 color = (-tl-t-tr) + (-l+8*m-r) + (-bl-b-br);
	float4 color2 = tex2D(input,middle);
	float avg=color.r+color.g+color.b;
	avg/=3;
	color.rgb=avg;
	color.a = 1;

	return color2+color;
}

The following links may help you start writing HLSL shaders.
Programming Guide for HLSL, from Microsoft
HLSL (Pixel Shader) effects tutorial, Tamir Khason

2. Compile it Into Byte-Code

In case you decided to use Shazzam, then as soon as you apply a shadder pressing F5 or using the menu options Tools -> Apply Shader, you will find the compiled shaders directory via the menu option Tools -> Explore Compiled Shaders (*.ps).

If you decided to use the DirectX SDK, then you will need to use fxc.exe, an effect-compiler command-line tool included at the DirectX SDK allowing you to compile your .fx file. Navigate to the directory containing the .fx file and issue the following command:

fxc /T ps_2_0 /E main /FoFrostyOutline.ps FrostyOutline.fx

Note: This line says to compile to the Pixel Shader 2.0 profile (/T ps_2_0), to look for the entrypoint named “main” (/E main) and set the output/compiled file to FrostyOutline.ps (/FoFrostyOutline.ps)

3. Convert the HLSL shader to GLSL and GLSL/ES

Until now you only compiled the shader to be used in a Windows application, but in case you want to use the same on Mac OS X and iOS, then you will need to convert the shader to proper OpenGL shader language. You could learn the different shader languages and convert the shader manually, or you could use the NVIDIA Cg Toolkit to do it. It may exists other tools to convert the shaders, but I found this the most suitable for this task.

As you may have noticed, there are many different low-level (assembly-like and/or C-Like) shader languages that different APIs (such as OpenGL and Direct3D), and even different GPUs, may support. Cg is another one. It allows you you to write one shader and use it with practically any API and GPU; the Cg compiler from NVIDIA can generate output in many of the low-level shader languages, including OpenGL fragment profile for multi-vendor (arbfp1) and OpenGL fragment profile for the OpenGL Shading Language (glslf) used on Mac OS X and iOS respectively.

But wait, do I need to write now the shader in Cg language? No, the Cg compiler  (cgc.exe) included at Cg Toolkit from NVIDIA allows you to translate shaders programs from many different sources to other many different shading language code. If you haven’t yet, you may want to download the NVIDIA Cg Toolkit, which contains the NVIDIA Cg Compiler and some good documentation. You will need to join the NVIDIA Registered Developer Program to be able to download the toolkit, but it’s free.

Assuming you have now the Cg compiler, use the following command line to translate your HLSL shader to ARBFP1 to be used on Mac OS X:

cgc -profile arbfp1 -strict -fx -O3 -q FrostyOutline.fx -o FrostyOutline.arbfp1

Note: We convert the HLSL shader to ARBFP1 (OpenGL GPU Assembly Language Fragment Program) because the resulting code will be the OpenGL assembly instructions the GPU will process.

The following command will convert the shader to be used on iOS

cgc -profile glslf -po version=100 -po userTexCoord -bestprecision -strict -fx -O3 -q FrostyOutline.fx -o FrostyOutline.glslf

OK, but it’s not that easy, you will require to do some minor changes to the GLSLF file. GLSL and GLSL/ES is not the same and I had to spend MANY hours trying to find an easy way to convert the HLSL shader to GLSL/ES using other tools/software and have it working properly on iOS. At end I couldn’t find a useful converter and I was only successful after looking at the existent shaders in FireMonkey source code and reading some documents about it.

Below is the file FrostyOutline.arbfp1 after the conversion.

!!ARBfp1.0
# cgc version 3.0.0016, build date Feb 11 2011
# command line args: -profile arbfp1 -strict -fx -O3 -q
# source file: Frosty.fx
#vendor NVIDIA Corporation
#version 3.0.0.16
#profile arbfp1
#program main
#semantic Width : C0
#semantic Height : C1
#semantic input : TEXUNIT0
#var float2 middle : $vin.TEXCOORD : TEX0 : 0 : 1
#var float Width : C0 : c[0] : -1 : 1
#var float Height : C1 : c[1] : -1 : 1
#var sampler2D input : TEXUNIT0 : texunit 0 : -1 : 1
#var float4 main : $vout.COLOR : COL : -1 : 1
#const c[2] = 0.33333334 1 8
PARAM c[3] = { program.local[0..1],
{ 0.33333334, 1, 8 } };
TEMP R0;
TEMP R1;
TEMP R2;
TEMP R3;
TEMP R4;
TEMP R5;
RCP R0.w, c[0].x;
RCP R1.w, c[1].x;
ADD R3.y, fragment.texcoord[0], R1.w;
ADD R3.x, fragment.texcoord[0], R0.w;
TEX R1.xyz, R3, texture[0], 2D;
ADD R2.w, fragment.texcoord[0].x, -R0;
MOV R2.x, R3.y;
TEX R2.xyz, R2.wxzw, texture[0], 2D;
MOV R0.y, R3;
MOV R0.x, fragment.texcoord[0];
TEX R0.xyz, R0, texture[0], 2D;
ADD R0.xyz, -R2, -R0;
ADD R5.xyz, R0, -R1;
TEX R0, fragment.texcoord[0], texture[0], 2D;
MOV R2.x, R3;
MOV R2.y, fragment.texcoord[0];
TEX R2.xyz, R2, texture[0], 2D;
MOV R1.x, R2.w;
MOV R1.y, fragment.texcoord[0];
TEX R1.xyz, R1, texture[0], 2D;
MAD R1.xyz, R0, c[2].z, -R1;
ADD R4.xyz, R1, -R2;
ADD R1.y, fragment.texcoord[0], -R1.w;
MOV R1.x, R3;
TEX R3.xyz, R1, texture[0], 2D;
MOV R1.w, R1.y;
MOV R1.z, fragment.texcoord[0].x;
TEX R2.xyz, R1.zwzw, texture[0], 2D;
MOV R1.x, R2.w;
TEX R1.xyz, R1, texture[0], 2D;
ADD R1.xyz, -R1, -R2;
ADD R1.xyz, R1, -R3;
ADD R1.xyz, R1, R4;
ADD R1.xyz, R1, R5;
ADD R1.x, R1, R1.y;
ADD R1.x, R1, R1.z;
MUL R1.xyz, R1.x, c[2].x;
MOV R1.w, c[2].y;
ADD result.color, R1, R0;
END

And here is the FrostyOutline.glslf file after the conversion.

// glslf output by Cg compiler
// cgc version 3.0.0016, build date Feb 11 2011
// command line args: -profile glslf -po version=100 -po userTexCoord -bestprecision -strict -fx -O3 -q
// source file: Frosty.fx
//vendor NVIDIA Corporation
//version 3.0.0.16
//profile glslf
//program main
//semantic Width : C0
//semantic Height : C1
//semantic input : TEXUNIT0
//var float Width : C0 : _Width : -1 : 1
//var float Height : C1 : _Height : -1 : 1
//var sampler2D input : TEXUNIT0 : _input 0 : -1 : 1
//var float2 middle : $vin.TEXCOORD : $TEX0 : 0 : 1
//var float4 main : $vout.COLOR : COL : -1 : 1

vec4 _ret_0;
varying vec4 TEX0;
uniform float _Width;
uniform float _Height;
uniform sampler2D _input;

 // main procedure, the original name was main
void main()
{

    vec2 _topLeft;
    vec2 _left;
    vec2 _bottomLeft;
    vec2 _top;
    vec2 _bottom;
    vec2 _topRight;
    vec2 _right;
    vec2 _bottomRight;
    vec4 _m;
    vec4 _tl;
    vec4 _l;
    vec4 _bl;
    vec4 _t;
    vec4 _b;
    vec4 _tr;
    vec4 _r;
    vec4 _br;
    vec4 _color;
    vec4 _color2;
    float _avg;

    _topLeft.x = TEX0.x - 1.00000000E+000/_Width;
    _topLeft.y = TEX0.y - 1.00000000E+000/_Height;
    _top.x = TEX0.x;
    _top.y = TEX0.y - 1.00000000E+000/_Height;
    _topRight.x = TEX0.x + 1.00000000E+000/_Width;
    _topRight.y = TEX0.y - 1.00000000E+000/_Height;
    _left.x = TEX0.x - 1.00000000E+000/_Width;
    _left.y = TEX0.y;
    _right.x = TEX0.x + 1.00000000E+000/_Width;
    _right.y = TEX0.y;
    _bottomLeft.x = TEX0.x - 1.00000000E+000/_Width;
    _bottomLeft.y = TEX0.y + 1.00000000E+000/_Height;
    _bottom.x = TEX0.x;
    _bottom.y = TEX0.y + 1.00000000E+000/_Height;
    _bottomRight.x = TEX0.x + 1.00000000E+000/_Width;
    _bottomRight.y = TEX0.y + 1.00000000E+000/_Height;
    _m = texture2D(_input, TEX0.xy);
    _tl = texture2D(_input, _topLeft);
    _l = texture2D(_input, _left);
    _bl = texture2D(_input, _bottomLeft);
    _t = texture2D(_input, _top);
    _b = texture2D(_input, _bottom);
    _tr = texture2D(_input, _topRight);
    _r = texture2D(_input, _right);
    _br = texture2D(_input, _bottomRight);
    _color = ((-_tl - _t) - _tr) + ((-_l + 8.00000000E+000*_m) - _r) + ((-_bl - _b) - _br);
    _color2 = texture2D(_input, TEX0.xy);
    _avg = _color.x + _color.y + _color.z;
    _avg = _avg/3.00000000E+000;
    _color.xyz = vec3(_avg, _avg, _avg);
    _color.w = 1.00000000E+000;
    _ret_0 = _color2 + _color;
    gl_FragColor = _ret_0;
    return;
} // main end

To use it on FireMonkey and working on iOS, I had to make some changes as mentioned before. Here the resulting code:

precision mediump float;
vec4 _ret_0;
varying vec4 TEX0;
uniform vec4 PSParam0;
uniform vec4 PSParam1;
uniform sampler2D texture0;
void main()
{
    vec2 _topLeft;
    vec2 _left;
    vec2 _bottomLeft;
    vec2 _top;
    vec2 _bottom;
    vec2 _topRight;
    vec2 _right;
    vec2 _bottomRight;
    vec4 _m;
    vec4 _tl;
    vec4 _l;
    vec4 _bl;
    vec4 _t;
    vec4 _b;
    vec4 _tr;
    vec4 _r;
    vec4 _br;
    vec4 _color;
    vec4 _color2;
    float _avg;
    _topLeft.x = TEX0.x - 1.00000000E+000/PSParam0.x;
    _topLeft.y = TEX0.y - 1.00000000E+000/PSParam1.x;
    _top.x = TEX0.x;
    _top.y = TEX0.y - 1.00000000E+000/PSParam1.x;
    _topRight.x = TEX0.x + 1.00000000E+000/PSParam0.x;
    _topRight.y = TEX0.y - 1.00000000E+000/PSParam1.x;
    _left.x = TEX0.x - 1.00000000E+000/PSParam0.x;
    _left.y = TEX0.y;
    _right.x = TEX0.x + 1.00000000E+000/PSParam0.x;
    _right.y = TEX0.y;
    _bottomLeft.x = TEX0.x - 1.00000000E+000/PSParam0.x;
    _bottomLeft.y = TEX0.y + 1.00000000E+000/PSParam1.x;
    _bottom.x = TEX0.x;
    _bottom.y = TEX0.y + 1.00000000E+000/PSParam1.x;
    _bottomRight.x = TEX0.x + 1.00000000E+000/PSParam0.x;
    _bottomRight.y = TEX0.y + 1.00000000E+000/PSParam1.x;
    _m = texture2D(texture0, TEX0.xy);
    _tl = texture2D(texture0, _topLeft);
    _l = texture2D(texture0, _left);
    _bl = texture2D(texture0, _bottomLeft);
    _t = texture2D(texture0, _top);
    _b = texture2D(texture0, _bottom);
    _tr = texture2D(texture0, _topRight);
    _r = texture2D(texture0, _right);
    _br = texture2D(texture0, _bottomRight);
    _color = ((-_tl - _t) - _tr) + ((-_l + 8.00000000E+000*_m) - _r) + ((-_bl - _b) - _br);
    _color2 = texture2D(texture0, TEX0.xy);
    _avg = _color.x + _color.y + _color.z;
    _avg = _avg/3.00000000E+000;
    _color.xyz = vec3(_avg, _avg, _avg);
    _color.w = 1.00000000E+000;
    _ret_0 = _color2 + _color;
    gl_FragColor = _ret_0;
    return;
}

If you compare the two files, you will see that I made some changes:

  1. removed the comments,
  2. added precision mediump float; at the beginning,
  3. changed the parameter type from float to vec4
  4. changed the input parameters name from
    • _Width to PSParam0 (variable declaration)
    • _Height to PSParam1 (variable declaration)
    • _input to texture0
    • _Width to PSParam0.x (main code)
    • _Height to PSParam1.x (main code)

The files above were compiled with the Cg compiler version 3.0.0016, build date Feb 11 2011 12:40:07. But I suggest everyone compiles using a different version of the Cg compiler. Why? Because the compiler optimization may not work as expected generating unnecessary instructions. Have a look at this post on stackoverflow.com.

4. Include the Shader in an Unit

Now the final part is just add the shaders to an unit.

In case you try to open the compiled DirectX file ForstyOutline.ps, you will notice that it’s a binary file and you may have to do the same as Embarcadero have done with the other effects by converting it into an array of bytes. This is a simple task, but to make everyone’s life easier, you can use the code below.

Just create a new Delphi application, add one OpenDialog, one Memo and one Button. The code may not be perfect, but should work without problem.

procedure TForm1.Button1Click(Sender: TObject);
var
  myFile : File;
  i, bytesRead, myFileSize: Integer;
  byteArray: array [1 .. 40] of byte;
  line : string;
begin
  Memo1.Lines.Clear;
  if OpenDialog1.Execute then
  begin
    AssignFile(myFile, OpenDialog1.FileName );
    FileMode := fmOpenRead;
    Reset(myFile, 1);
    myFileSize := FileSize(myFile);
    Memo1.Lines.Insert(0,'MyFilter : Array[1..'+IntToStr(myFileSize)+'] of byte = (');
    while not Eof(myFile) do
    begin
      BlockRead(myFile, byteArray, 40, bytesRead);
      line := '    $'+IntToHex(byteArray[1],2);
      for i := 2 to bytesRead do
      begin
         line := line + ', $'+IntToHex(byteArray[i],2);
      end;
      myFileSize := myFileSize - bytesRead;
      if myFileSize > 0 then
         line := line+',';
      Memo1.Lines.Add(line);
    end;
    Memo1.Lines.Add(')');
    CloseFile(myFile);
  end;
end;

And to prepare the other shaders (text format) to be a constant, you can simply use the code below.

procedure TForm1.Button1Click(Sender: TObject);
var count : integer;
begin
   for count := 0 to Memo1.Lines.Count - 2 do
   begin
      Memo1.Lines[count] := '    '''+Memo1.Lines[count]+'''#13+';
   end;
   Memo1.Lines[Memo1.Lines.Count -1] := '    '''+Memo1.Lines[Memo1.Lines.Count -1]+''';';
end;

The unit for the new filter is quite simple and should be easy to understand.

unit FMX.MyFiltersCatStyle;

interface

uses
  FMX.Filter;

type

  TFrostyOutlineFilter = class(TShaderFilter)
  protected
    class function FilterAttr: TFilterRec; override;
  public
    constructor Create; override;
  end;

implementation

uses
  FMX.Types3D;

{ TFrostyOutlineFilter }

constructor TFrostyOutlineFilter.Create;
const
  DX: array[1..892] of byte = (
    $00, $02, $FF, $FF, $FE, $FF, $32, $00, $43, $54, $41, $42, $1C, $00, $00, $00, $93, $00, $00, $00, $00, $02, $FF, $FF, $03, $00, $00, $00, $1C, $00, $00, $00, $00, $01, $00, $20, $8C, $00, $00, $00,
    $58, $00, $00, $00, $02, $00, $01, $00, $01, $00, $06, $00, $60, $00, $00, $00, $00, $00, $00, $00, $70, $00, $00, $00, $02, $00, $00, $00, $01, $00, $02, $00, $60, $00, $00, $00, $00, $00, $00, $00,
    $76, $00, $00, $00, $03, $00, $00, $00, $01, $00, $00, $00, $7C, $00, $00, $00, $00, $00, $00, $00, $48, $65, $69, $67, $68, $74, $00, $AB, $00, $00, $03, $00, $01, $00, $01, $00, $01, $00, $00, $00,
    $00, $00, $00, $00, $57, $69, $64, $74, $68, $00, $69, $6E, $70, $75, $74, $00, $04, $00, $0C, $00, $01, $00, $01, $00, $01, $00, $00, $00, $00, $00, $00, $00, $70, $73, $5F, $32, $5F, $30, $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, $34, $2E, $39,
    $35, $30, $2E, $32, $36, $35, $36, $00, $51, $00, $00, $05, $02, $00, $0F, $A0, $00, $00, $00, $41, $AB, $AA, $AA, $3E, $00, $00, $80, $3F, $00, $00, $00, $00, $1F, $00, $00, $02, $00, $00, $00, $80,
    $00, $00, $03, $B0, $1F, $00, $00, $02, $00, $00, $00, $90, $00, $08, $0F, $A0, $01, $00, $00, $02, $00, $00, $0C, $80, $00, $00, $1B, $B0, $06, $00, $00, $02, $01, $00, $08, $80, $01, $00, $00, $A0,
    $02, $00, $00, $03, $00, $00, $02, $80, $01, $00, $FF, $81, $00, $00, $55, $B0, $02, $00, $00, $03, $01, $00, $02, $80, $01, $00, $FF, $80, $00, $00, $55, $B0, $01, $00, $00, $02, $02, $00, $01, $80,
    $00, $00, $FF, $80, $01, $00, $00, $02, $02, $00, $02, $80, $00, $00, $55, $80, $06, $00, $00, $02, $00, $00, $08, $80, $00, $00, $00, $A0, $02, $00, $00, $03, $00, $00, $01, $80, $00, $00, $FF, $81,
    $00, $00, $00, $B0, $02, $00, $00, $03, $03, $00, $01, $80, $00, $00, $FF, $80, $00, $00, $00, $B0, $01, $00, $00, $02, $03, $00, $02, $80, $02, $00, $55, $80, $01, $00, $00, $02, $04, $00, $01, $80,
    $00, $00, $00, $80, $01, $00, $00, $02, $04, $00, $02, $80, $00, $00, $AA, $80, $01, $00, $00, $02, $01, $00, $01, $80, $04, $00, $00, $80, $01, $00, $00, $02, $03, $00, $04, $80, $00, $00, $55, $B0,
    $01, $00, $00, $02, $05, $00, $01, $80, $03, $00, $00, $80, $01, $00, $00, $02, $05, $00, $02, $80, $03, $00, $AA, $80, $01, $00, $00, $02, $06, $00, $01, $80, $05, $00, $00, $80, $01, $00, $00, $02,
    $06, $00, $02, $80, $01, $00, $55, $80, $01, $00, $00, $02, $01, $00, $04, $80, $00, $00, $00, $B0, $01, $00, $00, $02, $07, $00, $01, $80, $01, $00, $AA, $80, $01, $00, $00, $02, $07, $00, $02, $80,
    $01, $00, $55, $80, $42, $00, $00, $03, $02, $00, $0F, $80, $02, $00, $E4, $80, $00, $08, $E4, $A0, $42, $00, $00, $03, $00, $00, $0F, $80, $00, $00, $E4, $80, $00, $08, $E4, $A0, $42, $00, $00, $03,
    $03, $00, $0F, $80, $03, $00, $E4, $80, $00, $08, $E4, $A0, $42, $00, $00, $03, $01, $00, $0F, $80, $01, $00, $E4, $80, $00, $08, $E4, $A0, $42, $00, $00, $03, $04, $00, $0F, $80, $04, $00, $E4, $80,
    $00, $08, $E4, $A0, $42, $00, $00, $03, $08, $00, $0F, $80, $00, $00, $E4, $B0, $00, $08, $E4, $A0, $42, $00, $00, $03, $05, $00, $0F, $80, $05, $00, $E4, $80, $00, $08, $E4, $A0, $42, $00, $00, $03,
    $06, $00, $0F, $80, $06, $00, $E4, $80, $00, $08, $E4, $A0, $42, $00, $00, $03, $07, $00, $0F, $80, $07, $00, $E4, $80, $00, $08, $E4, $A0, $02, $00, $00, $03, $00, $00, $07, $80, $02, $00, $E4, $81,
    $00, $00, $E4, $81, $02, $00, $00, $03, $00, $00, $07, $80, $03, $00, $E4, $81, $00, $00, $E4, $80, $04, $00, $00, $04, $02, $00, $07, $80, $08, $00, $E4, $80, $02, $00, $00, $A0, $04, $00, $E4, $81,
    $02, $00, $00, $03, $02, $00, $07, $80, $05, $00, $E4, $81, $02, $00, $E4, $80, $02, $00, $00, $03, $00, $00, $07, $80, $00, $00, $E4, $80, $02, $00, $E4, $80, $02, $00, $00, $03, $01, $00, $07, $80,
    $01, $00, $E4, $81, $07, $00, $E4, $81, $02, $00, $00, $03, $01, $00, $07, $80, $06, $00, $E4, $81, $01, $00, $E4, $80, $02, $00, $00, $03, $00, $00, $07, $80, $00, $00, $E4, $80, $01, $00, $E4, $80,
    $02, $00, $00, $03, $00, $00, $01, $80, $00, $00, $55, $80, $00, $00, $00, $80, $02, $00, $00, $03, $00, $00, $01, $80, $00, $00, $AA, $80, $00, $00, $00, $80, $05, $00, $00, $03, $00, $00, $07, $80,
    $00, $00, $00, $80, $02, $00, $55, $A0, $01, $00, $00, $02, $00, $00, $08, $80, $02, $00, $AA, $A0, $02, $00, $00, $03, $00, $00, $0F, $80, $08, $00, $E4, $80, $00, $00, $E4, $80, $01, $00, $00, $02,
    $00, $08, $0F, $80, $00, $00, $E4, $80, $FF, $FF, $00, $00
  );

  GL: PAnsiChar =
    '!!ARBfp1.0'#13 +
    'PARAM c[3] = { program.local[0..1],'#13+
    '		{ 0.33333334, 1, 8 } };'#13+
    'TEMP R0;'#13+
    'TEMP R1;'#13+
    'TEMP R2;'#13+
    'TEMP R3;'#13+
    'TEMP R4;'#13+
    'TEMP R5;'#13 +
    'RCP R0.w, c[0].x;'#13+
    'RCP R1.w, c[1].x;'#13+
    'ADD R3.y, fragment.texcoord[0], R1.w;'#13+
    'ADD R3.x, fragment.texcoord[0], R0.w;'#13+
    'TEX R1.xyz, R3, texture[0], 2D;'#13+
    'ADD R2.w, fragment.texcoord[0].x, -R0;'#13+
    'MOV R2.x, R3.y;'#13+
    'TEX R2.xyz, R2.wxzw, texture[0], 2D;'#13+
    'MOV R0.y, R3;'#13+
    'MOV R0.x, fragment.texcoord[0];'#13+
    'TEX R0.xyz, R0, texture[0], 2D;'#13+
    'ADD R0.xyz, -R2, -R0;'#13+
    'ADD R5.xyz, R0, -R1;'#13+
    'TEX R0, fragment.texcoord[0], texture[0], 2D;'#13+
    'MOV R2.x, R3;'#13+
    'MOV R2.y, fragment.texcoord[0];'#13+
    'TEX R2.xyz, R2, texture[0], 2D;'#13+
    'MOV R1.x, R2.w;'#13+
    'MOV R1.y, fragment.texcoord[0];'#13+
    'TEX R1.xyz, R1, texture[0], 2D;'#13+
    'MAD R1.xyz, R0, c[2].z, -R1;'#13+
    'ADD R4.xyz, R1, -R2;'#13+
    'ADD R1.y, fragment.texcoord[0], -R1.w;'#13+
    'MOV R1.x, R3;'#13+
    'TEX R3.xyz, R1, texture[0], 2D;'#13+
    'MOV R1.w, R1.y;'#13+
    'MOV R1.z, fragment.texcoord[0].x;'#13+
    'TEX R2.xyz, R1.zwzw, texture[0], 2D;'#13+
    'MOV R1.x, R2.w;'#13+
    'TEX R1.xyz, R1, texture[0], 2D;'#13+
    'ADD R1.xyz, -R1, -R2;'#13+
    'ADD R1.xyz, R1, -R3;'#13+
    'ADD R1.xyz, R1, R4;'#13+
    'ADD R1.xyz, R1, R5;'#13+
    'ADD R1.x, R1, R1.y;'#13+
    'ADD R1.x, R1, R1.z;'#13+
    'MUL R1.xyz, R1.x, c[2].x;'#13+
    'MOV R1.w, c[2].y;'#13+
    'ADD result.color, R1, R0;'#13+
    'END';

  GLES: PAnsiChar =
   	'precision mediump float;'#13+
	  'vec4 _ret_0;'#13+
	  'varying vec4 TEX0;'#13+
	  'uniform vec4 PSParam0;'#13+
   	'uniform vec4 PSParam1;'#13+
   	'uniform sampler2D texture0;'#13+
   	'void main()'#13+
   	'{'#13+
   	'    vec2 _topLeft;'#13+
	  '    vec2 _left;'#13+
   	'    vec2 _bottomLeft;'#13+
  	'    vec2 _top;'#13+
  	'    vec2 _bottom;'#13+
  	'    vec2 _topRight;'#13+
  	'    vec2 _right;'#13+
  	'    vec2 _bottomRight;'#13+
  	'    vec4 _m;'#13+
  	'    vec4 _tl;'#13+
  	'    vec4 _l;'#13+
  	'    vec4 _bl;'#13+
  	'    vec4 _t;'#13+
  	'    vec4 _b;'#13+
  	'    vec4 _tr;'#13+
  	'    vec4 _r;'#13+
  	'    vec4 _br;'#13+
  	'    vec4 _color;'#13+
  	'    vec4 _color2;'#13+
  	'    float _avg;'#13+
  	'    _topLeft.x = TEX0.x - 1.00000000E+000/PSParam0.x;'#13+
  	'    _topLeft.y = TEX0.y - 1.00000000E+000/PSParam1.x;'#13+
  	'    _top.x = TEX0.x;'#13+
  	'    _top.y = TEX0.y - 1.00000000E+000/PSParam1.x;'#13+
  	'    _topRight.x = TEX0.x + 1.00000000E+000/PSParam0.x;'#13+
  	'    _topRight.y = TEX0.y - 1.00000000E+000/PSParam1.x;'#13+
  	'    _left.x = TEX0.x - 1.00000000E+000/PSParam0.x;'#13+
  	'    _left.y = TEX0.y;'#13+
  	'    _right.x = TEX0.x + 1.00000000E+000/PSParam0.x;'#13+
  	'    _right.y = TEX0.y;'#13+
  	'    _bottomLeft.x = TEX0.x - 1.00000000E+000/PSParam0.x;'#13+
  	'    _bottomLeft.y = TEX0.y + 1.00000000E+000/PSParam1.x;'#13+
  	'    _bottom.x = TEX0.x;'#13+
  	'    _bottom.y = TEX0.y + 1.00000000E+000/PSParam1.x;'#13+
  	'    _bottomRight.x = TEX0.x + 1.00000000E+000/PSParam0.x;'#13+
  	'    _bottomRight.y = TEX0.y + 1.00000000E+000/PSParam1.x;'#13+
  	'    _m = texture2D(texture0, TEX0.xy);'#13+
  	'    _tl = texture2D(texture0, _topLeft);'#13+
  	'    _l = texture2D(texture0, _left);'#13+
  	'    _bl = texture2D(texture0, _bottomLeft);'#13+
  	'    _t = texture2D(texture0, _top);'#13+
  	'    _b = texture2D(texture0, _bottom);'#13+
  	'    _tr = texture2D(texture0, _topRight);'#13+
  	'    _r = texture2D(texture0, _right);'#13+
  	'    _br = texture2D(texture0, _bottomRight);'#13+
  	'    _color = ((-_tl - _t) - _tr) + ((-_l + 8.00000000E+000*_m) - _r) + ((-_bl - _b) - _br);'#13+
  	'    _color2 = texture2D(texture0, TEX0.xy);'#13+
  	'    _avg = _color.x + _color.y + _color.z;'#13+
  	'    _avg = _avg/3.00000000E+000;'#13+
  	'    _color.xyz = vec3(_avg, _avg, _avg);'#13+
  	'    _color.w = 1.00000000E+000;'#13+
  	'    _ret_0 = _color2 + _color;'#13+
  	'    gl_FragColor = _ret_0;'#13+
  	'    return;'#13+
	  '}';

begin
  inherited;
  FShaders[1] := ShaderDevice.CreatePixelShader(@DX, GL, GLES);
end;

class function TFrostyOutlineFilter.FilterAttr: TFilterRec;
begin
  Result := FilterRec('FrostyOutline','An effect that turns into shades of a single color.',
    [FilterValueRec('Width', 'The width of the frost.', TShaderValueType.vtFloat, 350 {Default value}, 0 {min Value}, 500 {Max Value}),
     FilterValueRec('Height', 'The heigth of the frost.', TShaderValueType.vtFloat, 300 {Default value}, 0 {min Value}, 500 {Max Value})]);
end;

initialization

RegisterFilter('Style', TFrostyOutlineFilter);

end.

Note: I’m using 0 as the minimum value for the Height and Width parameters as I liked the strong effect using lower values. The original shader uses 150 as minimum value for those parameters.

To use this effect at the ShaderFilters application, just add this unit to the uses.

The file for iOS is near the same, you should change file names (dot with underscore) and don’t forget to use the equivalent files at uses.

To test the filter at iOS, create a new FireMonkey HD iOS Application, add two TImage one called Source with an image and the second called Dest.

procedure TForm1.FormCreate(Sender: TObject);
const
  FilterName = 'FrostyOutline';
  //FilterName = 'Emboss';
  //FilterName = 'Invert';
begin
  Filter := FilterByName(FilterName);
  Filter.Values['Height'] := 300;
  Filter.Values['Width'] := 350;
  Filter.ValuesAsBitmap['Input'] := Source.Bitmap;
  Dest.Bitmap := TBitmap(Filter.ValuesAsBitmap['output']);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  if Filter <> nil then
    Filter.Free;
end;

Here some screenshots of the ShaderFilters application on Windows and Mac OS X and of the small iOS application described above.


Figure 2. FireMonkey ShaderFilters on Windows 7


Figure 3. FireMonkey ShaderFilters on Mac OS X


Figure 4. FrostyOutline on iOS

Summary

I hope the explanation was easy to understand. I believe that with the information provided everyone should be able to start playing around and create new effects for FireMonkey. Something that I will leave for the readers is how to create a component for the new effects to use it at design time. If you create some nice effects, please don’t forget to share 🙂

Links

Microsoft – Writing HLSL Shaders in Direct3D 9 – http://msdn.microsoft.com/en-us/library/windows/desktop/bb944006(v=VS.85).aspx#Pixel_Shader_Basics
Microsoft – DirectX Pixel Shader 2 – http://msdn.microsoft.com/en-us/library/windows/desktop/bb219843(v=vs.85).aspx
Microsoft DirectX SDK – http://go.microsoft.com/fwlink/?LinkId=150942
NVIDIA – Cg Toolkit – http://developer.nvidia.com/cg-toolkit
Shazzam – WPF/Silverlight Pixel Shader Utility – http://shazzam-tool.com
The Open GL ES Shading Language – http://www.khronos.org/registry/gles/specs/2.0/GLSL_ES_Specification_1.0.17.pdf

Posted in FMX Tagged with: ,
4 comments on “How to Create Your Own FireMonkey Image Effect
  1. gabr says:

    Excellent, thanks!

  2. Jakub Król says:

    Great, just great article!

2 Pings/Trackbacks for "How to Create Your Own FireMonkey Image Effect"

2016 Symposium

Our Annual Symposium for 2016 was held in Sydney and Melbourne in August.
The presenters were
* Malcolm Groves of CodePartners (formerly Embarcadero)
* Garry Wood of the Ultibo open source project
* Alistair Christie of LearnDelphi.tv
* Shane van de Vorstenbosch of OnSolution.

The 2017 Symposium will be held in Melbourne and Brisbane. Date to be determined

Archives