Last active
December 12, 2018 01:28
-
-
Save z3nth10n/7d60f22c7e906f645d53c9622507c23b to your computer and use it in GitHub Desktop.
TriangleUtils - Useful for triangle rasterizations (has several methods), this work should be done once, because Rasterization is a specialized task for GPUs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using UnityEngine; | |
namespace UnityEngine.Utilities.Drawing | |
{ | |
// Uncomment this if you don't a an own implementation | |
/*public struct Point | |
{ | |
public int x; | |
public int y; | |
public Point(int x, int y) | |
{ | |
this.x = 0; | |
this.y = 0; | |
} | |
}*/ | |
public static class TriangleUtils | |
{ | |
public static void Rasterize(Point pt1, Point pt2, Point pt3, Action<int, int> action, bool debug = false) | |
{ | |
/* | |
// https://www.geeksforgeeks.org/check-whether-triangle-valid-not-sides-given/ | |
a + b > c | |
a + c > b | |
b + c > a | |
*/ | |
if (!CheckIfValidTriangle(pt1, pt2, pt3, debug)) | |
return; | |
if (TriangleArea(pt1, pt2, pt3) <= 1) | |
{ | |
Point center = GetTriangleCenter(pt1, pt2, pt3); | |
action?.Invoke(center.x, center.y); | |
return; | |
} | |
Point tmp; | |
if (pt2.x < pt1.x) | |
{ | |
tmp = pt1; | |
pt1 = pt2; | |
pt2 = tmp; | |
} | |
if (pt3.x < pt2.x) | |
{ | |
tmp = pt2; | |
pt2 = pt3; | |
pt3 = tmp; | |
if (pt2.x < pt1.x) | |
{ | |
tmp = pt1; | |
pt1 = pt2; | |
pt2 = tmp; | |
} | |
} | |
var baseFunc = CreateFunc(pt1, pt3); | |
var line1Func = pt1.x == pt2.x ? (x => pt2.y) : CreateFunc(pt1, pt2); | |
for (var x = pt1.x; x < pt2.x; ++x) | |
{ | |
int maxY; | |
int minY = GetRange(line1Func(x), baseFunc(x), out maxY); | |
for (int y = minY; y <= maxY; ++y) | |
action?.Invoke(x, y); | |
} | |
var line2Func = pt2.x == pt3.x ? (x => pt2.y) : CreateFunc(pt2, pt3); | |
for (var x = pt2.x; x <= pt3.x; ++x) | |
{ | |
int maxY; | |
int minY = GetRange(line2Func(x), baseFunc(x), out maxY); | |
for (int y = minY; y <= maxY; ++y) | |
action?.Invoke(x, y); | |
} | |
} | |
private static int GetRange(float y1, float y2, out int maxY) | |
{ | |
if (y1 < y2) | |
{ | |
maxY = Mathf.FloorToInt(y2); | |
return Mathf.CeilToInt(y1); | |
} | |
maxY = Mathf.FloorToInt(y1); | |
return Mathf.CeilToInt(y2); | |
} | |
private static Func<int, float> CreateFunc(Point pt1, Point pt2) | |
{ | |
var y0 = pt1.y; | |
if (y0 == pt2.y) | |
return x => y0; | |
float m = (float)(pt2.y - y0) / (pt2.x - pt1.x); | |
return x => m * (x - pt1.x) + y0; | |
} | |
public static void RasterizeStandard(Point p1, Point p2, Point p3, Action<int, int> action, bool debug = false) | |
{ | |
// Thanks to: https://www.davrous.com/2013/06/21/tutorial-part-4-learning-how-to-write-a-3d-software-engine-in-c-ts-or-js-rasterization-z-buffering/ | |
if (!CheckIfValidTriangle(p1, p2, p3, debug)) | |
return; | |
// Sorting the points in order to always have this order on screen p1, p2 & p3 | |
// with p1 always up (thus having the y the lowest possible to be near the top screen) | |
// then p2 between p1 & p3 | |
if (p1.y > p2.y) | |
{ | |
var temp = p2; | |
p2 = p1; | |
p1 = temp; | |
} | |
if (p2.y > p3.y) | |
{ | |
var temp = p2; | |
p2 = p3; | |
p3 = temp; | |
} | |
if (p1.y > p2.y) | |
{ | |
var temp = p2; | |
p2 = p1; | |
p1 = temp; | |
} | |
// inverse slopes | |
float dP1P2, dP1P3; | |
// http://en.wikipedia.org/wiki/Slope | |
// Computing inverse slopes | |
if (p2.y - p1.y > 0) | |
dP1P2 = (p2.x - p1.x) / (p2.y - p1.y); | |
else | |
dP1P2 = 0; | |
if (p3.y - p1.y > 0) | |
dP1P3 = (p3.x - p1.x) / (p3.y - p1.y); | |
else | |
dP1P3 = 0; | |
// First case where triangles are like that: | |
// P1 | |
// - | |
// -- | |
// - - | |
// - - | |
// - - P2 | |
// - - | |
// - - | |
// - | |
// P3 | |
if (dP1P2 > dP1P3) | |
{ | |
for (var y = p1.y; y <= p3.y; y++) | |
{ | |
if (y < p2.y) | |
ProcessScanLine(y, p1, p3, p1, p2, action); | |
else | |
ProcessScanLine(y, p1, p3, p2, p3, action); | |
} | |
} | |
// First case where triangles are like that: | |
// P1 | |
// - | |
// -- | |
// - - | |
// - - | |
// P2 - - | |
// - - | |
// - - | |
// - | |
// P3 | |
else | |
{ | |
for (var y = p1.y; y <= p3.y; y++) | |
{ | |
if (y < p2.y) | |
ProcessScanLine(y, p1, p2, p1, p3, action); | |
else | |
ProcessScanLine(y, p2, p3, p1, p3, action); | |
} | |
} | |
} | |
// drawing line between 2 points from left to right | |
// papb -> pcpd | |
// pa, pb, pc, pd must then be sorted before | |
private static void ProcessScanLine(int y, Point pa, Point pb, Point pc, Point pd, Action<int, int> action) | |
{ | |
// Thanks to current y, we can compute the gradient to compute others values like | |
// the starting x (sx) and ending x (ex) to draw between | |
// if pa.y == pb.y or pc.y == pd.y, gradient is forced to 1 | |
var gradient1 = pa.y != pb.y ? (y - pa.y) / (pb.y - pa.y) : 1; | |
var gradient2 = pc.y != pd.y ? (y - pc.y) / (pd.y - pc.y) : 1; | |
int sx = (int)Mathf.Lerp(pa.x, pb.x, gradient1); | |
int ex = (int)Mathf.Lerp(pc.x, pd.x, gradient2); | |
// drawing a line from left (sx) to right (ex) | |
for (var x = sx; x < ex; x++) | |
action?.Invoke(x, y); | |
} | |
public static void RasterizeBarycentric(Point v1, Point v2, Point v3, Action<int, int> action, bool debug = false) | |
{ | |
// Thanks to: http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html#algo3 && https://www.cs.unm.edu/~joel/cs491_VR/src/DrawUtilities.cs | |
/* checks for a valid triangle */ | |
if (!CheckIfValidTriangle(v1, v2, v3, debug)) | |
return; | |
/* get the bounding box of the triangle */ | |
int maxX = Mathf.Max(v1.x, Mathf.Max(v2.x, v3.x)); | |
int minX = Mathf.Min(v1.x, Mathf.Min(v2.x, v3.x)); | |
int maxY = Mathf.Max(v1.y, Mathf.Max(v2.y, v3.y)); | |
int minY = Mathf.Min(v1.y, Mathf.Min(v2.y, v3.y)); | |
//if (debug) | |
// Debug.Log($"({minX}, {minY}, {maxX}, {maxY})"); | |
/* spanning vectors of edge (v1,v2) and (v1,v3) */ | |
Point vs1 = new Point(v2.x - v1.x, v2.y - v1.y); | |
Point vs2 = new Point(v3.x - v1.x, v3.y - v1.y); | |
for (int x = minX; x <= maxX; x++) | |
{ | |
for (int y = minY; y <= maxY; y++) | |
{ | |
Point q = new Point(x - v1.x, y - v1.y); | |
float s = Vector2.Dot(q, vs2) / Vector2.Dot(vs1, vs2); | |
float t = Vector2.Dot(vs1, q) / Vector2.Dot(vs1, vs2); | |
if ((s >= 0) && (t >= 0) && (s + t <= 1)) | |
{ /* inside triangle */ | |
action?.Invoke(x, y); | |
} | |
} | |
} | |
} | |
public static void RasterizeBresenham(Point vt1, Point vt2, Point vt3, Action<int, int> action, bool debugException = false) | |
{ | |
// Thanks to: http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html#algo3 && https://www.cs.unm.edu/~joel/cs491_VR/src/DrawUtilities.cs | |
/* checks for a valid triangle */ | |
if (!CheckIfValidTriangle(vt1, vt2, vt3, debugException)) | |
return; | |
string invalidTriangle = $"The given points must form a triangle. {{{vt1}, {vt2}, {vt3}}}"; | |
/* at first sort the three vertices by y-coordinate ascending so v1 is the topmost vertice */ | |
sortVerticesAscendingByY(ref vt1, ref vt2, ref vt3); | |
/* here we know that v1.y <= v2.y <= v3.y */ | |
/* check for trivial case of bottom-flat triangle */ | |
if (vt2.y == vt3.y) | |
{ | |
if (!fillBottomFlatTriangle(vt1, vt2, vt3, action, debugException)) | |
{ | |
if (debugException) | |
Debug.LogWarning(invalidTriangle); | |
return; | |
} | |
} | |
/* check for trivial case of top-flat triangle */ | |
else if (vt1.y == vt2.y) | |
{ | |
if (!fillTopFlatTriangle(vt1, vt2, vt3, action, debugException)) | |
{ | |
if (debugException) | |
Debug.LogWarning(invalidTriangle); | |
return; | |
} | |
} | |
else | |
{ | |
/* general case - split the triangle in a topflat and bottom-flat one */ | |
Point v4 = new Point((int)(vt1.x + (vt2.y - vt1.y) / (float)(vt3.y - vt1.y) * (vt3.x - vt1.x)), vt2.y); | |
if (!fillBottomFlatTriangle(vt1, vt2, v4, action, debugException) || !fillTopFlatTriangle(vt2, v4, vt3, action, debugException)) | |
{ | |
if (debugException) | |
Debug.LogWarning(invalidTriangle); | |
return; | |
} | |
} | |
} | |
public static bool CheckIfValidTriangle(Point v1, Point v2, Point v3, bool debug = false) | |
{ | |
/* | |
// https://www.geeksforgeeks.org/check-whether-triangle-valid-not-sides-given/ | |
a + b > c | |
a + c > b | |
b + c > a | |
*/ | |
float a = Vector2.Distance(new Vector2(v1.x, v1.y), new Vector2(v2.x, v2.y)), | |
b = Vector2.Distance(new Vector2(v2.x, v2.y), new Vector2(v3.x, v3.y)), | |
c = Vector2.Distance(new Vector2(v3.x, v3.y), new Vector2(v1.x, v1.y)); | |
if (a + b <= c || a + c <= b || b + c <= a) | |
{ | |
if (debug) | |
Debug.LogWarning($"The given points must form a triangle. {{{v1}, {v2}, {v3}}}"); | |
return false; | |
} | |
return true; | |
} | |
public static bool CheckIfValidTriangle(Point v1, Point v2, Point v3, out float a, out float b, out float c) | |
{ | |
a = Vector2.Distance(new Vector2(v1.x, v1.y), new Vector2(v2.x, v2.y)); | |
b = Vector2.Distance(new Vector2(v2.x, v2.y), new Vector2(v3.x, v3.y)); | |
c = Vector2.Distance(new Vector2(v3.x, v3.y), new Vector2(v1.x, v1.y)); | |
if (a + b <= c || a + c <= b || b + c <= a) | |
{ | |
// Debug.LogWarning($"The given points must form a triangle. {{{v1}, {v2}, {v3}}}"); | |
return false; | |
} | |
return true; | |
} | |
private static bool fillBottomFlatTriangle(Point v1, Point v2, Point v3, Action<int, int> action, bool debugException = false) | |
{ | |
try | |
{ | |
float invslope1 = (v2.x - v1.x) / (v2.y - v1.y); | |
float invslope2 = (v3.x - v1.x) / (v3.y - v1.y); | |
float curx1 = v1.x; | |
float curx2 = v1.x; | |
for (int scanlineY = v1.y; scanlineY <= v2.y; scanlineY++) | |
{ | |
DrawLine((int)curx1, scanlineY, (int)curx2, scanlineY, action); | |
curx1 += invslope1; | |
curx2 += invslope2; | |
} | |
} | |
catch (Exception ex) | |
{ | |
if (debugException) | |
Debug.LogException(ex); | |
return false; | |
} | |
return true; | |
} | |
private static bool fillTopFlatTriangle(Point v1, Point v2, Point v3, Action<int, int> action, bool debugException = false) | |
{ | |
try | |
{ | |
float invslope1 = (v3.x - v1.x) / (v3.y - v1.y); | |
float invslope2 = (v3.x - v2.x) / (v3.y - v2.y); | |
float curx1 = v3.x; | |
float curx2 = v3.x; | |
for (int scanlineY = v3.y; scanlineY > v1.y; scanlineY--) | |
{ | |
DrawLine((int)curx1, scanlineY, (int)curx2, scanlineY, action); | |
curx1 -= invslope1; | |
curx2 -= invslope2; | |
} | |
} | |
catch (Exception ex) | |
{ | |
if (debugException) | |
Debug.LogException(ex); | |
return false; | |
} | |
return true; | |
} | |
private static void sortVerticesAscendingByY(ref Point v0, ref Point v1, ref Point v2) | |
{ | |
if (v2.y < v1.y) | |
Swap(ref v1, ref v2); | |
if (v1.y < v0.y) | |
Swap(ref v0, ref v1); | |
if (v2.y < v1.y) | |
Swap(ref v1, ref v2); | |
} | |
private static void Swap<T>(ref T lhs, ref T rhs) | |
{ | |
(lhs, rhs) = (rhs, lhs); | |
// Impl for versions lower than C# 7 | |
//T temp; | |
//temp = lhs; | |
//lhs = rhs; | |
//rhs = temp; | |
} | |
public static float TriangleArea(Point p1, Point p2, Point p3) | |
{ | |
float a, b, c; | |
if (!CheckIfValidTriangle(p1, p2, p3, out a, out b, out c)) | |
return 0; | |
return TriangleArea(a, b, c); | |
} | |
public static float TriangleArea(float a, float b, float c) | |
{ | |
// Thanks to: http://james-ramsden.com/area-of-a-triangle-in-3d-c-code/ | |
float s = (a + b + c) / 2.0f; | |
return Mathf.Sqrt(s * (s - a) * (s - b) * (s - c)); | |
} | |
// Bresenham impl: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm | |
public static void DrawLine(Vector2 p1, Vector2 p2, Action<int, int> action) | |
{ | |
DrawLine((int)p1.x, (int)p1.y, (int)p2.x, (int)p2.y, action); | |
} | |
public static void DrawLine(int x0, int y0, int x1, int y1, Action<int, int> action) | |
{ | |
int sx = 0, | |
sy = 0; | |
int dx = Mathf.Abs(x1 - x0), | |
dy = Mathf.Abs(y1 - y0); | |
if (x0 < x1) { sx = 1; } else { sx = -1; } | |
if (y0 < y1) { sy = 1; } else { sy = -1; } | |
int err = dx - dy, | |
e2 = 0; | |
while (true) | |
{ | |
//colors[P(x0 % width, y0 % height, width, height)] = x0 / width >= 1 || y0 / height >= 1 ? UnityEngine.Color.yellow : c; | |
action?.Invoke(x0, y0); | |
if ((x0 == x1) && (y0 == y1)) | |
break; | |
e2 = 2 * err; | |
if (e2 > -dy) | |
{ | |
err = err - dy; | |
x0 = x0 + sx; | |
} | |
if (e2 < dx) | |
{ | |
err = err + dx; | |
y0 = y0 + sy; | |
} | |
} | |
} | |
public static Point GetTriangleCenter(Point p0, Point p1, Point p2) | |
{ | |
// Thanks to: https://stackoverflow.com/questions/524755/finding-center-of-2d-triangle | |
return new Point(p0.x + p1.x + p2.x / 3, p0.y + p1.y + p2.y / 3); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment