Creating 3D scenes dynamically in FireMonkey
In my last post I created a wind turbine (or propeller if you like 🙂 ). When I demonstrated this wind turbine at a recent ADUG meeting here in Melbourne, Brian Watson asked whether it was possible to create objects in code and add them to a scene at run time. I said it was – never having tried it (!) – so this is a post to show that it is possible and which will allow the user to “paint” simple 3D objects onto a landscape by holding the left mouse button down and moving the mouse around. Here’s a (clockwise from top left) collection of screen shots showing the program in use. In the last screen shot the angle of the sun has also been altered to show that the objects really are still 3D objects and that they are not simply painted on a background image.
And, below, a close up shot of the last image above, but with a different angle of the sun. If you have young children they can have quite a bit of fun with this program – especially if you extend it and add more interesting objects such as cars, animals and people. Be warned – you’ll then be asked to add loading, saving and printing!
If you run the program you’ll be more convinced that every house and tree is a 3D object as you can move the Sun across the sky using the “Sun” button and you can Undo objects you have added to the landscape. Both the Sun and Undo buttons have their auto-repeat property set – which generates continuous click events when the mouse button is held down.
I’ll briefly describe how the form is constructed. As in the previous post, you start out by choosing to create a new 3D Firemonkey application.
Add a 3D light (as the sun) and a TImage3D control (as the landscape) to the form, load the TImage3D with some 2D artwork you have created (or found on the internet) and then rotate it about the x axis: I have set it to -90 degrees as this works well with the default camera position.
Next add a TCone as a tree and adjust it’s size and material properties to your liking. All trees added by the user will be clones of this tree.
Next add a house. For the house, we’ll add a TModel3D object and load into it the simplest model of a house (it looks like a Monopoly hotel the way I have coloured it!) I could find in the 3D Warehouse. Load the model using the MeshCollection property editor.
I have found it a bit tricky to “colour” 3D models. You need to make sure the structure view shows the components of the TModel3D. I usually save, close and reopen the project and then the house will have, in this case, a single Mesh component which is the walls and roof of the house. I immediately name these TMesh components, like all objects added at design time, to something that I can work more easily with – in this case name the TMesh “walls”.
The Material property of a TMesh allows us to control how it reacts to light. The material property’s sub-properties need to be set to something like what is shown in the next picture (you can choose your own colours of course). It is the Diffuse material property that is critical as it determines how light is diffused by the walls. If you set the Emissive material property then your walls will glow at night when the sun has gone done!
The menu at the top of the form is a TLayer3D. The critical property that has been set here is Projection; it has been set to pjScreen so that no matter what you do with the camera (we are only using the default Design and Run time camera in this project), the palette of buttons will remain where it is.
This post is already quite long so in the interests of brevity, I will only briefly describe how the “painting” of the trees and houses is done. You can study the source code below for more details. The 3 events on the TImage3D landscape, MouseDown, MouseMove and MouseUp are used to control painting of the currently selected object. In our 3D world these 3 events have 2 additional parameters which we can use to transform our 2D screen coordinates to a point on the landscape using the RayCastIntersect() method.
procedure TMain.landMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Single; RayPos, RayDir: TVector3D);
Cloning the tree (a TCone) and the house (a TModel3D) is done by creating a TProxyObject that references the source object. In the case of the house we must make our TProxyObject instance reference the walls of our house, as you can see in the following code for the MouseMove event.
procedure TMain.landMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Single; RayPos, RayDir: TVector3D); var newObj: TProxyObject; HitPos: TVector3D; begin if FMouseDown then begin land.RayCastIntersect(RayPos, RayDir, HitPos); // if moved far enough if (Abs(HitPos.X - FLastX) > FSeparation) or (Abs(HitPos.Z - FLastZ) > FSeparation) then begin // add new object as a proxy object newObj := TProxyObject.Create(self); land.AddObject(newObj); case FAddable of aTree: begin newObj.SourceObject := tree; newObj.Scale.Assign(tree.Scale); newObj.RotationAngle.Y := tree.RotationAngle.Y; FSeparation := 0.3; end; aHouse: begin newObj.SourceObject := walls; // need to point at material newObj.Scale.Assign(house.Scale); newObj.RotationAngle.Y := house.RotationAngle.Y; newObj.RotationAngle.Z := FHouseAngle; FSeparation := 1.0; end; end; newObj.Position.Z := newObj.SourceObject.Position.Z; newObj.Position.X := HitPos.X; newObj.Position.Y := -HitPos.Z; // Z -> Y newObj.HitTest := False; FLastX := HitPos.X; FLastZ := HitPos.Z; end; end; end;
Cloning objects with TProxyObject saves memory (especially with complex materials and lighting) and improves run time performance as some processor intensive operations need only be done once on the original (source) object and not repeated for all cloned objects.
Here is a design time snapshot of the project with the structure view visible and showing the small set of objects placed on the form at design time. The house is selected in the Object Inspector.
Undo was simple to add: I simply ensure we are not deleting our original 2 prototype objects and, if not, remove the last object from the list of objects added to the landscape (named “land” in the project):
procedure TMain.BtnUndoClick(Sender: TObject); begin if land.ChildrenCount > 2 then land.RemoveObject(land.ChildrenCount-1); end;
Mac OSX Update
I compiled the program for OSX (no changes required!), borrowed my daughter’s Mac and captured this screen shot for those that are interested. I had to resize the window to start with to get the image to appear and the houses don’t appear to be shaded as richly as under Windows – but otherwise it is quite amazing to see the same source code produce both a Windows and Mac program. The Mac application can be downloaded from here (unzip all 4 files into a folder and run landscape).