Skip to content

Instantly share code, notes, and snippets.

@JettMonstersGoBoom
Last active March 23, 2023 01:27
Show Gist options
  • Save JettMonstersGoBoom/92e64daec13fb08a1048211318d1d6c6 to your computer and use it in GitHub Desktop.
Save JettMonstersGoBoom/92e64daec13fb08a1048211318d1d6c6 to your computer and use it in GitHub Desktop.
triangle rasterizer with psx style dither and bpp per gun.
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <math.h>
#include "tigr.h"
#include "vec2.h"
// extension of this code
// https://github.com/gustavopezzi/triangle-rasterizer-float
// added dithering
// added bpp per color gun
// added flat uncolored
// added texturing
// NOTE the dithering and color conversion could have been added to put_pixel
// using https://github.com/erkkah/tigr
// removes the dep of SDL and the trickyness to get that setup.
// just gitclone into the folder
// I also used https://poloviiinkin.itch.io/textures for testing
Tigr* screen;
Tigr* texture;
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 192
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
bool is_running = false;
vec2_t vertices[4] = {
{ .x = -128, .y = -128 },
{ .x = 128, .y = -128 },
{ .x = -128, .y = 128 },
{ .x = 128, .y = 128 }
};
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} color_t;
color_t colors[] = {
{ .r = 0xFF, .g = 0xff, .b = 0xff },
{ .r = 0x00, .g = 0xFF, .b = 0x00 },
{ .r = 0x00, .g = 0x00, .b = 0xFF },
{ .r = 0xFF, .g = 0x00, .b = 0x00 },
};
// MGB added
typedef struct {
uint16_t u;
uint16_t v;
} uv_t;
uv_t uvs[4] = {
{ .u = 0, .v = 0 }, // values manually converted to 16.16 fixed point
{ .u = 512, .v = 0 }, // values manually converted to 16.16 fixed point
{ .u = 0, .v = 512 }, // values manually converted to 16.16 fixed point
{ .u = 512, .v = 512 }, // values manually converted to 16.16 fixed point
};
int count = 0;
bool is_top_left(vec2_t* start, vec2_t* end) {
vec2_t edge = { end->x - start->x, end->y - start->y };
bool is_top_edge = edge.y == 0 && edge.x > 0;
bool is_left_edge = edge.y < 0;
return is_left_edge || is_top_edge;
}
float edge_cross(vec2_t* a, vec2_t* b, vec2_t* p) {
vec2_t ab = { b->x - a->x, b->y - a->y };
vec2_t ap = { p->x - a->x, p->y - a->y };
return ab.x * ap.y - ab.y * ap.x;
}
#define MAPW 16
#define MAPH 16
#define TILEW 48
#define TILEH 48
uint8_t map[MAPW*MAPH];
static float psx_dither_table[4*4] =
{
0, 8, 2, 10,
12, 4, 14, 6,
3, 11, 1, 9,
15, 7, 13, 5
};
struct
{
uint16_t dither:1;
uint16_t texture:1;
uint16_t gouraud:1;
uint16_t tiled:1;
uint16_t rbpp:3;
uint16_t gbpp:3;
uint16_t bbpp:3;
} render_flags;
uint8_t bpp_table[] = {
0b10000000,
0b11000000,
0b11100000,
0b11110000,
0b11111000,
0b11111100,
0b11111110,
0b11111111,
};
// for tigr specific get and plot
TPixel get_pixel(int x,int y)
{
if (render_flags.tiled==0)
return(tigrGet(texture,x % texture->w,y % texture->h));
int map_x = (x/TILEW)%MAPW;
int map_y = (y/TILEH)%MAPH;
uint8_t tile = map[map_x+(map_y*MAPW)];
int t_x = (((tile&0xf)*TILEW) + (x%TILEW)) % texture->w;
int t_y = (((tile/16)*TILEH) + (y%TILEH)) % texture->h;
return tigrGet(texture,t_x,t_y);
}
void put_pixel(int x,int y,uint8_t r,uint8_t g,uint8_t b)
{
tigrPlot(screen,x,y,tigrRGB(r,g,b));
}
void triangle_fill(vec2_t v0, vec2_t v1, vec2_t v2,int c0,int c1,int c2,int u0,int u1,int u2) {
// Finds the bounding box with all candidate pixels
int x_min = floor(MIN(MIN(v0.x, v1.x), v2.x));
int y_min = floor(MIN(MIN(v0.y, v1.y), v2.y));
int x_max = ceil(MAX(MAX(v0.x, v1.x), v2.x));
int y_max = ceil(MAX(MAX(v0.y, v1.y), v2.y));
// Compute the area of the entire triangle/parallelogram
float area = edge_cross(&v0, &v1, &v2);
// Compute the constant delta_s that will be used for the horizontal and vertical steps
float delta_w0_col = (v1.y - v2.y);
float delta_w1_col = (v2.y - v0.y);
float delta_w2_col = (v0.y - v1.y);
float delta_w0_row = (v2.x - v1.x);
float delta_w1_row = (v0.x - v2.x);
float delta_w2_row = (v1.x - v0.x);
// Rasterization fill rule, not 100% precise due to floating point innacuracy
float bias0 = is_top_left(&v1, &v2) ? 0 : -0.0001;
float bias1 = is_top_left(&v2, &v0) ? 0 : -0.0001;
float bias2 = is_top_left(&v0, &v1) ? 0 : -0.0001;
// Compute the edge functions for the fist (top-left) point
vec2_t p0 = { x_min + 0.5f , y_min + 0.5f };
float w0_row = edge_cross(&v1, &v2, &p0) + bias0;
float w1_row = edge_cross(&v2, &v0, &p0) + bias1;
float w2_row = edge_cross(&v0, &v1, &p0) + bias2;
// Loop all candidate pixels inside the bounding box
for (int y = y_min; y <= y_max; y++) {
float w0 = w0_row;
float w1 = w1_row;
float w2 = w2_row;
for (int x = x_min; x <= x_max; x++) {
bool is_inside = w0 >= 0 && w1 >= 0 && w2 >= 0;
if (is_inside) {
float alpha = w0 / area;
float beta = w1 / area;
float gamma = w2 / area;
int r;
int g;
int b;
// if gouraud then apply it here
if (render_flags.gouraud==1)
{
r = (alpha) * colors[c0].r + (beta) * colors[c1].r + (gamma) * colors[c2].r;
g = (alpha) * colors[c0].g + (beta) * colors[c1].g + (gamma) * colors[c2].g;
b = (alpha) * colors[c0].b + (beta) * colors[c1].b + (gamma) * colors[c2].b;
}
else
{
r=colors[c0].r;
g=colors[c0].g;
b=colors[c0].b;
}
// if textured then sample and mix
if (render_flags.texture==1)
{
TPixel tex;
int U = (alpha) * uvs[u0].u + (beta) * uvs[u1].u + (gamma) * uvs[u2].u;
int V = (alpha) * uvs[u0].v + (beta) * uvs[u1].v + (gamma) * uvs[u2].v;
tex = get_pixel(U,V);
r=(tex.r*r)>>8;
g=(tex.g*g)>>8;
b=(tex.b*b)>>8;
}
// if dithered , apply it
if (render_flags.dither==1)
{
float dither = (psx_dither_table[(x & 3) + ((y & 3)<<2)])/2.0f+4.0f;
r+=dither;
g+=dither;
b+=dither;
}
// clamp to 0-255 range
r=MIN(r,255);
g=MIN(g,255);
b=MIN(b,255);
// convert to bits per pixel output
r&=bpp_table[render_flags.rbpp];
g&=bpp_table[render_flags.gbpp];
b&=bpp_table[render_flags.bbpp];
// plot pixel
put_pixel(x,y,r,g,b);
}
w0 += delta_w0_col;
w1 += delta_w1_col;
w2 += delta_w2_col;
}
w0_row += delta_w0_row;
w1_row += delta_w1_row;
w2_row += delta_w2_row;
}
}
void render(void) {
static float time = 0;
time+=tigrTime();
float angle = time * 0.4f;
map[rand()%sizeof(map)]=rand();
vec2_t center = { SCREEN_WIDTH / 2.0f, SCREEN_HEIGHT / 2.0f };
vec2_t v0 = vec2_rotate(vertices[0], center, angle);
vec2_t v1 = vec2_rotate(vertices[1], center, angle);
vec2_t v2 = vec2_rotate(vertices[2], center, angle);
vec2_t v3 = vec2_rotate(vertices[3], center, angle);
render_flags.texture=1;
triangle_fill(v0, v1, v2,0,0,2,0,1,2);
render_flags.texture=0;
triangle_fill(v3, v2, v1,3,2,1,0,0,0);
}
int main(int argc, char* argv[]) {
screen = tigrWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "texture test", 0);
texture = tigrLoadImage("texture2.png");
while (!tigrClosed(screen) && !tigrKeyDown(screen, TK_ESCAPE)) {
tigrClear(screen, tigrRGB(0x80, 0x90, 0xa0));
if (tigrKeyDown(screen,'D'))
render_flags.dither=!render_flags.dither;
if (tigrKeyDown(screen,'T'))
render_flags.tiled=!render_flags.tiled;
if (tigrKeyDown(screen,'S'))
render_flags.gouraud=!render_flags.gouraud;
if (tigrKeyHeld(screen,TK_SHIFT))
{
if (tigrKeyDown(screen,'R'))
render_flags.rbpp--;
if (tigrKeyDown(screen,'G'))
render_flags.gbpp--;
if (tigrKeyDown(screen,'B'))
render_flags.bbpp--;
}
else
{
if (tigrKeyDown(screen,'R'))
render_flags.rbpp++;
if (tigrKeyDown(screen,'G'))
render_flags.gbpp++;
if (tigrKeyDown(screen,'B'))
render_flags.bbpp++;
}
render();
tigrPrint(screen, tfont, 0, 0, tigrRGB(0xff, 0xff, 0xff),
"R %d G %d B %d\nDither %s\nTiled %s\nGouraud %s",
1+render_flags.rbpp,
1+render_flags.gbpp,
1+render_flags.bbpp,
render_flags.dither ? "Enabled" : "Disabled",
render_flags.tiled ? "Enabled" : "Disabled",
render_flags.gouraud ? "Enabled" : "Disabled");
tigrUpdate(screen);
}
tigrFree(texture);
tigrFree(screen);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment