#include #include #include #include #include "pxl8_ase.h" #include "pxl8_blit.h" #include "pxl8_font.h" #include "pxl8_gfx.h" #include "pxl8_macros.h" #include "pxl8_math.h" #include "pxl8_types.h" typedef struct pxl8_atlas_entry { bool active; char path[256]; u32 texture_id; i32 x, y, w, h; } pxl8_atlas_entry; typedef struct pxl8_skyline_fit { bool found; u32 node_idx; pxl8_point pos; } pxl8_skyline_fit; typedef struct pxl8_skyline_node { i32 x, y, width; } pxl8_skyline_node; typedef struct pxl8_skyline { pxl8_skyline_node* nodes; u32 count; u32 capacity; } pxl8_skyline; struct pxl8_atlas { u32 height, width; u8* pixels; SDL_Texture* texture; bool dirty; u32 entry_capacity, entry_count; pxl8_atlas_entry* entries; u32 free_capacity, free_count; u32* free_list; pxl8_skyline skyline; }; struct pxl8_gfx { SDL_Renderer* renderer; SDL_Texture* framebuffer_texture; SDL_Window* window; pxl8_atlas* atlas; pxl8_color_mode color_mode; u8* framebuffer; i32 framebuffer_height; i32 framebuffer_width; bool initialized; u32* palette; u32 palette_size; pxl8_viewport viewport; bool backface_culling; pxl8_mat4 model; pxl8_mat4 projection; pxl8_mat4 view; bool wireframe; f32* zbuffer; i32 zbuffer_height; i32 zbuffer_width; bool affine_textures; }; static pxl8_skyline_fit pxl8_skyline_find_position(const pxl8_skyline* skyline, u32 atlas_w, u32 atlas_h, u32 rect_w, u32 rect_h) { pxl8_skyline_fit result = {.found = false}; i32 best_y = INT32_MAX; i32 best_x = 0; u32 best_idx = 0; for (u32 i = 0; i < skyline->count; i++) { i32 x = skyline->nodes[i].x; i32 y = skyline->nodes[i].y; if (x + (i32)rect_w > (i32)atlas_w) continue; i32 max_y = y; for (u32 j = i; j < skyline->count && skyline->nodes[j].x < x + (i32)rect_w; j++) { if (skyline->nodes[j].y > max_y) { max_y = skyline->nodes[j].y; } } if (max_y + (i32)rect_h > (i32)atlas_h) continue; if (max_y < best_y || (max_y == best_y && x < best_x)) { best_y = max_y; best_x = x; best_idx = i; } } if (best_y != INT32_MAX) { result.found = true; result.pos.x = best_x; result.pos.y = best_y; result.node_idx = best_idx; } return result; } static void pxl8_skyline_add_rect(pxl8_skyline* skyline, pxl8_point pos, u32 w, u32 h) { u32 node_idx = 0; for (u32 i = 0; i < skyline->count; i++) { if (skyline->nodes[i].x == pos.x) { node_idx = i; break; } } pxl8_skyline_node new_node = {pos.x, pos.y + (i32)h, (i32)w}; u32 nodes_to_remove = 0; for (u32 i = node_idx; i < skyline->count; i++) { if (skyline->nodes[i].x < pos.x + (i32)w) { nodes_to_remove++; } else { break; } } if (skyline->count - nodes_to_remove + 1 > skyline->capacity) { skyline->capacity = (skyline->count - nodes_to_remove + 1) * 2; skyline->nodes = (pxl8_skyline_node*)SDL_realloc(skyline->nodes, skyline->capacity * sizeof(pxl8_skyline_node)); } if (nodes_to_remove > 0) { SDL_memmove(&skyline->nodes[node_idx + 1], &skyline->nodes[node_idx + nodes_to_remove], (skyline->count - node_idx - nodes_to_remove) * sizeof(pxl8_skyline_node)); } skyline->nodes[node_idx] = new_node; skyline->count = skyline->count - nodes_to_remove + 1; } static void pxl8_skyline_compact(pxl8_skyline* skyline) { for (u32 i = 0; i < skyline->count - 1; ) { if (skyline->nodes[i].y == skyline->nodes[i + 1].y) { skyline->nodes[i].width += skyline->nodes[i + 1].width; SDL_memmove(&skyline->nodes[i + 1], &skyline->nodes[i + 2], (skyline->count - i - 2) * sizeof(pxl8_skyline_node)); skyline->count--; } else { i++; } } } static pxl8_atlas* pxl8_atlas_create(SDL_Renderer* renderer, u32 width, u32 height, pxl8_color_mode color_mode) { pxl8_atlas* atlas = (pxl8_atlas*)SDL_calloc(1, sizeof(pxl8_atlas)); if (!atlas) return NULL; atlas->height = height; atlas->width = width; i32 bytes_per_pixel = (color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1; atlas->pixels = (u8*)SDL_calloc(width * height, bytes_per_pixel); if (!atlas->pixels) { SDL_free(atlas); return NULL; } atlas->texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, width, height); if (!atlas->texture) { SDL_free(atlas->pixels); SDL_free(atlas); return NULL; } atlas->entry_capacity = 64; atlas->entries = (pxl8_atlas_entry*)SDL_calloc(atlas->entry_capacity, sizeof(pxl8_atlas_entry)); if (!atlas->entries) { SDL_DestroyTexture(atlas->texture); SDL_free(atlas->pixels); SDL_free(atlas); return NULL; } atlas->free_capacity = 16; atlas->free_list = (u32*)SDL_calloc(atlas->free_capacity, sizeof(u32)); if (!atlas->free_list) { SDL_free(atlas->entries); SDL_DestroyTexture(atlas->texture); SDL_free(atlas->pixels); SDL_free(atlas); return NULL; } atlas->skyline.capacity = 16; atlas->skyline.nodes = (pxl8_skyline_node*)SDL_calloc(atlas->skyline.capacity, sizeof(pxl8_skyline_node)); if (!atlas->skyline.nodes) { SDL_free(atlas->free_list); SDL_free(atlas->entries); SDL_DestroyTexture(atlas->texture); SDL_free(atlas->pixels); SDL_free(atlas); return NULL; } atlas->skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)width}; atlas->skyline.count = 1; return atlas; } static void pxl8_atlas_destroy(pxl8_atlas* atlas) { if (!atlas) return; SDL_free(atlas->free_list); SDL_free(atlas->entries); SDL_free(atlas->skyline.nodes); if (atlas->texture) SDL_DestroyTexture(atlas->texture); SDL_free(atlas->pixels); SDL_free(atlas); } static bool pxl8_atlas_expand(pxl8_atlas* atlas, SDL_Renderer* renderer, pxl8_color_mode color_mode) { if (!atlas || atlas->width >= 4096) return false; u32 new_size = atlas->width * 2; u32 old_width = atlas->width; i32 bytes_per_pixel = (color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1; u8* new_pixels = (u8*)SDL_calloc(new_size * new_size, bytes_per_pixel); if (!new_pixels) return false; SDL_Texture* new_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, new_size, new_size); if (!new_texture) { SDL_free(new_pixels); return false; } pxl8_skyline new_skyline; new_skyline.nodes = (pxl8_skyline_node*)SDL_calloc(16, sizeof(pxl8_skyline_node)); if (!new_skyline.nodes) { SDL_DestroyTexture(new_texture); SDL_free(new_pixels); return false; } new_skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)new_size}; new_skyline.count = 1; new_skyline.capacity = 16; for (u32 i = 0; i < atlas->entry_count; i++) { if (!atlas->entries[i].active) continue; pxl8_skyline_fit fit = pxl8_skyline_find_position(&new_skyline, new_size, new_size, atlas->entries[i].w, atlas->entries[i].h); if (!fit.found) { SDL_free(new_skyline.nodes); SDL_DestroyTexture(new_texture); SDL_free(new_pixels); return false; } for (u32 y = 0; y < (u32)atlas->entries[i].h; y++) { for (u32 x = 0; x < (u32)atlas->entries[i].w; x++) { u32 src_idx = (atlas->entries[i].y + y) * old_width + (atlas->entries[i].x + x); u32 dst_idx = (fit.pos.y + y) * new_size + (fit.pos.x + x); if (bytes_per_pixel == 4) { ((u32*)new_pixels)[dst_idx] = ((u32*)atlas->pixels)[src_idx]; } else { new_pixels[dst_idx] = atlas->pixels[src_idx]; } } } atlas->entries[i].x = fit.pos.x; atlas->entries[i].y = fit.pos.y; pxl8_skyline_add_rect(&new_skyline, fit.pos, atlas->entries[i].w, atlas->entries[i].h); pxl8_skyline_compact(&new_skyline); } SDL_DestroyTexture(atlas->texture); SDL_free(atlas->pixels); SDL_free(atlas->skyline.nodes); atlas->pixels = new_pixels; atlas->texture = new_texture; atlas->skyline = new_skyline; atlas->width = new_size; atlas->height = new_size; atlas->dirty = true; pxl8_debug("Atlas expanded to %ux%u", atlas->width, atlas->height); return true; } static u32 pxl8_atlas_add_texture(pxl8_atlas* atlas, const u8* pixels, u32 w, u32 h, const char* path, pxl8_color_mode color_mode) { if (!atlas || !pixels) return UINT32_MAX; pxl8_skyline_fit fit = pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h); if (!fit.found) { if (!pxl8_atlas_expand(atlas, atlas->texture ? SDL_GetRendererFromTexture(atlas->texture) : NULL, color_mode)) { return UINT32_MAX; } fit = pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h); if (!fit.found) return UINT32_MAX; } u32 texture_id; if (atlas->free_count > 0) { texture_id = atlas->free_list[--atlas->free_count]; } else { if (atlas->entry_count >= atlas->entry_capacity) { atlas->entry_capacity *= 2; atlas->entries = (pxl8_atlas_entry*)SDL_realloc(atlas->entries, atlas->entry_capacity * sizeof(pxl8_atlas_entry)); } texture_id = atlas->entry_count++; } pxl8_atlas_entry* entry = &atlas->entries[texture_id]; entry->active = true; entry->texture_id = texture_id; entry->x = fit.pos.x; entry->y = fit.pos.y; entry->w = w; entry->h = h; if (path) { strncpy(entry->path, path, sizeof(entry->path) - 1); entry->path[sizeof(entry->path) - 1] = '\0'; } else { snprintf(entry->path, sizeof(entry->path), "procgen_%u", texture_id); } i32 bytes_per_pixel = (color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1; for (u32 y = 0; y < h; y++) { for (u32 x = 0; x < w; x++) { u32 src_idx = y * w + x; u32 dst_idx = (fit.pos.y + y) * atlas->width + (fit.pos.x + x); if (bytes_per_pixel == 4) { ((u32*)atlas->pixels)[dst_idx] = ((const u32*)pixels)[src_idx]; } else { atlas->pixels[dst_idx] = pixels[src_idx]; } } } pxl8_skyline_add_rect(&atlas->skyline, fit.pos, w, h); pxl8_skyline_compact(&atlas->skyline); atlas->dirty = true; return texture_id; } static inline void pxl8_color_unpack(u32 color, u8* r, u8* g, u8* b, u8* a) { *r = color & 0xFF; *g = (color >> 8) & 0xFF; *b = (color >> 16) & 0xFF; *a = (color >> 24) & 0xFF; } static inline u32 pxl8_color_pack(u8 r, u8 g, u8 b, u8 a) { return r | (g << 8) | (b << 16) | (a << 24); } static inline u8 pxl8_color_lerp_channel(u8 c1, u8 c2, f32 t) { return c1 + (i32)((c2 - c1) * t); } static u32 pxl8_get_palette_size(pxl8_color_mode mode) { switch (mode) { case PXL8_COLOR_MODE_HICOLOR: return 0; case PXL8_COLOR_MODE_FAMI: return 64; case PXL8_COLOR_MODE_MEGA: return 512; case PXL8_COLOR_MODE_GBA: return 32768; case PXL8_COLOR_MODE_SUPERFAMI: return 32768; default: return 256; } } void pxl8_gfx_get_resolution_dimensions(pxl8_resolution resolution, i32* width, i32* height) { switch (resolution) { case PXL8_RESOLUTION_240x160: *width = 240; *height = 160; break; case PXL8_RESOLUTION_320x180: *width = 320; *height = 180; break; case PXL8_RESOLUTION_320x240: *width = 320; *height = 240; break; case PXL8_RESOLUTION_640x360: *width = 640; *height = 360; break; case PXL8_RESOLUTION_640x480: *width = 640; *height = 480; break; case PXL8_RESOLUTION_800x600: *width = 800; *height = 600; break; case PXL8_RESOLUTION_960x540: *width = 960; *height = 540; break; default: *width = 640; *height = 360; break; } } pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx) { pxl8_bounds bounds = {0}; if (!gfx || !gfx->window) { return bounds; } SDL_GetWindowPosition(gfx->window, &bounds.x, &bounds.y); SDL_GetWindowSize(gfx->window, &bounds.w, &bounds.h); return bounds; } pxl8_color_mode pxl8_gfx_get_color_mode(pxl8_gfx* gfx) { return gfx ? gfx->color_mode : PXL8_COLOR_MODE_FAMI; } u8* pxl8_gfx_get_framebuffer(pxl8_gfx* gfx) { return gfx ? gfx->framebuffer : NULL; } i32 pxl8_gfx_get_height(const pxl8_gfx* gfx) { return gfx ? gfx->framebuffer_height : 0; } i32 pxl8_gfx_get_width(const pxl8_gfx* gfx) { return gfx ? gfx->framebuffer_width : 0; } u32 pxl8_gfx_get_palette_size(const pxl8_gfx* gfx) { return gfx ? gfx->palette_size : 0; } pxl8_gfx* pxl8_gfx_create(pxl8_color_mode mode, pxl8_resolution resolution, const char* title, i32 window_width, i32 window_height) { pxl8_gfx* gfx = (pxl8_gfx*)SDL_calloc(1, sizeof(*gfx)); if (!gfx) { pxl8_error("Failed to allocate graphics context"); return NULL; } gfx->color_mode = mode; pxl8_gfx_get_resolution_dimensions(resolution, &gfx->framebuffer_width, &gfx->framebuffer_height); gfx->window = SDL_CreateWindow( title, window_width, window_height, SDL_WINDOW_RESIZABLE ); if (!gfx->window) { pxl8_error("Failed to create window: %s", SDL_GetError()); pxl8_gfx_destroy(gfx); return NULL; } gfx->renderer = SDL_CreateRenderer(gfx->window, NULL); if (!gfx->renderer) { pxl8_error("Failed to create renderer: %s", SDL_GetError()); pxl8_gfx_destroy(gfx); return NULL; } SDL_SetRenderLogicalPresentation(gfx->renderer, gfx->framebuffer_width, gfx->framebuffer_height, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); i32 bytes_per_pixel = (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1; i32 fb_size = gfx->framebuffer_width * gfx->framebuffer_height * bytes_per_pixel; gfx->framebuffer = (u8*)SDL_calloc(1, fb_size); if (!gfx->framebuffer) { pxl8_error("Failed to allocate framebuffer"); pxl8_gfx_destroy(gfx); return NULL; } gfx->framebuffer_texture = SDL_CreateTexture( gfx->renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, gfx->framebuffer_width, gfx->framebuffer_height ); SDL_SetTextureScaleMode(gfx->framebuffer_texture, SDL_SCALEMODE_NEAREST); if (!gfx->framebuffer_texture) { pxl8_error("Failed to create framebuffer texture: %s", SDL_GetError()); pxl8_gfx_destroy(gfx); return NULL; } gfx->palette_size = pxl8_get_palette_size(mode); if (gfx->palette_size > 0) { gfx->palette = (u32*)SDL_calloc(gfx->palette_size, sizeof(u32)); if (!gfx->palette) { pxl8_error("Failed to allocate palette"); pxl8_gfx_destroy(gfx); return NULL; } for (u32 i = 0; i < 256 && i < gfx->palette_size; i++) { u8 gray = (u8)(i * 255 / 255); gfx->palette[i] = 0xFF000000 | (gray << 16) | (gray << 8) | gray; } } gfx->viewport.offset_x = 0; gfx->viewport.offset_y = 0; gfx->viewport.scaled_width = gfx->framebuffer_width; gfx->viewport.scaled_height = gfx->framebuffer_height; gfx->viewport.scale = 1.0f; gfx->backface_culling = true; gfx->model = pxl8_mat4_identity(); gfx->projection = pxl8_mat4_identity(); gfx->view = pxl8_mat4_identity(); gfx->wireframe = false; gfx->zbuffer = NULL; gfx->zbuffer_height = 0; gfx->zbuffer_width = 0; gfx->affine_textures = false; gfx->initialized = true; return gfx; } void pxl8_gfx_destroy(pxl8_gfx* gfx) { if (!gfx) return; pxl8_atlas_destroy(gfx->atlas); if (gfx->framebuffer_texture) { SDL_DestroyTexture(gfx->framebuffer_texture); gfx->framebuffer_texture = NULL; } if (gfx->renderer) { SDL_DestroyRenderer(gfx->renderer); gfx->renderer = NULL; } if (gfx->window) { SDL_DestroyWindow(gfx->window); gfx->window = NULL; } SDL_free(gfx->framebuffer); SDL_free(gfx->palette); SDL_free(gfx->zbuffer); SDL_free(gfx); } pxl8_result pxl8_gfx_create_texture(pxl8_gfx* gfx, const u8* pixels, u32 width, u32 height) { if (!gfx || !gfx->initialized || !pixels) return PXL8_ERROR_INVALID_ARGUMENT; if (!gfx->atlas) { gfx->atlas = pxl8_atlas_create(gfx->renderer, 1024, 1024, gfx->color_mode); if (!gfx->atlas) return PXL8_ERROR_OUT_OF_MEMORY; } u32 texture_id = pxl8_atlas_add_texture(gfx->atlas, pixels, width, height, NULL, gfx->color_mode); if (texture_id == UINT32_MAX) { pxl8_error("Texture doesn't fit in atlas"); return PXL8_ERROR_INVALID_SIZE; } pxl8_debug("Created texture %u: %ux%u at (%d,%d)", texture_id, width, height, gfx->atlas->entries[texture_id].x, gfx->atlas->entries[texture_id].y); return texture_id; } pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path) { if (!gfx || !gfx->initialized || !path) return PXL8_ERROR_INVALID_ARGUMENT; if (!gfx->atlas) { gfx->atlas = pxl8_atlas_create(gfx->renderer, 1024, 1024, gfx->color_mode); if (!gfx->atlas) return PXL8_ERROR_OUT_OF_MEMORY; } for (u32 i = 0; i < gfx->atlas->entry_count; i++) { if (gfx->atlas->entries[i].active && strcmp(gfx->atlas->entries[i].path, path) == 0) { return gfx->atlas->entries[i].texture_id; } } pxl8_ase_file ase_file; pxl8_result result = pxl8_ase_load(path, &ase_file); if (result != PXL8_OK) { pxl8_error("Failed to load ASE file: %s", path); return result; } if (ase_file.frame_count == 0) { pxl8_error("No frames in ASE file"); pxl8_ase_destroy(&ase_file); return PXL8_ERROR_INVALID_FORMAT; } u32 sprite_w = ase_file.header.width; u32 sprite_h = ase_file.header.height; u32 texture_id = pxl8_atlas_add_texture(gfx->atlas, ase_file.frames[0].pixels, sprite_w, sprite_h, path, gfx->color_mode); pxl8_ase_destroy(&ase_file); if (texture_id == UINT32_MAX) { pxl8_error("Sprite doesn't fit in atlas"); return PXL8_ERROR_INVALID_SIZE; } pxl8_debug("Loaded sprite %u: %ux%u at (%d,%d)", texture_id, sprite_w, sprite_h, gfx->atlas->entries[texture_id].x, gfx->atlas->entries[texture_id].y); return texture_id; } pxl8_result pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* path) { if (!gfx || !gfx->initialized || !path) return PXL8_ERROR_INVALID_ARGUMENT; if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) return PXL8_OK; pxl8_debug("Loading palette from: %s", path); pxl8_ase_file ase_file; pxl8_result result = pxl8_ase_load(path, &ase_file); if (result != PXL8_OK) { pxl8_error("Failed to load ASE file for palette: %s", path); return result; } if (ase_file.palette.entry_count == 0) { pxl8_error("No palette data in ASE file"); pxl8_ase_destroy(&ase_file); return PXL8_ERROR_INVALID_FORMAT; } u32 copy_size = (ase_file.palette.entry_count < gfx->palette_size) ? ase_file.palette.entry_count : gfx->palette_size; memcpy(gfx->palette, ase_file.palette.colors, copy_size * sizeof(u32)); u32 last_color = (copy_size > 0) ? gfx->palette[copy_size - 1] : 0xFF000000; for (u32 i = copy_size; i < gfx->palette_size; i++) { gfx->palette[i] = last_color; } pxl8_ase_destroy(&ase_file); pxl8_debug("Loaded palette with %u colors", copy_size); return PXL8_OK; } pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx) { (void)gfx; return PXL8_OK; } static void pxl8_upload_indexed_texture(SDL_Texture* texture, const u8* indexed, const u32* palette, u32 palette_size, i32 width, i32 height, u32 default_color) { static u32* rgba_buffer = NULL; static size_t buffer_size = 0; size_t needed_size = width * height; if (buffer_size < needed_size) { rgba_buffer = (u32*)SDL_realloc(rgba_buffer, needed_size * 4); buffer_size = needed_size; } if (!rgba_buffer) return; for (i32 i = 0; i < width * height; i++) { u8 index = indexed[i]; rgba_buffer[i] = (index < palette_size) ? palette[index] : default_color; } SDL_UpdateTexture(texture, NULL, rgba_buffer, width * 4); } void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx) { if (!gfx || !gfx->initialized || !gfx->framebuffer_texture) return; if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { SDL_UpdateTexture(gfx->framebuffer_texture, NULL, gfx->framebuffer, gfx->framebuffer_width * 4); } else { pxl8_upload_indexed_texture(gfx->framebuffer_texture, gfx->framebuffer, gfx->palette, gfx->palette_size, gfx->framebuffer_width, gfx->framebuffer_height, 0xFF000000); } } void pxl8_gfx_upload_atlas(pxl8_gfx* gfx) { if (!gfx || !gfx->initialized || !gfx->atlas || !gfx->atlas->texture || !gfx->atlas->dirty) return; if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { SDL_UpdateTexture(gfx->atlas->texture, NULL, gfx->atlas->pixels, gfx->atlas->width * 4); } else { pxl8_upload_indexed_texture(gfx->atlas->texture, gfx->atlas->pixels, gfx->palette, gfx->palette_size, gfx->atlas->width, gfx->atlas->height, 0x00000000); } gfx->atlas->dirty = false; pxl8_debug("Atlas uploaded to GPU"); } void pxl8_gfx_present(pxl8_gfx* gfx) { if (!gfx || !gfx->initialized) return; SDL_SetRenderDrawColor(gfx->renderer, 0, 0, 0, 255); SDL_RenderClear(gfx->renderer); if (gfx->framebuffer_texture) { SDL_RenderTexture(gfx->renderer, gfx->framebuffer_texture, NULL, NULL); } SDL_RenderPresent(gfx->renderer); } pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height) { pxl8_viewport vp = {0}; vp.scale = fminf(bounds.w / (f32)width, bounds.h / (f32)height); vp.scaled_width = (i32)(width * vp.scale); vp.scaled_height = (i32)(height * vp.scale); vp.offset_x = (bounds.w - vp.scaled_width) / 2; vp.offset_y = (bounds.h - vp.scaled_height) / 2; return vp; } void pxl8_gfx_set_viewport(pxl8_gfx* gfx, pxl8_viewport vp) { if (!gfx) return; gfx->viewport = vp; } void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom) { (void)gfx; (void)left; (void)right; (void)top; (void)bottom; } void pxl8_clr(pxl8_gfx* gfx, u32 color) { if (!gfx || !gfx->framebuffer) return; i32 bytes_per_pixel = (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1; i32 size = gfx->framebuffer_width * gfx->framebuffer_height; if (bytes_per_pixel == 4) { u32* fb32 = (u32*)gfx->framebuffer; for (i32 i = 0; i < size; i++) { fb32[i] = color; } } else { memset(gfx->framebuffer, color & 0xFF, size); } } void pxl8_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color) { if (!gfx || !gfx->framebuffer) return; if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return; i32 idx = y * gfx->framebuffer_width + x; if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { ((u32*)gfx->framebuffer)[idx] = color; } else { gfx->framebuffer[idx] = color & 0xFF; } } u32 pxl8_get_pixel(pxl8_gfx* gfx, i32 x, i32 y) { if (!gfx || !gfx->framebuffer) return 0; if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return 0; i32 idx = y * gfx->framebuffer_width + x; if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { return ((u32*)gfx->framebuffer)[idx]; } else { return gfx->framebuffer[idx]; } } void pxl8_line(pxl8_gfx* gfx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color) { if (!gfx) return; i32 dx = abs(x1 - x0); i32 dy = abs(y1 - y0); i32 sx = x0 < x1 ? 1 : -1; i32 sy = y0 < y1 ? 1 : -1; i32 err = dx - dy; while (1) { pxl8_pixel(gfx, x0, y0, color); if (x0 == x1 && y0 == y1) break; i32 e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } void pxl8_rect(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) { if (!gfx) return; pxl8_line(gfx, x, y, x + w - 1, y, color); pxl8_line(gfx, x + w - 1, y, x + w - 1, y + h - 1, color); pxl8_line(gfx, x + w - 1, y + h - 1, x, y + h - 1, color); pxl8_line(gfx, x, y + h - 1, x, y, color); } static inline void pxl8_pixel_unchecked(pxl8_gfx* gfx, i32 x, i32 y, u32 color) { i32 idx = y * gfx->framebuffer_width + x; if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { ((u32*)gfx->framebuffer)[idx] = color; } else { gfx->framebuffer[idx] = color & 0xFF; } } void pxl8_rect_fill(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) { if (!gfx || !gfx->framebuffer) return; i32 x0 = (x < 0) ? 0 : x; i32 y0 = (y < 0) ? 0 : y; i32 x1 = (x + w > gfx->framebuffer_width) ? gfx->framebuffer_width : x + w; i32 y1 = (y + h > gfx->framebuffer_height) ? gfx->framebuffer_height : y + h; for (i32 py = y0; py < y1; py++) { for (i32 px = x0; px < x1; px++) { pxl8_pixel_unchecked(gfx, px, py, color); } } } void pxl8_circle(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color) { if (!gfx) return; i32 x = radius; i32 y = 0; i32 err = 0; while (x >= y) { pxl8_pixel(gfx, cx + x, cy + y, color); pxl8_pixel(gfx, cx + y, cy + x, color); pxl8_pixel(gfx, cx - y, cy + x, color); pxl8_pixel(gfx, cx - x, cy + y, color); pxl8_pixel(gfx, cx - x, cy - y, color); pxl8_pixel(gfx, cx - y, cy - x, color); pxl8_pixel(gfx, cx + y, cy - x, color); pxl8_pixel(gfx, cx + x, cy - y, color); if (err <= 0) { y += 1; err += 2 * y + 1; } else { x -= 1; err -= 2 * x + 1; } } } void pxl8_circle_fill(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color) { if (!gfx || !gfx->framebuffer) return; i32 x0 = (cx - radius < 0) ? -cx : -radius; i32 y0 = (cy - radius < 0) ? -cy : -radius; i32 x1 = (cx + radius >= gfx->framebuffer_width) ? gfx->framebuffer_width - cx - 1 : radius; i32 y1 = (cy + radius >= gfx->framebuffer_height) ? gfx->framebuffer_height - cy - 1 : radius; for (i32 y = y0; y <= y1; y++) { for (i32 x = x0; x <= x1; x++) { if (x * x + y * y <= radius * radius) { pxl8_pixel_unchecked(gfx, cx + x, cy + y, color); } } } } void pxl8_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color) { if (!gfx || !text || !gfx->framebuffer) return; const pxl8_font* font = &pxl8_default_font; i32 cursor_x = x; i32 cursor_y = y; for (const char* c = text; *c; c++) { const pxl8_glyph* glyph = pxl8_font_find_glyph(font, (u32)*c); if (!glyph) continue; for (i32 gy = 0; gy < glyph->height; gy++) { for (i32 gx = 0; gx < glyph->width; gx++) { i32 px = cursor_x + gx; i32 py = cursor_y + gy; if (px < 0 || px >= gfx->framebuffer_width || py < 0 || py >= gfx->framebuffer_height) continue; u8 pixel_bit = 0; if (glyph->format == PXL8_FONT_FORMAT_INDEXED) { u8 pixel_byte = glyph->data.indexed[gy]; pixel_bit = (pixel_byte >> gx) & 1; } else { i32 glyph_idx = gy * 8 + gx; u32 rgba_pixel = glyph->data.rgba[glyph_idx]; pixel_bit = ((rgba_pixel >> 24) & 0xFF) > 128 ? 1 : 0; } if (pixel_bit) { i32 fb_idx = py * gfx->framebuffer_width + px; if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { ((u32*)gfx->framebuffer)[fb_idx] = color; } else { gfx->framebuffer[fb_idx] = (u8)color; } } } } cursor_x += font->default_width; } } void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) { if (!gfx || !gfx->atlas || !gfx->framebuffer || sprite_id >= gfx->atlas->entry_count) return; if (!gfx->atlas->entries[sprite_id].active) return; pxl8_atlas_entry* entry = &gfx->atlas->entries[sprite_id]; i32 clip_left = (x < 0) ? -x : 0; i32 clip_top = (y < 0) ? -y : 0; i32 clip_right = (x + w > gfx->framebuffer_width) ? x + w - gfx->framebuffer_width : 0; i32 clip_bottom = (y + h > gfx->framebuffer_height) ? y + h - gfx->framebuffer_height : 0; i32 draw_width = w - clip_left - clip_right; i32 draw_height = h - clip_top - clip_bottom; if (draw_width <= 0 || draw_height <= 0) return; i32 dest_x = x + clip_left; i32 dest_y = y + clip_top; bool is_1to1_scale = (w == entry->w && h == entry->h); bool is_unclipped = (clip_left == 0 && clip_top == 0 && clip_right == 0 && clip_bottom == 0); if (is_1to1_scale && is_unclipped && pxl8_is_simd_aligned(w)) { const u8* sprite_data = gfx->atlas->pixels + entry->y * gfx->atlas->width + entry->x; if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { pxl8_blit_simd_hicolor((u32*)gfx->framebuffer, gfx->framebuffer_width, (const u32*)sprite_data, gfx->atlas->width, x, y, w, h); } else { pxl8_blit_simd_indexed(gfx->framebuffer, gfx->framebuffer_width, sprite_data, gfx->atlas->width, x, y, w, h); } } else { for (i32 py = 0; py < draw_height; py++) { for (i32 px = 0; px < draw_width; px++) { i32 src_x = entry->x + ((px + clip_left) * entry->w) / w; i32 src_y = entry->y + ((py + clip_top) * entry->h) / h; i32 src_idx = src_y * gfx->atlas->width + src_x; i32 dest_idx = (dest_y + py) * gfx->framebuffer_width + (dest_x + px); if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { u32 pixel = ((u32*)gfx->atlas->pixels)[src_idx]; if (pixel & 0xFF000000) { ((u32*)gfx->framebuffer)[dest_idx] = pixel; } } else { u8 pixel = gfx->atlas->pixels[src_idx]; if (pixel != 0) { gfx->framebuffer[dest_idx] = pixel; } } } } } } void pxl8_gfx_cycle_palette(pxl8_gfx* gfx, u8 start, u8 count, i32 step) { if (!gfx || !gfx->palette || count == 0) return; u32 temp[256]; for (u8 i = 0; i < count; i++) { temp[i] = gfx->palette[start + i]; } for (u8 i = 0; i < count; i++) { u8 src_idx = i; u8 dst_idx = (i + step) % count; gfx->palette[start + dst_idx] = temp[src_idx]; } } void pxl8_gfx_swap_palette(pxl8_gfx* gfx, u8 start, u8 count, u32* new_colors) { if (!gfx || !gfx->palette || !new_colors) return; for (u8 i = 0; i < count && (start + i) < gfx->palette_size; i++) { gfx->palette[start + i] = new_colors[i]; } } void pxl8_gfx_fade_palette(pxl8_gfx* gfx, u8 start, u8 count, f32 amount, u32 target_color) { if (!gfx || !gfx->palette || count == 0) return; if (amount < 0.0f) amount = 0.0f; if (amount > 1.0f) amount = 1.0f; u8 target_r, target_g, target_b, target_a; pxl8_color_unpack(target_color, &target_r, &target_g, &target_b, &target_a); for (u8 i = 0; i < count && (start + i) < gfx->palette_size; i++) { u8 cur_r, cur_g, cur_b, cur_a; pxl8_color_unpack(gfx->palette[start + i], &cur_r, &cur_g, &cur_b, &cur_a); u8 new_r = pxl8_color_lerp_channel(cur_r, target_r, amount); u8 new_g = pxl8_color_lerp_channel(cur_g, target_g, amount); u8 new_b = pxl8_color_lerp_channel(cur_b, target_b, amount); u8 new_a = pxl8_color_lerp_channel(cur_a, target_a, amount); gfx->palette[start + i] = pxl8_color_pack(new_r, new_g, new_b, new_a); } } void pxl8_gfx_interpolate_palettes(pxl8_gfx* gfx, u32* palette1, u32* palette2, u8 start, u8 count, f32 t) { if (!gfx || !gfx->palette || !palette1 || !palette2 || count == 0) return; if (t < 0.0f) t = 0.0f; if (t > 1.0f) t = 1.0f; for (u8 i = 0; i < count && (start + i) < gfx->palette_size; i++) { u8 r1, g1, b1, a1, r2, g2, b2, a2; pxl8_color_unpack(palette1[i], &r1, &g1, &b1, &a1); pxl8_color_unpack(palette2[i], &r2, &g2, &b2, &a2); u8 r = pxl8_color_lerp_channel(r1, r2, t); u8 g = pxl8_color_lerp_channel(g1, g2, t); u8 b = pxl8_color_lerp_channel(b1, b2, t); u8 a = pxl8_color_lerp_channel(a1, a2, t); gfx->palette[start + i] = pxl8_color_pack(r, g, b, a); } } void pxl8_gfx_color_ramp(pxl8_gfx* gfx, u8 start, u8 count, u32 from_color, u32 to_color) { if (!gfx || !gfx->palette || count <= 1) return; u8 from_r, from_g, from_b, from_a; u8 to_r, to_g, to_b, to_a; pxl8_color_unpack(from_color, &from_r, &from_g, &from_b, &from_a); pxl8_color_unpack(to_color, &to_r, &to_g, &to_b, &to_a); for (u8 i = 0; i < count && (start + i) < gfx->palette_size; i++) { f32 t = (f32)i / (f32)(count - 1); u8 r = pxl8_color_lerp_channel(from_r, to_r, t); u8 g = pxl8_color_lerp_channel(from_g, to_g, t); u8 b = pxl8_color_lerp_channel(from_b, to_b, t); u8 a = pxl8_color_lerp_channel(from_a, to_a, t); gfx->palette[start + i] = pxl8_color_pack(r, g, b, a); } } void pxl8_gfx_process_effects(pxl8_gfx* gfx, pxl8_effects* effects, f32 dt) { if (!gfx || !effects) return; effects->time += dt; for (i32 i = 0; i < 8; i++) { pxl8_palette_cycle* cycle = &effects->palette_cycles[i]; if (!cycle->active) continue; cycle->timer += dt * cycle->speed; if (cycle->timer >= 1.0f) { cycle->timer -= 1.0f; i32 count = cycle->end_index - cycle->start_index + 1; pxl8_gfx_cycle_palette(gfx, cycle->start_index, count, 1); } } } static bool pxl8_3d_init_zbuffer(pxl8_gfx* gfx) { if (gfx->zbuffer) return true; gfx->zbuffer_width = gfx->framebuffer_width; gfx->zbuffer_height = gfx->framebuffer_height; gfx->zbuffer = (f32*)SDL_calloc(gfx->zbuffer_width * gfx->zbuffer_height, sizeof(f32)); if (!gfx->zbuffer) { return false; } pxl8_3d_clear_zbuffer(gfx); return true; } void pxl8_3d_clear_zbuffer(pxl8_gfx* gfx) { if (!gfx || !gfx->zbuffer) return; for (i32 i = 0; i < gfx->zbuffer_width * gfx->zbuffer_height; i++) { gfx->zbuffer[i] = 1e30f; } } void pxl8_3d_set_backface_culling(pxl8_gfx* gfx, bool culling) { if (!gfx) return; gfx->backface_culling = culling; } void pxl8_3d_set_model(pxl8_gfx* gfx, pxl8_mat4 mat) { if (!gfx) return; gfx->model = mat; } void pxl8_3d_set_projection(pxl8_gfx* gfx, pxl8_mat4 mat) { if (!gfx) return; gfx->projection = mat; } void pxl8_3d_set_view(pxl8_gfx* gfx, pxl8_mat4 mat) { if (!gfx) return; gfx->view = mat; } void pxl8_3d_set_wireframe(pxl8_gfx* gfx, bool wireframe) { if (!gfx) return; gfx->wireframe = wireframe; } void pxl8_3d_set_affine_textures(pxl8_gfx* gfx, bool affine) { if (!gfx) return; gfx->affine_textures = affine; } static inline pxl8_vec4 pxl8_transform_vertex(pxl8_gfx* gfx, pxl8_vec3 pos) { pxl8_vec4 v = { .x = pos.x, .y = pos.y, .z = pos.z, .w = 1.0f, }; pxl8_mat4 mvp = pxl8_mat4_multiply(gfx->projection, pxl8_mat4_multiply(gfx->view, gfx->model)); return pxl8_mat4_multiply_vec4(mvp, v); } static inline void pxl8_project_to_screen(pxl8_gfx* gfx, pxl8_vec4 clip, i32* x, i32* y, f32* z) { if (fabsf(clip.w) < 1e-6f) { *x = *y = 0; *z = 1e30f; return; } f32 inv_w = 1.0f / clip.w; f32 ndc_x = clip.x * inv_w; f32 ndc_y = clip.y * inv_w; *x = (i32)((ndc_x + 1.0f) * 0.5f * gfx->framebuffer_width); *y = (i32)((1.0f - ndc_y) * 0.5f * gfx->framebuffer_height); *z = clip.z * inv_w; } void pxl8_3d_draw_line_3d(pxl8_gfx* gfx, pxl8_vec3 p0, pxl8_vec3 p1, u32 color) { if (!gfx) return; pxl8_vec4 v0 = pxl8_transform_vertex(gfx, p0); pxl8_vec4 v1 = pxl8_transform_vertex(gfx, p1); i32 x0, y0, x1, y1; f32 z0, z1; pxl8_project_to_screen(gfx, v0, &x0, &y0, &z0); pxl8_project_to_screen(gfx, v1, &x1, &y1, &z1); pxl8_line(gfx, x0, y0, x1, y1, color); } static inline void pxl8_fill_scanline(pxl8_gfx* gfx, i32 y, i32 xs, i32 xe, f32 z0, f32 z1, u32 color) { if (y < 0 || y >= gfx->framebuffer_height) return; if (xs > xe) { i32 tmp = xs; xs = xe; xe = tmp; } for (i32 x = xs; x <= xe; x++) { if (x >= 0 && x < gfx->framebuffer_width) { f32 t = (xe == xs) ? 0.0f : (f32)(x - xs) / (f32)(xe - xs); f32 z = z0 + t * (z1 - z0); i32 idx = y * gfx->zbuffer_width + x; if (z <= gfx->zbuffer[idx]) { gfx->zbuffer[idx] = z; pxl8_pixel(gfx, x, y, color); } } } } static inline u32 pxl8_sample_texture(pxl8_gfx* gfx, u32 texture_id, f32 u, f32 v) { if (!gfx->atlas || texture_id >= gfx->atlas->entry_count) return 0; if (!gfx->atlas->entries[texture_id].active) return 0; pxl8_atlas_entry* entry = &gfx->atlas->entries[texture_id]; u = u - floorf(u); v = v - floorf(v); i32 tx = (i32)(u * entry->w) % entry->w; i32 ty = (i32)(v * entry->h) % entry->h; i32 atlas_x = entry->x + tx; i32 atlas_y = entry->y + ty; i32 idx = atlas_y * gfx->atlas->width + atlas_x; if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { return ((u32*)gfx->atlas->pixels)[idx]; } else { return gfx->atlas->pixels[idx]; } } static inline void pxl8_fill_scanline_textured( pxl8_gfx* gfx, i32 y, i32 xs, i32 xe, f32 z0, f32 z1, f32 u0, f32 v0, f32 w0, f32 u1, f32 v1, f32 w1, u32 texture_id ) { if (y < 0 || y >= gfx->framebuffer_height) return; if (xs > xe) { i32 tmp = xs; xs = xe; xe = tmp; f32 tmpf; tmpf = z0; z0 = z1; z1 = tmpf; tmpf = u0; u0 = u1; u1 = tmpf; tmpf = v0; v0 = v1; v1 = tmpf; tmpf = w0; w0 = w1; w1 = tmpf; } for (i32 x = xs; x <= xe; x++) { if (x >= 0 && x < gfx->framebuffer_width) { f32 t = (xe == xs) ? 0.0f : (f32)(x - xs) / (f32)(xe - xs); f32 z = z0 + t * (z1 - z0); i32 idx = y * gfx->zbuffer_width + x; if (z <= gfx->zbuffer[idx]) { f32 u = u0 + t * (u1 - u0); f32 v = v0 + t * (v1 - v0); if (!gfx->affine_textures) { f32 w = w0 + t * (w1 - w0); if (fabsf(w) > 1e-6f) { u /= w; v /= w; } } u32 color = pxl8_sample_texture(gfx, texture_id, u, v); if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { if (color & 0xFF000000) { gfx->zbuffer[idx] = z; pxl8_pixel(gfx, x, y, color); } } else { if (color != 0) { gfx->zbuffer[idx] = z; pxl8_pixel(gfx, x, y, color); } } } } } } static void pxl8_draw_flat_bottom_triangle( pxl8_gfx* gfx, i32 x0, i32 y0, f32 z0, i32 x1, i32 y1, f32 z1, i32 x2, i32 y2, f32 z2, u32 color ) { (void)z2; if (y1 == y0) return; f32 inv_slope_1 = (f32)(x1 - x0) / (f32)(y1 - y0); f32 inv_slope_2 = (f32)(x2 - x0) / (f32)(y2 - y0); f32 cur_x1 = (f32)x0; f32 cur_x2 = (f32)x0; for (i32 y = y0; y <= y1; y++) { pxl8_fill_scanline(gfx, y, (i32)cur_x1, (i32)cur_x2, z0, z1, color); cur_x1 += inv_slope_1; cur_x2 += inv_slope_2; } } static void pxl8_draw_flat_top_triangle( pxl8_gfx* gfx, i32 x0, i32 y0, f32 z0, i32 x1, i32 y1, f32 z1, i32 x2, i32 y2, f32 z2, u32 color ) { (void)z2; if (y2 == y0) return; f32 inv_slope_1 = (f32)(x2 - x0) / (f32)(y2 - y0); f32 inv_slope_2 = (f32)(x2 - x1) / (f32)(y2 - y1); f32 cur_x1 = (f32)x2; f32 cur_x2 = (f32)x2; for (i32 y = y2; y > y0; y--) { pxl8_fill_scanline(gfx, y, (i32)cur_x1, (i32)cur_x2, z0, z1, color); cur_x1 -= inv_slope_1; cur_x2 -= inv_slope_2; } } void pxl8_3d_draw_triangle_raw(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, pxl8_vec3 v2, u32 color) { pxl8_triangle tri; tri.v[0].position = v0; tri.v[0].color = color; tri.v[1].position = v1; tri.v[1].color = color; tri.v[2].position = v2; tri.v[2].color = color; pxl8_3d_draw_triangle(gfx, tri); } void pxl8_3d_draw_triangle(pxl8_gfx* gfx, pxl8_triangle tri) { if (!gfx || !pxl8_3d_init_zbuffer(gfx)) return; pxl8_vec4 v0 = pxl8_transform_vertex(gfx, tri.v[0].position); pxl8_vec4 v1 = pxl8_transform_vertex(gfx, tri.v[1].position); pxl8_vec4 v2 = pxl8_transform_vertex(gfx, tri.v[2].position); if (v0.w <= 0 || v1.w <= 0 || v2.w <= 0) return; i32 x0, y0, x1, y1, x2, y2; f32 z0, z1, z2; pxl8_project_to_screen(gfx, v0, &x0, &y0, &z0); pxl8_project_to_screen(gfx, v1, &x1, &y1, &z1); pxl8_project_to_screen(gfx, v2, &x2, &y2, &z2); if (gfx->backface_culling) { i32 cross = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0); if (cross >= 0) return; } if (gfx->wireframe) { u32 color = tri.v[0].color; pxl8_line(gfx, x0, y0, x1, y1, color); pxl8_line(gfx, x1, y1, x2, y2, color); pxl8_line(gfx, x2, y2, x0, y0, color); return; } if (y0 > y1) { i32 tmp_i = x0; x0 = x1; x1 = tmp_i; tmp_i = y0; y0 = y1; y1 = tmp_i; f32 tmp_f = z0; z0 = z1; z1 = tmp_f; } if (y0 > y2) { i32 tmp_i = x0; x0 = x2; x2 = tmp_i; tmp_i = y0; y0 = y2; y2 = tmp_i; f32 tmp_f = z0; z0 = z2; z2 = tmp_f; } if (y1 > y2) { i32 tmp_i = x1; x1 = x2; x2 = tmp_i; tmp_i = y1; y1 = y2; y2 = tmp_i; f32 tmp_f = z1; z1 = z2; z2 = tmp_f; } u32 color = tri.v[0].color; if (y1 == y2) { pxl8_draw_flat_bottom_triangle(gfx, x0, y0, z0, x1, y1, z1, x2, y2, z2, color); } else if (y0 == y1) { pxl8_draw_flat_top_triangle(gfx, x0, y0, z0, x1, y1, z1, x2, y2, z2, color); } else { i32 x3 = x0 + (i32)(((f32)(y1 - y0) / (f32)(y2 - y0)) * (x2 - x0)); i32 y3 = y1; f32 z3 = z0 + ((f32)(y1 - y0) / (f32)(y2 - y0)) * (z2 - z0); pxl8_draw_flat_bottom_triangle(gfx, x0, y0, z0, x1, y1, z1, x3, y3, z3, color); pxl8_draw_flat_top_triangle(gfx, x1, y1, z1, x3, y3, z3, x2, y2, z2, color); } } typedef struct pxl8_textured_vertex { i32 x, y; f32 z, u, v, w; } pxl8_textured_vertex; static void pxl8_draw_flat_bottom_triangle_textured( pxl8_gfx* gfx, pxl8_textured_vertex v0, pxl8_textured_vertex v1, pxl8_textured_vertex v2, u32 texture_id ) { if (v1.y == v0.y) return; f32 inv_slope_1 = (f32)(v1.x - v0.x) / (f32)(v1.y - v0.y); f32 inv_slope_2 = (f32)(v2.x - v0.x) / (f32)(v2.y - v0.y); f32 inv_slope_z1 = (v1.z - v0.z) / (f32)(v1.y - v0.y); f32 inv_slope_z2 = (v2.z - v0.z) / (f32)(v2.y - v0.y); f32 inv_slope_u1 = (v1.u - v0.u) / (f32)(v1.y - v0.y); f32 inv_slope_u2 = (v2.u - v0.u) / (f32)(v2.y - v0.y); f32 inv_slope_v1 = (v1.v - v0.v) / (f32)(v1.y - v0.y); f32 inv_slope_v2 = (v2.v - v0.v) / (f32)(v2.y - v0.y); f32 inv_slope_w1 = (v1.w - v0.w) / (f32)(v1.y - v0.y); f32 inv_slope_w2 = (v2.w - v0.w) / (f32)(v2.y - v0.y); f32 cur_x1 = (f32)v0.x, cur_x2 = (f32)v0.x; f32 cur_z1 = v0.z, cur_z2 = v0.z; f32 cur_u1 = v0.u, cur_u2 = v0.u; f32 cur_v1 = v0.v, cur_v2 = v0.v; f32 cur_w1 = v0.w, cur_w2 = v0.w; for (i32 y = v0.y; y <= v1.y; y++) { pxl8_fill_scanline_textured(gfx, y, (i32)cur_x1, (i32)cur_x2, cur_z1, cur_z2, cur_u1, cur_v1, cur_w1, cur_u2, cur_v2, cur_w2, texture_id); cur_x1 += inv_slope_1; cur_x2 += inv_slope_2; cur_z1 += inv_slope_z1; cur_z2 += inv_slope_z2; cur_u1 += inv_slope_u1; cur_u2 += inv_slope_u2; cur_v1 += inv_slope_v1; cur_v2 += inv_slope_v2; cur_w1 += inv_slope_w1; cur_w2 += inv_slope_w2; } } static void pxl8_draw_flat_top_triangle_textured( pxl8_gfx* gfx, pxl8_textured_vertex v0, pxl8_textured_vertex v1, pxl8_textured_vertex v2, u32 texture_id ) { if (v2.y == v0.y) return; f32 inv_slope_1 = (f32)(v2.x - v0.x) / (f32)(v2.y - v0.y); f32 inv_slope_2 = (f32)(v2.x - v1.x) / (f32)(v2.y - v1.y); f32 inv_slope_z1 = (v2.z - v0.z) / (f32)(v2.y - v0.y); f32 inv_slope_z2 = (v2.z - v1.z) / (f32)(v2.y - v1.y); f32 inv_slope_u1 = (v2.u - v0.u) / (f32)(v2.y - v0.y); f32 inv_slope_u2 = (v2.u - v1.u) / (f32)(v2.y - v1.y); f32 inv_slope_v1 = (v2.v - v0.v) / (f32)(v2.y - v0.y); f32 inv_slope_v2 = (v2.v - v1.v) / (f32)(v2.y - v1.y); f32 inv_slope_w1 = (v2.w - v0.w) / (f32)(v2.y - v0.y); f32 inv_slope_w2 = (v2.w - v1.w) / (f32)(v2.y - v1.y); f32 cur_x1 = (f32)v2.x, cur_x2 = (f32)v2.x; f32 cur_z1 = v2.z, cur_z2 = v2.z; f32 cur_u1 = v2.u, cur_u2 = v2.u; f32 cur_v1 = v2.v, cur_v2 = v2.v; f32 cur_w1 = v2.w, cur_w2 = v2.w; for (i32 y = v2.y; y > v0.y; y--) { pxl8_fill_scanline_textured(gfx, y, (i32)cur_x1, (i32)cur_x2, cur_z1, cur_z2, cur_u1, cur_v1, cur_w1, cur_u2, cur_v2, cur_w2, texture_id); cur_x1 -= inv_slope_1; cur_x2 -= inv_slope_2; cur_z1 -= inv_slope_z1; cur_z2 -= inv_slope_z2; cur_u1 -= inv_slope_u1; cur_u2 -= inv_slope_u2; cur_v1 -= inv_slope_v1; cur_v2 -= inv_slope_v2; cur_w1 -= inv_slope_w1; cur_w2 -= inv_slope_w2; } } void pxl8_3d_draw_triangle_textured(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, pxl8_vec3 v2, f32 u0, f32 v0f, f32 u1, f32 v1f, f32 u2, f32 v2f, u32 texture_id) { if (!gfx || !pxl8_3d_init_zbuffer(gfx)) return; pxl8_vec4 cv0 = pxl8_transform_vertex(gfx, v0); pxl8_vec4 cv1 = pxl8_transform_vertex(gfx, v1); pxl8_vec4 cv2 = pxl8_transform_vertex(gfx, v2); if (cv0.w <= 0 || cv1.w <= 0 || cv2.w <= 0) return; pxl8_textured_vertex tv[3]; f32 z_tmp; pxl8_project_to_screen(gfx, cv0, &tv[0].x, &tv[0].y, &z_tmp); tv[0].z = z_tmp; tv[0].u = gfx->affine_textures ? u0 : u0 * cv0.w; tv[0].v = gfx->affine_textures ? v0f : v0f * cv0.w; tv[0].w = cv0.w; pxl8_project_to_screen(gfx, cv1, &tv[1].x, &tv[1].y, &z_tmp); tv[1].z = z_tmp; tv[1].u = gfx->affine_textures ? u1 : u1 * cv1.w; tv[1].v = gfx->affine_textures ? v1f : v1f * cv1.w; tv[1].w = cv1.w; pxl8_project_to_screen(gfx, cv2, &tv[2].x, &tv[2].y, &z_tmp); tv[2].z = z_tmp; tv[2].u = gfx->affine_textures ? u2 : u2 * cv2.w; tv[2].v = gfx->affine_textures ? v2f : v2f * cv2.w; tv[2].w = cv2.w; if (gfx->backface_culling) { i32 cross = (tv[1].x - tv[0].x) * (tv[2].y - tv[0].y) - (tv[1].y - tv[0].y) * (tv[2].x - tv[0].x); if (cross >= 0) return; } if (tv[0].y > tv[1].y) { pxl8_textured_vertex tmp = tv[0]; tv[0] = tv[1]; tv[1] = tmp; } if (tv[0].y > tv[2].y) { pxl8_textured_vertex tmp = tv[0]; tv[0] = tv[2]; tv[2] = tmp; } if (tv[1].y > tv[2].y) { pxl8_textured_vertex tmp = tv[1]; tv[1] = tv[2]; tv[2] = tmp; } if (tv[1].y == tv[2].y) { pxl8_draw_flat_bottom_triangle_textured(gfx, tv[0], tv[1], tv[2], texture_id); } else if (tv[0].y == tv[1].y) { pxl8_draw_flat_top_triangle_textured(gfx, tv[0], tv[1], tv[2], texture_id); } else { f32 t = (f32)(tv[1].y - tv[0].y) / (f32)(tv[2].y - tv[0].y); pxl8_textured_vertex v3; v3.x = tv[0].x + (i32)(t * (tv[2].x - tv[0].x)); v3.y = tv[1].y; v3.z = tv[0].z + t * (tv[2].z - tv[0].z); v3.u = tv[0].u + t * (tv[2].u - tv[0].u); v3.v = tv[0].v + t * (tv[2].v - tv[0].v); v3.w = tv[0].w + t * (tv[2].w - tv[0].w); pxl8_draw_flat_bottom_triangle_textured(gfx, tv[0], tv[1], v3, texture_id); pxl8_draw_flat_top_triangle_textured(gfx, tv[1], v3, tv[2], texture_id); } }