Three Dimensional FireMonkey Component Design

FireMonkey provides access to three dimensional object creation and manipulation. While it is fun to build complex objects by connecting together the supplied primitives  (Rectangle, Cone, Sphere, etc.) this could get very tedious so I want to explore creating more complex objects directly in code.

I chose a simple rod with bends as an initial aim. The problem was simplified considerably by keeping the bends in the XY plain and once you have the object FireMonkey makes rotating, moving and scaling it easy. This was a proof of concept only, there are still issues but I think the results are useful.

By inheriting from TCustomMesh I was able to build the mesh data and let Firemonkey do the rest

.

There are two aspects of the mesh data to populate these are Data.VertexBuffer and Data.IndexBuffer.

The Vertex buffer contains an array of TPoint3D and the Index buffer holds the integer index of the Vertex buffer of the three points of a triangle to be rendered. I used a basic building block of a square so each square added four point to the Vertex buffer and six integers to the index buffer to show which TPoint3D’s make up each of two triangles that make up the square. In XE2 the actual order of the indexs determines which face of the triangle is rendered, I have not tested this in XE3 and it is possible that both faces are now rendered.  http://www.directxtutorial.com/tutorial9/b-direct3dbasics/dx9B3.aspx provides an overview of this structure as required by the rendering engine.

The basic building function AddSq then becomes

 procedure TRodSectionBase.AddSq(ASq: TSqArray; var AVOffset, AIOffset: Integer);
 begin
   VBuffer.Vertices[AVOffset] := ASq[0];
   VBuffer.Vertices[AVOffset + 1] := ASq[1];;
   VBuffer.Vertices[AVOffset + 2] := ASq[2];;
   VBuffer.Vertices[AVOffset + 3] := ASq[3];;
   IBuffer.Indices[AIOffset] := AVOffset + 3;
   IBuffer.Indices[AIOffset + 1] := AVOffset + 2;
   IBuffer.Indices[AIOffset + 2] := AVOffset + 1;
   IBuffer.Indices[AIOffset + 3] := AVOffset + 1;
   IBuffer.Indices[AIOffset + 4] := AVOffset + 0;
   IBuffer.Indices[AIOffset + 5] := AVOffset + 3;
   Inc(AVOffset, 4);
   Inc(AIOffset, 6);
 end;

This takes an Array[0..3] of TPoint3D and adds the appropriate entries on to the Vertex and Index buffer at the location indicated by the offsets. Having set up the mesh to render a square it is easy to build a cylinder by calculating the points around the radius at each end and calling

procedure TRodSectionBase.BuildCylinder(RadiusArray1, RadiusArray2: TPathArray;
 var AVOffset, AIOffset: Integer);
 Var
   I, sz: Integer;
 begin
   sz := high(RadiusArray1);
   if high(RadiusArray2) <> sz then
      raise Exception.Create('BuildCylinder');

   I := 0;
   while I < sz do
     begin
       AddSq(SqArray(RadiusArray1[I + 1], RadiusArray1[I], RadiusArray2[I],
        RadiusArray2[I + 1]), AVOffset, AIOffset);
       Inc(I);
     end;
   AddSq(SqArray(RadiusArray1[0], RadiusArray1[sz], RadiusArray2[sz],
   RadiusArray2[0]), AVOffset, AIOffset);
 end;

While the two radius arrays require the same number of points the disk planes do not have to be parallel so these cylinder sections can be put together to make a bend.

So we can now build straight cylinders or bend segments and the complex object is built by stacking these segments together. In this case I use a linked list. Each segment calculates its own contribution including that of all its children to the size of the two buffers. This allows the parent control to set the total buffer size required before asking the top segment to first build its own mesh data and then request its child segment to do the same.

procedure TThreeDRod.RebuildMesh;
 var
    NxtData, NxtIndex: Integer;
    I: Integer;
 begin
   NxtData := 0;
   NxtIndex := 0;
   Data.VertexBuffer.Length := FSections.NoOfVertexes;
   Data.IndexBuffer.Length := FSections.NoOfIndexes;
   FSections.AddData(NxtData, NxtIndex);
{$IFDEF VER230}
   Data.CalcNormals;  // XE2
{$ELSE}
   Data.CalcFaceNormals;  // XE3
{$ENDIF}
 end;

The original plan was to demonstrate the ability to build objects on the fly in code but it was a relatively simple exercise to specify the segment parameters as text and making the TThreeDRod a FireMonkey component which can be manipulated in the IDE. This required adding a TStringlist object to the properties and building the segments by adding a little code to the rebuild mesh.

procedure TThreeDRod.RebuildMesh;
 var
   NxtData, NxtIndex: Integer;
   I: Integer;
 begin
   if FSections = nil then
     Begin
      if FConstructionText.Count > 0 then
        for I := 0 to FConstructionText.Count - 1 do
             AddSectionFrmText(FConstructionText[I]);
      if FSections = nil then
        exit;
     End;
   NxtData := 0;
   NxtIndex := 0;
   Data.VertexBuffer.Length := FSections.NoOfVertexes;
   Data.IndexBuffer.Length := FSections.NoOfIndexes;
   FSections.AddData(NxtData, NxtIndex);
{$IFDEF VER230}
   Data.CalcNormals; // XE2
{$ELSE}
   Data.CalcFaceNormals; // XE3
{$ENDIF}
end;

The code snippets highlight the methods used but the full code and package file can be accessed from http://www.innovasolutions.com.au/delphistuf/AThreeDRod.htm.

What I am doing now

Posted in FMX, IDE Tagged with: , , ,
One comment on “Three Dimensional FireMonkey Component Design
  1. Thx, Roger. I’ve been wanting to change my mapping to 3D and FireMonkey looks like the thing to use.