Close |
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.
OnPaint()
he creates each time 100 brushes and disposes them afterwards.
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 }, // 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 }; 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; // X must be related to colum double d_Y = R * 5.0; // Y must be related to row 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); // gray 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()
.
Use Demo Surface Fill to test the checkboxes "Mirror X" and "Mirror Y".
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.
When you use functions make sure that the relation between X,Y and Z values is not distorted by using the parameter eNormalize.MaintainXYZ
.
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();
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();
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[,] { // 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 their own Brush 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();
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); // X must be related to colum double d_Y = (R - POINTS / 2.3) / (POINTS / 5.5); // Y must be related to row 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();
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"); // Add the 4 vertical lines which are rendered as 50 parts with different colors 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); // Add the 4 base lines with solid color 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();
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.
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.
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.
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; // toggle selection // After changing the selection status the objects must be redrawn. 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); } // Set flag to recalculate the coordinate system, then Invalidate() 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:
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.
// Add all parts of your 3D figure to a list 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; // toggle selection foreach (cObject3D i_Part in (List<cObject3D>)i_Object.Tag) { i_Part.Selected = b_Selected; } return eInvalidate.Invalidate; } return eInvalidate.NoChange; }
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(); }
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 ++; // ======== SCATTER ========= 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); } // ======== PYRAMID ========= double d_Angle = ms32_AnimationAngle / 30.0; double d_Sinus = Math.Sin(d_Angle) * 50.0; // -50 ... +50 double d_Cosinus = Math.Cos(d_Angle) * 50.0; // -50 ... +50 double d_DeltaZ = d_Sinus / 2.0; // -25 ... +25 // Top mi_Pyramid[0].X = -100.0; mi_Pyramid[0].Y = -100.0; mi_Pyramid[0].Z = 70.0 + d_DeltaZ; // Edge 1 mi_Pyramid[1].X = -100.0 + d_Sinus; mi_Pyramid[1].Y = -100.0 + d_Cosinus; mi_Pyramid[1].Z = 40.0 + d_DeltaZ; // Edge 2 mi_Pyramid[2].X = -100.0 + d_Cosinus; mi_Pyramid[2].Y = -100.0 - d_Sinus; mi_Pyramid[2].Z = 40.0 + d_DeltaZ; // Edge 3 mi_Pyramid[3].X = -100.0 - d_Sinus; mi_Pyramid[3].Y = -100.0 - d_Cosinus; mi_Pyramid[3].Z = 40.0 + d_DeltaZ; // Edge 4 mi_Pyramid[4].X = -100.0 - d_Cosinus; mi_Pyramid[4].Y = -100.0 + d_Sinus; mi_Pyramid[4].Z = 40.0 + d_DeltaZ; }
ePolygonMode.Fill
you will see the tooltip also for corners which are invisible.
editor3D.TooltipMode = eTooltip.Off;
And last but not least:
Well, this demo has just been written on 14th february 2021.
Program | Version | Date | Size | |
---|---|---|---|---|
3D Editor Control | 12 | 6.feb.2025 | 1 MB |
Have fun with my library. Read the plenty of comments in the code!
Software from ElmüSoft |
Desktop Organizer & Event Calendar
My program PTBSync (in english + deutsch) has an event calendar / scheduler with lots of features, calculates the holidays of 70 countries, can display multiple gadgets on the desktop (current weather, birthdays, moon phase, menstruation calendar, world clocks,...) pins notes on the desktop, replaces the primitive Windows trayclock and adds a detailed tooltip, synchronizes with an atomic clock, and much more. |
ECU Hacker & Tuner
connects to ECU's (Engine Control Units) from motorbikes, cars and trucks, shows parameters, clears DTC's, has tuning for a Delphi ECU, has an ECU emulator, can analyze the CAN bus using cheap adapters, supports many protocols, has a sniffer and manual data injection for K-Line and CAN Bus, allows you to write your own macro scripts that communicate over K-Line and CAN Bus. |
Oscilloscope Waveform Logicanalyzer
displays oscilloscope waveforms and has A/D converters, noise suppression, precise time measurement, logic analyzer with decoders for UART, SPI, I²C, CAN bus (also CAN FD) and chip-specific decoders. Can split half-duplex communication. Transfers signals over USB (SCPI) from the oscilloscope to the computer and remote controls the oscilloscope. It is open-source and you can extend it with new features. |
Subtitle Tuner
calculates the display duration of the subtitles based on the count of characters in the text. This avoids that badly created subtitles disappear before you could read them. Can remove deaf texts, lyrics, italic/bold. Can resynchronize a subtile to a movie with different framerate or change the subtitle offset. |
Secure Doorlock with RFID Cards
is a project with an RFID antenna and a Teensy processor that opens a door with Mifare Desfire cards. The RFID cards of up to 64 users can be stored in the flash memory. It is extremely secure because it uses strong encryption. A big battery assures that the door can be opened during a power failure. |
Remote Control your Computer with Infrared
An infrared receiver and a Teensy processor allow to remote control the music player on your computer with any old remote control that you have from a TV, amplifier, DVD player, SAT tuner or whatever. The Teensy simulates a keyboard and sends keystrokes to the music player. |
3D Render and Editor Control in C#
A .NET control that can be easily implemented into System.Windows.Forms applications to display 3D graphs. The user can move and rotate the graph with the mouse and edit single points. Many display modes are available: 3D surface, 3D shapes, 3D lines, 3D objects, also animated 3D objects. |
USB HID Touchscreen
A Teensy processor emulates keyboard, mouse and touchscreen at the same time to remote control a computer for automation purposes. |
![]() |
![]() |