A minimalist 3D engine written in C# that runs entirely inside the system console. This project was built from scratch without any external graphical libraries or modern graphics APIs (like OpenGL or DirectX). Everythingβfrom custom vector mathematics to ray casting/tracing and the TCP network stackβis computed entirely on the CPU.
This project was designed for educational and demonstration purposes for a YouTube video.
- CPU Raycasting / Raytracing:
- Renders polygonal meshes (
.objformat) and geometric spheres. - Intersection optimization using Bounding Sphere checks before performing detailed triangle intersection calculations.
- Renders polygonal meshes (
- Dynamic Lighting and Shadows:
- Light intensity attenuation based on distance.
- Lambertian diffuse shading using surface normals.
- Real-time shadow rendering via Shadow Rays cast from intersection points back to light sources.
- Multi-threaded Rendering:
- Leverages
Parallel.Forto distribute pixel rendering calculations across all available CPU cores.
- Leverages
- Optimized Console Output:
- Map light intensity to a character gradient string (
" .:!/r(l1Z4H9W8$@") to simulate shading. - Frame buffering with grouped-color output to minimize slow, native OS terminal print API calls.
- Map light intensity to a character gradient string (
- Custom 3D Mathematics:
- Custom implementations of
Vector3,Vector2,Ray, and rotation matrices (Euler rotations around X, Y, and Z axes). - Manual implementation of the MΓΆllerβTrumbore ray-triangle intersection algorithm.
- Custom implementations of
- Low-level Window Input:
- Asynchronous, non-blocking keyboard polling using the Win32 API (
GetAsyncKeyState), ensuring key presses are detected only when the console window is active.
- Asynchronous, non-blocking keyboard polling using the Win32 API (
- Custom Network Multiplayer:
- Client-server TCP network manager built from scratch.
- Custom binary packet serialization, routing via stable hash-based type IDs, and a decoupled event subscription model.
- Included multiplayer demo featuring real-time player position synchronization and a text-based chat lobby.
The engine is decoupled into clear logical layers:
βββ AbstractClass # Base classes for GameObjects, Scenes, Screens, and Lights
βββ Implementation # Concrete cameras, rendering managers, and screen renderers
βββ Interfaces # Contracts for loose coupling (ICamera, IScreen, etc.)
βββ Shape # Geometric primitives (Sphere, Triangle, Object3D)
βββ StaticClass # Helper utilities (ObjLoader, GameTime tracking)
βββ Structure # Core structs (Vectors, Rays, RenderData payload)
βββ UI # Overlay system for rendering text on top of the frame buffer
βββ Network # Networking layer (TCP Manager, Packet abstractions, Serializer)
Frame: The heart of the engine loop. It starts the cycle, updates the active scene's logic (Update()), triggers the screen renderer (RenderFrame()), prints FPS/diagnostics, and tracks delta-time.Screen(ConsoleScreenAsync): Manages resolution buffers for brightness and color. ItsRenderFrameimplementation parallelizes the conversion of brightness values into gradient characters and presents the entire frame buffer to the console as grouped color chunks.Scene: A container for world elements. It manages the active camera, light lists, UI elements, and renderable objects (IDisplays). It computes individual pixel states viaGetPixelDataon demand.DisplaysManager: Calculates intersections, searching for the nearest object intersected by rays projected from the viewport.
Every frame is rendered using the following steps:
- Ray Generation: The camera translates the 2D UV screen coordinates into a 3D direction vector in world space, adjusting for the camera's position and orientation (Pitch, Yaw, Roll).
- Intersection / Raycast:
The ray is evaluated against all active objects in the scene via the
DisplayManager:- Spheres: Evaluated analytically using the quadratic formula for ray-sphere intersection.
- Polygonal Objects (3D Models): First, a quick intersection check is run against the model's Bounding Sphere. If the ray hits the sphere, a detailed loop checks all individual triangles using the MΓΆllerβTrumbore algorithm.
- Shading & Shadow Calculation:
If an intersection is found, the engine calculates the light influence at that point:
- It determines the direction vector toward each light source.
- It calculates the dot product between the surface normal and the light direction (diffuse component).
- It casts a Shadow Ray from the intersection point toward the light. If another object intersects this shadow ray, the point is shaded as in shadow (brightness = 0).
- Buffering & Output: The final light intensity is mapped to a character from the gradient array, buffered, and written out to the console terminal.
The networking module is written on raw sockets (TcpListener/TcpClient) with zero third-party dependencies.
- Network Packet Layout:
Packets are serialized into a sequential byte array containing a 12-byte header followed by the payload data:
[ Type ID (4 bytes) ] [ Sender ID (4 bytes) ] [ Payload Length (4 bytes) ] [ Payload Data (N bytes) ] - Registration & Serialization (
PacketManager): Packets inheritINetworkPacketand implement customSerialize/Deserializemethods usingBinaryWriter/BinaryReader. They are mapped to unique integer IDs using a stable hashing function on the class name string. - Event Dispatching:
Scripts subscribe to specific packet types asynchronously using the manager:
PacketManager.Subscribe<T>((packet, senderId) => { ... }).
To build and run this project, make sure you have the .NET 8.0 SDK (or newer) installed.
Organize the codebase according to the directory structure. Make sure you have a valid .obj model file (such as Blender's classic low-poly Suzanne β monkey.obj) located in your output execution directory.
Navigate to your project directory and run:
dotnet run --configuration Release(Running with the Release configuration is highly recommended to ensure maximum parallel performance on your CPU).
When the console starts, choose your network role:
- Press
Sto host as a Server. The terminal will display your local IP and port. Share this with a client. - On another machine (or another terminal instance), press
Cto connect as a Client, input the Server's IP address, and connect.
W, A, S, Dβ Move camera (Forward, Left, Backward, Right).Space/Left Shiftβ Fly Up / Down.- Arrow Keys β Look around (Pitch and Yaw rotations).
Ctrl+W, A, S, D, Space, Shiftβ Move the active light source.+/-β Increase / Decrease light intensity.Tβ Open multiplayer chat (Type message ->Enterto send,Escto cancel).
You can create your own scene by inheriting from the base Scene class. Here is a simple example rendering a single red sphere illuminated by a light source:
using _3dEngine;
using _3dEngine.AbstractClass;
using _3dEngine.Implementation;
using _3dEngine.Interfaces;
using _3dEngine.Shape;
public class MyCustomScene : Scene
{
private Camera _camera;
private Sphere _sphere;
private Light _light;
public MyCustomScene(IDisplaysManagerAsync manager) : base(manager) { }
public override void Start()
{
// 1. Initialize and set up the camera
_camera = new Camera(new Vector3(0, 0, -5), Vector3.Zero);
SetMainCamera(_camera);
// 2. Add a red sphere at the origin
_sphere = new Sphere(Vector3.Zero, Vector3.Zero, r: 1.5f);
_sphere.Color = ConsoleColor.Red;
AddDisplaysObject(_sphere);
// 3. Add a light source pointing at the sphere
_light = new Light(new Vector3(-2, 3, -4), lightPower: 15f);
AddLight(_light);
}
public override void Update()
{
// Add frame update logic here (e.g., rotate objects over time using GameTime.GetDeltaTime())
}
}