Wednesday, March 24, 2021

FireMonkey 3D - Sun, Earth and Moon

Following up on an earlier post, Simple FireMonkey 3D, I'm going to make a simplified model of the solar system to explore multiple 3D objects interacting with each other in a scene. I'll also play around a little with light sources.

Source code for the project is available here and is compatible with the Delphi Community Edition.

The project starts off like most of my 3D projects; A blank application with a TViewport3D and a separate camera that can be used to rotate the scene and zoom in and out. Don't forget to set the TViewport3D's Color to Black. See the previous post for more details.

Our Solar System

Start by dropping a TDummy control onto the TViewport3D. This acts as a container that groups 3D controls together.

Add three TSpheres, which will represent the Sun, Earth and Moon. Size and space them appropriately with the Sun in the centre. And by "appropriately", I mean something that looks good on screen. This is definitely not to scale. 

Don't forget to set the HitTest property to False for any 3D objects. Otherwise, they could get in the way if you try to click and drag to change the camera angle with the mouse.

Add a TTextureMaterialSource, set its Texture property to an image of the Sun and set the MaterialSource property for the appropriate sphere.

Repeat this for the Earth and Moon sphere's, except use TLightMaterialSource.

TLightMaterialSource is similar to TTextureMaterialSource in that it supports an optional texture image, but it works with one or more light sources (TLight) to simulate shadows on 3D objects. I made the Ambient property a little lighter so shadows aren't as dark, making objects that aren't directly lit easier to see. Otherwise, the scene just seems too dark.

The light's behaviour can be changed using the Type property:
  • Directional (default) - All light comes into the scene from one direction with no single source.
  • Point - Light originates from a single point and shines out in all directions.
  • Spot - Light originates from a single point and shines in one direction.

Add a TLight to the Dummy and set its type to Point. By default, it's in the middle of the scene, which is good, because that's where the sun is.

Next, I'll animate the Earth orbiting around the Sun and the Moon orbiting around the Earth using a TTimer. Each time the timer fires, I increment a day counter. I can use the number of days the Earth or Moon takes to make a complete orbit and the number of days that have passed to find the angle from the body that they are orbiting. With this angle, often represented by the Greek letter θ (theta), and the radius of the orbit (r), I can get the X and Y position at that point in time.

I'm using the 2-dimensional polar coordinate system, but in this simplified model, everything is orbiting in the same plane, so it suits my purposes. If the angle exceeds one full circle, everything wraps around. The calculation for 360 degrees is the same as it is for 720 degrees, so there's no need to reset the day counter after anything makes one complete orbit. I can just keep counting. To confuse things a little, the angles used here are in radians. One full circle = 360 degrees = 2 * pi radians.

The Earth doesn't seem to be rotating in the animation. I'm incrementing the day counter by 1 on each timer tick, so each frame shows the same point in the rotation. If I wanted the Earth to rotate smoothly, I'd have to slow it down so much that one revolution around the sun would take a lot longer and who has that kind of time?

To make things even clearer, I use the same formula to draw the path of each orbit. In the TDummy's Render method, I calculate the points for angles between 0 and 2 * pi and draw lines between them.

The Moon is tidally locked to the Earth, which means the same side is always facing us. I'm adjusting the Moon's RotationAngle.Y property to do the same thing in the model.

And finally, I've set the Earth's RotationAngle.Z to -26 (may show as 334 in the object inspector) to match Earth's 26 degree axial tilt. Because seasons.

Less Traditional View

Before Copernicus, Kepler and Galileo, some people thought the Earth was at the centre of everything and that the heavenly bodies revolved around us. This is the geocentric model.

Simulating this model is very similar to the previous example with a new TDummy and three spheres, but with slightly different spacing and the Earth in the middle. I can even reuse the existing textures. Of course, this is also not to scale. I dropped a TLight onto the Sun and set the Type property to Point. Now, when the sun moves, the light source automatically moves with it.

The orbits need to be in the opposite direction as the previous example so the sun comes up in the right place, which is as simple as multiplying the angle by -1.

I'm using a day counter here, too, but since one revolution happens every day, I'm using a separate, slower counter and adding 0.005 every timer tick.

Yeah, Sure, Why Not...

Before Eratosthenes demonstrated otherwise over two thousand years ago, if people thought about the shape of the Earth at all, they might have thought that it was flat. It looks flat, right?

The map most commonly used to show this is the azimuthal equidistant (AE) projection of the Earth, centred on the North Pole with a small and local sun and moon orbiting over the surface. This projection accurately represents distances from the pole, but distorts the shape of land masses, especially in the southern hemisphere. Seriously, Australia isn't supposed to look like that.

This model is a little more complicated than the other two. Add a new TDummy and two spheres for the Sun and Moon. Again, I can reuse the existing textures. I added a TDisk and TLightMaterialSource for the Earth and set the texture to an image of the AE projection.

Ironically, this one might actually be (roughly) to scale. I mean, who the heck knows?

I want to illuminate part of the disk under the Sun and the side of the Moon facing the Sun, so I used two spot lights (TLight with type of Spot).

I placed the first light above the middle of the disk and turned it sideways (RotationAngle.Y to 90) to point at the Sun. I tweaked SpotCutOff to cast a wider beam and the brightness using SpotExponent to get the effect I was looking for; About half of the disk is lit. This light doesn't automatically move with the Sun, so every time it moves, I use the Sun's angle from the centre of the scene to keep the light pointed at it (RotationAngle.X).

The second light is also over the middle of the disk, but slight lower and is pointed at the moon. I narrowed the beam width so it wouldn't also shine on the disk and, just like with the sun, I update the angle every time the Moon moves. The calculation is identical to the previous light, but in exactly the opposite direction.

The way a light interacts with a 3D object is to simulate reflections and shadows based on the object's mesh. How each triangle in the mesh behaves is determined as the scene animates. By default, a TDisk doesn't have many subdivisions, so any animation involving light is quite blocky. Increasing the SubdivisionsAxes and SubdivisionsCap increases the number of triangles in the mesh. This additional resolution makes the disk look rounder and provides a smoother, less jagged animation. If these values are too high, it can affect performance, so you'll need to play around to strike the right balance. The same applies to other objects, like TSphere, but it's much more obvious on something simple, like TDisk.

Using the Application

The user interface is really straight forward. Select the model you want to see (or all of them at once). Toggle the animation on or off. If you rotate the camera or zoom with the mouse, you can reset these to their starting values. And finally, you can reset the orbits, which sets the day counters back to 0.

Since each simulation is grouped together on a separate TDummy component, all related components can be hidden or shown just by changing the TDummy's Visible property. Very convenient. 

I did run into a couple of issues when hiding components that make sense after the fact, but tripped me up when I first ran into them. Just be aware:

If you hide a TLight, it still emits light. You have to explicitly use its Enabled property.

Hiding and showing objects with animations attached to them disables the animations and they aren't automatically re-enabled when the objects are shown again. It's part of the reason I chose to use a TTimer instead.


I'm not sure it's obvious, but the cover image, and this alternate version, were created in Delphi using the same techniques that I used for the animations, except with much higher resolution textures. Even the polar coordinate diagram, which wasn't created in Delphi, still uses images of the Earth and Moon from the Delphi application.

Many thanks to all of the people providing free, high quality images.

High resolution textures of the Earth and clouds are from NASA's Blue Marble collection.

The other Sun, Earth and Moon textures are from Solar System Scope.

The AE projection is from


admin5150 said...

Very interesting, i never been imaging this tools can do such amazing visualization loke this...!

Guillermo Rivero said...

Excellent! Amazing!