Tuesday, January 12, 2021

Simple FireMonkey 3D

Creating 3D software is undeniably cool, but it might be intimidating, what with all of the math and unfamiliar concepts. And the math...

Fortunately, FireMonkey does a lot of the heavy lifting for us. You can get started while writing little or no code and still have the option of digging deeper when you need to. It's a familiar concept for Delphi developers.

We'll create a simple FireMonkey 3D application and explore some of the basics.

FireMonkey is Embarcadero's cross platform GUI framework that takes advantage of native APIs on each supported platform. Under the covers, FireMonkey wraps DirectX on Windows, OpenGL on Mac and Linux and a subset of OpenGL called OpenGL ES on iOS and Android. The abstraction means that code written for one platform will work (with some caveats) on all of the other platforms and takes advantage of hardware acceleration where it's available.

Let's start with a popular choice for first time 3D demos - An animated globe.

Source code for the sample project is available here and can be built with recent versions of Delphi, including the Community Edition.

Create the application

From the main menu, select File|New|Multi-Device Application - Delphi or click Create a new Multi-Device Application - Delphi on the Welcome Page. Then select which template to use from the dialog box. Pick one of these two options:

1) 3D Application, which creates an app with a TForm3D where you can place your 3D content. This seems like an obvious choice for 3D. Obviously.

2) Blank Application, which creates a more standard app with a TForm. Drop a TViewport3D component onto the form. Now you can mix 2D controls and 3D content with the TViewport3D performing the same task as TForm3D in the previous option.

I usually prefer the second option, but TViewport3D and TForm3D are essentially interchangeable, so the instructions and code work equally well with either one.

Next, set the Color property on Viewport3D (or Form3D) to Black. It is space, after all.

Add the globe

Drop a TSphere control onto the Viewport3D.
- Set the sphere's Depth, Height and Width properties all to 10.

Now that the sphere is a little larger, you might notice that the outline is a little blocky. A TSphere is actually a mesh made up of triangles. The SubdivisionAxes and SubdivisionHeight properties tell the sphere how many subdivisions to use when creating the mesh. The more subdivisions, the smoother the surface. These default to 16 and 12, respectively, and can go as high as 50. I used a nice middle ground of 30 for both of these. No sense drawing more detail than we need.

Add a Texture

By default, our sphere doesn't have any features. You can't even tell that it's a 3D object. It's just a big red circle. Let's add an image to give it some character.

NASA has a huge public media library, including their Blue Marble collection, named for the famous image taken of the Earth from space in 1972 by the Apollo 17 crew. There are lots of detailed composite images of the Earth from satellite imagery taken over the course of several months. They also have the Earth at Night collection with some similar images tagged Black Marble. You gotta love NASA's catchy and descriptive names.

The form factor we want to use is called an equirectangular projection or geographic projection, which is what you get when you transform a sphere onto a flat plane. It also makes these images ideal for mapping back onto our sphere. We don't need to know how FireMonkey does this mapping right now, but it might be an interesting topic for a future post.

Download one of the projection images that you like. Each one comes in a variety of sizes. If you can't find the right size, you can download something with a really high resolution and shrink it down using your favourite image editing software. For our purposes, I might not go bigger than 1024 x 512 pixels.

Drop a TTextureMaterialSource component onto the form.
- Set the Texture property to point to the image of the Earth.
- Set the sphere's MaterialSource property to point to the TextureMaterialSource component.

Add Rotation

Drop a TFloatAnimation component onto the sphere. This will animate one single property of the sphere, but you can have more than one animation, each one operating on a different property.

Note that the TFloatAnimation has to actually be attached to the component that you want to animate. If you make a mistake and drop it in the wrong place, which I do all the time, you can just drag it onto the sphere in the structure pane.

Make the following changes to the FloatAnimation component:
- Set Duration to 10 (seconds).
- Set Loop to True.
- Set PropertyName to RotationAngle.Y.
- Set StartValue to 360.
- Set StopValue to 1.
- Set Enabled to True.

This will smoothly animate the sphere around its Y axis over the course of ten seconds and then repeat, giving us a continuously rotating globe.

The start and stop values might seem a little odd.

We count down from 360 instead of up from 1 so that the globe rotates in the proper direction and we don't get grief from some pedantic astrophysicist. If you wanted to, you could also count up from 1 to 360  and set the Inverse property to True. Either will give you the same effect.

The count goes down to 1 instead of 0 because rotation is measured in degrees, so 0 and 360 are the same angle and having two identical frames one after another looks like a stutter in the animation.

Manipulate the Scene

For extra credit, we'll add some interactivity - The ability to zoom and change the viewing angle.

And we finally get to write some code.

TViewport3D (and TForm3D) comes with its own built-in design camera, which is great to get started, but if we want to go beyond the default behaviour, we can create a separate container (TDummy) and add our own camera that we can control. By rotating the container and changing the distance of the camera, we can pan and zoom without having to interact with or change any of the other objects in the scene.

This concept is shown in the Arrows3D demo that ships with RAD Studio. Sure the demo isn't written in Delphi, but the code isn't difficult to read and a little C++ never hurt anyone. If you prefer, Pawel Glowacki showed off pretty much the same thing using Delphi.

Drop a TDummy component onto the form. This is our container.
- Set RotationAngle.X to -20. Don't be surprised if the IDE changes this to 340. They're essentially the same value.

Drop a TCamera component onto the TDummy component.
- Set Position.Z to -20.

Change the ViewPort3D (or Form3D):
- Set the Camera property to point to the new TCamera component.
- Set UseDesignCamera to False.

Change the TSphere:
- Set HitTest to False (so it doesn't get in the way when we click and drag).

This duplicates the behaviour of the built-in design camera, except now we can manipulate things.

When you click on the scene, the application remembers where you click with the mouse. When you click and drag, the viewing angle is changed relative to that point.

Add a private field to the form to record where a mouse click or screen touch happens:

    FMouseDown: TPointF;

To change the viewing angle by clicking and dragging, add the following code to the ViewPort3D's MouseDown and MouseMove events:

// ----------------------------------------------------------------------------
procedure TfrmMain.Viewport3D1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Single);
  FMouseDown := PointF(X, Y);

// ----------------------------------------------------------------------------
procedure TfrmMain.Viewport3D1MouseMove(Sender: TObject; Shift: TShiftState;
  X, Y: Single);
  if ssLeft in Shift then
    Dummy1.RotationAngle.X := Dummy1.RotationAngle.X -
      ((Y - FMouseDown.Y) * 0.3);
    Dummy1.RotationAngle.Y := Dummy1.RotationAngle.Y +
      ((X - FMouseDown.X) * 0.3);
    FMouseDown := PointF(X, Y);

To zoom using the mouse wheel, add the following code to the ViewPort3D's MouseWheel event:

// ----------------------------------------------------------------------------
procedure TfrmMain.Viewport3D1MouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; var Handled: Boolean);
  Camera1.Position.Z := Camera1.Position.Z + WheelDelta / 40;

What About Mobile?

I hinted earlier that not all platforms are identical and some might need additional consideration. Mobile devices, for example, don't usually have a mouse wheel, but we can use the "pinch zoom" gesture to get the same zooming behaviour. Another one of the demos that ships with RAD Studio, ImageZoom, shows how to work with the gesture. Not to worry; This demo is written in Delphi. :)

When a zoom (pinch) gesture begins, the application notes the distance between the user's fingers. When the user moves their fingers, the application zooms by how much this distance has changed.

Add another private field to the form:

    FLastDistance: Integer;

Add the following code to the ViewPort3D's Gesture event:

// ----------------------------------------------------------------------------
procedure TfrmMain.Viewport3D1Gesture(Sender: TObject;
  const EventInfo: TGestureEventInfo; var Handled: Boolean);
  LDelta: Single;
  if EventInfo.GestureID = igiZoom then
    if (TInteractiveGestureFlag.gfBegin in EventInfo.Flags) then
      FLastDistance := EventInfo.Distance;

    LDelta := (EventInfo.Distance - FLastDistance) / 40;
    Camera1.Position.Z := Camera1.Position.Z + LDelta;

    FLastDistance := EventInfo.Distance;


I hope this was an interesting and useful introduction to FireMonkey 3D. 

I'll expand on this and explore some more advanced features in future posts.

1 comment:

Unknown said...

Thanks for sharing it..it is interesting for beginner like me