#3 Draw a triangle

This commit is contained in:
Bobby Lucero 2025-06-02 22:16:24 -04:00
parent 1df89e00e4
commit 1fcce95e9d
7 changed files with 164 additions and 14 deletions

5
IPixelShader.cs Normal file
View File

@ -0,0 +1,5 @@
public interface IPixelShader
{
float3 GetColor(float3 barycentric, int screenX, int screenY, int frame);
}

14
MathHelpers.cs Normal file
View File

@ -0,0 +1,14 @@
public class MathHelpers
{
public static float Edge(float2 a, float2 b, float2 c)
{
return (c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x);
}
public static float2 NDCToScreen(float2 ndc, int width, int height)
{
float x = (ndc.x + 1f) * 0.5f * width;
float y = (1f - (ndc.y + 1f) * 0.5f) * height; // flip Y
return new float2(x, y);
}
}

View File

@ -18,6 +18,11 @@
} }
public static float3 operator -(float3 a, float3 b) => new float3(a.x - b.x, a.y - b.y, a.z - b.y); public static float3 operator -(float3 a, float3 b) => new float3(a.x - b.x, a.y - b.y, a.z - b.y);
public static float3 operator *(float scalar, float3 vec) => new float3(vec.x * scalar, vec.y * scalar, vec.z * scalar);
public static float3 operator +(float3 a, float3 b) => new float3(a.x + b.x, a.y + b.y, a.z + b.z);
public static float3 cross(float3 a, float3 b) => new float3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x); public static float3 cross(float3 a, float3 b) => new float3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);

View File

@ -6,7 +6,7 @@ Writing a simple 3D software renderer from scratch to understand the math behind
## 🎯 Milestones ## 🎯 Milestones
- [X] Setup Raylib C# project - [X] Setup Raylib C# project
- [X] Draw an image from a byte array - [X] Draw an image from a byte array
- [ ] Draw a triangle - [X] Draw a triangle
- [ ] Render Lots of triangles - [ ] Render Lots of triangles
- [ ] OBJ Loader - [ ] OBJ Loader
- [ ] Ortho Projection - [ ] Ortho Projection

View File

@ -1,4 +1,5 @@
using System.Numerics; using System.Diagnostics;
using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Raylib_cs; using Raylib_cs;
@ -13,6 +14,8 @@ public class RenderDevice
private Texture2D renderTarget; private Texture2D renderTarget;
private IntPtr pixelPtr; private IntPtr pixelPtr;
private byte[] pixelData; private byte[] pixelData;
private int frame = 1;
public RenderDevice(int width, int height, int screenWidth, int screenHeight) public RenderDevice(int width, int height, int screenWidth, int screenHeight)
{ {
this.width = width; this.width = width;
@ -38,17 +41,69 @@ public class RenderDevice
} }
Raylib.DrawTexturePro(renderTarget, new Rectangle(0,0, width, height), new Rectangle(0, 0, screenWidth, screenHeight), Vector2.Zero, 0f, Color.White); Raylib.DrawTexturePro(renderTarget, new Rectangle(0,0, width, height), new Rectangle(0, 0, screenWidth, screenHeight), Vector2.Zero, 0f, Color.White);
frame++;
} }
public void SetPixel(int x, int y, float3 color) public void SetPixel(int x, int y, float3 color)
{ {
if (x < 0 || x >= width || y < 0 || y >= height) return;
int idx = (y * width + x) * 4; int idx = (y * width + x) * 4;
pixelData[idx] = (byte)color.r; pixelData[idx] = (byte)(Math.Clamp(color.r * 255, 0, 255));
pixelData[idx + 1] = (byte)color.g; pixelData[idx + 1] = (byte)(Math.Clamp(color.g * 255, 0, 255));
pixelData[idx + 2] = (byte)color.b; pixelData[idx + 2] = (byte)(Math.Clamp(color.b * 255, 0, 255));
pixelData[idx + 3] = 0xFF; pixelData[idx + 3] = 0xFF;
} }
public void Clear()
{
for (int i = 0; i < pixelData.Length; i++) pixelData[i] = 0;
}
public void DrawTriangle(float2 v0, float2 v1, float2 v2, IPixelShader pixelShader)
{
v0 = MathHelpers.NDCToScreen(v0, width, height);
v1 = MathHelpers.NDCToScreen(v1, width, height);
v2 = MathHelpers.NDCToScreen(v2, width, height);
Debug.WriteLine(v0.x);
int minX = (int)MathF.Floor(MathF.Min(v0.x, MathF.Min(v1.x, v2.x)));
int maxX = (int)MathF.Ceiling(MathF.Max(v0.x, MathF.Max(v1.x, v2.x)));
int minY = (int)MathF.Floor(MathF.Min(v0.y, MathF.Min(v1.y, v2.y)));
int maxY = (int)MathF.Ceiling(MathF.Max(v0.y, MathF.Max(v1.y, v2.y)));
float area = MathHelpers.Edge(v0, v1, v2);
if (area == 0f) return; // Degenerate
for (int y = minY; y <= maxY; y++)
{
for (int x = minX; x <= maxX; x++)
{
float2 p = new float2(x + 0.5f, y + 0.5f);
float w0 = MathHelpers.Edge(v1, v2, p);
float w1 = MathHelpers.Edge(v2, v0, p);
float w2 = MathHelpers.Edge(v0, v1, p);
if (w0 <= 0 && w1 <= 0 && w2 <= 0)
{
w0 /= area;
w1 /= area;
w2 /= area;
float3 bary = new float3(w0, w1, w2);
// Inside triangle
SetPixel(x, y, pixelShader.GetColor(bary, x, y, frame));
}
}
}
}
public void cleanup() public void cleanup()
{ {
Raylib.UnloadTexture(renderTarget); Raylib.UnloadTexture(renderTarget);

View File

@ -1,4 +1,5 @@
using Raylib_cs; using System.Runtime.CompilerServices;
using Raylib_cs;
class SoftwareRasterizer class SoftwareRasterizer
{ {
@ -16,27 +17,66 @@ class SoftwareRasterizer
sr.start(); sr.start();
} }
struct BouncingPoint()
{
float posX, posY;
float velX, velY;
public float2 pos => new(posX, posY);
public BouncingPoint(float posX, float posY, float velX, float velY) : this()
{
this.posX = posX;
this.posY = posY;
this.velX = velX;
this.velY = velY;
}
public void Update()
{
posX += velX;
posY += velY;
if (posX >= 1.0 || posX <= -1.0f)
{
velX = -velX;
}
if (posY >= 1.0 || posY <= -1.0f)
{
velY = -velY;
}
}
}
public void start() public void start()
{ {
Raylib.InitWindow(screenWidth, screenHeight, "Software Rasterizer"); Raylib.InitWindow(screenWidth, screenHeight, "Software Rasterizer");
Raylib.SetTargetFPS(0); Raylib.SetTargetFPS(60);
renderer = new RenderDevice(screenWidth / scale, screenHeight / scale, screenWidth, screenHeight); renderer = new RenderDevice(screenWidth / scale, screenHeight / scale, screenWidth, screenHeight);
VertexColorShader shader = new VertexColorShader();
int frameCounter = 0; int frameCounter = 0;
Random rand = new Random(); Random rand = new Random();
BouncingPoint v0 = new BouncingPoint( 0.0f, 0.8f, 0.01f, 0.01f); // Top middle
BouncingPoint v1 = new BouncingPoint( 0.8f, -0.8f, 0.01f, -0.01f); // Bottom right
BouncingPoint v2 = new BouncingPoint(-0.8f, -0.8f, -0.01f, 0.01f); // Bottom left
while (!Raylib.WindowShouldClose()) while (!Raylib.WindowShouldClose())
{ {
for (int i = 0; i < renderer.height; i++)
{ renderer.Clear();
for (int j = 0; j < renderer.width; j++)
{ v0.Update();
renderer.SetPixel(j, i, new float3((j / (float)(renderer.width)) * 255, (i / (float)(renderer.height)) * 255, rand.Next(1,256))); v1.Update();
} v2.Update();
}
renderer.DrawTriangle(v0.pos, v1.pos, v2.pos, shader);
// Render // Render
Raylib.BeginDrawing(); Raylib.BeginDrawing();
Raylib.ClearBackground(Color.Black); Raylib.ClearBackground(Color.Black);
@ -52,5 +92,9 @@ class SoftwareRasterizer
renderer.cleanup(); renderer.cleanup();
Raylib.CloseWindow(); Raylib.CloseWindow();
} }
} }

27
VertexColorShader.cs Normal file
View File

@ -0,0 +1,27 @@
public class VertexColorShader : IPixelShader
{
float3[] vertexColors = new[]
{
new float3(1.0f, 0.0f, 0.0f),
new float3(0.0f, 1.0f, 0.0f),
new float3(0.0f, 0.0f, 1.0f),
};
public float3 GetColor(float3 barycentric, int screenX, int screenY, int frame)
{
float amount1 = MathF.Cos(frame * 0.02f) * 0.5f + 0.5f;
float amount2 = MathF.Sin(frame * 0.02f) * 0.5f + 0.5f;
float3 c1 = new float3(vertexColors[0].x * amount1, vertexColors[0].y * amount1, vertexColors[0].z * amount1);
float3 c2 = new float3(vertexColors[1].x * amount2, vertexColors[1].y * amount2, vertexColors[1].z * amount2);
float3 color = barycentric.x * c1 +
barycentric.y * c2 +
barycentric.z * vertexColors[2];
// if((int)(screenX + MathF.Cos(frame * 0.02f) * 30) % 5 == 0 && (int)(screenY + MathF.Sin(frame * 0.02f) * 30) % 5 == 0) color += new float3(0.0f, 0.25f, 0.0f);
if((int)(screenX) % 5 == 0 && (int)(screenY) % 5 == 0) color += new float3(0.0f, 0.25f, 0.0f);
return color;
}
}