i was using .net framework windowsforms gdi+ to draw my simulation on form and render it. So far, i want it to render the same, but i want render it in directX(SharpDX 2d). I know my simulation have bugs and other such a things, but for me it looks great, as simple simulation(I'm still learning about making simulations). If someone know how can i do this with my code, i appreciate it, and also i want this to be optimized!
Code:
- Body class:
public class Body
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public double Mass { get; set; }
public double X { get; set; }
public double Y { get; set; }
public double VX { get; set; }
public double VY { get; set; }
public double AX { get; set; }
public double AY { get; set; }
public int Radius { get; set; }
public bool IsSelected { get; set; }
public bool IsStatic { get; set; }
public bool Visible { get; set; }
public Brush BodyColor { get; set; } = Brushes.Red;
public List<Point> OrbitTrail { get; set; } = new List<Point>();
public double Elasticity { get; set; }
public double FrictionCoefficient { get; set; }
public double Rotation { get; set; }
public double AngularVelocity { get; set; }
public double AngularAcceleration { get; set; }
public BigInteger Temperature { get; set; }
public List<Body> AttachedObjects { get; set; } = new List<Body>();
public string Type { get; set; } = "Ellipse";
public Body() { }
}
- Main code:
public partial class GravitySim : Form
{
#region variables
private double G = 6.67430e-11;
private QuadTree quadTree;
private double timeStep = 1.0;
private int numSimulationSteps = 5000;
private const double scaleFactor = 1e5;
private const double MassScaleFactor = 1e22;
private bool drawQuadTree = false;
private bool selectionMode = true;
private bool velocityVectors = true;
private List<Body> currentPreset = null;
// Declare a variable for real-time acceleration factor
private double realTimeAcceleration = 1.0;
private int maxOrbitPoints = 50000;
private const double TargetFPS = 120.0;
private const double TargetFrameTime = 1000.0 / TargetFPS;
private List<Body> bodies;
private Timer timer;
private Stopwatch frameStopwatch = new Stopwatch();
private Stopwatch renderStopwatch = new Stopwatch();
private Stopwatch SimStopwatch = new Stopwatch();
private int framesCount = 0;
private double frameRate = 0.0;
private double frames;
#endregion
public GravitySim()
{
InitializeComponent();
InitializeSimulation();
this.SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
// Inicjalizacja KONTROLEK
this.MouseDoubleClick += Form1_MouseDoubleClick;
}
#region basicFNC
private void StartRenderTimer()
{
renderStopwatch.Restart();
}
private void StopRenderTimer()
{
renderStopwatch.Stop();
double renderTime = renderStopwatch.Elapsed.TotalMilliseconds;
label28.Text = $"Render Time: {renderTime:F2} ms";
}
private void InitializeSimulation()
{
InitializeColorComboBox();
InitializePresets();
List<Body> bodies = new List<Body>()
{
new Body { Mass = 6e5, X = 200 + 350, Y = 500, VX = 0, VY = 0, Radius = 20, IsStatic = true, BodyColor = Brushes.Red },
new Body { Mass = 4e4, X = 450 + 300, Y = 350, VX = 0, VY = -22 / scaleFactor, Radius = 5, IsStatic = false, BodyColor = Brushes.Blue },
new Body { Mass = 2.5e4, X = 100 + 300, Y = 400, VX = -40 / scaleFactor, VY = 18 / scaleFactor, Radius = 5, IsStatic = false, BodyColor = Brushes.Yellow },
// Dodaj kolejne ciała według potrzeb
};
LoadPreset(bodies);
quadTree = new QuadTree(new Rectangle(0, 0, ClientSize.Width - panel1.Width, ClientSize.Height), 1);
QuadTreeInsertBodies();
// Inicjalizacja timera
timer = new Timer();
timer.Interval = 1; // Czas w milisekundach między krokami symulacji
timer.Tick += Timer_Tick;
SimStopwatch.Start();
}
private void QuadTreeInsertBodies()
{
quadTree.Clear();
foreach (var body in bodies)
{
quadTree.Insert(body);
}
}
private void LoadPreset(List<Body> bodies)
{
this.bodies = bodies;
this.Invalidate();
}
private void InitializePresets()
{
comboBox2.Items.Add("Example preset");
comboBox2.Items.Add("Earth orbiting Sun - 2 bodies");
comboBox2.SelectedIndex = 0;
}
private void InitializeColorComboBox()
{
// Pobierz właściwości Brushes za pomocą refleksji
var brushProperties = typeof(Brushes).GetProperties();
// Dodaj kolory do ComboBox
foreach (var brushProperty in brushProperties)
{
Brush brush = (Brush)brushProperty.GetValue(null, null);
// Sprawdź, czy kolor jest SolidColorBrush (możesz dostosować warunek do innych rodzajów pędzli, jeśli to konieczne)
if (brush is SolidBrush)
{
comboBox1.Items.Add(brushProperty.Name);
}
}
// Ustaw domyślny kolor
comboBox1.SelectedIndex = 0;
}
#endregion
private Body selectedBody = null;
private void Form1_MouseDoubleClick(object sender, MouseEventArgs e)
{
if (selectionMode)
{
Point mousePosition = e.Location;
// Check if the mouse click is within the bounds of any body
bool clickedOnBody = false;
foreach (var body in bodies)
{
if (IsMouseOnBody(mousePosition, body))
{
// Unselect the currently selected body
if (selectedBody != null)
{
selectedBody.IsSelected = false;
}
body.IsSelected = true;
selectedBody = body;
clickedOnBody = true;
// Pobierz kolor z Brush
Color BodyColor = (body.BodyColor as SolidBrush)?.Color ?? Color.Black;
// Znajdź indeks koloru ciała w comboBox1
int colorIndex = comboBox1.Items.IndexOf(BodyColor.Name);
// Jeśli indeks jest większy lub równy zeru, ustaw go jako zaznaczony
if (colorIndex >= 0)
{
comboBox1.SelectedIndex = colorIndex;
this.BodyColor.BackColor = BodyColor;
}
checkBox4.Checked = selectedBody.IsStatic;
if (body.Mass > Math.Pow(10, 9) || body.Mass < Math.Pow(10, -3)) // Sprawdź, czy masa jest większa niż miliard
{
label9.Text = $"Mass: {ConvertToScientificNotation(body.Mass)} kg";
bodyMass.Text = ConvertToScientificNotation(body.Mass) + " kg";
}
else
{
label9.Text = $"Mass: {body.Mass.ToString("F2")} kg";
bodyMass.Text = body.Mass.ToString("F2") + " kg";
}
break;
}
}
// Clicked outside of any body, unselect all bodies
if (!clickedOnBody)
{
if (selectedBody != null)
{
selectedBody.IsSelected = false;
selectedBody = null;
}
foreach (var body in bodies)
{
body.IsSelected = false;
}
}
this.Invalidate(); // Redraw to reflect the changes
// Dodaj kod, który decyduje, czy panel2 powinien być widoczny
if (selectedBody != null)
{
// Panel powinien być widoczny, ponieważ jest wybrana jakakolwiek ciało
rjButton8.Location = new Point(5, 403);
label24.Location = new Point(75, 443);
comboBox2.Location = new Point(15, 460);
rjButton10.Location = new Point(6, 487);
panel2.Show();
rjButton9.Show();
rjButton13.Show();
label16.Show();
}
else
{
// Panel powinien być ukryty, ponieważ nie jest wybrane żadne ciało
rjButton8.Location = new Point(5, 191);
label24.Location = new Point(76, 231);
comboBox2.Location = new Point(12, 248);
rjButton10.Location = new Point(3, 275);
panel2.Hide();
rjButton9.Hide();
rjButton13.Hide();
label16.Hide();
}
}
}
private void Timer_Tick(object sender, EventArgs e)
{
if (timeStep >= 5000 | numSimulationSteps > 75000)
{
Application.Exit();
this.Close();
MessageBox.Show("An unexpected error occured! Too high timestep. Program has succesfully exited");
}
framesCount++;
frames++;
for (int step = 0; step < numSimulationSteps; step++)
{
UpdatePositions();
CalculateGravity();
// Check collisions and handle them
HandleCollisions();
// Update the QuadTree every frame
UpdateQuadTree();
foreach (var body in bodies)
{
CheckWindowCollision(body);
}
}
this.Invalidate(); // Wywołuje ponownie Paint, aby zaktualizować wyświetlanie ciał
// Update frame rate every second
if (frameStopwatch.ElapsedMilliseconds >= 1000)
{
frameRate = framesCount / (frameStopwatch.ElapsedMilliseconds / 1000.0);
framesCount = 0;
frameStopwatch.Restart();
}
}
private void UpdateQuadTree()
{
// Clear the QuadTree and reinsert all bodies
quadTree.Clear();
foreach (var body in bodies)
{
quadTree.Insert(body);
}
}
private void HandleCollisions()
{
for (int i = 0; i < bodies.Count; i++)
{
for (int j = i + 1; j < bodies.Count; j++)
{
Body body1 = bodies[i];
Body body2 = bodies[j];
double distanceSquared = (body1.X - body2.X) * (body1.X - body2.X) + (body1.Y - body2.Y) * (body1.Y - body2.Y);
double sumOfRadiiSquared = (body1.Radius + body2.Radius) * (body1.Radius + body2.Radius);
if (distanceSquared <= sumOfRadiiSquared)
{
// Introduce a damping factor to reduce velocity after collision
double dampingFactor = 0; // Możesz dostosować tę wartość według potrzeb
body1.VX *= dampingFactor;
body1.VY *= dampingFactor;
// Usunięcie zjedzonego ciała
if (body1.Mass > body2.Mass)
{
// Destroy body2
bodies.Remove(body2);
}
else
{
// Destroy body1
bodies.Remove(body1);
// Dodałem dodatkowy współczynnik tłumienia prędkości dla ciała, które pozostało po zjedzeniu innego ciała
body2.VX *= dampingFactor;
body2.VY *= dampingFactor;
}
}
}
}
}
private bool CheckWindowCollision(Body body)
{
int windowWidth = this.ClientSize.Width - 251;
int windowHeight = this.ClientSize.Height;
bool collisionDetected = false;
if (body.X - body.Radius < 0)
{
//body.VX = Math.Abs(body.VX); // Bounce off left border
collisionDetected = true;
}
else if (body.X + body.Radius > windowWidth)
{
//body.VX = -Math.Abs(body.VX); // Bounce off right border
collisionDetected = true;
}
if (body.Y - body.Radius < 0)
{
//body.VY = Math.Abs(body.VY); // Bounce off top border
collisionDetected = true;
}
else if (body.Y + body.Radius > windowHeight)
{
//body.VY = -Math.Abs(body.VY); // Bounce off bottom border
collisionDetected = true;
}
return collisionDetected;
}
private void UpdatePositions()
{
foreach (var body in bodies)
{
if (!body.IsStatic)
{
body.X += body.VX * timeStep;
body.Y += body.VY * timeStep;
}
}
}
private void CalculateGravity()
{
for (int i = 0; i < bodies.Count; i++)
{
for (int j = i + 1; j < bodies.Count; j++)
{
Body body1 = bodies[i];
Body body2 = bodies[j];
double dx = body2.X - body1.X;
double dy = body2.Y - body1.Y;
double distanceSquared = dx * dx + dy * dy;
double distance = Math.Sqrt(distanceSquared);
double forceMagnitude = (G * body1.Mass * body2.Mass) / distanceSquared;
double forceX = forceMagnitude * (dx / distance);
double forceY = forceMagnitude * (dy / distance);
body1.VX += forceX / body1.Mass * timeStep;
body1.VY += forceY / body1.Mass * timeStep;
body2.VX -= forceX / body2.Mass * timeStep;
body2.VY -= forceY / body2.Mass * timeStep;
}
}
}
private void Form1_Load(object sender, EventArgs e)
{
frameStopwatch.Start();
label13.Text = $"Body count: {bodies.Count()}";
label2.Text = "VX: N/A, VY: N/A";
label7.Text = "PX: N/A, PY: N/A";
label9.Text = "Mass: N/A kg";
checkBox1.Checked = Settings.Default.debugMode;
checkBox2.Checked = Settings.Default.selectionMode;
checkBox3.Checked = Settings.Default.velocityVectors;
rjButton8.Location = new Point(5, 191);
label24.Location = new Point(76, 231);
comboBox2.Location = new Point(12, 248);
rjButton10.Location = new Point(3, 275);
}
private TimeSpan simulatedTime = TimeSpan.Zero;
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver;
g.TextContrast = 0;
StartRenderTimer();
// Rysowanie ciał
foreach (var body in bodies)
{
DrawBody(g, body, body.BodyColor); // Kolor można dostosować w zależności od ciała
if (body.IsSelected)
{
DrawSelectionSquare(g, body);
}
if (velocityVectors)
{
// Rysowanie wektorów prędkości
DrawVelocityVectors(g, body);
}
}
if (drawQuadTree)
{
// Rysuj Quad Tree
DrawQuadTree(g, quadTree.Root);
}
StopRenderTimer();
double totalForceX = 0.0;
double totalForceY = 0.0;
for (int i = 0; i < bodies.Count; i++)
{
for (int j = i + 1; j < bodies.Count; j++)
{
Body body1 = bodies[i];
Body body2 = bodies[j];
double dx = body2.X - body1.X;
double dy = body2.Y - body1.Y;
double distanceSquared = dx * dx + dy * dy;
double distance = Math.Sqrt(distanceSquared);
double forceMagnitude = (G * body1.Mass * body2.Mass) / distanceSquared;
double forceX = forceMagnitude * (dx / distance);
double forceY = forceMagnitude * (dy / distance);
body1.VX += forceX / body1.Mass * timeStep;
body1.VY += forceY / body1.Mass * timeStep;
body2.VX -= forceX / body2.Mass * timeStep;
body2.VY -= forceY / body2.Mass * timeStep;
totalForceX += forceX;
totalForceY += forceY;
}
}
if (selectedBody != null)
{
label2.Text = $"VX: {(selectedBody.VX * scaleFactor).ToString("F2")}, VY: {(selectedBody.VY * scaleFactor).ToString("F2")}";
label7.Text = $"PX: {selectedBody.X.ToString("F2")}, PY: {selectedBody.Y.ToString("F2")}";
if (selectedBody.Mass > Math.Pow(10, 9) || selectedBody.Mass < Math.Pow(10, -3)) // Sprawdź, czy masa jest większa niż miliard
{
label9.Text = $"Mass: {ConvertToScientificNotation(selectedBody.Mass)} kg";
}
else
{
label9.Text = $"Mass: {selectedBody.Mass.ToString("F2")} kg";
}
}
else
{
// If no body is selected, reset the labels
label2.Text = "VX: N/A, VY: N/A";
label7.Text = "PX: N/A, PY: N/A";
label9.Text = "Mass: N/A kg";
}
// Update label26.Text with the formatted time
double adjustedElapsedMilliseconds = SimStopwatch.ElapsedMilliseconds;
TimeSpan formattedRealTime = TimeSpan.FromMilliseconds(adjustedElapsedMilliseconds);
string formattedTime = $"{formattedRealTime.Hours:D2}h:{formattedRealTime.Minutes:D2}m:{formattedRealTime.Seconds:D2}s";
label26.Text = $"Real Time: {formattedTime}";
// Update label27.Text with the formatted simulated time
simulatedTime += TimeSpan.FromSeconds(timeStep); // Increment simulated time
// Ensure that simulatedTime is not negative
// Dodaj obliczenia dla formatowania czasu symulacji w latach, dniach, godzinach, minutach i sekundach
int years = simulatedTime.Days / 365;
int days = simulatedTime.Days % 365;
string formattedSimTime = $"{years:D2}yr:{days:D3}d:{simulatedTime.Hours:D2}h:{simulatedTime.Minutes:D2}m:{simulatedTime.Seconds:D2}s";
label27.Text = $"Simulated Time: {formattedSimTime}";
label3.Text = $"F: {Math.Sqrt(totalForceX * totalForceX + totalForceY * totalForceY).ToString("F8")}N";
label4.Text = $"Timestep: {timeStep.ToString("F3")}";
label5.Text = $"numSimulationSteps: {numSimulationSteps}";
label10.Text = $"Frame Rate: {frameRate.ToString("F2")} fps";
label6.Text = $"Frames: {frames} frames";
label13.Text = $"Body count: {bodies.Count()}";
if (bodies.Count <= 1)
{
timer.Stop();
MessageBox.Show("Simulation has ended! Reason: No enough bodies, F=0");
//Dodaj w tej linijce resetowanie do pozycji początkowych
// Wywołanie metody resetowania
ResetBodiesToInitialPositions();
rjButton1.Enabled = true;
rjButton2.Enabled = false;
timeStep = 1;
floatTrackBar1.Value = 1;
trackBar1.Value = 5000;
numSimulationSteps = 5000;
frames = 0;
panel2.Hide();
// Odświeżenie widoku
this.Invalidate();
}
}
private void DrawQuadTree(Graphics g, QuadTree.QuadTreeNode node)
{
if (node != null)
{
float thickness = 0.5f; // Grubość linii
Pen pen = new Pen(Brushes.White, thickness);
Point[] polygonPoints = new Point[]
{
new Point((int)node.Bounds.Left, (int)node.Bounds.Top),
new Point((int)node.Bounds.Right, (int)node.Bounds.Top),
new Point((int)node.Bounds.Right, (int)node.Bounds.Bottom),
new Point((int)node.Bounds.Left, (int)node.Bounds.Bottom)
};
g.DrawPolygon(pen, polygonPoints);
if (node.HasChildren)
{
foreach (var child in node.Children)
{
DrawQuadTree(g, child);
}
}
}
}
static string ConvertToScientificNotation(double value)
{
return value.ToString("0.###e0");
}
static string ConvertFromScientificNotation(string value)
{
if (double.TryParse(value, out double result))
{
return result.ToString();
}
return "Invalid input";
}
private void DrawSelectionSquare(Graphics g, Body body)
{
float x = (float)(body.X - body.Radius - 3); // Adjusting for padding
float y = (float)(body.Y - body.Radius - 3); // Adjusting for padding
float diameter = 2 * body.Radius + 6; // Adjusting for padding
g.DrawRectangle(new Pen(Brushes.Yellow, 1), x, y, diameter, diameter);
}
private bool IsMouseOnBody(Point mousePosition, Body body)
{
double distance = Math.Sqrt(Math.Pow(mousePosition.X - body.X, 2) + Math.Pow(mousePosition.Y - body.Y, 2));
return distance <= body.Radius;
}
private void DrawBody(Graphics g, Body body, Brush brush)
{
float x = (float)(body.X - body.Radius);
float y = (float)(body.Y - body.Radius);
float diameter = 2 * body.Radius;
g.FillEllipse(brush, x, y, diameter, diameter);
}
}
.......
- Quad Tree Class(Soon, i will be adding this algorithm):
public class QuadTree
{
public class QuadTreeNode
{
public Rectangle Bounds { get; }
public List<Body> Bodies { get; } = new List<Body>();
public QuadTreeNode[] Children { get; private set; }
public int MaxBodiesPerNode { get; private set; } = 4;
public bool HasChildren => Children != null;
private bool ShouldSubdivide() => !HasChildren && Bodies.Count >= MaxBodiesPerNode;
public QuadTreeNode(Rectangle bounds, int maxBodiesPerNode)
{
Bounds = bounds;
MaxBodiesPerNode = maxBodiesPerNode;
}
public void Subdivide()
{
int width = Bounds.Width / 2;
int height = Bounds.Height / 2;
Children = new QuadTreeNode[4];
Children[0] = new QuadTreeNode(new Rectangle(Bounds.Left, Bounds.Top, width, height), MaxBodiesPerNode);
Children[1] = new QuadTreeNode(new Rectangle(Bounds.Left + width, Bounds.Top, width, height), MaxBodiesPerNode);
Children[2] = new QuadTreeNode(new Rectangle(Bounds.Left, Bounds.Top + height, width, height), MaxBodiesPerNode);
Children[3] = new QuadTreeNode(new Rectangle(Bounds.Left + width, Bounds.Top + height, width, height), MaxBodiesPerNode);
}
}
public QuadTreeNode Root { get; private set; }
public QuadTree(Rectangle bounds, int maxBodiesPerNode = 4)
{
Root = new QuadTreeNode(bounds, maxBodiesPerNode);
}
public void Clear()
{
Root = new QuadTreeNode(Root.Bounds, Root.MaxBodiesPerNode);
}
public void Insert(Body body)
{
Insert(Root, body);
}
private void Insert(QuadTreeNode node, Body body)
{
if (node.Bounds.Contains(new Point((int)body.X, (int)body.Y)))
{
if (!node.HasChildren && node.Bodies.Count < node.MaxBodiesPerNode)
{
node.Bodies.Add(body);
}
else
{
if (!node.HasChildren)
{
node.Subdivide();
DistributeBodies(node);
}
foreach (var child in node.Children)
{
Insert(child, body);
}
}
}
}
private void DistributeBodies(QuadTreeNode node)
{
foreach (var storedBody in node.Bodies)
{
foreach (var child in node.Children)
{
Insert(child, storedBody);
}
}
node.Bodies.Clear();
}
}
Thats all for it, i hope someone can help me with rendering my simulation using sharpDX 2D. It is my second time trying sharpDx and it is too hard for me now!
The drawing part of the code is not hard, since the Direct2D equivalent of the draw calls that you use in your code are mostly the same.
It's the part of setting up a Direct2D device that is hard...
In the function
DrawQuadTree, I can see that you are drawing a rectangle, then you can just useDrawRectangle. Is there a reason why you were usingDrawPolygon?I've noticed that you have a function
DrawVelocityVectorsbut you didn't post its source code here. If you need to draw a line in that function, theDrawLinefunction is available in Direct2D.now there are two ways to do this, the legacy way and the modern way.
with the legacy way, you will generally need to manage fewer things. with the modern way you need to manage much more stuff like swap chain, but you will have access to
ID2D1DeviceContext, which inherits from and expands uponID2D1RenderTarget, although you didn't seem to need it yet, every draw function you use is already available in theID2D1RenderTargetinterface so far.The legacy way:
You need to surround your draw operation with
BeginDrawandEndDrawNotice that it's your responsibility to close all the DirectX interfaces after you exit the application
The modern way:
Apart from
BeginDrawandEndDraw, you will additionally need to set render target and callPresenton the swap chainIf the size of the control element you rendering to may be resized, you will need to handle it as well
remember to close all the interfaces