menu
  Home  ==>  papers  ==>  graphic  ==>  delphi_3d_designer   

Delphi 3D Designer - Felix John COLIBRI.

  • abstract : drawing 3D shapes and changing the camera and screen position with the mouse
  • key words : 3D graphics, geometry, perspective, floodfill, vector graphics, camera, World view, homogeneous transforms
  • software used : Windows XP, Delphi 6
  • hardware used : Pentium 1.400Mhz, 256 M memory, 140 G hard disc
  • scope : Delphi 1 to 8 for Windows, Kylix
  • level : all developers
  • plan :


1 - Introduction

I needed to draw some figures in 3D for code reengineering (module dependency graphs and the like). I had 2 choices:
  • use any OpenGL or other wonderful, but huge, tool
  • grab my old DOS 3D program, and adapt it to Delphi.
I chose the last solution, and present here the whole library allowing to draw 3D figures. The main addition is the control of the point of view parameters with the mouse.

Here is an example of the program with the project of our kitchen furniture:




3D Geometry

Let us take the simple example of a small house. Here is our house in world coordinates:

home in world coordinates

We look at this home using a camera, which is places somewhere in the world:

home and camera

To represent this home, we place a screen between the camera and the origin of the World:

image



We want to compute the screen coordinates of each point in the World.



2.1 - Homogeneous coordinates

To derive the projection formulas, we will use homogeneous coordinates. Transforms are computed using 4 by 4 matrices, where the last row is used to compute translation. The matrices of the 3 basic transforms are
  • scaling

    homogeneous scaling

  • rotation around the vertical axis:

    homogeneous rotation

  • translation

    homogeneous translation

If we apply successive transforms, we simply multiply the corresponding matrices.



2.2 - 2d projection

Let us assume that the camera is at position (6, 8, 7.5):

world to camera

Here are the transforms which will bring the World coordinates into the Camera coordinates:

  • first we translate the world to the camera origin

    1 world translate

    This corresponds to the following transform matrix:

    1 world translate

  • we rotate the camera axes around Ox by 90 degrees

    2 world Ox'

    This corresponds to the following transform matrix:

    1 world translate

  • rotate the Y'OZ' plane around OY' so that it contains the world's origin

    homogeneous translation

    The transform is:

    1 world translate

  • now point Oz' thru the world's origin by rotating around Ox':

    2 world Oz' thru origin

    The matrix is:

    1 world translate



The complete change of axes is then T1* T2* T3* T4



Finally we place the screen at different distance from the camera:

scale wr to screen

and here is the formula:

screen scale



2.3 - The Transform formulas

Using matrix multiplication is correct, but indexing matrices is very costly. So it is quicker to use the corresponding formulas. Replacing our litteral values with variables, and with the following notations
  • X Y Z are the World coordinates
  • ABC is the camera position
  • X' Y' Z' are the screen values
Here are the formulas for the change of axes:

transform formulas



2.4 - Hidden faces

Our old program used full Gouraud / Phong shading. Since this was not the most important point, we decided to use simple floodfill for the coloring of our scene. So a place surface is declared visible if the cross product of the origin-to-camera vector with the normal to the surface is negative

hidden surface




3 - The Mouse Control

3.1 - Camera position control

To set and change the camera position, we first started with usual tEdit. Once the program was working, this quickly became tedious, so we tried to use mouse control.



Using 3 tTrackbars for the X, Y, Z camera position is easy, but not very satisfying for change of the result. The rotations are more intuitive.



So the next trial was to use 3 parameters:

  • azimuth which is the angle in the xOy plane
  • elevation, which is the angle in the vertical plane
  • radius, which is the distance between the camera and the origin
hidden surface

Conversion of an (azimuth, elevation, radius) system to the (x, y, z) system is quite easy.



The previous system requires 3 different mouse controls. We tried to use fewer controls, trying to represent the camera in a 3d space (on our flat screen). To achieve this, we represented the axes in perspective in a combo box, and added

  • an ellipse in perspective, representing the unit cylinder
  • the camera with its projection on the xOy plane
scaler

and changing the camera position is done by moving the 2 resizing plots:

scaler movements

The most difficult part was to keep the elevation constant when changing the azimuth or the scale. In addition, changing the elevation also changes the distance of the camera to the origin, and therefore the size of the result is modified. Here is the difference between the "spherical" and the "projection" approaches:

spherical sizing

We kept the 2 mouse control in the Delphi code, but we more often use the classic 3 controls adjustments.



3.2 - Screen position control

The screen position is controlled by another visual component, which represents the position of the screen in the camera system.

screen movement

Here is this control:

screen
scale control



3.3 - Object movements

Once all 3d drawings were correct, we tried to use the mouse to move several objects around.

The objects are placed in a list. An object is selected when the mouse is clicked inside the object's screen representation. We will not be able to reach nested objets, but those objects will also not be visible.

We tried here also to move object with 2D mouse movements, but this did not work very well. We assumed that comparing the mouse direction with the 3 axes could yield the direction closes to the mouse movement. However the mouse events are usually fired for each 1 or 2 pixels movements, and for those distances, angle computations are not accurate. Accumulating several movements could be a solution. Or waiting for an initial accumulation of 5 or 10 pixels, and after the direction selection, allow 1 pixel sensibility.

In the project below, we settled for a separate direction movement:

  • the user selects the direction with x, y and z tRadioButtons
  • the mouse is moved around
  • the object is moved in this direction proportionally to the mouse movements
Even with this simplification, moves are not ideal: the mouse moves in projection space, and the objects are in 3d, with different scale. So we still have to match somehow the screen distance with the 3d distance.




4 - the Delphi Source Code

4.1 - The vector library

To represent points, lines, surfaces and volume, we use records with (X, Y, Y) coordinates.

The main question is whether we should use simple records, or CLASSes. I first started with CLASSes, and whenever some geometric computations had to be performed, the objects had to be created with a CONSTRUCTOR and freed after the use. This is not necessary with simple records, and if they are local variables, the stack nicely does all the housecleaning.

Here is an example with the computation of the cosine of two vectors. We have three points: the origin, and the two points. Here are some possibilities:

  • when we use CLASSes, the computation could look like this

    procedure compute(p_x_ap_y_bp_x_bp_y_bDouble);
      var l_c_al_c_bc_2d_point;
      begin
        l_c_a:= c_2d_point.create_2d_point(p_x_ap_y_a);
        l_c_b:= c_2d_point.create_2d_point(p_x_bp_y_b);

        l_c_a.normalizel_c_b.normalize;
        l_cosine:= f_cross_product(l_c_al_c_b);

        l_c_a.Freel_c_b.Free;

        // -- ...
      end// compute

  • using RECORDs, since Delphi FUNCTIONs can return RECORDs (and not only pointers to RECORDs like Standard Pascal) the style could be:

    procedure compute(p_x_ap_y_bp_x_bp_y_bDouble);
      var l_al_bt_2d_point;
      begin
        l_a:= f_2d_point(p_x_ap_y_a);
        l_b:= f_2d_point(p_x_bp_y_b);

        l_a:= f_2d_normalize(l_a); l_b:= f_2d_normalize(l_b);
        l_cosine:= f_cross_product(l_al_b);

        // -- ...
      end// compute

    or even:

    procedure compute(p_x_ap_y_bp_x_bp_y_bDouble);
      begin
        l_cosine:= f_cross_product(
            f_2d_normalize(f_2d_point(p_x_ap_y_a)),
            f_2d_normalize(f_2d_point(p_x_bp_y_b)));
        // -- ...
      end// compute

Since in this application we did a lot of angle computations, normals, directions etc, we decided to use RECORDs with the functional style. We kept the CLASSes to handle the structuring of elements (lists of points, segments, surfaces etc)



4.2 - The 3D point UNIT

Our UNIT has the following INTERFACE:

type t_2d_pointrecord
                   m_xm_yDouble;
                 end// t_2d_integer_point
     t_pt_2d_point= ^ t_2d_point;

function f_2d_point(p_xp_yDouble): t_2d_point;
function f_zero_2d_pointt_2d_point;
function f_2d_cross_product(const pk_2d_point_1pk_2d_point_2t_2d_point): Double;
function f_2d_norm(const pk_2d_pointt_2d_point): Double;
function f_normalize_2d_point(const pk_2d_pointt_2d_point): t_2d_point;

function f_2d_cosine(const pk_2d_point_1pk_2d_point_2t_2d_point): Double;
function f_2d_swap_xy(const pk_2d_pointt_2d_point): t_2d_point;
function f_2d_on_same_side(const pk_2d_line_point_1pk_2d_line_point_2pk_2d_point_1pk_2d_point_2t_2d_point): Boolean;

function f_add_2d_point(const pk_2d_point_1pk_2d_point_2t_2d_point): t_2d_point;
function f_subtract_2d_point(const pk_2d_point_1pk_2d_point_2t_2d_point): t_2d_point;
function f_multiply_2d_point_by_scalar(const pk_2d_pointt_2d_pointp_doubleDouble): t_2d_point;
function f_2d_are_identical(const pk_2d_point_1pk_2d_point_2t_2d_point): boolean;
function f_2d_are_aligned(const pk_2d_point_1pk_2d_point_2pk_2d_point_3t_2d_point): boolean;

function f_display_2d_point(const pk_2d_pointt_2d_point): String;
function f_rotate_2d_point(p_2d_pointt_2d_pointp_angleDouble): t_2d_point;

procedure draw_2d_point(p_2d_pointt_2d_pointp_c_canvastCanvasp_pen_colorp_radiusInteger);



4.3 - The 2D UNIT

In a similar fashion, the 3d version is:

type t_3d_pointrecord
                   m_xm_ym_zDouble;
                 end// t_3d_point

function f_3d_point(p_xp_yp_zDouble): t_3d_point;
Function f_3d_clone(const pk_3d_pointt_3d_point): t_3d_point;
function f_zero_3d_pointt_3d_point;
procedure reset_3d_point(var pv_3d_pointt_3d_point);
Function f_3d_cross_product(const pk_3d_point_1pk_3d_point_2t_3d_point): Double;
function f_3d_external_product(const pk_3d_point_1pk_3d_point_2t_3d_point): t_3d_point;
Function f_3d_point_norm(const pk_3d_pointt_3d_point): Double;
function f_normalize_3d_point(const pk_3d_pointt_3d_point): t_3d_point;
Procedure normalize_3d_point(var pv_3d_pointt_3d_point);

function f_add_3d_point(const pk_3d_point_1pk_3d_point_2t_3d_point): t_3d_point;
function f_subtract_3d_point(const pk_3d_point_1pk_3d_point_2t_3d_point): t_3d_point;
function f_multiply_3d_point_by_scalar(const pk_3d_pointt_3d_pointp_doubleDouble): t_3d_point;
function f_3d_are_identical(const pk_3d_point_1pk_3d_point_2t_3d_point): boolean;
function f_3d_are_aligned(const pk_3d_point_1pk_3d_point_2pk_3d_point_3t_3d_point): boolean;

function f_display_3d_point(const pk_3d_pointt_3d_point): String;



4.4 - The Perspective CLASS

Each time that we change the camera or the screen positions, we have to recompute the transform parameters. So is rather natural to encapsulate those computations in a CLASS, and save the results in attributes. This CLASS will be handed over to the point lists of our scene. Since the CLASS will be used for all drawing, we also included attributes needed for the drawing: the tCanvas, the drawing parameters (do fill the facet with colors or not, draw the facet normals etc.)

Here is the CLASS definition:

c_perspective=
    Class(c_basic_object)
      m_c_canvas_reftCanvas;

      m_3d_camerat_3d_point;

      s_11s_12s_13s_21s_22s_23s_32s_33s_43Double;
      _m_screen_width_m_screen_heightinteger;
      _m_screen_center_x_m_screen_center_yinteger;

      // -- for the axis display
      m_3d_origint_3d_point;
      m_2d_origint_2d_integer_point;

      // -- options
      m_draw_colorBoolean;
      m_draw_normalBoolean;

      constructor create_perspective(p_nameStringp_c_canvastCanvas);

      procedure initialize_screen_position(p_center_xp_center_y,
          p_widthp_heightinteger);
      procedure initialise_camera_position(p_camera_x,
          p_camera_yp_camera_zDoublep_scaleDouble);

      function f_project_3d_point(const pk_3d_pointt_3d_point): t_2d_point;
      function f_project_3d_point_integer(const pk_3d_pointt_3d_point): t_2d_integer_point;

      procedure draw_3d_segment(pk_3d_point_1pk_3d_point_2t_3d_point;
          p_colorInteger);
      procedure draw_2d_point(const pk_2d_pointt_2d_pointp_pen_colorInteger);
      procedure draw_3d_point(const pk_3d_pointt_3d_pointp_pen_colorInteger);
      Procedure draw_unit_axes(p_colorInteger);
    end// c_perspective

and here is the transform computation:

procedure c_perspective.initialise_camera_position(p_camera_x,
      p_camera_yp_camera_zDoublep_scaleDouble);
    // -- compute the transformation coefficients
  var l_xy_radiusl_radiusl_d23Double;
  begin
    l_xy_radius:= Sqrt(Sqr(p_camera_x)+ Sqr(p_camera_y));
    l_radius:= Sqrt(Sqr(p_camera_x)+ Sqr(p_camera_y)+ Sqr(p_camera_z));
    l_d23:= l_xy_radiusl_radius;

    s_11:= - p_camera_yp_scalel_xy_radius;
    s_12:= - p_camera_xp_camera_zp_scalel_d23;
    s_13:= - p_camera_xl_radius;

    s_21:=   p_camera_xp_scalel_xy_radius;
    s_22:= - p_camera_yp_camera_zp_scalel_d23;
    s_23:= - p_camera_yl_radius;

    s_32:= l_xy_radiusp_scalel_radius;
    s_33:= - p_camera_zl_radius;
    s_43:= l_radius;

    normalize_3d_point(m_3d_camera);
  end// initialise_point_de_vue

with the associated computation of a point projection:

function c_perspective.f_project_3d_point(const
    pk_3d_pointt_3d_point): t_2d_point;
  Var l_x_tl_y_tl_z_tDouble;
  begin
    l_z_t:= pk_3d_point.m_xs_13
        + pk_3d_point.m_ys_23pk_3d_point.m_zs_33s_43;
    l_x_t:= (pk_3d_point.m_xs_11pk_3d_point.m_ys_21)/ l_z_t;
    l_y_t:= (pk_3d_point.m_xs_12
        + pk_3d_point.m_ys_22pk_3d_point.m_zs_32)/ l_z_t;

    // -- simple translation / scaling for screen
    Result.m_x:= 1.0* _m_screen_center_xl_x_t_m_screen_width;
    Result.m_y:= 1.0* _m_screen_center_yl_y_t_m_screen_height;
  end// f_project_3d_point



4.5 - The Screen controller

This CLASS will allow the user to resize and move the screen representation. Basically, it includes a tPaintBox (since we must draw rectangles) and mouse events.

Usually, we have two options:

  • either drop the tPaintBox on the tForm, and delegate the events to the tForm
  • or build a component and place it on the Palette
The first solution is a one shot implementation. Building a component is easy, but requires reinstallation when we change PC's, Windows or Delphi version.

So we prefer a third solution where the most possible is encapsulated in a CLASS which is simple included in the main UNIT. To do so

  • we build the c_screen_controller CLASS, complete with then paint and mouse events
  • the CONSTRUCTOR receives a container (a tPanel) creates a tPaintbox inside of this container, and initialize the events of this tPaintBox
  • when the user clicks the mouse, the events are directly forwarded to this CLASS. We eventually prepare notifications to the user, if required.
The user of this CLASS simply specifies which tPanel will contain the controller, and uses the notifications to update the display or interrogates the CLASSe's attributes for other processings. So this is like a component, but without the installation procedure (and without the Object Inspector facilities).



Here is the definition of the c_screen_controller:

 t_notify_changeProcedure;
 c_screen_controlerclass(c_basic_object)
                       m_c_parent_panel_reftPanel;
                       m_c_paintboxtPaintBox;
                       m_base_ratioDouble;

                       m_scale_start_xm_scale_start_yInteger;
                       m_scale_end_xm_scale_end_yInteger;

                       m_ratio_heightm_ratio_widthDouble;
                       m_ratio_xm_ratio_yDouble;

                       // -- the fixed figure representing the screen
                       m_base_xm_base_yInteger;
                       m_base_widthm_base_heightInteger;

                       // -- for moving (Right click)
                       m_mouse_xm_mouse_yInteger;

                       m_on_notify_changet_notify_change;

                       Constructor create_screen_controler(p_nameString;
                           p_c_parent_panel_reftPanel;
                           p_base_ratioDouble;
                           p_ratio_widthp_ratio_heightDouble;
                           p_ratio_xp_ratio_yDouble;
                           p_handle_scaler_changet_notify_change);

                       function f_display_scaleString;
                       function f_display_ratiosString;
                       procedure draw_rectangle(p_scale_end_xp_scale_end_yInteger);
                       procedure handle_paint(SenderTObject);

                       procedure handle_mouse_down(SenderTObject;
                           p_mouse_buttonTMouseButton;
                           p_shift_stateTShiftStatep_xp_yInteger);
                       procedure handle_mouse_move(SenderTObject;
                           p_shift_stateTShiftStatep_xp_yInteger);

                       Destructor DestroyOverride;
                     end// c_screen_controler



4.6 - The Azimuth / Elevation / Scale control

The CLASS representing the camera class plays exactly the same role, but for handling the 2 point camera control. This CLASS is defined as:

 t_procedureProcedure(p_beforeBoolean);
 c_camera_controlerClass(c_basic_object)
                            m_c_paintbox_reftPaintBox;
                            _m_c_canvas_reftCanvas;

                            // -- optimization: avoid recalculations
                            m_2d_integer_origint_2d_integer_point;
                            m_2d_origint_2d_point;
                            m_2d_origin_xm_2d_origin_yt_2d_point;

                            // -- the "unit" circle (half the paintbox)
                            m_unit_widthm_unit_heightInteger;

                            // -- the mouse in the world xy plane
                            m_2d_azimutht_2d_point;
                            // -- the mouse in the vertical
                            m_2d_elevationt_2d_point;

                            m_azimuthm_elevationm_scale_ratioDouble;
                            // -- when arrives in Oz // Omouse
                            m_last_known_elevationDouble;
                            m_is_azimuthm_is_elevationBoolean;

                            m_on_change_controlert_procedure;

                            constructor create_camera_controler(p_nameString;
                                p_c_paintbox_reftPaintBox;
                                p_controler_ratioDouble;
                                p_on_change_controlert_procedure);

                            function f_azimuth_angle(p_xp_yDouble): Double;
                            function f_elevation_angle(p_xp_yDouble): Double;

                            Procedure draw_unit_axes(p_colorInteger);
                            procedure draw_all(p_colorInteger);

                            procedure update_controler(p_xp_yInteger);

                            procedure handle_mouse_down(SenderTObject;
                                p_mouse_buttonTMouseButton;
                                p_shift_stateTShiftStatep_xp_yInteger);
                            procedure handle_mouse_move(SenderTObject;
                                p_shift_stateTShiftStatep_xp_yInteger);

                            procedure initialize_position;

                            Destructor DestroyOverride;
                          end// c_camera_controler_controler



With the 2d and 3d point units, the perspective primitives and the camera / screen controllers, we are able to build a 2d projection of any 3d vector graphic scene. We will illustrate the use of our units with a block world.



4.7 - Line, Surface, Volume, Scene

Our figure elements use
  • the point RECORDs presented above
  • those records are placed in c_vertex CLASS (for on the fly computations, RECORDs are used, but for container storage, CLASSes are better)

    c_3d_vertex// one "3d_vertex"
      Class(c_basic_object)
        m_vertex_idinteger;
        m_3d_pointt_3d_point;

        constructor create_3d_vertex(p_nameStringp_3d_pointt_3d_point);
        function f_display_3d_vertexString;
        function f_c_selfc_3d_vertex;

        procedure translate_3d_vertex(p_3d_deltat_3d_point);
     end// c_3d_vertex

  • the 3D poly line (not necessary a planar shape) defined as a tStringlist of c_vertex

    c_3d_vertex_list// "3d_vertex" list
      Class(c_basic_object)
        _m_c_3d_vertex_listtStringList;
        m_is_closedBoolean;

        Constructor create_3d_vertex_list(p_nameString);

        function f_3d_vertex_countInteger;
        function f_c_3d_vertex(p_3d_vertex_indexInteger): c_3d_vertex;
        function f_index_of(p_3d_vertex_nameString): Integer;
        function f_c_find_by_3d_vertex(p_3d_vertex_nameString): c_3d_vertex;
        procedure add_3d_vertex(p_3d_vertex_nameString;
            p_c_3d_vertexc_3d_vertex);
        function f_c_add_3d_vertex(p_3d_vertex_nameString;
            p_3d_pointt_3d_point): c_3d_vertex;
        procedure display_3d_vertex_list;
        function f_vertex_list_namesString;

        procedure draw_vertex_list(p_c_perspectivec_perspective;
            p_pen_colorInteger);
        procedure translate_3d_vertex_list(p_3d_deltat_3d_point);

        Destructor DestroyOverride;
      end// c_3d_vertex_list

  • the planar surfaces, delimited with a border poly-line are defined by the c_facet CLASS

    c_facet// one "facet"
      Class(c_basic_object)
        m_c_3d_vertex_listc_3d_vertex_list;

        m_normal_signInteger;
        m_3d_normalt_3d_point;

        m_facet_colorInteger;

        Constructor create_facet(p_nameStringp_normal_signInteger);
        function f_display_facetString;
        function f_c_selfc_facet;
        procedure compute_normal;
        procedure draw_facet(p_c_perspectivec_perspective
            p_pen_colorInteger);
        function f_3d_normal_endt_3d_point;

        function f_is_2d_point_in_2d_facet(p_2d_pointt_2d_point;
            p_c_perspectivec_perspective): boolean;

        procedure translate(p_dxp_dyp_dzDouble);
        procedure translate_facet(p_3d_deltat_3d_point);

        Destructor DestroyOverride;
      end// c_facet

    Note that

    • c_facet and c_vertex list are nearly identical, and one of them could be removed (or the c_facet could be a planar c_vertex_list descendent). In our case, we consider the c_facet as a planar c_vertex_list, with a m_3d_normal vector, and this is the reason why we kept those separate.
    • the normal is build using the first 3 points. We assume they are not aligned (no check).
    • the sign is introduced to orient the facets (clock wise or anti clock wise), and this is specified at creation time
  • the volume are built from c_facets and are handled with the c_facet_list CLASS

    c_facet_list// "facet" list
      Class(c_basic_object)
        m_c_facet_listtStringList;

        Constructor create_facet_list(p_nameString);

        function f_facet_countInteger;
        function f_c_facet(p_facet_indexInteger): c_facet;
        function f_index_of(p_facet_nameString): Integer;
        function f_c_find_by_facet(p_facet_nameString): c_facet;
        procedure add_facet(p_facet_nameStringp_c_facetc_facet);
        function f_c_add_facet(p_facet_nameString;
            p_normal_signInteger): c_facet;
        procedure display_facet_list;

        function f_c_selfc_facet_list;

        procedure compute_facet_normals;
        procedure draw(p_c_perspectivec_perspective;
            p_pen_colorInteger);

        function f_is_selected(p_2d_mouset_2d_point;
            p_c_perspectivec_perspective): Boolean;
        procedure translate(p_dxp_dyp_dzDouble);
        procedure translate_facet_list(p_3d_deltat_3d_point);
        procedure display_unique_facet_list;
        procedure set_position(p_3d_positiont_3d_point);

        Destructor DestroyOverride;
      end// c_facet_list

  • several volumes are managed with a c_volume_list

    c_volume_list// "volume" list
      Class(c_basic_object)
         m_c_volume_listtStringList;

         Constructor create_volume_list(p_nameString);

         function f_volume_countInteger;
         function f_c_volume(p_volume_indexInteger): c_facet_list;
         function f_index_of(p_volume_nameString): Integer;
         function f_c_find_by_volume(p_volume_nameString): c_facet_list;
         procedure add_volume(p_volume_nameStringp_c_volumec_facet_list);
         function f_c_add_volume(p_volume_nameString): c_facet_list;
         procedure display_volume_list;

         procedure draw(p_c_perspectivec_perspectivep_pen_colorInteger);

         procedure add_cube(p_cube_nameString);
         procedure add_pyramid(p_pyramid_nameString);
         procedure add_horizontal_plane(p_plane_nameString);
         procedure add_vertical_x0z_plane(p_plane_nameString);
         procedure add_vertical_y0z_plane(p_plane_nameString);

         function f_c_find_selected_volume(p_2d_mouset_2d_point;
             p_c_perspectivec_perspective): c_facet_list;

         Destructor DestroyOverride;
       end// c_volume_list

    and the volumes are build by separate functions like this cube:

    function f_c_cube(p_nameString;
        p_3d_originp_3d_xp_3d_yp_3d_zt_3d_point): c_facet_list;
      var l_3d_Al_3d_Bl_3d_Cl_3d_D,
            l_3d_El_3d_Fl_3d_Gl_3d_Ht_3d_point;
      begin
        l_3d_A:= p_3d_origin;
        l_3d_B:= f_add_3d_point(l_3d_Ap_3d_y);
        l_3d_C:= f_add_3d_point(l_3d_Bp_3d_z);
        l_3d_D:= f_add_3d_point(l_3d_Ap_3d_z);

        l_3d_F:= f_add_3d_point(l_3d_Bp_3d_x);
        l_3d_G:= f_add_3d_point(l_3d_Cp_3d_x);
        l_3d_H:= f_add_3d_point(l_3d_Dp_3d_x);
        l_3d_E:= f_add_3d_point(l_3d_Ap_3d_x);

        Result:= c_facet_list.create_facet_list(p_name);

        with Result do
        begin
          with f_c_add_facet('face1', - 1), m_c_3d_vertex_list do
          begin
            f_c_add_3d_vertex('A'l_3d_A);
            f_c_add_3d_vertex('B'l_3d_B);
            f_c_add_3d_vertex('C'l_3d_C);
            f_c_add_3d_vertex('D'l_3d_D);
          end// with f_c_add_facet

          with f_c_add_facet('face2', 1), m_c_3d_vertex_list do
          begin
            f_c_add_3d_vertex('B'l_3d_B);
            // -- ... C G F
          end// with f_c_add_facet

          // -- front
          with f_c_add_facet('face3',  - 1), m_c_3d_vertex_list do
            // -- ... G F E H

          // -- top
          with f_c_add_facet('face4', 1), m_c_3d_vertex_list do
            // -- ... H G C D

          compute_facet_normals;
        end// with p_c_facet_list
      end// f_c_cube

    Notice that we did not build the 8 faces, since we mainly displayed the scene with the camera in the first quadrant (x, y, z >= 0). Using the 3 spherical tTrackbar, those movements are not limited though (we can view the scene from "behind", or even "below")

  • the volume list, the perspective, and the final display are enclosed in a c_scene CLASS, where all mouse selection and movements in the block display are handled:

    t_direction= (e_unknown_directione_x_directione_y_directione_z_direction);
    t_notify_changeProcedure;

    c_sceneclass(c_basic_object)
               m_c_scene_paintbox_reftPaintBox;
               m_c_volume_list_refc_volume_list;
               m_c_perspective_refc_perspective;

               m_directiont_direction;
               m_on_selection_change_eventt_notify_change;

               m_c_selected_volumec_facet_list;

               Constructor create_scene(p_nameString;
                   p_c_scene_paintbox_reftPaintBox;
                   p_c_volume_list_refc_volume_list;
                   p_c_perspective_refc_perspective;
                   p_on_selection_change_eventt_notify_change);

               procedure set_direction(p_directiont_direction);

               procedure handle_paint(SenderTObject);
               procedure handle_mouse_down(SenderTObject;
                   p_mouse_buttonTMouseButton;
                   p_shift_stateTShiftStatep_xp_yInteger);
               procedure handle_mouse_move(SenderTObject;
                   p_shift_stateTShiftStatep_xp_yInteger);

               Destructor DestroyOverride;
            end// c_scene




4.8 - UML Class diagram

The UML diagram of this graphic library is the following:

image

The nice thing about UML diagrams is that they highlighte possible reorganizations. In our case, the main tForm was used to check compilation of each CLASS. Then references of some CLASSes were placed in container CLASSes and the structure was constructed in a progressive fashion. But the diagram shows that:

  • we could get rid of the g_c_volume_list in the main tForm, and let the c_scene regroup all volumes
  • this is also true for the g_c_perspective global variable
image



4.9 - Building the Scene

We then added a Palette with some standard volumes (a cube, a pyramid, and 2 planes. The mouse then is used to move those blocks around.



4.10 - The result

Here is a snapshot of the final application, with a couple of cubes:

3 cubes

If we want to see at which hight the block are, we place the camera at ground level (xOy):

3 cubes

Notice that we removed coloring to accelerate the movements (floodfill is very slow). The movement was performed using the spherical controls. And here is a top-view, after centering the figure with the screen controller, and without the optional red normals

3 cubes



4.11 - Mini HowTo

To use the project
  • compile
  • create the screen controller, camera controler and an empty volume list by clicking "create_"
  • add a object by clicking the "cube" speed button (or the "pyramid" ...)
  • select a direction ("x_", "y_", "z_") and move the object away from the origin by dragging the mouse
  • change the scaling or centering by using the screen control
  • change the camera position using either the 2 point representation or the spherical 3 trackbars



5 - Improvements

This tools was build to solve a specific problem (3d representation and easy camera / screen movement). So the volume part is not optimized in any way. We could improve, or add:
  • an object designer, which could be used:
    • to build other shapes than those already defined
    • to modify an object (move the vertices, the facets, add facets etc)
  • add other volume movements than translations (rotations, scaling)
  • on the representation side
    • use different coloring (gouraud, phong)
  • for object management
    • add load and save (streaming)
    • add object hierarchies
    • use some scripting language to build the scene
    • using this textual representation, implement constraints
  • on the control side
    • allow multi selection



6 - Download the Sources

Here are the source code files:

Those .ZIP files contain:
  • the main program (.DPR, .DOF, .RES), the main form (.PAS, .DFM), and any other auxiliary form
  • any .TXT for parameters
  • all units (.PAS) for units
Those .ZIP
  • are self-contained: you will not need any other product (unless expressly mentioned).
  • can be used from any folder (the pathes are RELATIVE)
  • will not modify your PC in any way beyond the path where you placed the .ZIP (no registry changes, no path creation etc).
To use the .ZIP:
  • create or select any folder of your choice
  • unzip the downloaded file
  • using Delphi, compile and execute
To remove the .ZIP simply delete the folder.



As usual:

  • please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs, broken links or had some problem downloading the file. Resulting corrections will be helpful for other readers
  • we welcome any comment, criticism, enhancement, other sources or reference suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
  • or more simply, enter your (anonymous or with your e-mail if you want an answer) comments below and clic the "send" button
    Name :
    E-mail :
    Comments * :
     

  • and if you liked this article, talk about this site to your fellow developpers, add a link to your links page ou mention our articles in your blog or newsgroup posts when relevant. That's the way we operate: the more traffic and Google references we get, the more articles we will write.



7 - Conclusion

This project presents a 3d perspective library which can be used for individual point 2d representations, or to build more complex object scenes




8 - The author

Felix John COLIBRI works at the Pascal Institute. Starting with Pascal in 1979, he then became involved with Object Oriented Programming, Delphi, Sql, Tcp/Ip, Html, UML. Currently, he is mainly active in the area of custom software development (new projects, maintenance, audits, BDE migration, Delphi Xe_n migrations, refactoring), Delphi Consulting and Delph training. His web site features tutorials, technical papers about programming with full downloadable source code, and the description and calendar of forthcoming Delphi, FireBird, Tcp/IP, Web Services, OOP  /  UML, Design Patterns, Unit Testing training sessions.
Created: mar-05. Last updated: jul-15 - 98 articles, 131 .ZIP sources, 1012 figures
Copyright © Felix J. Colibri   http://www.felix-colibri.com 2004 - 2015. All rigths reserved
Back:    Home  Papers  Training  Delphi developments  Links  Download
the Pascal Institute

Felix J COLIBRI

+ Home
  + articles_with_sources
    + database
    + web_internet_sockets
    + oop_components
    + uml_design_patterns
    + debug_and_test
    + graphic
      – delphi_3d_designer
      – write_a_flash_player
      – delphi_video_player
    + controls
    + colibri_utilities
    + colibri_helpers
    + delphi
    + firemonkey
    + compilers
  + delphi_training
  + delphi_developments
  + sweet_home
  – download_zip_sources
  + links
Contacts
Site Map
– search :

RSS feed  
Blog