├── .gitignore ├── AsciiRender.csproj ├── BoolBox.cs ├── BrightnessBuffer.cs ├── Camera.cs ├── Commands ├── ChangeCameraFOV.cs ├── ClearConsole.cs ├── Command.cs ├── CompoundCommand.cs ├── DrawHelp.cs ├── MoveCamera.cs ├── RotateCamera.cs └── ToggleBoolean.cs ├── Controller.cs ├── ConvertListToNew.py ├── Manager.cs ├── Math ├── Matrix.cs ├── Rect.cs └── Vector3D.cs ├── Photos ├── AxesRotation.png ├── BeautifulDrawing.png ├── ChangeOfBaseTriangleI.png ├── ChangeOfBaseTriangleJ.png ├── ClassDiagram.png ├── DonutAisle.png ├── FOVGraph.png ├── FinishedRotation.png ├── MovingCamera.gif ├── NoRotation.png ├── SpinningDonut.gif └── VariousSurfaces.png ├── Program.cs ├── README.md ├── Renderer.cs └── Surfaces ├── Cuboid.cs ├── Decorators ├── DecoratorExample.cs ├── Position.cs ├── Rotation.cs ├── Spinning.cs └── SurfaceDecorator.cs ├── Donut.cs ├── Plane.cs ├── Surface.cs └── Swirly.cs /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /AsciiRender.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | disable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /BoolBox.cs: -------------------------------------------------------------------------------- 1 | // Boxes a boolean value so that it behaves as a reference type 2 | public class BoolBox 3 | { 4 | public bool value; 5 | 6 | public BoolBox(bool value) 7 | { 8 | this.value = value; 9 | } 10 | } -------------------------------------------------------------------------------- /BrightnessBuffer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Stores a buffer of pixels where each pixel contains a brightness 3 | and a z coordinate. Write points to the buffer and use ToString 4 | to see all the points that are not blocked by other points. 5 | */ 6 | public class BrightnessBuffer 7 | { 8 | Pixel[,] buffer; 9 | 10 | // Contains which characters to swap for brightness levels. 11 | // Make sure the minBrightness is in descending order 12 | static readonly BrightnessLevel[] levels = { 13 | new(0.9999, '@'), new(0.8037, '&'), new(0.7834, '%'), new(0.7602, 'Q'), new(0.7332, 'W'), new(0.7302, 'N'), new(0.7235, 'M'), new(0.7086, '0'), new(0.7039, 'g'), new(0.6925, 'B'), new(0.6816, '$'), new(0.6809, '#'), new(0.6759, 'D'), new(0.6714, 'R'), new(0.6631, '8'), new(0.6595, 'm'), new(0.6561, 'H'), new(0.6465, 'X'), new(0.6099, 'K'), new(0.6093, 'A'), new(0.6049, 'U'), new(0.6043, 'b'), new(0.5999, 'G'), new(0.5972, 'O'), new(0.587, 'p'), new(0.5818, 'V'), new(0.5777, '4'), new(0.5776, 'd'), new(0.565, '9'), new(0.5602, 'h'), new(0.5602, '6'), new(0.5591, 'P'), new(0.5569, 'k'), new(0.5567, 'q'), new(0.5509, 'w'), new(0.4992, 'S'), new(0.4953, 'E'), new(0.4944, '2'), new(0.4881, ']'), new(0.4833, 'a'), new(0.4703, 'y'), new(0.4693, 'j'), new(0.4686, 'x'), new(0.4667, 'Y'), new(0.4638, '5'), new(0.461, 'Z'), new(0.458, 'o'), new(0.4562, 'e'), new(0.4503, 'n'), new(0.4477, '['), new(0.4473, 'u'), new(0.442, 'l'), new(0.4385, 't'), new(0.4382, '1'), new(0.4328, '3'), new(0.4293, 'I'), new(0.4274, 'f'), new(0.4247, '}'), new(0.423, 'C'), new(0.42, '{'), new(0.4101, 'i'), new(0.4091, 'F'), new(0.4075, '|'), new(0.3993, '('), new(0.3984, '7'), new(0.396, 'J'), new(0.3921, ')'), new(0.3838, 'v'), new(0.3747, 'T'), new(0.3737, 'L'), new(0.3667, 's'), new(0.3619, '?'), new(0.3609, 'z'), new(0.3384, '/'), new(0.3294, '*'), new(0.3232, 'c'), new(0.3192, 'r'), new(0.3099, '!'), new(0.2919, '+'), new(0.2902, '<'), new(0.2852, '>'), new(0.2571, ';'), new(0.2417, '='), new(0.2183, '^'), new(0.185, ','), new(0.1559, '_'), new(0.1403, ':'), new(0.1227, '\''), new(0.0848, '-'), new(0.0829, '.'), new(0.0751, '`'), new(0, ' ') 14 | //new(0.85, '\u2593'), new(0.05, '\u2592'), new(0.01, '\u2591'), new(0, ' ') 15 | }; 16 | 17 | // Initializes a buffer of size width by height. Each pixel is defaulted to a brightness of zero on the z plane 18 | public BrightnessBuffer(int width, int height) 19 | { 20 | buffer = new Pixel[width, height]; 21 | } 22 | 23 | // Sets a pixel at a point to a brightness 24 | public void SetPixel(int x, int y, double z, double brightness) 25 | { 26 | buffer[x, y].brightness = brightness; 27 | buffer[x, y].z = z; 28 | } 29 | 30 | // Returns whether the given coordinates are in the range in be in the buffer 31 | public bool IsPixelInBoundaries(int x, int y) 32 | { 33 | return (0 <= x && x < buffer.GetLength(0)) && (0 <= y && y < buffer.GetLength(1)); 34 | } 35 | 36 | // Returns whether there is another pixel in front and would be blocking the view of the given pixel 37 | public bool IsPixelBlocked(int x, int y, double z) 38 | { 39 | // z values of 0 indicate an unset pixel 40 | return buffer[x, y].z > 0 && buffer[x, y].z <= z; 41 | } 42 | 43 | // Returns the buffer width 44 | public int GetWidth() 45 | { 46 | return buffer.GetLength(0); 47 | } 48 | 49 | // Returns the buffer height 50 | public int GetHeight() 51 | { 52 | return buffer.GetLength(1); 53 | } 54 | 55 | public override string ToString() 56 | { 57 | int width = buffer.GetLength(0); 58 | int height = buffer.GetLength(1); 59 | // The additional height is for the end line characters for each row 60 | char[] strBuilder = new char[width * height + height]; 61 | 62 | // i keeps track of the current array element to write to 63 | int i = 0; 64 | for (int y = 0; y < height; y++) { 65 | for (int x = 0; x < width; x++) { 66 | foreach (var level in levels) { 67 | if (buffer[x, y].brightness >= level.minBrightness) { 68 | strBuilder[i] = level.character; 69 | break; 70 | } 71 | } 72 | i++; 73 | } 74 | strBuilder[i] = '\n'; 75 | i++; 76 | } 77 | 78 | return new string(strBuilder); 79 | } 80 | 81 | struct Pixel 82 | { 83 | public double brightness; 84 | public double z; 85 | } 86 | 87 | // Stores the character to use for each brightness level 88 | struct BrightnessLevel 89 | { 90 | public double minBrightness; 91 | public char character; 92 | 93 | public BrightnessLevel(double minBrightness, char character) 94 | { 95 | this.minBrightness = minBrightness; 96 | this.character = character; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /Camera.cs: -------------------------------------------------------------------------------- 1 | public class Camera 2 | { 3 | double FOV; 4 | double FOVFactor; 5 | Vector3D position; 6 | Vector3D rotation; 7 | Matrix rotationMatrix; 8 | 9 | // An orthographic view is used if set to false 10 | bool perspectiveView = true; 11 | 12 | public Camera(double FOV, Vector3D position, Vector3D rotation) 13 | { 14 | SetFOV(FOV); 15 | SetPosition(position); 16 | SetRotation(rotation); 17 | } 18 | 19 | // Moves a point based on how the camera is set up 20 | public Vector3D ApplyView(Vector3D point) 21 | { 22 | Vector3D newPoint = ApplyRotation(ApplyPosition(point)); 23 | return perspectiveView ? ApplyPerspectiveView(newPoint) : newPoint; 24 | } 25 | 26 | // Returns true if the normal of the face is pointing towards the camera such that the front is seen 27 | public bool IsFrontFace(Vector3D position, Vector3D normal) 28 | { 29 | if (perspectiveView) { 30 | return 0 >= Vector3D.DotProduct(ApplyRotation(ApplyPosition(position)), ApplyRotation(normal)); 31 | } else { 32 | return 0 >= Vector3D.DotProduct(new Vector3D(0, 0, 1), ApplyRotation(normal)); 33 | } 34 | } 35 | 36 | public void SetPosition(Vector3D position) 37 | { 38 | this.position = position; 39 | } 40 | 41 | public void ChangePosition(Vector3D displacement, bool relativeToRotation) 42 | { 43 | if (relativeToRotation) 44 | displacement.RotateXYZ(rotation); 45 | position += displacement; 46 | } 47 | 48 | public void SetRotation(Vector3D rotation) 49 | { 50 | this.rotation = rotation; 51 | rotationMatrix = Matrix.XYZRotation(rotation * -1); 52 | } 53 | 54 | public void ChangeRotation(Vector3D rotation) 55 | { 56 | SetRotation(this.rotation + rotation); 57 | } 58 | 59 | // Changes FOV by the amount, delta in radians. The FOV is clamped between 0 and 180 degrees. 60 | public void ChangeFOV(double delta) 61 | { 62 | SetFOV(System.Math.Clamp(FOV + delta, 0.01, System.Math.PI - 0.01)); 63 | } 64 | 65 | public double GetFOV() 66 | { 67 | return FOV; 68 | } 69 | 70 | // Moves a point based on camera's position 71 | Vector3D ApplyPosition(Vector3D point) 72 | { 73 | // Substract position because when the camera moves in the +x direction, 74 | // in reality, everything is moving in the -x direction while the camera remains stationary 75 | return point - position; 76 | } 77 | 78 | // Moves a point based on camera's rotation 79 | Vector3D ApplyRotation(Vector3D point) 80 | { 81 | return rotationMatrix * point; 82 | } 83 | 84 | // Applies a perspective view opposed to an ordinary 85 | // orthographic view so that the camera acts like our eyes 86 | Vector3D ApplyPerspectiveView(Vector3D point) 87 | { 88 | return new Vector3D( 89 | FOVFactor * point.GetX() / point.GetZ(), 90 | FOVFactor * point.GetY() / point.GetZ(), 91 | point.GetZ() 92 | ); 93 | } 94 | 95 | // Call this method when setting the FOV so that the FOVFactor is computed as well 96 | void SetFOV(double FOV) 97 | { 98 | this.FOV = FOV; 99 | FOVFactor = ComputeFOVFactor(); 100 | } 101 | 102 | // Returns the factor used in the perspective equation for the given FOV in radians 103 | double ComputeFOVFactor() 104 | { 105 | return 1 / System.Math.Tan(FOV / 2); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Commands/ChangeCameraFOV.cs: -------------------------------------------------------------------------------- 1 | namespace Commands; 2 | 3 | public class ChangeCameraFOV : Command 4 | { 5 | Camera camera; 6 | double delta; 7 | 8 | public ChangeCameraFOV(Camera camera, double delta) 9 | { 10 | this.camera = camera; 11 | this.delta = delta; 12 | } 13 | 14 | public override void Execute() 15 | { 16 | camera.ChangeFOV(delta); 17 | } 18 | } -------------------------------------------------------------------------------- /Commands/ClearConsole.cs: -------------------------------------------------------------------------------- 1 | namespace Commands; 2 | 3 | public class ClearConsole : Command 4 | { 5 | public override void Execute() 6 | { 7 | System.Console.Clear(); 8 | } 9 | } -------------------------------------------------------------------------------- /Commands/Command.cs: -------------------------------------------------------------------------------- 1 | // Class for command pattern to execute an abitrary task 2 | public abstract class Command 3 | { 4 | public abstract void Execute(); 5 | } -------------------------------------------------------------------------------- /Commands/CompoundCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Commands; 2 | 3 | // Command that executes any number of commands 4 | public class CompoundCommand : Command 5 | { 6 | // commands to execute 7 | Command[] commands; 8 | 9 | public CompoundCommand(params Command[] commands) 10 | { 11 | this.commands = commands; 12 | } 13 | 14 | public override void Execute() 15 | { 16 | foreach (Command command in commands) { 17 | command.Execute(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Commands/DrawHelp.cs: -------------------------------------------------------------------------------- 1 | namespace Commands; 2 | 3 | // Displays keybindings and helpful information 4 | public class DrawHelp : Command 5 | { 6 | BoolBox showHelp; 7 | 8 | /* 9 | The value of showHelp determines whether help dialog is shown 10 | or the screen is just cleared on command execution 11 | */ 12 | public DrawHelp(BoolBox showHelp) 13 | { 14 | this.showHelp = showHelp; 15 | } 16 | 17 | public override void Execute() 18 | { 19 | System.Console.Clear(); 20 | if (showHelp.value) { 21 | System.Console.WriteLine(""" 22 | Press H to hide Help 23 | 24 | CAMERA 25 | Movement 26 | W - Forward 27 | A - Left 28 | S - Backwards 29 | D - Right 30 | 31 | Q - Up 32 | E - Down 33 | 34 | N - Reset position to origin 35 | 36 | Rotating 37 | I - Up 38 | J - Left 39 | K - Down 40 | L - Right 41 | 42 | U - Counterclockwise 43 | O - Clockwise 44 | 45 | M - Reset rotation to original 46 | 47 | Other 48 | Left Arrow - Decrease FOV 49 | Right Arrow - Increase FOV 50 | 51 | ----------------------------------- 52 | MISCELLANEOUS 53 | G - Toggle Debugging 54 | """); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Commands/MoveCamera.cs: -------------------------------------------------------------------------------- 1 | namespace Commands; 2 | 3 | public class MoveCamera : Command 4 | { 5 | Camera camera; 6 | Vector3D displacement; 7 | bool relativeToRotation; 8 | bool relativeToPosition; 9 | 10 | /* 11 | If relativeToRotation is true, the displacement of the camera 12 | will be relative to which direction it is pointing. 13 | 14 | If relativeToPosition is true, the camera's position is displaced rather than set. 15 | */ 16 | public MoveCamera(Camera camera, Vector3D displacement, bool relativeToRotation, bool relativeToPosition = true) 17 | { 18 | this.camera = camera; 19 | this.displacement = displacement; 20 | this.relativeToRotation = relativeToRotation; 21 | this.relativeToPosition = relativeToPosition; 22 | } 23 | 24 | public override void Execute() 25 | { 26 | if (relativeToPosition) { 27 | camera.ChangePosition(displacement, relativeToRotation); 28 | } else { 29 | camera.SetPosition(displacement); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Commands/RotateCamera.cs: -------------------------------------------------------------------------------- 1 | namespace Commands; 2 | 3 | public class RotateCamera : Command 4 | { 5 | Camera camera; 6 | Vector3D rotation; 7 | bool setRotation; 8 | 9 | /* 10 | if setRotation is true, sets the camera's rotation rather than changing it by a delta 11 | */ 12 | public RotateCamera(Camera camera, Vector3D rotation, bool setRotation = false) 13 | { 14 | this.camera = camera; 15 | this.rotation = rotation; 16 | this.setRotation = setRotation; 17 | } 18 | 19 | public override void Execute() 20 | { 21 | if (setRotation) { 22 | camera.SetRotation(rotation); 23 | } else { 24 | camera.ChangeRotation(rotation); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Commands/ToggleBoolean.cs: -------------------------------------------------------------------------------- 1 | namespace Commands; 2 | 3 | public class ToggleBoolean : Command 4 | { 5 | BoolBox target; 6 | 7 | // target is the wrapped boolean value to toggle on command execution 8 | public ToggleBoolean(BoolBox target) 9 | { 10 | this.target = target; 11 | } 12 | 13 | public override void Execute() 14 | { 15 | target.value = !target.value; 16 | } 17 | } -------------------------------------------------------------------------------- /Controller.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Keys = System.ConsoleKey; 3 | 4 | /* 5 | Controller class handles console inputs while the program is running 6 | */ 7 | public class Controller 8 | { 9 | Dictionary keyBindings = new(); 10 | 11 | double moveSpeed = 1; 12 | double rotateSpeed = 0.1; 13 | double deltaFOV = System.Math.PI / 180; 14 | 15 | public Controller(Camera camera, BoolBox showHelp, BoolBox showDebug) 16 | { 17 | SetDefaultKeyBindings(camera, showHelp, showDebug); 18 | } 19 | 20 | // Checks and handles keyboard input 21 | public void Update() 22 | { 23 | if (System.Console.KeyAvailable) { 24 | var key = System.Console.ReadKey().Key; 25 | if (keyBindings.ContainsKey(key)) { 26 | keyBindings[key].Execute(); 27 | } 28 | } 29 | } 30 | 31 | void SetDefaultKeyBindings(Camera camera, BoolBox showHelp, BoolBox showDebug) 32 | { 33 | //////////// 34 | // CAMERA // 35 | //////////// 36 | 37 | // Movement 38 | keyBindings.Add(Keys.W, new Commands.MoveCamera(camera, new Vector3D(0, 0, moveSpeed), true)); 39 | keyBindings.Add(Keys.A, new Commands.MoveCamera(camera, new Vector3D(-moveSpeed, 0, 0), true)); 40 | keyBindings.Add(Keys.S, new Commands.MoveCamera(camera, new Vector3D(0, 0, -moveSpeed), true)); 41 | keyBindings.Add(Keys.D, new Commands.MoveCamera(camera, new Vector3D(moveSpeed, 0, 0), true)); 42 | keyBindings.Add(Keys.Q, new Commands.MoveCamera(camera, new Vector3D(0, -moveSpeed, 0), true)); 43 | keyBindings.Add(Keys.E, new Commands.MoveCamera(camera, new Vector3D(0, moveSpeed, 0), true)); 44 | 45 | // Rotation 46 | keyBindings.Add(Keys.I, new Commands.RotateCamera(camera, new Vector3D(rotateSpeed, 0, 0))); 47 | keyBindings.Add(Keys.J, new Commands.RotateCamera(camera, new Vector3D(0, rotateSpeed, 0))); 48 | keyBindings.Add(Keys.K, new Commands.RotateCamera(camera, new Vector3D(-rotateSpeed, 0, 0))); 49 | keyBindings.Add(Keys.L, new Commands.RotateCamera(camera, new Vector3D(0, -rotateSpeed, 0))); 50 | keyBindings.Add(Keys.U, new Commands.RotateCamera(camera, new Vector3D(0, 0, -rotateSpeed))); 51 | keyBindings.Add(Keys.O, new Commands.RotateCamera(camera, new Vector3D(0, 0, rotateSpeed))); 52 | 53 | // Change FOV 54 | keyBindings.Add(Keys.LeftArrow, new Commands.ChangeCameraFOV(camera, -deltaFOV)); 55 | keyBindings.Add(Keys.RightArrow, new Commands.ChangeCameraFOV(camera, deltaFOV)); 56 | 57 | // Reset position to origin 58 | keyBindings.Add(Keys.N, new Commands.MoveCamera(camera, new Vector3D(0, 0, 0), false, false)); 59 | 60 | // Reset rotation and FOV to default 61 | keyBindings.Add(Keys.M, new Commands.RotateCamera(camera, new Vector3D(0, 0, 0), true)); 62 | 63 | /////////// 64 | // OTHER // 65 | /////////// 66 | 67 | // Toggle help, the order of command execution does matter 68 | // since DrawHelp relies on the value of showHelp 69 | keyBindings.Add(Keys.H, new Commands.CompoundCommand( 70 | new Commands.ToggleBoolean(showHelp), 71 | new Commands.DrawHelp(showHelp))); 72 | 73 | // Toggle debugging 74 | keyBindings.Add(Keys.G, new Commands.CompoundCommand( 75 | new Commands.ClearConsole(), 76 | new Commands.ToggleBoolean(showDebug))); 77 | } 78 | } -------------------------------------------------------------------------------- /ConvertListToNew.py: -------------------------------------------------------------------------------- 1 | # Used to instantiate the static readonly member levels in the BrightnessBuffer class without handtyping it 2 | 3 | levels = [0, 0.0751, 0.0829, 0.0848, 0.1227, 0.1403, 0.1559, 0.185, 0.2183, 0.2417, 0.2571, 0.2852, 0.2902, 0.2919, 0.3099, 0.3192, 0.3232, 0.3294, 0.3384, 0.3609, 0.3619, 0.3667, 0.3737, 0.3747, 0.3838, 0.3921, 0.396, 0.3984, 0.3993, 0.4075, 0.4091, 0.4101, 0.42, 0.423, 0.4247, 0.4274, 0.4293, 0.4328, 0.4382, 0.4385, 0.442, 0.4473, 0.4477, 0.4503, 0.4562, 0.458, 0.461, 0.4638, 0.4667, 0.4686, 0.4693, 0.4703, 0.4833, 0.4881, 0.4944, 0.4953, 0.4992, 0.5509, 0.5567, 0.5569, 0.5591, 0.5602, 0.5602, 0.565, 0.5776, 0.5777, 0.5818, 0.587, 0.5972, 0.5999, 0.6043, 0.6049, 0.6093, 0.6099, 0.6465, 0.6561, 0.6595, 0.6631, 0.6714, 0.6759, 0.6809, 0.6816, 0.6925, 0.7039, 0.7086, 0.7235, 0.7302, 0.7332, 0.7602, 0.7834, 0.8037, 0.9999] 4 | characters = " `.-':_,^=;><+!rc*/z?sLTv)J7(|Fi{C}fI31tlu[neoZ5Yxjya]2ESwqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@" 5 | 6 | for i in range(len(levels)): 7 | print("new(" + str(levels[-1 - i]) + ", '" + characters[-1 - i] + "'), ", end="") -------------------------------------------------------------------------------- /Manager.cs: -------------------------------------------------------------------------------- 1 | using static System.Console; 2 | using System.Globalization; 3 | using Math = System.Math; 4 | using DateTime = System.DateTime; 5 | using TimeSpan = System.TimeSpan; 6 | 7 | // Handles the AsciiRender program 8 | // Supply the surfaces and use the Run method 9 | public class Manager 10 | { 11 | Vector3D sunDirection; 12 | Camera camera; 13 | Surface[] surfaces; 14 | 15 | Renderer renderer; 16 | Controller controller; 17 | double FPSLimit = 0; 18 | BoolBox showHelp = new(false); 19 | BoolBox showDebug = new(true); 20 | 21 | // sunDirection must be a unit vector pointing in the direction of the sun. 22 | public Manager(Vector3D sunDirection, Camera camera, params Surface[] surfaces) 23 | { 24 | this.sunDirection = sunDirection; 25 | this.surfaces = surfaces; 26 | renderer = new Renderer(sunDirection, camera); 27 | this.camera = camera; 28 | 29 | controller = new Controller(camera, showHelp, showDebug); 30 | } 31 | 32 | // Prints the surfaces in an infinite loop 33 | public void Run() 34 | { 35 | Clear(); 36 | while (true) 37 | Update(); 38 | } 39 | 40 | // Handles each frame 41 | void Update() 42 | { 43 | DateTime initialTime = DateTime.Now; 44 | controller.Update(); 45 | CursorVisible = false; 46 | 47 | if (!showHelp.value) { 48 | DrawScreen(); 49 | if (showDebug.value) 50 | DrawDebug(initialTime); 51 | } 52 | 53 | if (FPSLimit > 0) 54 | LimitFPS(initialTime); 55 | } 56 | 57 | // Pauses the program temporarily to limit FPS 58 | void LimitFPS(DateTime initialTime) 59 | { 60 | TimeSpan dt = DateTime.Now.Subtract(initialTime); 61 | TimeSpan limit = TimeSpan.FromSeconds(1.0 / FPSLimit); 62 | if (dt.CompareTo(limit) < 0) { 63 | System.Threading.Thread.Sleep(limit.Subtract(dt)); 64 | } 65 | } 66 | 67 | // Displays the surfaces 68 | void DrawScreen() 69 | { 70 | string screen = renderer.Render(surfaces); 71 | SetCursorPosition(0, 0); 72 | WriteLine(screen); 73 | } 74 | 75 | // Displays debug info such as FPS 76 | void DrawDebug(DateTime initialTime) 77 | { 78 | // FPS 79 | TimeSpan dt = DateTime.Now.Subtract(initialTime); 80 | int FPS = (int)Math.Ceiling(1.0 / dt.TotalSeconds); 81 | Write("FPS: " + FPS); 82 | if (FPSLimit > 0) 83 | Write(" limited at " + FPSLimit); 84 | else 85 | Write(" "); // Prevents FOV from being printed right after 86 | 87 | // Camera 88 | double FOVInDegrees = Math.Floor(10 * camera.GetFOV() * 180 / Math.PI) / 10; 89 | Write("\tFOV: " + FOVInDegrees); 90 | WriteLine(); 91 | 92 | Write("Press H for help"); 93 | } 94 | } -------------------------------------------------------------------------------- /Math/Matrix.cs: -------------------------------------------------------------------------------- 1 | using Math = System.Math; 2 | 3 | public class Matrix 4 | { 5 | double[,] entries; 6 | 7 | public Matrix(int numRows, int numColumns) 8 | { 9 | entries = new double[numRows, numColumns]; 10 | } 11 | 12 | public void SetEntry(int row, int column, double value) 13 | { 14 | entries[row, column] = value; 15 | } 16 | 17 | public static Matrix operator*(Matrix lhs, Matrix rhs) 18 | { 19 | if (lhs.entries.GetLength(1) != rhs.entries.GetLength(0)) { 20 | throw new System.ArgumentException("The number of columns in the left matrix must match the number of rows in the right matrix to do matrix multiplication"); 21 | } 22 | 23 | Matrix output = new(lhs.entries.GetLength(0), rhs.entries.GetLength(1)); 24 | for (int i = 0; i < output.entries.GetLength(0); i++) { 25 | for (int j = 0; j < output.entries.GetLength(1); j++) { 26 | for (int k = 0; k < lhs.entries.GetLength(1); k++) { 27 | output.entries[i, j] += lhs.entries[i, k] * rhs.entries[k, j]; 28 | } 29 | } 30 | } 31 | 32 | return output; 33 | } 34 | 35 | public static Vector3D operator*(Matrix lhs, Vector3D transposedRhs) 36 | { 37 | if (lhs.entries.GetLength(0) != 3 || lhs.entries.GetLength(1) != 3) 38 | throw new System.ArgumentException("The matrix must be a 3x3 when multiplying with a 3d vector"); 39 | 40 | return new Vector3D( 41 | lhs.entries[0, 0] * transposedRhs.GetX() + lhs.entries[0, 1] * transposedRhs.GetY() + lhs.entries[0, 2] * transposedRhs.GetZ(), 42 | lhs.entries[1, 0] * transposedRhs.GetX() + lhs.entries[1, 1] * transposedRhs.GetY() + lhs.entries[1, 2] * transposedRhs.GetZ(), 43 | lhs.entries[2, 0] * transposedRhs.GetX() + lhs.entries[2, 1] * transposedRhs.GetY() + lhs.entries[2, 2] * transposedRhs.GetZ() 44 | ); 45 | } 46 | 47 | public static Matrix XAxisRotation(double yzAngle) 48 | { 49 | // Precompute cos and sin so that they only need to be calculated once 50 | double cos = Math.Cos(yzAngle); 51 | double sin = Math.Sin(yzAngle); 52 | Matrix output = new(3, 3); 53 | output.entries = new double[3,3] { 54 | {1, 0, 0}, 55 | {0, cos, -sin}, 56 | {0, sin, cos} 57 | }; 58 | return output; 59 | } 60 | 61 | public static Matrix YAxisRotation(double xzAngle) 62 | { 63 | double cos = Math.Cos(xzAngle); 64 | double sin = Math.Sin(xzAngle); 65 | Matrix output = new(3, 3); 66 | output.entries = new double[3,3] { 67 | {cos, 0, -sin}, 68 | {0, 1, 0}, 69 | {sin, 0, cos} 70 | }; 71 | return output; 72 | } 73 | 74 | public static Matrix ZAxisRotation(double xyAngle) 75 | { 76 | double cos = Math.Cos(xyAngle); 77 | double sin = Math.Sin(xyAngle); 78 | Matrix output = new(3, 3); 79 | output.entries = new double[3,3] { 80 | {cos, -sin, 0}, 81 | {sin, cos, 0}, 82 | {0, 0, 1} 83 | }; 84 | return output; 85 | } 86 | 87 | public static Matrix XYZRotation(Vector3D rotation) 88 | { 89 | // The order of transformations in the matrix multiplication is right to left 90 | return ZAxisRotation(rotation.GetZ()) * YAxisRotation(rotation.GetY()) * XAxisRotation(rotation.GetX()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Math/Rect.cs: -------------------------------------------------------------------------------- 1 | public struct Rect 2 | { 3 | double x, y; 4 | double width, height; 5 | 6 | public Rect(double x, double y, double width, double height) 7 | { 8 | this.x = x; 9 | this.y = y; 10 | this.width = width; 11 | this.height = height; 12 | } 13 | 14 | public double GetX() => x; 15 | public double GetY() => y; 16 | public double GetWidth() => width; 17 | public double GetHeight() => height; 18 | } -------------------------------------------------------------------------------- /Math/Vector3D.cs: -------------------------------------------------------------------------------- 1 | using Math = System.Math; 2 | 3 | public struct Vector3D 4 | { 5 | double x, y, z; 6 | 7 | public Vector3D() 8 | { 9 | } 10 | 11 | public Vector3D(double x, double y, double z) 12 | { 13 | this.x = x; 14 | this.y = y; 15 | this.z = z; 16 | } 17 | 18 | public double GetX() => x; 19 | public double GetY() => y; 20 | public double GetZ() => z; 21 | 22 | // Returns the length of the vector 23 | public double GetLength() 24 | { 25 | return System.Math.Sqrt(x * x + y * y + z * z); 26 | } 27 | 28 | // Changes the length of the vector to one while maintaining its direction 29 | public void Normalize() 30 | { 31 | double length = GetLength(); 32 | x /= length; 33 | y /= length; 34 | z /= length; 35 | } 36 | 37 | // Returns the cross product between vectors lhs and rhs 38 | public static Vector3D CrossProduct(Vector3D lhs, Vector3D rhs) 39 | { 40 | return new Vector3D( 41 | lhs.y * rhs.z - lhs.z * rhs.y, 42 | lhs.z * rhs.x - lhs.x * rhs.z, 43 | lhs.x * rhs.y - lhs.y * rhs.x 44 | ); 45 | } 46 | 47 | // Returns the dot product between vectors lhs and rhs 48 | public static double DotProduct(Vector3D lhs, Vector3D rhs) 49 | { 50 | return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; 51 | } 52 | 53 | // Adds two vectors as expected 54 | public static Vector3D operator +(Vector3D lhs, Vector3D rhs) 55 | { 56 | return new Vector3D(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z); 57 | } 58 | 59 | // Subtracts two vectors as expected 60 | public static Vector3D operator -(Vector3D lhs, Vector3D rhs) 61 | { 62 | return new Vector3D(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z); 63 | } 64 | 65 | // Multiplies a vector by a scalar as expected 66 | public static Vector3D operator *(Vector3D vector, double scalar) 67 | { 68 | return new Vector3D(scalar * vector.x, scalar * vector.y, scalar * vector.z); 69 | } 70 | 71 | // Rotates the vector around the x axis first, then y, and then the z axis. 72 | public void RotateXYZ(Vector3D rotation) 73 | { 74 | RotateAboutXAxis(rotation.x); 75 | RotateAboutYAxis(rotation.y); 76 | RotateAboutZAxis(rotation.z); 77 | } 78 | 79 | /* 80 | Rotates the vector around the x axis. 81 | yzAngle is an angle made from the +y axis towards the +z axis. 82 | */ 83 | public void RotateAboutXAxis(double yzAngle) 84 | { 85 | double y_0 = y; 86 | y = y * Math.Cos(yzAngle) - z * Math.Sin(yzAngle); 87 | z = y_0 * Math.Sin(yzAngle) + z * Math.Cos(yzAngle); 88 | } 89 | 90 | /* 91 | Rotates the vector around the y axis. 92 | xzAngle is an angle made from the +x axis towards the +z axis. 93 | */ 94 | public void RotateAboutYAxis(double xzAngle) 95 | { 96 | double x_0 = x; 97 | x = x * Math.Cos(xzAngle) - z * Math.Sin(xzAngle); 98 | z = x_0 * Math.Sin(xzAngle) + z * Math.Cos(xzAngle); 99 | } 100 | 101 | /* 102 | Rotates the vector around the z axis. 103 | xyAngle is an angle made from the +x axis towards the +y axis. 104 | */ 105 | public void RotateAboutZAxis(double xyAngle) 106 | { 107 | double x_0 = x; 108 | x = x * Math.Cos(xyAngle) - y * Math.Sin(xyAngle); 109 | y = x_0 * Math.Sin(xyAngle) + y * Math.Cos(xyAngle); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Photos/AxesRotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/AxesRotation.png -------------------------------------------------------------------------------- /Photos/BeautifulDrawing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/BeautifulDrawing.png -------------------------------------------------------------------------------- /Photos/ChangeOfBaseTriangleI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/ChangeOfBaseTriangleI.png -------------------------------------------------------------------------------- /Photos/ChangeOfBaseTriangleJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/ChangeOfBaseTriangleJ.png -------------------------------------------------------------------------------- /Photos/ClassDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/ClassDiagram.png -------------------------------------------------------------------------------- /Photos/DonutAisle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/DonutAisle.png -------------------------------------------------------------------------------- /Photos/FOVGraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/FOVGraph.png -------------------------------------------------------------------------------- /Photos/FinishedRotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/FinishedRotation.png -------------------------------------------------------------------------------- /Photos/MovingCamera.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/MovingCamera.gif -------------------------------------------------------------------------------- /Photos/NoRotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/NoRotation.png -------------------------------------------------------------------------------- /Photos/SpinningDonut.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/SpinningDonut.gif -------------------------------------------------------------------------------- /Photos/VariousSurfaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanGyori/AsciiRender/c54296d1704f62028be5cabf71b0a0b20613b1bb/Photos/VariousSurfaces.png -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using static System.Console; 2 | using Decs = Surfaces.Decorators; 3 | const double PI = System.Math.PI; 4 | 5 | Vector3D sunDirection = new(1, -1, -1); 6 | sunDirection.Normalize(); 7 | 8 | Camera camera = new(PI / 2, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0)); 9 | 10 | Surface[] surfaces; 11 | 12 | // Spinning Donut 13 | surfaces = [new Decs.Position(new Vector3D(0.0, 0.0, 20.0), new Decs.Spinning(new Vector3D(1, 1, 0), new Surfaces.Donut(8, 3)))]; 14 | 15 | // Spinning Cube 16 | //surfaces = [new Decs.Position(new Vector3D(0, 0, 20), new Decs.Spinning(new Vector3D(1, 1, 0), new Surfaces.Cuboid(12, 12, 12)))]; 17 | 18 | // Swirly Thing and Donut and Cube 19 | /* 20 | surfaces = [ 21 | new Decs.Position(new Vector3D(-5.0, 0.0, 10.0), 22 | new Decs.Rotation(new Vector3D(1, 1, 0), 23 | new Surfaces.Donut(5, 1))), 24 | new Decs.Position(new Vector3D(12.0, 0.0, 10.0), 25 | new Decs.Rotation(new Vector3D(0, 1, 0), 26 | new Surfaces.Swirly())), 27 | new Decs.Position(new Vector3D(-20, 0, 10), 28 | new Decs.Rotation(new Vector3D(1, 1, 0), 29 | new Surfaces.Cuboid(6, 3, 10))) 30 | ]; 31 | */ 32 | 33 | // Aisle of donuts 34 | /* 35 | int numDonuts = 10; 36 | surfaces = new Surface[2 * numDonuts]; 37 | for (int i = 0; i < numDonuts; i++) { 38 | surfaces[2 * i] = new Decs.Position(new Vector3D(-9.0, 0.0, 5.0 + 15 * i), 39 | new Decs.Spinning(new Vector3D(0, -1, 0), 40 | new Surfaces.Donut(4, 1))); 41 | surfaces[2 * i + 1] = new Decs.Position(new Vector3D(9.0, 0.0, 5.0 + 15 * i), 42 | new Decs.Spinning(new Vector3D(0, 1, 0), 43 | new Surfaces.Donut(4, 1))); 44 | } 45 | */ 46 | 47 | // Grid of Donuts 48 | /* 49 | int length = 5; 50 | surfaces = new Surface[length * length]; 51 | for (int x = 0; x < length; x++) { 52 | for (int y = 0; y < length; y++) { 53 | surfaces[x + y * length] = new Decs.Position(new Vector3D(15 * (x - length / 2) , 15 * (y - length / 2), 5 * length), 54 | new Decs.Spinning(new Vector3D(0.5 * x, 0.5 * y, 0), 55 | new Surfaces.Donut(4, 1))); 56 | } 57 | } 58 | */ 59 | 60 | Manager game = new(sunDirection, camera, surfaces); 61 | 62 | game.Run(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASCII Render Engine 2 | 3 | The program takes a different approach to rendering 3D objects from the conventional approach of using rays from the camera, or whatever the conventional approach is. The way it works requires that every 3D object be described by a parametric surface equation. Refer to [Details](#details) for the juicy math. Refer to [Usage](#usage) to run the program or add your own cool shapes. 4 | 5 | ## Features 6 | ### Spinning Donut 7 | [Equation](https://www.desmos.com/3d/hlziur9zvc) 8 | 9 | ![](Photos/SpinningDonut.gif) 10 | 11 | ### Any Parametric Surface 12 | [Equation for Swirly Thing](https://www.desmos.com/3d/x9zww0oxcp) (Not my equation) 13 | 14 | ![](Photos/VariousSurfaces.png) 15 | 16 | ### Perspective View 17 | Objects get smaller as they get further away which causes effects such as in a hallway where the walls seem to get closer at far distances. 18 | 19 | ![](Photos/DonutAisle.png) 20 | 21 | ### Moveable Camera 22 | Pressing various keys moves the camera and adjusts other environment settings. Press H while in the program to see all the keybinds. 23 | 24 | ![](Photos/MovingCamera.gif) 25 | 26 | ### Backface culling 27 | The back of surfaces are culled so that when you go inside an object you don't see its surface. Not much reason for this, just wanted to do it. 28 | 29 | ## Usage 30 | ### Windows or Mac (using Visual Studio) 31 | Install Visual Studio if not already installed 32 | 33 | Run `AsciiRender.csproj` using Visual Studio. 34 | 35 | Go to Debug -> Start Debugging 36 | 37 | ### Linux (using dotnet) 38 | [Install dotnet](https://learn.microsoft.com/en-us/dotnet/core/install/linux) 39 | 40 | Run the following inside the repository. 41 | ``` 42 | dotnet run 43 | ``` 44 | 45 | ### Adding Stuff 46 | 47 | #### New Surface 48 | Create a subclass of the Surface class and override each abstract method. The GetUSteps and GetVSteps methods determine how many parts to discretive the domain in the GetDomain function. These two methods may be removed in the future but for now, tinker with it until the object is rendered in full with no holes. 49 | 50 | Also, overriding two other methods requires knowing how to take a derivative. 51 | 52 | Look at `Plane.cs` for a simple example. 53 | 54 | Once the subclass is complete. pass an instance into the surfaces array in `Program.cs` so that it is shown in the world. 55 | 56 | #### Decorators 57 | In order to move, rotate, or do any modular things to surfaces, decorators are used. 58 | 59 | Examples of their use can be seen in `Program.cs` and to create a new one requires subclassing Decorator. 60 | 61 | ## Details 62 | 63 | ### Simplified Class Diagram 64 | This is the general structure of the classes. 65 | 66 | ![](Photos/ClassDiagram.png) 67 | 68 | The dashed lines indicate a has-a relationship with the arrowhead. The solid lines indicate an inheritance from the class at the arrowhead. 69 | 70 | ### Math 71 | 72 | #### Drawing Shapes 73 | 74 | The domain of the parametric surface (a UV plane) is split into discrete steps. A loop iterates over these steps giving a (u, v) coordinate which gives a (x, y, z) coordinate by the surface equation. x and y are the screen coordinates and z is depth. 75 | 76 | Now this pixel needs some brightness. The normal of the surface at a point can be found by taking the cross product with two lines tangent to the point. Let $\vec{r}(u, v)$ be a position function for the surface. Holding u constant at the point yields a line whose tangent is $\frac{\partial \vec{r}}{\partial v}$. Holding v constant yields $\frac{\partial \vec{r}}{\partial u}$. Thus the normal is $\frac{\partial \vec{r}}{\partial u} \times \frac{\partial \vec{r}}{\partial v}$. 77 | 78 | The more a surface "points" towards the sun, the brighter it is. The normal is the direction the surface points. So a dot product between the sun vector and the unit normal vector yields a value between -1 and 1. Where 1 is looking directly at the sun and -1 is looking directly away. This value is used for brightness. 79 | 80 | #### Camera 81 | 82 | In reality, there is no camera, just a shifting of the whole world. When the camera moves left, the world shifts to the right. Similarly, camera rotations apply an opposite rotation to the world about the origin. 83 | 84 | The perspective view is a bit more complicated. The final equation can be found [here](https://www.desmos.com/geometry/rupqiij9no). 85 | 86 | We want to achieve a view that looks like the following: 87 | 88 | ![](Photos/FOVGraph.png) 89 | 90 | On the y-axis is the depth, $z$, and on the x-axis is the original position, $x$. Everything in the highlighted area is a point to be included in our view. Essentially, the further away a point is --higher up the graph-- a wider range of x values is included in our view --we can see an entire building at a distance but only the wall when its in our face. 91 | 92 | The equation for the graph is $c|x| \leq z$ where $c$ is some constant determining the slope of the lines. Changing $c$ changes the FOV. 93 | 94 | ![](Photos/BeautifulDrawing.png) 95 | 96 | Then we have $c = \cot(\frac{\theta}{2})$. 97 | 98 | Rearranging $c|x| \leq z$ yields $\frac{|x|\cot(\frac{\theta}{2})}{z} \leq 1$. Now let $x_f$ be the actual output position on the screen and $w$ be the width of the screen. We could set $|x_f| = \frac{|x|\cot(\frac{\theta}{2})}{z}$ which gives $|x_f| \leq 1$ but that would mean we only see $x$ values from -1 to 1. We actually see values from $-w$ to $w$. so first obtain $\frac{w|x|\cot(\frac{\theta}{2})}{z} \leq w$. This gives us the final equation: 99 | 100 | $x_f = \frac{wx*\cot(\frac{\theta}{2})}{z}$ 101 | 102 | #### Rotations 103 | 104 | I have heard of quaternions and hope to learn and implement them in the future. However, for now, rotations are done by first rotating a point about the x-axis, then y-axis, and then the z-axis using three separate angles. The following math applies to rotations about the z-axis but is identical for rotations about other axes. 105 | 106 | Let $\vec{r} = x\hat{i} + y\hat{j} + z\hat{k}$ be the position vector pre-rotation. The $z$ coordinate will remain unchanged since we are rotating about the z-axis. To do a rotation, we can think about the positions changing, rotating each vector, making sure magnitude stays the same, and all that stuff. But an easier approach is thinking about the axes just rotating in the opposite direction. Then, when we move the axes back to their original orientation, we will see that the object is rotated. 107 | 108 | Square object prior to rotation: 109 | 110 | ![](Photos/NoRotation.png) 111 | 112 | New axes to base position vector on: 113 | 114 | ![](Photos/AxesRotation.png) 115 | 116 | New axes in place of original axes so that the object has now rotated: 117 | 118 | ![](Photos/FinishedRotation.png) 119 | 120 | For the rotated axes, let its unit vectors be $\hat{i'}$, $\hat{j'}$, and $\hat{k'}$. 121 | 122 | ![](Photos/ChangeOfBaseTriangleI.png) 123 | 124 | From the image, we have $\hat{i} = \cos(\theta) \hat{i'} + \sin(\theta) \hat{j'}$. 125 | 126 | ![](Photos/ChangeOfBaseTriangleJ.png) 127 | 128 | Again from the image: $\hat{j} = -\sin(\theta) \hat{i'} + \cos(\theta) \hat{j'}$. 129 | 130 | And since $z$ is unchanged, $\hat{k} = \hat{k'}$. 131 | 132 | Applying these equations to $\vec{r} = x\hat{i} + y\hat{j} + z\hat{k}$ and simplifying the result gives: 133 | 134 | $\vec{r} = (x * \cos(\theta) - y * \sin(\theta)) \hat{i'} + (x * \sin(\theta) + y * \cos(\theta)) \hat{j'} + z \hat{k'}$ 135 | 136 | So, for example, the $x$ coordinate after the rotation is $x * \cos(\theta) - y * \sin(\theta)$ where $x$ and $y$ are the original coordinates. 137 | -------------------------------------------------------------------------------- /Renderer.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using TimeSpan = System.TimeSpan; 3 | using DateTime = System.DateTime; 4 | using Math = System.Math; 5 | 6 | // Takes in surfaceects and outputs a string of ascii characters resembling the surfaceects rendered 7 | public class Renderer 8 | { 9 | readonly Vector3D sunDirection; 10 | readonly Camera camera; 11 | double time; 12 | 13 | bool cullBackFaces = true; 14 | 15 | /* 16 | sunDirection must be a normal vector pointing from the center to where the sun is at 17 | to determine the direction of light. 18 | */ 19 | public Renderer(Vector3D sunDirection, Camera camera) 20 | { 21 | this.sunDirection = sunDirection; 22 | this.camera = camera; 23 | } 24 | 25 | /* 26 | Returns a string of a screen to which the given surfaces were rendered to. 27 | 28 | screenWidth and screenHeight are the number of characters for the width and height of the output buffer. 29 | Default value of zero using the terminal's max size 30 | */ 31 | public string Render(Surface[] surfaces, int screenWidth = 0, int screenHeight = 0) 32 | { 33 | if (screenWidth == 0) 34 | screenWidth = System.Console.WindowWidth - 1; 35 | if (screenHeight == 0) 36 | screenHeight = System.Console.WindowHeight - 4; 37 | 38 | BrightnessBuffer buffer = new(screenWidth, screenHeight); 39 | time = GetTime(); 40 | foreach (Surface surface in surfaces) { 41 | surface.Update(time); 42 | WriteSurfaceToBuffer(buffer, surface); 43 | } 44 | 45 | return buffer.ToString(); 46 | } 47 | 48 | /* 49 | Writes one surface to the brightness buffer. 50 | 51 | Goes through a discrete set of u and v values in the surface surface's domain 52 | to draw each pixel. 53 | */ 54 | void WriteSurfaceToBuffer(BrightnessBuffer buffer, Surface surface) 55 | { 56 | Rect domain = surface.GetDomain(); 57 | int uSteps = surface.GetUSteps(); 58 | int vSteps = surface.GetVSteps(); 59 | double du = (domain.GetWidth() - domain.GetX()) / uSteps; 60 | double dv = (domain.GetHeight() - domain.GetY()) / vSteps; 61 | 62 | double u = domain.GetX(); 63 | // Loop executed +1 time to include the end of the domain 64 | // so that the domain is closed 65 | for (int i = 0; i <= uSteps; i++) { 66 | double v = domain.GetY(); 67 | for (int j = 0; j <= vSteps; j++) { 68 | WritePixelToBuffer(buffer, surface, u, v); 69 | v += dv; 70 | } 71 | u += du; 72 | } 73 | } 74 | 75 | // For specific u and v values, determines and writes a pixel of an surface to the brightness buffer 76 | void WritePixelToBuffer(BrightnessBuffer buffer, Surface surface, double u, double v) 77 | { 78 | // Original position stored so that it does not need to be recalculated 79 | Vector3D position = surface.GetPosition(u, v, time); 80 | Vector3D positionFromView = camera.ApplyView(surface.GetPosition(u, v, time)); 81 | // Map point to a point from -1 to 1 and then to a pixel that can fit on the buffer 82 | int x = (int)System.Math.Floor(positionFromView.GetX() * System.Math.Min(buffer.GetWidth(), buffer.GetHeight()) / 2) + buffer.GetWidth() / 2; 83 | int y = (int)System.Math.Floor(positionFromView.GetY() * System.Math.Min(buffer.GetWidth(), buffer.GetHeight()) / 2) + buffer.GetHeight() / 2; 84 | 85 | // checks if pixel can be seen 86 | if (positionFromView.GetZ() > 0 && buffer.IsPixelInBoundaries(x, y) 87 | && !buffer.IsPixelBlocked(x, y, positionFromView.GetZ())) { 88 | Vector3D normal = surface.GetNormal(u, v, time); 89 | if (!cullBackFaces || camera.IsFrontFace(position, normal)) { 90 | double brightness = ComputeBrightness(normal); 91 | buffer.SetPixel(x, y, positionFromView.GetZ(), brightness); 92 | } 93 | } 94 | } 95 | 96 | // Returns a brightness level in [0, 1] of a point on a surface 97 | double ComputeBrightness(Vector3D normal) 98 | { 99 | return Math.Max(0.08, Vector3D.DotProduct(sunDirection, normal)); 100 | } 101 | 102 | double GetTime() 103 | { 104 | TimeSpan timeElapsed = DateTime.Now.Subtract(Process.GetCurrentProcess().StartTime); 105 | return timeElapsed.TotalSeconds; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Surfaces/Cuboid.cs: -------------------------------------------------------------------------------- 1 | namespace Surfaces; 2 | using Math = System.Math; 3 | 4 | public class Cuboid : Surface 5 | { 6 | const double PI = System.Math.PI; 7 | 8 | double width, height, length; // x, y, z 9 | Surface top, bottom, front, back, right, left; 10 | 11 | public Cuboid(double width, double height, double length) 12 | { 13 | this.width = width; 14 | this.height = height; 15 | this.length = length; 16 | 17 | top = new Decorators.Position(new Vector3D(0, -height / 2, 0), 18 | new Decorators.Rotation(new Vector3D(PI / 2, 0, 0), 19 | new Plane(width, length))); 20 | bottom = new Decorators.Position(new Vector3D(0, height / 2, 0), 21 | new Decorators.Rotation(new Vector3D(-PI / 2, 0, 0), 22 | new Plane(width, length))); 23 | front = new Decorators.Position(new Vector3D(0, 0, -length / 2), 24 | new Decorators.Rotation(new Vector3D(PI, 0, 0), 25 | new Plane(width, height))); 26 | back = new Decorators.Position(new Vector3D(0, 0, length / 2), 27 | new Plane(width, height)); 28 | right = new Decorators.Position(new Vector3D(width / 2, 0, 0), 29 | new Decorators.Rotation(new Vector3D(0, -PI / 2, 0), 30 | new Plane(length, height))); 31 | left = new Decorators.Position(new Vector3D(-width / 2, 0, 0), 32 | new Decorators.Rotation(new Vector3D(0, PI / 2, 0), 33 | new Plane(length, height))); 34 | } 35 | 36 | public override Vector3D GetPosition(double u, double v, double time) 37 | { 38 | switch((int)Math.Floor(u)) 39 | { 40 | case 0: 41 | return top.GetPosition(u, v, time); 42 | case 1: 43 | return bottom.GetPosition(u - 1, v, time); 44 | case 2: 45 | return front.GetPosition(u - 2, v, time); 46 | case 3: 47 | return back.GetPosition(u - 3, v, time); 48 | case 4: 49 | return right.GetPosition(u - 4, v, time); 50 | case 5: 51 | case 6: 52 | return left.GetPosition(u - 5, v, time); 53 | } 54 | 55 | // TODO Error 56 | return new Vector3D(0, 0, 0); 57 | } 58 | 59 | public override Rect GetDomain() => new(0, 0, 6, 1); 60 | 61 | public override int GetUSteps() => 3 * 6 * (int)Math.Floor(Math.Max(width, length)); 62 | public override int GetVSteps() => 3 * (int)Math.Floor(Math.Max(height, length)); 63 | 64 | public override Vector3D GetDerivativeWithU(double u, double v, double time) 65 | { 66 | switch((int)Math.Floor(u)) 67 | { 68 | case 0: 69 | return top.GetDerivativeWithU(u, v, time); 70 | case 1: 71 | return bottom.GetDerivativeWithU(u - 1, v, time); 72 | case 2: 73 | return front.GetDerivativeWithU(u - 2, v, time); 74 | case 3: 75 | return back.GetDerivativeWithU(u - 3, v, time); 76 | case 4: 77 | return right.GetDerivativeWithU(u - 4, v, time); 78 | case 5: 79 | case 6: 80 | return left.GetDerivativeWithU(u - 5, v, time); 81 | } 82 | 83 | // TODO Error 84 | return new Vector3D(0, 0, 0); 85 | } 86 | 87 | public override Vector3D GetDerivativeWithV(double u, double v, double time) 88 | { 89 | switch((int)Math.Floor(u)) 90 | { 91 | case 0: 92 | return top.GetDerivativeWithV(u, v, time); 93 | case 1: 94 | return bottom.GetDerivativeWithV(u - 1, v, time); 95 | case 2: 96 | return front.GetDerivativeWithV(u - 2, v, time); 97 | case 3: 98 | return back.GetDerivativeWithV(u - 3, v, time); 99 | case 4: 100 | return right.GetDerivativeWithV(u - 4, v, time); 101 | case 5: 102 | case 6: 103 | return left.GetDerivativeWithV(u - 5, v, time); 104 | } 105 | 106 | // TODO Error 107 | return new Vector3D(0, 0, 0); 108 | } 109 | } -------------------------------------------------------------------------------- /Surfaces/Decorators/DecoratorExample.cs: -------------------------------------------------------------------------------- 1 | namespace Surfaces.Decorators; 2 | 3 | public class Example : Decorator 4 | { 5 | public Example(Surface next) : base(next) 6 | { 7 | } 8 | 9 | public override int GetUSteps() => 1 + base.GetUSteps(); 10 | } -------------------------------------------------------------------------------- /Surfaces/Decorators/Position.cs: -------------------------------------------------------------------------------- 1 | namespace Surfaces.Decorators; 2 | 3 | public class Position : Decorator 4 | { 5 | Vector3D position; 6 | 7 | public Position(Vector3D position, Surface next) : base(next) 8 | { 9 | this.position = position; 10 | } 11 | 12 | public override Vector3D GetPosition(double u, double v, double time) 13 | { 14 | return base.GetPosition(u, v, time) + position; 15 | } 16 | } -------------------------------------------------------------------------------- /Surfaces/Decorators/Rotation.cs: -------------------------------------------------------------------------------- 1 | namespace Surfaces.Decorators; 2 | using Math = System.Math; 3 | 4 | public class Rotation : Decorator 5 | { 6 | Matrix rotationMatrix; 7 | 8 | public Rotation(Vector3D angles, Surface next) : base(next) 9 | { 10 | SetAngles(angles); 11 | } 12 | 13 | public override Vector3D GetPosition(double u, double v, double time) 14 | { 15 | return rotationMatrix * base.GetPosition(u, v, time); 16 | } 17 | 18 | public override Vector3D GetDerivativeWithU(double u, double v, double time) 19 | { 20 | // The derivative of the rotation is obtained by just passing the derivative since the rotations 21 | // are linear in addition of the functions 22 | return rotationMatrix * base.GetDerivativeWithU(u, v, time); 23 | } 24 | 25 | public override Vector3D GetDerivativeWithV(double u, double v, double time) 26 | { 27 | return rotationMatrix * base.GetDerivativeWithV(u, v, time); 28 | } 29 | 30 | public void SetAngles(Vector3D angles) 31 | { 32 | rotationMatrix = Matrix.XYZRotation(angles); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Surfaces/Decorators/Spinning.cs: -------------------------------------------------------------------------------- 1 | namespace Surfaces.Decorators; 2 | using Math = System.Math; 3 | 4 | public class Spinning : Rotation 5 | { 6 | Vector3D rotationRate; 7 | 8 | public Spinning(Vector3D rotationRate, Surface next) : base(new Vector3D(0, 0, 0), next) 9 | { 10 | this.rotationRate = rotationRate; 11 | } 12 | 13 | public override void Update(double time) 14 | { 15 | base.SetAngles(rotationRate * time); 16 | base.Update(time); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Surfaces/Decorators/SurfaceDecorator.cs: -------------------------------------------------------------------------------- 1 | namespace Surfaces.Decorators; 2 | 3 | // Applies customizable functionality on top of a concrete surface. 4 | public abstract class Decorator : Surface 5 | { 6 | Surface next; 7 | 8 | public Decorator(Surface next) 9 | { 10 | this.next = next; 11 | } 12 | 13 | public override void Update(double time) 14 | { 15 | next.Update(time); 16 | } 17 | 18 | public override Vector3D GetPosition(double u, double v, double time) 19 | { 20 | return next.GetPosition(u, v, time); 21 | } 22 | 23 | public override Rect GetDomain() 24 | { 25 | return next.GetDomain(); 26 | } 27 | 28 | public override int GetUSteps() 29 | { 30 | return next.GetUSteps(); 31 | } 32 | 33 | public override int GetVSteps() 34 | { 35 | return next.GetVSteps(); 36 | } 37 | 38 | // Must be overriden if GetPosition is overriden 39 | public override Vector3D GetDerivativeWithU(double u, double v, double time) 40 | { 41 | return next.GetDerivativeWithU(u, v, time); 42 | } 43 | 44 | // Must be overriden if GetPosition is overriden 45 | public override Vector3D GetDerivativeWithV(double u, double v, double time) 46 | { 47 | return next.GetDerivativeWithV(u, v, time); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Surfaces/Donut.cs: -------------------------------------------------------------------------------- 1 | namespace Surfaces; 2 | using Math = System.Math; 3 | 4 | public class Donut : Surface 5 | { 6 | double radius; 7 | double thickness; 8 | 9 | public Donut(double radius, double thickness) 10 | { 11 | this.radius = radius; 12 | this.thickness = thickness; 13 | } 14 | 15 | public override Vector3D GetPosition(double u, double v, double time) 16 | { 17 | return new Vector3D( 18 | radius * Math.Cos(u) + thickness * Math.Cos(v) * Math.Cos(u), 19 | radius * Math.Sin(u) + thickness * Math.Cos(v) * Math.Sin(u), 20 | thickness * Math.Sin(v) 21 | ); 22 | } 23 | 24 | public override Rect GetDomain() 25 | { 26 | return new Rect(0, 0, 2 * Math.PI, 2 * Math.PI); 27 | } 28 | 29 | public override int GetUSteps() => 150; 30 | public override int GetVSteps() => 75; 31 | 32 | public override Vector3D GetDerivativeWithU(double u, double v, double time) 33 | { 34 | return new Vector3D( 35 | -1 * radius * Math.Sin(u) - thickness * Math.Cos(v) * Math.Sin(u), 36 | radius * Math.Cos(u) + thickness * Math.Cos(v) * Math.Cos(u), 37 | 0 38 | ); 39 | } 40 | 41 | public override Vector3D GetDerivativeWithV(double u, double v, double time) 42 | { 43 | return new Vector3D( 44 | -1 * thickness * Math.Sin(v) * Math.Cos(u), 45 | -1 * thickness * Math.Sin(v) * Math.Sin(u), 46 | thickness * Math.Cos(v) 47 | ); 48 | } 49 | } -------------------------------------------------------------------------------- /Surfaces/Plane.cs: -------------------------------------------------------------------------------- 1 | namespace Surfaces; 2 | using Math = System.Math; 3 | 4 | public class Plane : Surface 5 | { 6 | double width, height; 7 | 8 | public Plane(double width, double height) 9 | { 10 | this.width = width; 11 | this.height = height; 12 | } 13 | 14 | public override Vector3D GetPosition(double u, double v, double time) 15 | { 16 | return new Vector3D( 17 | width * u - width / 2, 18 | height * v - height / 2, 19 | 0 20 | ); 21 | } 22 | 23 | public override Rect GetDomain() => new(0, 0, 1, 1); 24 | 25 | public override int GetUSteps() => (int)Math.Floor(width); 26 | public override int GetVSteps() => (int)Math.Floor(height); 27 | 28 | 29 | public override Vector3D GetDerivativeWithU(double u, double v, double time) 30 | { 31 | return new Vector3D( 32 | width, 33 | 0, 34 | 0 35 | ); 36 | } 37 | 38 | public override Vector3D GetDerivativeWithV(double u, double v, double time) 39 | { 40 | return new Vector3D( 41 | 0, 42 | height, 43 | 0 44 | ); 45 | } 46 | } -------------------------------------------------------------------------------- /Surfaces/Surface.cs: -------------------------------------------------------------------------------- 1 | // Classes that implement the Surface interface are parametric surfaces that can be used for calculations or drawn 2 | public abstract class Surface 3 | { 4 | // Called once per frame prior to drawing 5 | public virtual void Update(double time) 6 | { 7 | } 8 | 9 | // x, y, and z components of the surface parameterized by u and v. 10 | public abstract Vector3D GetPosition(double u, double v, double time); 11 | 12 | // Returns the uv plane of the parametric surface 13 | public abstract Rect GetDomain(); 14 | 15 | // Returns the number of steps the u portion of the uv plane needs to be discretized to 16 | public abstract int GetUSteps(); 17 | 18 | // Returns the number of steps the v portion of the uv plane needs to be discretized to 19 | public abstract int GetVSteps(); 20 | 21 | // Returns the partial derivative at a point with respect to u 22 | public abstract Vector3D GetDerivativeWithU(double u, double v, double time); 23 | 24 | // Returns the partial derivative at a point with respect to v 25 | public abstract Vector3D GetDerivativeWithV(double u, double v, double time); 26 | 27 | // Returns the unit normal vector for some point on the surface determined by the parameters and time. 28 | public Vector3D GetNormal(double u, double v, double time) 29 | { 30 | Vector3D normal = Vector3D.CrossProduct( 31 | GetDerivativeWithU(u, v, time), 32 | GetDerivativeWithV(u, v, time)); 33 | normal.Normalize(); 34 | return normal; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Surfaces/Swirly.cs: -------------------------------------------------------------------------------- 1 | namespace Surfaces; 2 | using Math = System.Math; 3 | 4 | public class Swirly : Surface 5 | { 6 | const double tau = 2 * Math.PI; 7 | public override Vector3D GetPosition(double u, double v, double time) 8 | { 9 | return new Vector3D( 10 | (4 + Math.Sin(tau * v) * Math.Sin(tau * v)) * Math.Sin(3 * Math.PI * v), 11 | Math.Sin(tau * v) * Math.Cos(tau * u) + 8 * v - 4, 12 | (4 + Math.Sin(tau * v) * Math.Sin(tau * u)) * Math.Cos(3 * Math.PI * v) 13 | ); 14 | } 15 | 16 | public override Rect GetDomain() 17 | { 18 | return new Rect(0, 0, 1, 1); 19 | } 20 | 21 | public override int GetUSteps() => 250; 22 | public override int GetVSteps() => 250; 23 | 24 | public override Vector3D GetDerivativeWithU(double u, double v, double time) 25 | { 26 | return new Vector3D( 27 | tau * Math.Sin(tau * v) * Math.Sin(3 * Math.PI * v) * Math.Cos(tau * u), 28 | -tau * Math.Sin(tau * v) * Math.Sin(tau * u), 29 | tau * Math.Sin(tau * v) * Math.Cos(tau * u) 30 | ); 31 | } 32 | 33 | public override Vector3D GetDerivativeWithV(double u, double v, double time) 34 | { 35 | return new Vector3D( 36 | 3 * Math.PI * (4 + Math.Sin(tau * v) * Math.Sin(tau * u)) * Math.Cos(3 * Math.PI * v) + tau * Math.Cos(tau * v) * Math.Sin(tau * u) * Math.Sin(3 * Math.PI * v), 37 | tau * Math.Cos(tau * v) * Math.Cos(tau * u) + 8, 38 | -3 * Math.PI * (4 + Math.Sin(tau * v) * Math.Sin(tau * u)) * Math.Sin(3 * Math.PI * v) + tau * Math.Cos(tau * v) * Math.Sin(tau * u) * Math.Cos(3 * Math.PI * v) 39 | ); 40 | } 41 | } --------------------------------------------------------------------------------