Click here to Skip to main content
15,672,178 members
Articles / Programming Languages / C# 4.0
Article
Posted 4 Feb 2021

Stats

74.1K views
5K downloads
130 bookmarked

Graph3D: A Windows.Forms Render Control in C#

Rate me:
Please Sign up or sign in to vote.
4.98/5 (105 votes)
1 Jun 2023CPOL7 min read
An easy to use 3D control which can be integrated into an application in a few minutes
A universal ready-to-use 3D Graph control for System.Windows.Forms applications. It displays 3D functions or X,Y,Z data. The control consists of a single C# file and is optimized for maximum speed.

System.Windows.Forms 3D Graph Control in C#

Features

  • NEW: Support for drawing 3D objects. Added example "Pyramid" and "Sphere"
  • NEW: Rendering speed optimized to the extreme
  • BUGFIX: Sometimes the Z axis was drawn on top of the 3D object instead behind
  • NEW: Display a tooltip when the mouse is over a 3D point
  • NEW: Resizing of 3D object when resizing 3D control
  • NEW: Completely rewritten to allow display of multiple graphs at the same time
  • NEW: Individual color scheme for each graph
  • NEW: Surface plots can also be drawn as grid
  • NEW: User messages can be drawn into the control
  • NEW: Added scatter squares and triangles
  • Copy Screenshot to Image
  • Set Rho, Theta, Phi programmatically
  • Drawing of scatterplots
  • Coordinate system now also with negative values
  • Universal ready-to-use 3D Graph control for System.Windows.Forms applications
  • Derived from UserControl
  • Target: Framework 4 (Visual Studio 2010 or higher)
  • Display of 3 dimensional functions or binary data (X, Y, Z values)
  • Very clean and reusable code written by an experienced programmer
  • All code is in one single C# file with < 1200 lines
  • Optional function compiler allows to enter formulas as strings
  • Optional coordinate system with raster lines and labels
  • Optionally multiple color schemes
  • The user can rotate, elevate and zoom with the mouse or with 3 optional TrackBars
  • Zooming is also possible with the mouse wheel, but only if the 3D Graph has the focus.
  • The entire code is optimized for the maximum speed that is possible.
  • An optional legend displays the current rotation angles to the user in the top left corner.
  • An optional legend displays a user defined text for the axis in the bottom left corner.
  • The black lines between the polygons can be turned off.
  • Automatic normalization of 3D input data with 3 options

Why this Project?

I'am writing an ECU tunig software HUD ECU Hacker for which I need a 3D Viewer which displays the calibration tables.
I searched a ready-to-use 3D Control in internet but could not find what fits my needs.
Huge 3D software projects like Helix Toolkit are completely overbloated (220 MB) for my small project.
Commercial 3D software from $250 USD up to $2900 USD is also not an option.

 

WPF 3D Chart (from Jianzhong Zhang)

I found WPF 3D Chart on Codeproject.

It is very fast because WPF uses hardware acceleration.
The graphics processor can render 3D surfaces which must be composed of triangles.
But it is difficult to render lines. Each line would have to be defined as 2 triangles.
I need lines for the coordinate system.
I also need lines which display discrete values on the 3D surface.
I want each value in a data table to be represented as a polygon on the 3D object.
The screenshot above shows the representation of a data table with 22 rows and 17 columns.
I found it too complicated to implement this in WPF.
Extra work must be done to integrate a WPF control into a Windows.Forms application. See this article.

Plot 3D (from Michal Brylka)

Then I found Plot 3D on Codeproject.
This is more what I'am looking for but the code is not reusable and has many issues.

It is one of these many projects on Codeproject or Github which the author never has finished, which are buggy and lack functionality.
There is no useful way to rotate the 3D object. Instead of specifying a rotation angle you must specify the 3D observer coordinates which is a complete misdesign.
After fixing this I found that rotation results in ugly drawing artifacts at certain angles.
The reason is that the polygons are not rendered in the correct order.
The code has a bad performance because of wrong programming. For example in OnPaint() he creates each time 100 brushes and disposes them afterwards.
The code has been designed only for formulas but assigning fix values from a data table is not possible.

Graph3D (from me)

I ended up rewriting Plot 3D from the scratch, bug fixing and adding a lot of missing functionality.
The result is a UserControl which you can copy unchanged into your project and which you get working in a few minutes.
The features of my control are already listed above.
As my code does not use hardware acceleration the number of polygons that you display determines the drawing speed.
Without problem you can rotate and elevate the 3D objects of the demos in real time with the mouse without any delay. However if you want to render far more polygons it will be obviously slower.
For my purpose I need less than 2000 polygons which allows real time rotating with the mouse.
Download the ZIP file and then run the already compiled EXE file and play around with it and you will see the speed.

Demo: Surface Fill

The screenshot above shows the data from a data table with 22x17 values displayed as 3D surface with coordinate system.

int[,] s32_Values = new int[,]
{
    { 9059,   9634, 10617, 11141, ....., 15368, 15368, 15368, 15368, 15368 }, // row 1
    { 9684,  10387, 11141, 11796, ....., 15794, 15794, 15794, 15794, 15794 }, // row 2
    .........
    { 34669, 34210, 33653, 33096, ....., 27886, 26492, 25167, 25167, 25167 }, // row 21
    { 34767, 34210, 33718, 33096, ....., 27984, 26492, 25167, 25167, 25167 }  // row 22
};

Color[]      c_Colors = ColorSchema.GetSchema(eSchema.Rainbow);
cColorScheme i_Scheme = new cColorScheme(2, c_Colors);
cSurfaceData i_Data   = new cSurfaceData(eSurfaceMode.Fill, s32_Values.GetLength(0), s32_Values.GetLength(1), i_Scheme);

for (int C=0; C<i_Data.Cols; C++)
{
    for (int R=0; R<i_Data.Rows; R++)
    {
        double d_X = C *  10.0;
        double d_Y = R * 500.0;
        double d_Z = s32_Values[C,R] / 327.68;

        i_Data.SetPointAt(C, R,  d_X, d_Y, d_Z);
    }
}

graph3D.BeginUpdate();
graph3D.SetAxisLegends("MAP (kPa)", "Engine Speed (rpm)", "Volume Efficiency (%)");
graph3D.AddRenderData(i_Data);
graph3D.EndUpdate(eNormalize.Separate);

 

When you use discrete values for X,Y and Z which are not related like in this example make sure that X,Y and Z values are normalized separately by using the parameter eNormalize.Separate because the axes have different ranges.

Demo: Callback

Or you can write a C# callback function which calculates the Z values from the given X and Y values.

delRendererFunction f_Callback = delegate(double X, double Y)
{
    double r = 0.15 * Math.Sqrt(X * X + Y * Y);
    if (r < 1e-10) return 120;
    else           return 120 * Math.Sin(r) / r;
};

PointF k_Start = new PointF(-120, -80);
PointF k_End   = new PointF( 120,  80);
graph3D.BeginUpdate();
graph3D.AddFunctionData(f_Callback, k_Start, k_End, 5, eSurfaceMode.Fill, i_ColorScheme);
graph3D.EndUpdate(eNormalize.MaintainXYZ);

This code defines a modulated sinus function which is displayed on the X axis from -120 to +120 and on the Y axis from -80 to +80.
The fourth parameter (5) is the density which defines the count of polygons: (2* 120) / 5 + 1 = 49 polygons on the X axis and (2* 80) / 5 + 1 = 33 polygons for Y, which results in totally 1617 poygons.

When you use functions make sure that the relation between X,Y and Z values is not distorted by using the parameter eNormalize.MaintainXYZ.

System.Windows.Forms 3D Graph Control in C#

 

Demo: Formula

Or you can let the user enter a string formula which will be compiled at run time:

String s_Formula = "12 * sin(x) * cos(y) / (sqrt(sqrt(x * x + y * y)) + 0.2)";

delRendererFunction f_Function = FunctionCompiler.Compile(s_Formula);

PointF k_Start = new PointF(-10, -10);
PointF k_End   = new PointF( 10,  10);

graph3D.BeginUpdate();
graph3D.AddFunctionData(f_Function, k_Start, k_End, 0.5, eSurfaceMode.Fill, i_ColorScheme);
graph3D.EndUpdate(eNormalize.MaintainXYZ);

System.Windows.Forms 3D Graph Control in C#

 

Demo: Scatter Plot

Color[]      c_Colors = ColorSchema.GetSchema(eSchema.Rainbow);
cScatterData i_Data = new cScatterData(eScatterMode.Shapes, new cColorScheme(3, c_Colors));

for (double P = -22.0; P < 22.0; P += 0.1)
{
    double d_X = Math.Sin(P) * P;
    double d_Y = Math.Cos(P) * P;
    double d_Z = P;
    if (d_Z > 0.0) d_Z/= 3.0;

    i_Data.AddShape(d_X, d_Y, d_Z, eScatterShape.Circle, 3, null);
}

graph3D.BeginUpdate();
graph3D.AddRenderData(i_Data);
graph3D.EndUpdate(eNormalize.Separate);

System.Windows.Forms 3D Graph Control in C#

 

Demo: Scatter Shapes

double[,] d_Values = new double[,]
{
    // Value  X        Y      Z
    {   0.39, 0.0051,  0.133, 0.66 },
    {   0.23, 0.0002,  0.114, 0.87 },
    {   1.46, 0.0007,  0.077, 0.72 },
    {  -1.85, 0.0137,  0.053, 0.87 },
    ......
}

// A ColorScheme is not needed because all points have a valid Brush
cScatterData i_Data = new cScatterData(eScatterMode.Shapes, null);

for (int P = 0; P < d_Values.GetLength(0); P++)
{
    double d_Value = d_Values[P, 0];
    int s32_Radius = (int)Math.Abs(d_Value) + 1;

    double X = d_Values[P,1];
    double Y = d_Values[P,2];
    double Z = d_Values[P,3];

    eScatterShape e_Shape = (d_Value < 0) ? eScatterShape.Square : eScatterShape.Triangle;
    Brush         i_Brush = (d_Value < 0) ? Brushes.Red          : Brushes.Lime;

    String s_Tooltip = "Value = " + Graph3D.FormatDouble(d_Value);
    i_Data.AddShape(X, Y, Z, e_Shape, s32_Radius, i_Brush, s_Tooltip);
}

graph3D.BeginUpdate();
graph3D.AddRenderData(i_Data);
graph3D.EndUpdate(eNormalize.Separate);

This demo shows negative values as red squares and positive values as green triangles.
The selected color scheme in the combobox is ignored.
Each point in this plot consists of 4 doubles: X,Y,Z and a value.
The value defines the size of the square or triangle while X,Y,Z define the position.

Here you also see the tooltip which appears when the mouse is over a 3D point.
Normally the tooltip shows only the coordinates X, Y, Z of a point.
But as we have 4 values per point here, a user defined tooltip has been added which shows the fourth line.

System.Windows.Forms 3D Graph Control in C#

 

Demo: Nested Graphs

This demo shows how to display 2 graphs at once.
It also shows how to add messages as a legend to the user.
The selected color scheme in the combobox is ignored.

const int POINTS = 8;
cSurfaceData i_Data1 = new cSurfaceData(eSurfaceMode.Lines, POINTS, POINTS, new cColorScheme(3, Color.Orange));
cSurfaceData i_Data2 = new cSurfaceData(eSurfaceMode.Lines, POINTS, POINTS, new cColorScheme(2, Color.Black));

for (int C=0; C<POINTS; C++)
{
    for (int R=0; R<POINTS; R++)
    {
        double d_X = (C - POINTS / 2.3) / (POINTS / 5.5);
        double d_Y = (R - POINTS / 2.3) / (POINTS / 5.5);
        double d_Radius = Math.Sqrt(d_X * d_X + d_Y * d_Y);
        double d_Z = Math.Cos(d_Radius) + 1.0;

        i_Data1.SetPointAt(C, R,  d_X, d_Y, d_Z);
        i_Data2.SetPointAt(C, R,  d_X, d_Y, d_Z * 0.6);
    }
}

cMessgData i_Mesg1 = new cMessgData("Graph with error data",   10, -10, Color.Orange);
cMessgData i_Mesg2 = new cMessgData("Graph with correct data", 10, -27, Color.Black);

graph3D.BeginUpdate();
graph3D.AddRenderData (i_Data1);
graph3D.AddRenderData (i_Data2);
graph3D.AddMessageData(i_Mesg1);
graph3D.AddMessageData(i_Mesg2);
graph3D.EndUpdate(eNormalize.MaintainXY);

System.Windows.Forms 3D Graph Control in C#

 

Demo: Pyramid

This demo shows a simple real 3D object which consists of lines.
Normally lines are drawn in one solid color.
But this demo splits the vertical lines into 50 parts where each part gets it's color from the rainbow scheme.

Color[]   c_Colors = ColorSchema.GetSchema(eSchema.Rainbow);
cLineData i_Data   = new cLineData(new cColorScheme(4, c_Colors));

cPoint3D i_Center  = new cPoint3D(25, 25, 40);
cPoint3D i_Corner1 = new cPoint3D(25,  5,  5);
cPoint3D i_Corner2 = new cPoint3D( 5, 25,  5);
cPoint3D i_Corner3 = new cPoint3D(25, 45,  5);
cPoint3D i_Corner4 = new cPoint3D(45, 25,  5);

// Add the 4 vertical lines which are rendered as 50 parts with different colors
const int PARTS = 50;
i_Data.AddMultiColorLine(i_Center, i_Corner1, PARTS);
i_Data.AddMultiColorLine(i_Center, i_Corner2, PARTS);
i_Data.AddMultiColorLine(i_Center, i_Corner3, PARTS);
i_Data.AddMultiColorLine(i_Center, i_Corner4, PARTS);

// Add the 4 base lines with solid color
i_Data.AddSolidLine(i_Corner1, i_Corner2, null);
i_Data.AddSolidLine(i_Corner2, i_Corner3, null);
i_Data.AddSolidLine(i_Corner3, i_Corner4, null);
i_Data.AddSolidLine(i_Corner4, i_Corner1, null);

graph3D.BeginUpdate();
graph3D.AddRenderData(i_Data);
graph3D.EndUpdate(eNormalize.MaintainXYZ);

System.Windows.Forms 3D Graph Control in C#

 

Demo: Sphere

This demo shows another real 3D object which is rendered with polygons.
If you have been working with other 3D libraries (WPF, Direct3D) you know that all surfaces must be rendered as triangles.
But my library does not limit you to use only triangles.
You can pass any polygon that you like.
This demo uses rectangles and for the top and bottom it uses a round polygon with 50 corners.

const int LONGI  = 50; // count of 3D points along the longitude (360 degree)
const int LATI   = 25; // count of 3D points along the latitude  (180 degree)
const int RADIUS = 20;

// ------ Calculate 3D points ------ 

cPoint3D[,] i_Points = new cPoint3D[LONGI, LATI];

for (int Long=0; Long<LONGI; Long++)
{
    double d_Theta = 2 * Math.PI / LONGI * Long;

    for (int Lati=0; Lati<LATI; Lati++)
    {
        double d_Phi = Math.PI / LATI * Lati;

        // Cartesian coordinates
        double X = RADIUS * Math.Sin(d_Phi) * Math.Cos(d_Theta);
        double Y = RADIUS * Math.Sin(d_Phi) * Math.Sin(d_Theta);
        double Z = RADIUS * Math.Cos(d_Phi);

        i_Points[Long, Lati] = new cPoint3D(X, Y, Z);
    }
}

// ------ Create rectangular 3D polygons ------ 

Color[]    c_Colors = ColorSchema.GetSchema(eSchema.Rainbow);
cPolygonData i_Data = new cPolygonData(e_Mode, new cColorScheme(2, c_Colors));

for (int Long=0; Long<LONGI; Long++) // 0 .... 360 degree
{
    // overflow to zero if maximum exceeded
    int NextLong = (Long + 1) % LONGI; 

    // omit top and bottom where ploygons become very narrow
    for (int Lati=1; Lati<LATI-1; Lati++) 
    {
        int NextLati = Lati + 1;

        List<cPoint3D> i_Poly = new List<cPoint3D>();
        i_Poly.Add(i_Points[Long,     Lati]);
        i_Poly.Add(i_Points[NextLong, Lati]);
        i_Poly.Add(i_Points[NextLong, NextLati]);
        i_Poly.Add(i_Points[Long,     NextLati]);
        i_Data.AddPolygon(i_Poly);
    }
}

// ----- Close the top and bottom with a round polygon ------

if (e_Mode == eSurfaceMode.Fill)
{
    List<cPoint3D> i_Top    = new List<cPoint3D>();
    List<cPoint3D> i_Bottom = new List<cPoint3D>();
    for (int Long=0; Long<LONGI; Long++) // 0 .... 360 degree
    {
        i_Top   .Add(i_Points[Long, 1]);
        i_Bottom.Add(i_Points[Long, LATI-1]);
    }
    i_Data.AddPolygon(i_Top);
    i_Data.AddPolygon(i_Bottom);
}

graph3D.BeginUpdate();
graph3D.AddRenderData(i_Data);
graph3D.EndUpdate(eNormalize.MaintainXYZ);

System.Windows.Forms 3D Graph Control in C#

 

Tooltip

System.Windows.Forms 3D Graph Control in C#

Each polygon corner shows a tooltip with the coordinates X, Y, Z when the mouse is over it.
I marked in magenta the locations for the tooltip of the back part of the sphere and in pink of the front part.
If you use eSurfaceMode.Fill you will see the tooltip also for corners which are invisible.
This means that in one rectangle on the right screenshot you may see 10 tooltips instead of 4.
Fixing this would require to detect if a corner is covered by a polygon which would extremely decrease the perfomance.
If you find this confusing, I recomend to turn off the tooltip:

graph3D.TooltipEnabled = false;

 

Demo: Valentine

Well, this code has just been written on 14th february 2021.
Also here the selected color scheme in the combobox is ignored.

cColorScheme i_Scheme = new cColorScheme(5, Color.Red);
cScatterData i_Data   = new cScatterData(eScatterMode.Lines, i_Scheme);

// Upper (round) part of heart
double X = 0.0;
double Z = 0.0;
for (double P = 0.0; P <= Math.PI * 1.32; P += 0.025)
{
    X = Math.Cos(P) * 1.5 - 1.5;
    Z = Math.Sin(P) * 3.0 + 6.0;
    i_Data.AddLine( X, -X, Z, true,  null); // right side
    i_Data.AddLine(-X,  X, Z, false, null); // left  side
}

// Lower (linear) part of heart
double d_X = X / 70;
double d_Z = Z / 70;
while (Z >= 0.0)
{
    i_Data.AddLine( X, -X, Z, true,  null); // right side
    i_Data.AddLine(-X,  X, Z, false, null); // left  side
    X -= d_X;
    Z -= d_Z;
}

cMessgData i_Mesg = new cMessgData("Happy Valentine's day, Sweetheart!", -10, -10, Color.Red);

graph3D.BeginUpdate();
graph3D.AddRenderData(i_Data);
graph3D.AddMessageData(i_Mesg);
graph3D.EndUpdate(eNormalize.MaintainXYZ);

System.Windows.Forms 3D Graph Control in C#

Elmü

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) ElmüSoft
Chile Chile
Software Engineer since 40 years.

Comments and Discussions

 
GeneralRe: Make project as opensource on github Pin
MauNguyenVan6-May-23 20:35
MauNguyenVan6-May-23 20:35 
AnswerRe: Make project as opensource on github Pin
Elmue7-May-23 9:35
Elmue7-May-23 9:35 
GeneralRe: Make project as opensource on github Pin
MauNguyenVan7-May-23 18:35
MauNguyenVan7-May-23 18:35 
GeneralRe: Make project as opensource on github Pin
MauNguyenVan12-May-23 22:31
MauNguyenVan12-May-23 22:31 
AnswerRe: Make project as opensource on github Pin
Elmue13-May-23 6:47
Elmue13-May-23 6:47 
GeneralThanks Elmue Pin
BartSystems Software Components Engineering5-Apr-23 3:39
BartSystems Software Components Engineering5-Apr-23 3:39 
AnswerRe: Thanks Elmue Pin
Elmue16-Apr-23 5:37
Elmue16-Apr-23 5:37 
QuestionRendering order error when rotating Pin
Member 1118204019-Feb-23 12:41
Member 1118204019-Feb-23 12:41 
Hi, I've a rendering order error when rotating around a mesh, do you have an idea where it could come from ?

Here is a GIF of the problem : Imgur: The magic of the Internet[^]

modified 19-Feb-23 20:16pm.

AnswerRe: Rendering order error when rotating Pin
Elmue20-Feb-23 4:55
Elmue20-Feb-23 4:55 
GeneralRe: Rendering order error when rotating Pin
Member 1118204020-Feb-23 12:18
Member 1118204020-Feb-23 12:18 
AnswerRe: Rendering order error when rotating Pin
Elmue20-Feb-23 23:25
Elmue20-Feb-23 23:25 
GeneralRe: Rendering order error when rotating Pin
Frédéric Lopez 202323-Feb-23 4:52
Frédéric Lopez 202323-Feb-23 4:52 
QuestionConvert in vb.net Pin
Member 1581408912-Nov-22 5:17
Member 1581408912-Nov-22 5:17 
Question3D bar chart Pin
Member 157322937-Oct-22 9:26
Member 157322937-Oct-22 9:26 
AnswerRe: 3D bar chart Pin
Elmue16-Apr-23 5:39
Elmue16-Apr-23 5:39 
QuestionMerge 2 data points Pin
lohrasb22-Aug-22 23:41
lohrasb22-Aug-22 23:41 
AnswerRe: Merge 2 data points Pin
Elmue26-Aug-22 11:56
Elmue26-Aug-22 11:56 
AnswerRe: Merge 2 data points Pin
lohrasb1-Sep-22 0:08
lohrasb1-Sep-22 0:08 
SuggestionRe: Merge 2 data points Pin
Jezza Soup4-Oct-22 23:27
Jezza Soup4-Oct-22 23:27 
QuestionChange the number of Y-axis intervals? Pin
mr. Duan4-Aug-22 22:51
professionalmr. Duan4-Aug-22 22:51 
AnswerRe: Change the number of Y-axis intervals? Pin
Elmue6-Aug-22 4:34
Elmue6-Aug-22 4:34 
QuestionChange scatterPoint size ? Pin
Frédéric Donnet6-May-22 2:32
Frédéric Donnet6-May-22 2:32 
Questionmy vote of 5 Pin
Southmountain30-Apr-22 6:11
Southmountain30-Apr-22 6:11 
GeneralMy vote of 5 Pin
Chpt Shiue15-Mar-22 16:57
Chpt Shiue15-Mar-22 16:57 
BugSurface plot phasing thru itself Pin
Member 1547864820-Jan-22 9:53
Member 1547864820-Jan-22 9:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.