A universal ready-to-use interactive 3D Editor control for System.Windows.Forms applications. It displays 3D data that the user can modify with the mouse. The control consists of a single C# file and is optimized for maximum speed.
Download Zip file (Source code + Compiled Exe) - 897 kB - Version 15.jun.2024
Features
- NEW: BUGFIX in drawing missing surface points in surface grid.
- New demo 'Surface Missing' shows how to draw a surface plot with missing points.
- User mouse actions can be defined 100% individually.
- Axis legends can be drawn also at the end of the main axes.
- X and Y axes can be mirrored.
- Option to include zero value of Z-axis or not.
- Added Undo / Redo buffer for user operations.
- BUGFIX: Drawing of selected multi-color lines
- The control has been completely rewritten (4100 lines of code, 170 kB filesize!)
- The user can select 3D objects with the mouse while pressing the ALT key.
- The user can move points or objects with the mouse in the 3D space.
- A callback provides full control over user actions and object selection.
- 3D objects can be added, modified and removed on the fly.
- A new demo "Animation" shows how to dynamically change properties of 3D objects.
- The border color changes when the 3D editor gets the keyboard focus
- Line width and scatter point size are adapted when zooming
- Can be configured to use only the left or middle mouse button for all movements
- Support for drawing 3D objects. Added example "Pyramid" and "Sphere"
- Rendering speed optimized to the extreme
- BUGFIX: Sometimes the Z axis was drawn on top of the 3D object instead behind
- Display a tooltip when the mouse is over a 3D point
- Resizing of 3D object when resizing 3D control
- Completely rewritten to allow display of multiple graphs at the same time
- Individual color scheme for each graph
- Surface plots can also be drawn as grid
- User messages can be drawn into the control
- 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 at the top 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.
Editor3D (from Elmü)
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
Here you see data from a 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 },
{ 9684, 10387, 11141, 11796, ....., 15794, 15794, 15794, 15794, 15794 },
.........
{ 34669, 34210, 33653, 33096, ....., 27886, 26492, 25167, 25167, 25167 },
{ 34767, 34210, 33718, 33096, ....., 27984, 26492, 25167, 25167, 25167 }
};
int s32_Cols = s32_Values.GetLength(1);
int s32_Rows = s32_Values.GetLength(0);
cColorScheme i_Scheme = new cColorScheme(me_ColorScheme);
cSurfaceData i_Data = new cSurfaceData(e_Mode, s32_Cols, s32_Rows, Pens.Black, i_Scheme);
for (int C=0; C<i_Data.Cols; C++)
{
for (int R=0; R<i_Data.Rows; R++)
{
int s32_RawValue = s32_Values[R,C];
double d_X = C * 640.0;
double d_Y = R * 5.0;
double d_Z = s32_RawValue / 327.68;
String s_Tooltip = String.Format("Speed = {0} rpm\nMAP = {1} kPa\n"
+ "Volume Eff. = {2} %\nColumn = {3}\nRow = {4}",
d_X, d_Y, Editor3D.FormatDouble(d_Z), C, R);
cPoint3D i_Point = new cPoint3D(d_X, d_Y, d_Z, s_Tooltip, s32_RawValue);
i_Data.SetPointAt(C, R, i_Point);
}
}
editor3D.Clear();
editor3D.Normalize = eNormalize.Separate;
editor3D.AxisY.Mirror = true;
editor3D.AxisX.LegendText = "Engine Speed (rpm)";
editor3D.AxisY.LegendText = "MAP (kPa)";
editor3D.AxisZ.LegendText = "Volume Efficiency (%)";
editor3D.AddRenderData(i_Data);
editor3D.Selection.Callback = OnSelectEvent;
editor3D.Selection.HighlightColor = Color.FromArgb(90, 90, 90);
editor3D.Selection.MultiSelect = true;
editor3D.Selection.Enabled = true;
editor3D.Invalidate();
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.
Each point in the grid has a tooltip assigned which shows the values X,Y,Z, Row, Column and Raw value.
This demo allows user selection of multiple polygons or points in the grid while pressing the ALT key.
The Z-values of the selected points can then be modified with the mouse while pressing ALT + CTRL.
The selection and movement are handled in a user defined callback: OnSelectEvent()
.
Mirroring Axes
Use Demo Surface Fill to test the checkboxes "Mirror X" and "Mirror Y".
Demo: Math Callback
Or you can write a C# callback function which calculates the Z values from the given X and Y values.
cColorScheme i_Scheme = new cColorScheme(me_ColorScheme);
cSurfaceData i_Data = new cSurfaceData(ePolygonMode.Fill, 49, 33, Pens.Black, i_Scheme);
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;
};
i_Data.ExecuteFunction(f_Callback, new PointF(-120, -80), new PointF(120, 80));
editor3D.Clear();
editor3D.Normalize = eNormalize.MaintainXYZ;
editor3D.AddRenderData(i_Data);
editor3D.Invalidate();
A modulated sinus function is displayed on the X axis from -120 to +120 and on the Y axis from -80 to +80.
The 49 columns and 33 rows of points result in 48 columns and 32 rows of polygons (totally 1536).
When you use functions make sure that the relation between X,Y and Z values is not distorted by using the parameter eNormalize.MaintainXYZ
.
Demo: Math Formula
Or you can let the user enter a string formula which will be compiled at run time:
cColorScheme i_Scheme = new cColorScheme(me_ColorScheme);
cSurfaceData i_Data = new cSurfaceData(ePolygonMode.Fill, 41, 41, Pens.Black, i_Scheme);
String s_Formula = "7 * sin(x) * cos(y) / (sqrt(sqrt(x * x + y * y)) + 0.2)";
delRendererFunction f_Function = FunctionCompiler.Compile(s_Formula);
i_Data.ExecuteFunction(f_Function, new PointF(-7, -7), new PointF(7, 7));
editor3D.Clear();
editor3D.Normalize = eNormalize.MaintainXYZ;
editor3D.AddRenderData(i_Data);
editor3D.Invalidate();
Demo: Scatter Plot
cColorScheme i_Scheme = new cColorScheme(me_ColorScheme);
cScatterData i_Data = new cScatterData(i_Scheme);
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;
cPoint3D i_Point = new cPoint3D(d_X, d_Y, d_Z, "Scatter Point");
i_Data.AddShape(i_Point, eScatterShape.Circle, 3, null);
}
editor3D.Clear();
editor3D.Normalize = eNormalize.Separate;
editor3D.AddRenderData(i_Data);
editor3D.Invalidate();
Demo: Scatter Shapes
This demo shows negative values as red squares and positive values as green triangles.
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.
The value is displayed in the tooltip.
4 shapes are selected (blue) and can be moved with the mouse in the 3D space.
double[,] d_Values = new double[,]
{
{ 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 },
......
}
cScatterData i_Data = new cScatterData(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 = " + Editor3D.FormatDouble(d_Value);
cPoint3D i_Point = new cPoint3D(X, Y, Z, s_Tooltip, d_Value);
i_Data.AddShape(i_Point, e_Shape, s32_Radius, i_Brush);
}
editor3D.Clear();
editor3D.Normalize = eNormalize.Separate;
editor3D.AddRenderData(i_Data);
editor3D.Invalidate();
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 (bottom left).
This demo demonstrates single point selection. The user can only select one point at a time.
const int POINTS = 8;
cSurfaceData i_Data1 = new cSurfaceData(ePolygonMode.Lines, POINTS, POINTS, new Pen(Color.Orange, 3), null);
cSurfaceData i_Data2 = new cSurfaceData(ePolygonMode.Lines, POINTS, POINTS, new Pen(Color.Green, 2), null);
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;
String s_Tooltip = String.Format("Col = {0}\nRow = {1}", C, R);
cPoint3D i_Point1 = new cPoint3D(d_X, d_Y, d_Z, s_Tooltip + "\nWrong Data");
cPoint3D i_Point2 = new cPoint3D(d_X, d_Y, d_Z * 0.6, s_Tooltip + "\nCorrect Data");
i_Data1.SetPointAt(C, R, i_Point1);
i_Data2.SetPointAt(C, R, i_Point2);
}
}
cMessgData i_Mesg1 = new cMessgData("Graph with error data", 7, -7, Color.Orange);
cMessgData i_Mesg2 = new cMessgData("Graph with correct data", 7, -24, Color.Green);
editor3D.Clear();
editor3D.Normalize = eNormalize.MaintainXY;
editor3D.AddRenderData (i_Data1, i_Data2);
editor3D.AddMessageData(i_Mesg1, i_Mesg2);
editor3D.Selection.MultiSelect = false;
editor3D.Selection.Enabled = true;
editor3D.Invalidate();
Demo: Pyramid
This demo shows a simple 3D object which consists of lines.
Normally lines are drawn in one solid color.
But this demo renders the vertical lines in 50 parts with colors from the rainbow scheme.
cLineData i_Data = new cLineData(new cColorScheme(me_ColorScheme));
cPoint3D i_Center = new cPoint3D(45, 45, 40, "Center");
cPoint3D i_Corner1 = new cPoint3D(45, 25, 20, "Corner 1");
cPoint3D i_Corner2 = new cPoint3D(25, 45, 20, "Corner 2");
cPoint3D i_Corner3 = new cPoint3D(45, 65, 20, "Corner 3");
cPoint3D i_Corner4 = new cPoint3D(65, 45, 20, "Corner 4");
cLine3D i_Vert1 = i_Data.AddMultiColorLine(50, i_Center, i_Corner1, 4, null);
cLine3D i_Vert2 = i_Data.AddMultiColorLine(50, i_Center, i_Corner2, 4, null);
cLine3D i_Vert3 = i_Data.AddMultiColorLine(50, i_Center, i_Corner3, 4, null);
cLine3D i_Vert4 = i_Data.AddMultiColorLine(50, i_Center, i_Corner4, 4, null);
cLine3D i_Hor1 = i_Data.AddSolidLine(i_Corner1, i_Corner2, 8, null);
cLine3D i_Hor2 = i_Data.AddSolidLine(i_Corner2, i_Corner3, 8, null);
cLine3D i_Hor3 = i_Data.AddSolidLine(i_Corner3, i_Corner4, 8, null);
cLine3D i_Hor4 = i_Data.AddSolidLine(i_Corner4, i_Corner1, 8, null);
editor3D.Clear();
editor3D.Normalize = eNormalize.Separate;
editor3D.AxisZ.IncludeZero = false;
editor3D.AddRenderData(i_Data);
editor3D.Invalidate();
Including Z Value Zero
Use Demo Pyramid to test the checkbox "Include Zero Z".
The Z values of the pyramid range from 20 to 40.
You can chose if the bottom of the Z axis is 0 or 20.
Demo: Sphere
This demo shows another 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 allows to pass polygons with any amount of corners (minimum 3).
This eliptic sphere contains a round polygon with 50 corners for the top and bottom.
The code of this demo is a bit longer.
Have a look into the source code.
Modifying 3D Objects
With the checkbox 'Point Selection' in the demo application you can chose if you want to select points or lines.
Press ALT and click a point of the pyramid to select it. A green circle marks it as selected.
Then press ALT + CTRL and drag the selecetd point(s) with the mouse in the 3D space.
All this is handled in the selection callback where you have 100% control over all user actions.
The Selection Callback
void DemoPyramid()
{
.....
editor3D.Selection.HighlightColor = Color.Green;
editor3D.Selection.Callback = OnSelectEvent;
editor3D.Selection.MultiSelect = true;
editor3D.Selection.Enabled = true;
.....
}
eInvalidate OnSelectEvent(eAltEvent e_Event, Keys e_Modifiers,
int s32_DeltaX, int s32_DeltaY, cObject3D i_Object)
{
eInvalidate e_Invalidate = eInvalidate.NoChange;
bool b_CTRL = (e_Modifiers & Keys.Control) > 0;
if (e_Event == eAltEvent.MouseDown && !b_CTRL && i_Object != null)
{
i_Object.Selected = !i_Object.Selected;
e_Invalidate = eInvalidate.Invalidate;
}
else if (e_Event == eAltEvent.MouseDrag && b_CTRL)
{
cPoint3D i_Project = editor3D.ReverseProject(s32_DeltaX, s32_DeltaY);
foreach (cPoint3D i_Selected in editor3D.Selection.GetSelectedPoints(eSelType.All))
{
i_Selected.Move(i_Project.X, i_Project.Y, i_Project.Z);
}
e_Invalidate = eInvalidate.CoordSystem;
}
return e_Invalidate;
}
The callback OnSelectEvent()
receives several parameters.
Read the comment for function Editor3D.SelectionCallback()
where they are explained.
In the first if()
the selection of the point/object is toggled when the mouse goes down with ALT key pressed but without CTRL key.
In the else if()
the relative movement of the mouse is reverse projected into the 3D space while the user drags the point/object.
This 3D movement in the X,Y,Z directions is then added to the X,Y,Z coordinates of the selected points.
You can write your own callback function which does whatever you like to manipulate the 3D objects.
You can change the coordinates of a 3D object, the color, the shape, the size, the selection status, the tooltip,...
Pay attention to the status bar which shows all mouse events:
Selecting an entire 3D figure
You can assign your own data to the property Tag
of any 3D object.
This data may be a List<cObject3D>
or any class or struct of your project.
When the callback is called because the user clicks or drags a 3D object you can obtain the data from the Tag
.
The following code shows how to select an entire 3D figure consisting of lines, shapes and polygons when the user clicks one of them.
List<cObject3D> i_Parts = new List<cObject3D>();
i_Parts.Add(i_MyLine1);
i_Parts.Add(i_MyLine2);
i_Parts.Add(i_MyShape1);
i_Parts.Add(i_MyPolygon1);
i_Parts.Add(i_MyPolygon2);
i_MyLine1.Tag = i_Parts;
i_MyLine2.Tag = i_Parts;
i_MyShape1.Tag = i_Parts;
i_MyPolygon1.Tag = i_Parts;
i_MyPolygon2.Tag = i_Parts;
.....
editor3D.Selection.SinglePoints = false;
.....
private eInvalidate OnSelectEvent(eAltEvent e_Event, Keys e_Modifiers,
int s32_DeltaX, int s32_DeltaY, cObject3D i_Object)
{
bool b_CTRL = (e_Modifiers & Keys.Control) > 0;
if (e_Event == eAltEvent.MouseDown && !b_CTRL &&
i_Object != null && i_Object.Tag is List<cObject3D>)
{
bool b_Selected = !i_Object.Selected;
foreach (cObject3D i_Part in (List<cObject3D>)i_Object.Tag)
{
i_Part.Selected = b_Selected;
}
return eInvalidate.Invalidate;
}
return eInvalidate.NoChange;
}
Deleting 3D Objects
In demo 'Sphere' you can select polygons and delete them by hitting the DEL key.
editor3D.KeyDown += new KeyEventHandler(OnEditorKeyDown);
void OnEditorKeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode != Keys.Delete)
return;
foreach (cObject3D i_Polygon in editor3D.Selection.GetSelectedObjects(eSelType.Polygon))
{
editor3D.RemoveObject(i_Polygon);
}
editor3D.Invalidate();
}
Demo Animation
This demo uses a timer which updates 50 scatter circles and a pyramid of 5 polygons.
The sinus is sweeping up and down slowly and changes through all colors of the rainbow.
The pyramid rotates around it's own axis and drifts up and down.
The timer calls this function every 100 ms:
void ProcessAnimation()
{
ms32_AnimationAngle ++;
cShape3D[] i_AllShapes = mi_SinusData.AllShapes;
cColorScheme i_ColorScheme = mi_SinusData.ColorScheme;
double d_DeltaX = 400.0 / i_AllShapes.Length;
double d_X = -200.0;
for (int S=0; S<i_AllShapes.Length; S++, d_X += d_DeltaX)
{
cShape3D i_Shape = i_AllShapes[S];
i_Shape.Points[0].X = d_X;
i_Shape.Points[0].Y = -d_X;
i_Shape.Points[0].Z = Math.Sin((ms32_AnimationAngle + d_X) / 50.0) * 50.0 + 50.0;
i_Shape.Brush = i_ColorScheme.GetBrush(ms32_AnimationAngle * 10);
}
double d_Angle = ms32_AnimationAngle / 30.0;
double d_Sinus = Math.Sin(d_Angle) * 50.0;
double d_Cosinus = Math.Cos(d_Angle) * 50.0;
double d_DeltaZ = d_Sinus / 2.0;
mi_Pyramid[0].X = -100.0;
mi_Pyramid[0].Y = -100.0;
mi_Pyramid[0].Z = 70.0 + d_DeltaZ;
mi_Pyramid[1].X = -100.0 + d_Sinus;
mi_Pyramid[1].Y = -100.0 + d_Cosinus;
mi_Pyramid[1].Z = 40.0 + d_DeltaZ;
mi_Pyramid[2].X = -100.0 + d_Cosinus;
mi_Pyramid[2].Y = -100.0 - d_Sinus;
mi_Pyramid[2].Z = 40.0 + d_DeltaZ;
mi_Pyramid[3].X = -100.0 - d_Sinus;
mi_Pyramid[3].Y = -100.0 - d_Cosinus;
mi_Pyramid[3].Z = 40.0 + d_DeltaZ;
mi_Pyramid[4].X = -100.0 - d_Cosinus;
mi_Pyramid[4].Y = -100.0 + d_Sinus;
mi_Pyramid[4].Z = 40.0 + d_DeltaZ;
}
Tooltip
Each polygon corner shows a tooltip 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 ePolygonMode.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:
editor3D.TooltipMode = eTooltip.Off;
Demo: Valentine
And last but not least:
Well, this demo has just been written on 14th february 2021.
Have fun with my library. Read the plenty of comments in the code!
Elmü