diff --git a/BasicProjectionRenderer/BasicProjectionRenderer/Form1.cs b/BasicProjectionRenderer/BasicProjectionRenderer/Form1.cs index f4aeb52..e371adf 100644 --- a/BasicProjectionRenderer/BasicProjectionRenderer/Form1.cs +++ b/BasicProjectionRenderer/BasicProjectionRenderer/Form1.cs @@ -114,6 +114,7 @@ public partial class Form1 : Form private void OnTick(object? sender, EventArgs e) { elapsedTime += RefreshTimer.Interval * 1e-3; + //temp += MathE.Sin(elapsedTime * 8) * 0.9; if (Mesh is not null) { DateTime start = DateTime.Now; diff --git a/BasicProjectionRenderer/BasicProjectionRenderer/Program.cs b/BasicProjectionRenderer/BasicProjectionRenderer/Program.cs index bd10bb8..c00bc4e 100644 --- a/BasicProjectionRenderer/BasicProjectionRenderer/Program.cs +++ b/BasicProjectionRenderer/BasicProjectionRenderer/Program.cs @@ -19,8 +19,8 @@ public static class Program { Application.SetCompatibleTextRenderingDefault(false); Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); - - string path = "cube.obj"; + + string path = "monkey.obj"; FileStream fs = new(path, FileMode.Open); Mesh obj = Mesh.FromObj(fs); fs.Close(); diff --git a/InverseKinematics/InverseKinematics.sln b/InverseKinematics/InverseKinematics.sln new file mode 100644 index 0000000..4678a4b --- /dev/null +++ b/InverseKinematics/InverseKinematics.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35506.116 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InverseKinematics", "InverseKinematics\InverseKinematics.csproj", "{74875265-97D7-42EA-A2F2-23D2A890FD85}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {74875265-97D7-42EA-A2F2-23D2A890FD85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74875265-97D7-42EA-A2F2-23D2A890FD85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74875265-97D7-42EA-A2F2-23D2A890FD85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74875265-97D7-42EA-A2F2-23D2A890FD85}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/InverseKinematics/InverseKinematics/Form1.Designer.cs b/InverseKinematics/InverseKinematics/Form1.Designer.cs new file mode 100644 index 0000000..0547934 --- /dev/null +++ b/InverseKinematics/InverseKinematics/Form1.Designer.cs @@ -0,0 +1,45 @@ +namespace InverseKinematics +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + SuspendLayout(); + // + // Form1 + // + AutoScaleDimensions = new SizeF(13F, 32F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1134, 720); + Name = "Form1"; + Text = "Form1"; + ResumeLayout(false); + } + + #endregion + } +} diff --git a/InverseKinematics/InverseKinematics/Form1.cs b/InverseKinematics/InverseKinematics/Form1.cs new file mode 100644 index 0000000..36a6bd4 --- /dev/null +++ b/InverseKinematics/InverseKinematics/Form1.cs @@ -0,0 +1,155 @@ +using Nerd_STF.Mathematics; +using System.Drawing.Drawing2D; + +namespace InverseKinematics; + +public partial class Form1 : Form +{ + public Float2 DesiredPoint { get; set; } + + public Float2[] Points { get; set; } + + public Float2 ZoomLevel { get; set; } = (3, 3); + public Float2 ScreenCenter { get; set; } = (0, 0); + + public Form1() + { + InitializeComponent(); + Points = new Float2[5]; + for (int i = 0; i < Points.Length; i++) + { + Points[i] = (0, i); + } + + SetStyle(ControlStyles.UserPaint, true); + SetStyle(ControlStyles.AllPaintingInWmPaint, true); + SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + } + + public Int2 ToScreenSpace(Float2 worldPoint) + { + float dpi = DeviceDpi; + worldPoint.y = -worldPoint.y; + + worldPoint.x -= ScreenCenter.x; + worldPoint.y -= ScreenCenter.y; + + worldPoint.x *= dpi / ZoomLevel.x; + worldPoint.y *= dpi / ZoomLevel.y; + + worldPoint.x += ClientRectangle.Width * 0.5; + worldPoint.y += ClientRectangle.Height * 0.5; + + return new((int)worldPoint.x, (int)worldPoint.y); + } + public Float2 FromScreenSpace(Int2 screenPoint) + { + Float2 result = (screenPoint.x, screenPoint.y); + + result.x -= ClientRectangle.Width / 2.0; + result.y -= ClientRectangle.Height / 2.0; + + float dpi = DeviceDpi; + result.x /= dpi / ZoomLevel.x; + result.y /= dpi / ZoomLevel.y; + + result.x += ScreenCenter.x; + result.y += ScreenCenter.y; + + result.y = -result.y; + + return result; + } + + protected override void OnMouseMove(MouseEventArgs e) + { + DesiredPoint = FromScreenSpace(e.Location); + Invalidate(); + } + protected override void OnPaint(PaintEventArgs e) + { + RunFabrik(); + + Graphics g = e.Graphics; + g.SmoothingMode = SmoothingMode.HighQuality; + float scaleFactor = g.DpiX / 96.0f; + + SolidBrush brush = new(Color.Black); + Pen pen = new(Color.Black, scaleFactor); + + Float2 radius = (2, 2); + Int2 prevPointPix = (0, 0); + for (int i = 0; i < Points.Length; i++) + { + Int2 pointPix = ToScreenSpace(Points[i]); + g.FillEllipse(brush, new RectangleF(pointPix - radius * scaleFactor, radius * scaleFactor * 2)); + + if (i > 0) + { + g.DrawLine(pen, prevPointPix, pointPix); + } + prevPointPix = pointPix; + } + + Int2 desiredPix = ToScreenSpace(DesiredPoint); + radius = (5, 5); + + brush.Color = Color.Red; + g.FillEllipse(brush, new RectangleF(desiredPix - radius * scaleFactor, radius * scaleFactor * 2)); + } + + private void RunFabrik() + { + // Initial setup. + for (int i = 0; i < Points.Length; i++) + { + // Reset points to 90 degrees away from goal. + // This produces a "nice" rotation no matter where + // you are. + Float2 dir = (-DesiredPoint.y, DesiredPoint.x); + dir.Normalize(); + + Points[i] = dir * i; + } + + RunFabrik(true, 16); + } + + // True = forwards, false = backwards. + private void RunFabrik(bool direction, int maxIters) + { + if (maxIters <= 0) return; + + const double maxLength = 1; + if (direction) + { + Points[^1] = DesiredPoint; + for (int i = Points.Length - 2; i >= 0; i--) + { + Float2 thisPoint = Points[i], + connected = Points[i + 1], + diff = connected - thisPoint; + + double step = maxLength - diff.Magnitude; + Float2 dir = diff.Normalized; + Points[i] = thisPoint - dir * step; + } + } + else + { + Points[0] = (0, 0); + for (int i = 1; i < Points.Length; i++) + { + Float2 thisPoint = Points[i], + connected = Points[i - 1], + diff = connected - thisPoint; + + double step = maxLength - diff.Magnitude; + Float2 dir = diff.Normalized; + Points[i] = thisPoint - dir * step; + } + } + + RunFabrik(!direction, maxIters - 1); + } +} diff --git a/InverseKinematics/InverseKinematics/Form1.resx b/InverseKinematics/InverseKinematics/Form1.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/InverseKinematics/InverseKinematics/Form1.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/InverseKinematics/InverseKinematics/InverseKinematics.csproj b/InverseKinematics/InverseKinematics/InverseKinematics.csproj new file mode 100644 index 0000000..caba88c --- /dev/null +++ b/InverseKinematics/InverseKinematics/InverseKinematics.csproj @@ -0,0 +1,15 @@ + + + + WinExe + net8.0-windows + enable + true + enable + + + + + + + \ No newline at end of file diff --git a/InverseKinematics/InverseKinematics/InverseKinematics.csproj.user b/InverseKinematics/InverseKinematics/InverseKinematics.csproj.user new file mode 100644 index 0000000..7814ea2 --- /dev/null +++ b/InverseKinematics/InverseKinematics/InverseKinematics.csproj.user @@ -0,0 +1,8 @@ + + + + + Form + + + diff --git a/InverseKinematics/InverseKinematics/Program.cs b/InverseKinematics/InverseKinematics/Program.cs new file mode 100644 index 0000000..10d8a50 --- /dev/null +++ b/InverseKinematics/InverseKinematics/Program.cs @@ -0,0 +1,20 @@ +/**********722871********** + * Date: 2/25/2025 + * Programmer: Kyle Gilbert + * Program Name: InverseKinematics + * Program Description: Calculates a series of joints with inverse kinematics. + **************************/ + +namespace InverseKinematics; + +internal static class Program +{ + [STAThread] + public static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.SetHighDpiMode(HighDpiMode.SystemAware); + Application.Run(new Form1()); + } +} diff --git a/README.md b/README.md index 5a71783..b1a514f 100644 --- a/README.md +++ b/README.md @@ -50,3 +50,8 @@ I have about 1-2 weeks for each project. Check the Git commits for specific date - Not super optimized, but I've made a few tweaks to speed it up. - All calculations are my own, from sin and cosine to matrix multiplication. - Math code comes from a library I wrote, Nerd_STF. +- InverseKinematics/ + - A program that calculates inverse kinematics for a series of joints. + - Took me less than an hour to write. Faster than I expected. + - Handles hundreds of nodes with very little optimization. + - Might improve in the future to work around obstacles.