Tips & Tricks · Uncategorized

Happy New Year from Team Grijjy!

Best wishes to all fellow Delphi developers and the team behind Delphi!

Let’s have some fun with FireMonkey!

The little video above is a recording of a small (3D) FireMonkey app. You can test it out yourself by cloning our JustAddCode repo. There, you will find it in the HappyNewYear subdirectory. For those of you not familiar with 3D FireMonkey and custom shaders, I’ll explain in short how this app is put together.

App Structure

This is a pretty basic 3D FireMonkey app using only a couple of controls:

  • The form is of type TForm3D, so you can (only) add 3D controls to it.
  • Aligned to the top is a TLayer3D control. This is a 3D control that accepts regular 2D FireMonkey controls as child controls. In this example, it hosts some text controls and track bars to control the number of fireworks and sparks per fireworks. You usually set to Projection property of 3D layers to Screen so it behaves more like a regular 2D control. I also set the fill color to 50% transparent white, so the fireworks will appear behind the layer.
  • The fireworks are rendered on a TMesh control with a custom shader (material). More on this later.
  • In front of the fireworks (with a lower Z coordinate) is a TPlane control with a TTextureMaterialSource material containing a transparent PNG bitmap with the text “Happy New Year”. This image is courtesy of HG Designs. The plane slowly rotates around the X and Y axes.

Fireworks Shader

The fireworks are rendered using a custom shader, adapted from Martijn Steinrucken’s “[SH17A] Fireworks” shader. To learn more about using custom GPU shaders in FireMonkey, check out our Introduction to Shader Programming post. For reference, this is the HLSL (DirectX) source code of the pixel/fragment shader:

float Time;
float FireworksCount;
float SparkCount;

#define random(x) frac(sin(float4(6.0, 9.0, 1.0 ,0.0) * x) * 9e2) 

float4 main(float4 position: SV_POSITION,
  float2 uv: TEXCOORD0): SV_Target0
{
  float4 color = float4(0.0, 0.0, 0.0, 0.0);
  
  for (float i = -2.0; i < FireworksCount; i++)
  {
    float fireworkTime = i * 9.1 + Time;
    float d = floor(fireworkTime);
    float4 fireworkPosAndColor = random(d) + 0.3;
    fireworkTime -= d;
    for (d = 0.0; d < SparkCount; d++)
      color += fireworkPosAndColor * (1.0 - fireworkTime) / 1e3 / 
        length(uv - (fireworkPosAndColor - fireworkTime * 
        (random(d * i) - 0.5)).xy);
  }
  
  return float4(color.r, color.g, color.b, 1.0);
}

The code is surprisingly small. It was actually written in an attempt to create fireworks in as little code as possible. As a result, the code is not very obvious, but you can just use it as-is. I updated it slightly to make the number of fireworks and sparks per firework configurable. In the GUI, you set these using the two sliders. These values are then passed to the shader in the uniform inputs FireworksCount and SparkCount, along with the current render time in seconds (Time). The code then performs some magic using these settings that results in the fireworks you see in the video.

It’s interesting to note that this shader performs a lot of calculations for each and every single pixel. For the default configuration of 9 fireworks and 50 sparks per firework, it performs the inner loop 450 times for each pixel. And this inner loop contains “expensive” calculations such as a vector length calculation (which involves a square root) and a sine calculation. As a result, the app only runs smoothly on more powerful (desktop) GPUs. So I didn’t add any build configurations to run the app on a mobile device. Although you can probably get it to work on these devices as well by lowering the number of fireworks and sparks.

Also interesting is that on my MacBook pro the app runs much faster using the Metal backend (the default configuration) than the OpenGL backend (selectable using the AlternativeBackend build configuration). Apple is clearly putting all their eggs in a Metal basket, and I suspect there will come a point in the not-too-distant future that OpenGL will not be supported at all anymore on Apple platforms.

On the Delphi side, this fireworks shader is encapsulated in a TFireworksMaterialSource class:

type
  TFireworksMaterialSource = class(TMaterialSource)
  private
    function GetTimeInSeconds: Single;
    procedure SetTimeInSeconds(const AValue: Single);
    function GetFireworksCount: Integer;
    procedure SetFireworksCount(const AValue: Integer);
    function GetSparkCount: Integer;
    procedure SetSparkCount(const AValue: Integer);
  protected
    function CreateMaterial: TMaterial; override;
  published
    property TimeInSeconds: Single read GetTimeInSeconds write SetTimeInSeconds;
    property SparkCount: Integer read GetSparkCount write SetSparkCount;
    property FireworksCount: Integer read GetFireworksCount write SetFireworksCount;
  end;

For details on creating custom FireMonkey materials, also check out our Introduction to Shader Programming post. Then the source code for this fireworks material should make sense.

Rotating around Multiple Axes

I’ll finish this post with a tip on how to rotate a 3D control around multiple axes. Every 3D control has a RotationAngle property with X, Y and Z members, so that you can rotate around a specific axis. However, rotating in 3 dimensions leads to unpredictable results if you rotate around multiple axes at the same time. This is because of the way rotations are handled in matrix calculations. A better and more predictable way to rotate 3D objects is by using quaternions instead of matrices. I won’t go into the details or math of this. There are plenty of resources available if you want to learn more about quaternions.

But 3D FireMonkey controls do not use quaternions for rotation. So, unless you want to perform these calculations yourself, we are stuck with regular rotations around 3 axes. But we can make this work for multiple axes at the same time by creating a visual hierarchy of controls, and rotate each control in the hierarchy around just a single axis. For 3D FireMonkey apps, you can use a TDummy control to build this hierarchy. A dummy control is invisible. It’s sole purpose is to use it as a parent for other controls. For example, to group multiple controls together, or to better control rotations as in this example. This app uses the following hierarchy:

When you transform (move, scale or rotate) a dummy control, it will automatically transform its child controls as well. That’s the key to rotating around multiple axes at the same time: The DummyHappyNewYear control has a float animation that rotates around the X axis. As a result, the child PlaneHappyNewYear control rotates around this axis as well. But the plane itself also as a float animation that rotates around the Y axis. This way, the plane rotates around both X and Y axes at the same time in a predictable way. If you would add both animations the to same plane control, then the result would not be what you would expect.

Have a Great 2022!

And that’s a wrap for this app and 2021. We hope you will have a great 2022 in your (Delphi) development endeavors and otherwise.

2 thoughts on “Happy New Year from Team Grijjy!

    1. That’s an interesting idea. Should be possible. The hardest part is probably calculating then the explosions occur and and sound effects need to play…

      Like

Leave a comment