diff --git a/BasicProjectionRenderer/BasicProjectionRenderer.sln b/BasicProjectionRenderer/BasicProjectionRenderer.sln
new file mode 100644
index 0000000..0467bb0
--- /dev/null
+++ b/BasicProjectionRenderer/BasicProjectionRenderer.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}") = "BasicProjectionRenderer", "BasicProjectionRenderer\BasicProjectionRenderer.csproj", "{C8F67C63-8D5D-4841-984B-7885A6268865}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C8F67C63-8D5D-4841-984B-7885A6268865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C8F67C63-8D5D-4841-984B-7885A6268865}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C8F67C63-8D5D-4841-984B-7885A6268865}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C8F67C63-8D5D-4841-984B-7885A6268865}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/BasicProjectionRenderer/BasicProjectionRenderer/BasicProjectionRenderer.csproj b/BasicProjectionRenderer/BasicProjectionRenderer/BasicProjectionRenderer.csproj
new file mode 100644
index 0000000..d8f8859
--- /dev/null
+++ b/BasicProjectionRenderer/BasicProjectionRenderer/BasicProjectionRenderer.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0-windows
+ enable
+ true
+ disable
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BasicProjectionRenderer/BasicProjectionRenderer/BasicProjectionRenderer.csproj.user b/BasicProjectionRenderer/BasicProjectionRenderer/BasicProjectionRenderer.csproj.user
new file mode 100644
index 0000000..7814ea2
--- /dev/null
+++ b/BasicProjectionRenderer/BasicProjectionRenderer/BasicProjectionRenderer.csproj.user
@@ -0,0 +1,8 @@
+
+
+
+
+ Form
+
+
+
diff --git a/BasicProjectionRenderer/BasicProjectionRenderer/Form1.Designer.cs b/BasicProjectionRenderer/BasicProjectionRenderer/Form1.Designer.cs
new file mode 100644
index 0000000..3363494
--- /dev/null
+++ b/BasicProjectionRenderer/BasicProjectionRenderer/Form1.Designer.cs
@@ -0,0 +1,57 @@
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace BasicProjectionRenderer
+{
+ 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()
+ {
+ components = new System.ComponentModel.Container();
+ RefreshTimer = new Timer(components);
+ SuspendLayout();
+ //
+ // RefreshTimer
+ //
+ RefreshTimer.Enabled = true;
+ RefreshTimer.Interval = 10;
+ //
+ // Form1
+ //
+ AutoScaleDimensions = new SizeF(13F, 32F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(1300, 783);
+ Name = "Form1";
+ Text = "Form1";
+ ResumeLayout(false);
+ }
+
+ #endregion
+
+ private Timer RefreshTimer;
+ }
+}
diff --git a/BasicProjectionRenderer/BasicProjectionRenderer/Form1.cs b/BasicProjectionRenderer/BasicProjectionRenderer/Form1.cs
new file mode 100644
index 0000000..f4aeb52
--- /dev/null
+++ b/BasicProjectionRenderer/BasicProjectionRenderer/Form1.cs
@@ -0,0 +1,137 @@
+using Nerd_STF.Mathematics;
+using Nerd_STF.Mathematics.Algebra;
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Windows.Forms;
+
+namespace BasicProjectionRenderer;
+
+public partial class Form1 : Form
+{
+ public Mesh? Mesh { get; set; }
+
+ public Float2 ZoomLevel = (1, 1);
+ public Float3 ScreenCenter = (0, 0, -2.5);
+
+ public Form1()
+ {
+ InitializeComponent();
+
+ SetStyle(ControlStyles.UserPaint, true);
+ SetStyle(ControlStyles.AllPaintingInWmPaint, true);
+ SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
+
+ RefreshTimer.Tick += OnTick;
+ }
+
+ public Int2 ToScreenSpace(Float3 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 Float3 FromScreenSpace(Int2 screenPoint)
+ {
+ Float3 result = new(screenPoint.x, screenPoint.y, ScreenCenter.z);
+
+ 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;
+ }
+
+ private int paintIters;
+ private double paintTime;
+ protected override void OnPaint(PaintEventArgs e)
+ {
+ DateTime start = DateTime.Now;
+ Graphics g = e.Graphics;
+ g.SmoothingMode = SmoothingMode.HighQuality;
+ if (Mesh is null) return;
+
+ SolidBrush colorBrush = new(Color.Black);
+ Pen colorPen = new(colorBrush);
+
+ /*// Draw edges.
+ for (int i = 0; i < Mesh.lines.Length; i++)
+ {
+ Line line = Mesh.lines[i];
+ //Color newColor = Color.FromArgb((int)(0xFF000000 | (0x00FFFFFF & line.GetHashCode())));
+ //colorPen.Color = newColor;
+
+ Float3 pointA = Mesh.GetPoint(line.IndA),
+ pointB = Mesh.GetPoint(line.IndB);
+ Int2 posA = ToScreenSpace(pointA), posB = ToScreenSpace(pointB);
+ g.DrawLine(colorPen, posA, posB);
+ }*/
+
+ // Draw faces.
+ for (int i = 0; i < Mesh.faces.Length; i++)
+ {
+ Face face = Mesh.faces[i];
+ Color newColor = Color.FromArgb((int)(0xFF000000 | (0x00FFFFFF & face.GetHashCode())));
+ colorBrush.Color = newColor;
+
+ Float3 pointA = Mesh.GetPoint(face.IndA),
+ pointB = Mesh.GetPoint(face.IndB),
+ pointC = Mesh.GetPoint(face.IndC);
+ Int2 posA = ToScreenSpace(pointA), posB = ToScreenSpace(pointB), posC = ToScreenSpace(pointC);
+ g.FillPolygon(colorBrush, [posA, posB, posC]);
+ }
+ DateTime end = DateTime.Now;
+ paintIters++;
+ paintTime += (end - start).TotalMilliseconds;
+ if (paintIters == 20)
+ {
+ double per = paintTime / 20;
+ Console.WriteLine($"{per:0.000} ms avg.");
+ paintIters = 0;
+ paintTime = 0;
+ }
+ }
+
+ private double elapsedTime, temp;
+ private void OnTick(object? sender, EventArgs e)
+ {
+ elapsedTime += RefreshTimer.Interval * 1e-3;
+ if (Mesh is not null)
+ {
+ DateTime start = DateTime.Now;
+ LookAtCursor();
+ DateTime end = DateTime.Now;
+ paintTime += (end - start).TotalMilliseconds;
+
+ void LookAtCursor()
+ {
+ Float3 mousePos = FromScreenSpace(PointToClient(Cursor.Position));
+
+ Float3 dist = Mesh!.Location - mousePos;
+ Angle leftRight = new(Math.Atan2(dist.x, dist.z), Angle.Units.Radians),
+ upDown = new(Math.Atan2(dist.y, dist.z), Angle.Units.Radians),
+ spin = new(temp, Angle.Units.Degrees);
+ Mesh.Rotation = (upDown, -leftRight, spin);
+ }
+ }
+ Invalidate();
+ }
+}
diff --git a/BasicProjectionRenderer/BasicProjectionRenderer/Form1.resx b/BasicProjectionRenderer/BasicProjectionRenderer/Form1.resx
new file mode 100644
index 0000000..c90283e
--- /dev/null
+++ b/BasicProjectionRenderer/BasicProjectionRenderer/Form1.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ 17, 17
+
+
\ No newline at end of file
diff --git a/BasicProjectionRenderer/BasicProjectionRenderer/Mesh.cs b/BasicProjectionRenderer/BasicProjectionRenderer/Mesh.cs
new file mode 100644
index 0000000..22edb24
--- /dev/null
+++ b/BasicProjectionRenderer/BasicProjectionRenderer/Mesh.cs
@@ -0,0 +1,142 @@
+using Nerd_STF.Mathematics;
+using Nerd_STF.Mathematics.Algebra;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace BasicProjectionRenderer;
+
+public record class Line(int IndA, int IndB) : IEquatable
+{
+ public virtual bool Equals(Line? other)
+ {
+ if (other is null) return false;
+ return (IndA == other.IndA && IndB == other.IndB) ||
+ (IndA == other.IndB && IndB == other.IndA);
+ }
+ public override int GetHashCode() => base.GetHashCode();
+}
+public record class Face(int IndA, int IndB, int IndC);
+
+public class Mesh
+{
+ public required Float3[] points;
+ public required Line[] lines;
+ public required Face[] faces;
+
+ public Float3 Location { get; set; } = (0, 0, 0);
+ public (Angle, Angle, Angle) Rotation
+ {
+ get => _rotation;
+ set
+ {
+ if (_rotation != value)
+ {
+ _rotation = value;
+ CalculateRotationMatrix();
+ }
+ }
+ }
+ public Float3 Scale { get; set; } = (1, 1, 1);
+
+ private readonly Dictionary _pointCache = [];
+ private (Angle, Angle, Angle) _rotation = (Angle.Zero, Angle.Zero, Angle.Zero);
+ private Matrix3x3 _rotMatrix = Matrix3x3.Identity;
+
+ private void CalculateRotationMatrix()
+ {
+ double radX = _rotation.Item1.Radians,
+ radY = _rotation.Item2.Radians,
+ radZ = _rotation.Item3.Radians;
+
+ (double cosX, double sinX) = (MathE.Cos(radX), MathE.Sin(radX));
+ (double cosY, double sinY) = (MathE.Cos(radY), MathE.Sin(radY));
+ (double cosZ, double sinZ) = (MathE.Cos(radZ), MathE.Sin(radZ));
+ Matrix3x3 rotX = new([
+ [ 1, 0, 0 ],
+ [ 0, cosX, -sinX ],
+ [ 0, sinX, cosX ]
+ ]);
+ Matrix3x3 rotY = new([
+ [ cosY, 0, sinY ],
+ [ 0, 1, 0 ],
+ [ -sinY, 0, cosY ]
+ ]);
+ Matrix3x3 rotZ = new([
+ [ cosZ, -sinZ, 0 ],
+ [ sinZ, cosZ, 0 ],
+ [ 0, 0, 1 ]
+ ]);
+ _rotMatrix = rotX * rotY * rotZ;
+ _pointCache.Clear();
+ Array.Sort(faces, SortFace);
+ }
+
+ public Float3 GetPoint(int index)
+ {
+ if (_pointCache.TryGetValue(index, out Float3 cached)) return cached;
+
+ Float3 p = points[index];
+ p = _rotMatrix * p;
+ p *= Scale;
+ p += Location;
+ _pointCache.Add(index, p);
+
+ return p;
+ }
+
+ private int SortFace(Face f1, Face f2)
+ {
+ Float3 f1a = GetPoint(f1.IndA), f2a = GetPoint(f2.IndA),
+ f1b = GetPoint(f1.IndB), f2b = GetPoint(f2.IndB),
+ f1c = GetPoint(f1.IndC), f2c = GetPoint(f2.IndC);
+ Float3 avg1 = (f1a + f1b + f1c) / 3,
+ avg2 = (f2a + f2b + f2c) / 3;
+ return avg1.z.CompareTo(avg2.z);
+ }
+
+ public static Mesh FromObj(Stream data)
+ {
+ StreamReader reader = new(data, leaveOpen: true);
+ string? line;
+ List points = [];
+ List lines = [];
+ List faces = [];
+ while ((line = reader.ReadLine()) is not null)
+ {
+ line = line.Trim();
+ if (line.StartsWith('#')) continue;
+
+ string[] parts = line.Split(' ');
+ switch (parts[0])
+ {
+ case "v":
+ points.Add((double.Parse(parts[1]), double.Parse(parts[2]), double.Parse(parts[3])));
+ break;
+
+ case "f":
+ int indA = int.Parse(parts[1]) - 1,
+ indB = int.Parse(parts[2]) - 1,
+ indC = int.Parse(parts[3]) - 1;
+
+ /*Line l1 = new(indA, indB), l2 = new(indB, indC), l3 = new(indC, indA);
+ if (!lines.Contains(l1)) lines.Add(l1);
+ if (!lines.Contains(l2)) lines.Add(l2);
+ if (!lines.Contains(l3)) lines.Add(l3);*/
+
+ faces.Add(new(indA, indB, indC));
+ break;
+ }
+ }
+ reader.Close();
+ Console.WriteLine($"{points.Count} verts, {lines.Count} edges, {faces.Count} faces");
+ return new()
+ {
+ points = [.. points],
+ lines = [.. lines],
+ faces = [.. faces]
+ };
+ }
+
+ public override int GetHashCode() => points.GetHashCode();
+}
diff --git a/BasicProjectionRenderer/BasicProjectionRenderer/Program.cs b/BasicProjectionRenderer/BasicProjectionRenderer/Program.cs
new file mode 100644
index 0000000..bd10bb8
--- /dev/null
+++ b/BasicProjectionRenderer/BasicProjectionRenderer/Program.cs
@@ -0,0 +1,33 @@
+/**********722871**********
+ * Date: 2/19/2025
+ * Programmer: Kyle Gilbert
+ * Program Name: BasicProjectionRenderer
+ * Program Description: Renders an OBJ file in orthographic view. All code
+ * (even the matrix multiplication) is my own.
+ **************************/
+
+using System;
+using System.IO;
+using System.Windows.Forms;
+
+namespace BasicProjectionRenderer;
+
+public static class Program
+{
+ [STAThread]
+ public static void Main()
+ {
+ Application.SetCompatibleTextRenderingDefault(false);
+ Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
+
+ string path = "cube.obj";
+ FileStream fs = new(path, FileMode.Open);
+ Mesh obj = Mesh.FromObj(fs);
+ fs.Close();
+ Form1 form = new()
+ {
+ Mesh = obj
+ };
+ Application.Run(form);
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index f7e9695..5a71783 100644
--- a/README.md
+++ b/README.md
@@ -31,17 +31,22 @@ I have about 1-2 weeks for each project. Check the Git commits for specific date
- A small game I made in like 2 hours. Using a list of words, it picks one at random and the user has to type it out.
- It has a one-minute timer, and highlights the letters you got right.
- Shows results as characters per minute, words per minute, and accuracy percentage at the end.
-- AirTrajectoryBuilder
+- AirTrajectoryBuilder/
- A program I wrote that simulates the air trajectory of a projectile. Finished a while ago.
- Create a `.sce` file (a somewhat easy to use plain text format)
- Nice colors for each object. Scales seamlessly with a higher DPI.
- Sweeps possible angles and speeds to try and find the path that brings the ball closest to the end point.
-- Fractal Visualizer
+- Fractal Visualizer/
- A program that can be used to visualize fractals.
- Allows you to zoom in and drag the screen around in real time.
- Renders in multiple resolution scales so as to be as responsive as possible. Upscales over time.
- Currently does the mandlebrot set. It has support for any complex iterative fractal, but you have to code it yourself.
-- Ciphers
+- Ciphers/
- Command-line tool that enciphers and deciphers text.
- Small thing. Not super good. I used it to complete homework for a different class.
- Uses an argument parsing library I also wrote.
+- BasicProjectionRenderer/
+ - A program that parses and renders an OBJ file.
+ - 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.