Skip to content

Instantly share code, notes, and snippets.

@bit-hack
Created April 9, 2018 12:15
Show Gist options
  • Save bit-hack/aad17e041e3af2f2ffbccd1c56ee9519 to your computer and use it in GitHub Desktop.
Save bit-hack/aad17e041e3af2f2ffbccd1c56ee9519 to your computer and use it in GitHub Desktop.
Scanline triangle rasterization experiment
#define _SDL_main_h
#include <SDL/SDL.h>
#include <assert.h>
#include <stdint.h>
#include <array>
struct coord_t {
float x, y;
};
// return true if a triangle is backfacing
bool is_backface(const coord_t &a, const coord_t &b, const coord_t &c) {
const float v1 = a.x - b.x;
const float v2 = a.y - b.y;
const float w1 = c.x - b.x;
const float w2 = c.y - b.y;
return 0.f > (v1 * w2 - v2 * w1);
}
// return true if a triangle intersect unit square [0,0] -> [1,1]
bool tri_vis(const coord_t &a, const coord_t &b, const coord_t &c) {
// cohen-sutherland style trivial clipping
{
const auto classify = [](const coord_t &p) -> int {
return (p.x < 0.f ? 1 : 0) | (p.x > 1.f ? 2 : 0) | (p.y < 0.f ? 4 : 0) |
(p.y > 1.f ? 8 : 0);
};
const int ca = classify(a), cb = classify(b), cc = classify(c);
if (0 == (ca | cb | cc)) {
// all in center, no clipping
return true;
}
const int code = ca & cb & cc;
if ((code & 1) || (code & 2) || (code & 4) || (code & 8)) {
// all outside one plane
return false;
}
}
// reject backfaces
if (is_backface(a, b, c)) {
return false;
}
// plane distance rejection
{
struct plane_t {
plane_t(const coord_t &a, const coord_t &b)
: nx(b.y - a.y), ny(a.x - b.x), d(a.x * nx + a.y * ny) {}
bool test(const coord_t &p) const { return d > (p.x * nx + p.y * ny); }
float nx, ny, d;
};
const coord_t ndc[4] = {{0.f, 0.f}, {1.f, 0.f}, {0.f, 1.f}, {1.f, 1.f}};
// XXX: we can do trivial rejects with closest vertex first
const plane_t pab{a, b};
if (pab.test(ndc[0]) && pab.test(ndc[1]) && pab.test(ndc[2]) &&
pab.test(ndc[3])) {
return false;
}
const plane_t pbc{b, c};
if (pbc.test(ndc[0]) && pbc.test(ndc[1]) && pbc.test(ndc[2]) &&
pbc.test(ndc[3])) {
return false;
}
const plane_t pca{c, a};
if (pca.test(ndc[0]) && pca.test(ndc[1]) && pca.test(ndc[2]) &&
pca.test(ndc[3])) {
return false;
}
}
// in, but needs clipping
return true;
}
// dot product between two 2d vectors
constexpr float dot(const coord_t &a, const coord_t &b) {
return a.x * b.x + a.y * b.y;
}
// squared length of two 2d vectors
float len_sqr(const coord_t &a, const coord_t &b) {
return (b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y);
}
// return true if 'p' falls inside triangle 'a, b, c'
bool point_in_tri(const coord_t &a, const coord_t &b, const coord_t &c,
const coord_t &p) {
// Compute vectors
const auto v0 = coord_t{c.x - a.x, c.y - a.y}; // C - A;
const auto v1 = coord_t{b.x - a.x, b.y - a.y}; // B - A;
const auto v2 = coord_t{p.x - a.x, p.y - a.y}; // P - A;
// Compute dot products
const float dot00 = dot(v0, v0);
const float dot01 = dot(v0, v1);
const float dot02 = dot(v0, v2);
const float dot11 = dot(v1, v1);
const float dot12 = dot(v1, v2);
// Compute barycentric coordinates
const float denom = (dot00 * dot11 - dot01 * dot01);
const float u = (dot11 * dot02 - dot01 * dot12) / denom;
const float v = (dot00 * dot12 - dot01 * dot02) / denom;
// Check if point is in triangle
return (u >= 0) && (v >= 0) && (u + v < 1);
}
// plot a pixel to the screen
void plot(SDL_Surface *surf, float x, float y, uint32_t rgb = 0xdadada) {
assert(surf);
if (x < 0.f || y < 0.f || x >= surf->w || y >= surf->h) {
return;
}
uint32_t *pix = (uint32_t *)surf->pixels;
pix[int(x) + int(y) * surf->w] = rgb;
}
// XXX: ignore this function
void draw_line(SDL_Surface *surf, const coord_t &a, const coord_t &b,
uint32_t rgb) {
// XXX: this is a weird line drawing functions that I just wanted to write for
// fun
struct interval_t {
coord_t a, b;
};
std::array<interval_t, 512> s;
uint32_t h = 0;
h = 1;
s[1] = a.y < b.y ? interval_t{a, b} : interval_t{b, a};
while (h >= 1) {
auto &i = s[h];
if (i.b.y - i.a.y > 1.f) {
const coord_t mid{(i.a.x + i.b.x) / 2, (i.a.y + i.b.y) / 2};
s[h + 1] = interval_t{mid, i.b};
s[h + 0] = interval_t{i.a, mid};
++h;
} else {
plot(surf, i.a.x, i.a.y, rgb);
--h;
}
}
}
constexpr int32_t minv(int32_t a, int32_t b) { return a < b ? a : b; }
constexpr int32_t maxv(int32_t a, int32_t b) { return a > b ? a : b; }
constexpr int32_t clampv(int32_t lo, int32_t v, int32_t hi) {
return minv(hi, maxv(lo, v));
}
enum clip_span_t { CLIP_SPAN_MIN_X, CLIP_SPAN_MAX_X };
template <clip_span_t CLIP, size_t SIZE>
void scan_convert(coord_t a, coord_t b, std::array<int32_t, SIZE> &span) {
// XXX: move into global attribute
static const int32_t screen_w = 511;
static const int32_t screen_h = 511;
// assume our vertices are pre-sorted
__assume(a.y < b.y);
// scanline rejection
if (b.y < 0.f || a.y > screen_h) {
return;
}
// reject if vertices are co-linear
if (b.y <= a.y) {
return;
}
const float dx = (b.x - a.x) / (b.y - a.y);
// clip to top of window edge
if (a.y < 0.f) {
a.x += dx * (0.f - a.y);
a.y = 0.f;
} else {
// align to start of scanline
{
const float ceily = ceilf(a.y);
a.x += dx * (ceily - a.y);
a.y = ceily;
}
}
const int32_t iay = maxv(int32_t(a.y), 0);
const int32_t iby = minv(int32_t(b.y), screen_h);
int32_t x = int32_t(a.x * float(0x10000));
const int32_t idx = int32_t(dx * float(0x10000));
switch (CLIP) {
case CLIP_SPAN_MAX_X:
for (uint32_t y = iay; y < iby; ++y, x += idx) {
span[y] = std::max<int32_t>(x >> 16, 0);
}
break;
case CLIP_SPAN_MIN_X:
for (uint32_t y = iay; y < iby; ++y, x += idx) {
span[y] = std::min<int32_t>(x >> 16, screen_w);
}
break;
}
}
// scan convert a triangle
bool scan_triangle(SDL_Surface *surf, std::array<coord_t, 3> v) {
// sort vertices: top (0), mid (1), bottom (2)
if (v[1].y < v[0].y)
std::swap(v[1], v[0]);
if (v[2].y < v[0].y)
std::swap(v[2], v[0]);
if (v[2].y < v[1].y)
std::swap(v[2], v[1]);
// check mid vertex side
const float nx = v[2].y - v[0].y;
const float ny = v[0].x - v[2].x;
const float d1 = v[0].x * nx + v[0].y * ny;
const float d2 = v[1].x * nx + v[1].y * ny;
if (d1 == d2) {
// coplanar, so reject triangle
return false;
}
// our y axis span buffers
std::array<int32_t, 512> lo, hi;
// scan convert edges
if (d1 > d2) {
scan_convert<CLIP_SPAN_MIN_X>(v[0], v[2], hi);
scan_convert<CLIP_SPAN_MAX_X>(v[0], v[1], lo);
scan_convert<CLIP_SPAN_MAX_X>(v[1], v[2], lo);
} else {
scan_convert<CLIP_SPAN_MAX_X>(v[0], v[2], lo);
scan_convert<CLIP_SPAN_MIN_X>(v[0], v[1], hi);
scan_convert<CLIP_SPAN_MIN_X>(v[1], v[2], hi);
}
// fill triangle
{
const int32_t y0 = std::max(int32_t(ceilf(v[0].y)), 0);
const int32_t y1 = std::min(int32_t(v[2].y), 511);
uint32_t *py = (uint32_t *)surf->pixels;
py += (y0 * surf->pitch) / 4;
for (int32_t y = y0; y < y1; ++y) {
// step to edge
uint32_t *px = py + lo[y];
// raster scanline
for (int32_t x = lo[y]; x < hi[y]; ++x, ++px) {
*px = 0x335577;
}
// step scanline
py += surf->pitch / 4;
}
}
}
// fast fixed point line drawing
void draw_line_2(SDL_Surface *surf, coord_t a, coord_t b, uint32_t rgb) {
const float dx = b.x - a.x, dy = b.y - a.y;
const float adx = fabsf(dx), ady = fabs(dy);
if (fabsf(dx) > fabsf(dy)) {
if (b.x < a.x)
std::swap(a, b);
const int32_t iy = int32_t(float(0x10000) * ((b.y - a.y) / adx));
int32_t y = int32_t(a.y * float(0x10000));
for (int32_t x = a.x; x < b.x; ++x, y += iy) {
plot(surf, x, y >> 16, rgb);
}
} else {
if (b.y < a.y)
std::swap(a, b);
const int32_t ix = int32_t(float(0x10000) * ((b.x - a.x) / ady));
int32_t x = int32_t(a.x * float(0x10000));
for (int32_t y = a.y; y < b.y; ++y, x += ix) {
plot(surf, x >> 16, y, rgb);
}
}
}
// draw a wireframe triangle
void draw_tri(SDL_Surface *surf, const std::array<coord_t, 3> &t,
uint32_t rgb) {
for (uint32_t j = 0; j < 3; ++j) {
const coord_t &a = t[j];
const coord_t &b = t[j == 2 ? 0 : j + 1];
draw_line_2(surf, a, b, rgb);
}
}
// find the nearest vertex to point p
coord_t *nearest(std::array<coord_t, 3> &tri, const coord_t &p) {
coord_t *out = nullptr;
float best = 100000.f;
for (auto &c : tri) {
const float dx = p.x - c.x;
const float dy = p.y - c.y;
const float ds = dx * dx + dy * dy;
if (ds < best) {
best = ds;
out = &c;
}
}
return best < 256.f ? out : nullptr;
}
// program entry
int main(const int argc, const char **args) {
if (SDL_Init(SDL_INIT_VIDEO)) {
return 1;
}
SDL_Surface *surf = SDL_SetVideoMode(512, 512, 32, 0);
if (!surf) {
return 2;
}
std::array<coord_t, 3> tri = {coord_t{256, 64}, coord_t{64, 512 - 64},
coord_t{512 - 64, 512 - 64}};
coord_t *drag = nullptr;
bool dragall = false;
bool active = true;
while (active) {
SDL_FillRect(surf, nullptr, 0x101010);
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
active = false;
break;
case SDL_MOUSEBUTTONUP:
drag = nullptr;
dragall = false;
break;
case SDL_MOUSEBUTTONDOWN:
if (!drag) {
const coord_t p =
coord_t{float(event.button.x), float(event.button.y)};
// check for drags
drag = nearest(tri, p);
if (!drag) {
dragall = point_in_tri(tri[0], tri[1], tri[2], p);
}
// clear relative mouse state
SDL_GetRelativeMouseState(nullptr, nullptr);
}
break;
}
}
if (drag || dragall) {
int rx, ry;
SDL_GetRelativeMouseState(&rx, &ry);
if (drag) {
drag->x += rx;
drag->y += ry;
}
if (dragall) {
for (auto &c : tri) {
c.x += rx;
c.y += ry;
}
}
}
uint32_t tri_rgb = 0xff6040;
// draw screen
{
SDL_Rect rect = {(512 - 256) / 2, (512 - 256) / 2, 256, 256};
SDL_FillRect(surf, &rect, 0x203040);
std::array<coord_t, 3> xtri = tri;
for (coord_t &c : xtri) {
c.x = (c.x - rect.x) / rect.w;
c.y = (c.y - rect.y) / rect.h;
}
if (tri_vis(xtri[0], xtri[1], xtri[2])) {
tri_rgb = 0x30ff60;
}
if (is_backface(tri[0], tri[1], tri[2])) {
tri_rgb = 0xdadada;
}
}
// draw triangle
scan_triangle(surf, tri);
draw_tri(surf, tri, tri_rgb);
// draw vertex nodes
for (const auto &p : tri) {
SDL_Rect r = {p.x - 3, p.y - 3, 7, 7};
SDL_FillRect(surf, &r, 0xdadada);
}
SDL_Flip(surf);
SDL_Delay(1);
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment