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.