/* Geometry rendering. */ #ifndef COM_GEM_H #define COM_GEM_H #include "fix.h" #include "mat.h" #include "pnt.h" #include "vec.h" #include "zord.h" #include #include /* Projects vertex position to a clip space via reordered row-major MVP matrix. It skips calculation of z depth component, as we assume to never use it. Instead, 3rd returned component is w value, for future projection. We must never render from inside geometry, as it will break no Z clipping assumption (see com_gem_clip_vis_project()). */ static inline com_vec_t com_gem_vec_project_clip(com_mat_t a, com_vec_t b) { com_vec_t result; #define CASE(m_c, m_n) \ result.a[m_c] = (((int64_t)a.a[m_n * 4 + 0] * b.a[0]) + \ ((int64_t)a.a[m_n * 4 + 1] * b.a[1]) + \ ((int64_t)a.a[m_n * 4 + 2] * b.a[2]) + a.a[m_n * 4 + 3]) >> \ COM_FIX_FRACBITS; CASE(0, 0); CASE(1, 1); CASE(2, 3); #undef CASE return result; } /* TODO: Move to separate render-specific file. */ /* TODO: If we clip test bounding volume of a model first we can skip * all clipping whatsoever, which might hold true more often, than the cost of * testing the volume. */ /* TODO: It should be possible to have results in pixel position, not -1 to 1. */ #define COM_GEM_VERTEX_UNCLIPPED (0 << 0) #define COM_GEM_VERTEX_CLIPPED_X (1 << 0) #define COM_GEM_VERTEX_CLIPPED_Y (1 << 1) /* Attempt to project clip space vertex to render pixel space, reporting which * component lies outside of view. Bit test against those per component. This is * needed for determining new view-lying clipped triangles. */ static inline uint8_t com_gem_clip_vis_project(com_vec_t a, com_pnt_t *out) { uint8_t mask = 0; if (a.a[0] < -a.a[2] || a.a[0] > a.a[2]) mask ^= COM_GEM_VERTEX_CLIPPED_X; if (a.a[1] < -a.a[2] || a.a[1] > a.a[2]) mask ^= COM_GEM_VERTEX_CLIPPED_Y; // out->a[0] = com_fix_mul((com_fix_div(a.a[0], a.a[2]) + COM_FIX_FRACUNIT) >> // 1, // 640 * COM_FIX_FRACUNIT); // out->a[1] = com_fix_mul((com_fix_div(a.a[1], a.a[2]) + COM_FIX_FRACUNIT) >> // 1, // 480 * COM_FIX_FRACUNIT); out->a[0] = (com_fix_mul(com_fix_div(a.a[0], a.a[2]), 640 * COM_FIX_FRACUNIT) >> COM_FIX_FRACBITS) + 320; out->a[1] = (com_fix_mul(com_fix_div(a.a[1], a.a[2]), 480 * COM_FIX_FRACUNIT) >> COM_FIX_FRACBITS) + 240; return mask; } static inline void com_gem_draw_triangle(com_pnt_t v0, com_pnt_t v1, com_pnt_t v2, uint8_t *out) { int minX = com_vec_min(com_vec_from(v0.a[0], v1.a[0], v2.a[0])); int minY = com_vec_min(com_vec_from(v0.a[1], v1.a[1], v2.a[1])); int maxX = com_vec_max(com_vec_from(v0.a[0], v1.a[0], v2.a[0])); int maxY = com_vec_max(com_vec_from(v0.a[1], v1.a[1], v2.a[1])); for (int y = minY; y <= maxY; y++) { for (int x = minX; x <= maxX; x++) { com_pnt_t p = {.a = {x, y}}; // Test the pixel center against all 3 edges com_vec_t ws = com_pnt_edge_weight(v0, v1, v2, p); // If the point is inside or on all edges, draw the pixel // (Assumes Counter Clockwise vertex ordering) if (ws.a[0] >= 0 && ws.a[1] >= 0 && ws.a[2] >= 0) { out[(y * 640 + x) * 4 + 0] = 125; out[(y * 640 + x) * 4 + 1] = 125; out[(y * 640 + x) * 4 + 2] = 125; } } } } /* TODO: Implement and compare: https://www.digipen.edu/sites/default/files/public/docs/theses/salem-haykal-digipen-master-of-science-in-computer-science-thesis-an-optimized-triangle-rasterizer.pdf Tile-based variant fares even worse than this implementation. */ static inline void com_gem_draw_triangle_textured_edge(com_pnt_t v0, com_pnt_t v1, com_pnt_t v2, uint8_t *out, com_pnt_t uv0, com_pnt_t uv1, com_pnt_t uv2, uint8_t *tex) { int minX = com_vec_min(com_vec_from(v0.a[0], v1.a[0], v2.a[0])); int minY = com_vec_min(com_vec_from(v0.a[1], v1.a[1], v2.a[1])); int maxX = com_vec_max(com_vec_from(v0.a[0], v1.a[0], v2.a[0])); int maxY = com_vec_max(com_vec_from(v0.a[1], v1.a[1], v2.a[1])); com_fix_t const area = com_pnt_area(v0, v1, v2); for (int y = minY; y <= maxY; y++) { for (int x = minX; x <= maxX; x++) { com_pnt_t p = {.a = {x, y}}; // Test the pixel center against all 3 edges com_vec_t ws = com_pnt_edge_weight(v0, v1, v2, p); // If the point is inside or on all edges, draw the pixel // (Assumes Clockwise vertex ordering) if (ws.a[0] >= 0 && ws.a[1] >= 0 && ws.a[2] >= 0) { // Barycentric weights ws = com_vec_div(ws, com_vec_scalar(area)); // Affine interpolation of UVs. com_fix_t const u = (ws.a[0] * uv0.a[0] + ws.a[1] * uv1.a[0] + ws.a[2] * uv2.a[0]) >> COM_FIX_FRACBITS; com_fix_t const v = (ws.a[0] * uv0.a[1] + ws.a[1] * uv1.a[1] + ws.a[2] * uv2.a[1]) >> COM_FIX_FRACBITS; // Nearest-neighbor texture lookup, in [0..32) range. int32_t tx = (int32_t)((u + (COM_FIX_FRACUNIT / 2)) / (COM_FIX_FRACUNIT / 16)); int32_t ty = (int32_t)((v + (COM_FIX_FRACUNIT / 2)) / (COM_FIX_FRACUNIT / 16)); // uint32_t idx = com_zord_order(tx, ty); uint32_t idx = ty * 32 + tx; // uint32_t bidx = com_zord_order(x, y); uint32_t bidx = y * 640 + x; out[bidx * 4 + 0] = tex[idx * 3 + 0]; out[bidx * 4 + 1] = tex[idx * 3 + 1]; out[bidx * 4 + 2] = tex[idx * 3 + 2]; } } } } static inline void draw_span(int32_t x_start_fp, int32_t x_end_fp, int y, uint8_t *out, uint8_t *tex) { int x1 = com_fix_int( x_start_fp + (1 << (COM_FIX_FRACBITS - 1))); // Rounding to nearest pixel int x2 = com_fix_int(x_end_fp + (1 << (COM_FIX_FRACBITS - 1))); if (x1 > x2) { int tmp = x1; x1 = x2; x2 = tmp; } for (int x = x1; x < x2; x++) { uint32_t idx = 0 * 32 + 0; uint32_t bidx = y * 640 + x; out[bidx * 4 + 0] = tex[idx * 3 + 0]; out[bidx * 4 + 1] = tex[idx * 3 + 1]; out[bidx * 4 + 2] = tex[idx * 3 + 2]; } } static inline void com_gem_draw_triangle_textured_span(com_pnt_t v0, com_pnt_t v1, com_pnt_t v2, uint8_t *out, com_pnt_t uv0, com_pnt_t uv1, com_pnt_t uv2, uint8_t *tex) { // 1. Sort vertices by Y coordinate (v0.a[1] <= v1.a[1] <= v2.a[1]) if (v0.a[1] > v1.a[1]) { com_pnt_t tmp = v0; v0 = v1; v1 = tmp; } if (v0.a[1] > v2.a[1]) { com_pnt_t tmp = v0; v0 = v2; v2 = tmp; } if (v1.a[1] > v2.a[1]) { com_pnt_t tmp = v1; v1 = v2; v2 = tmp; } // Guard against zero-height triangles if ((int)v0.a[1] == (int)v2.a[1]) return; // 2. Compute overall inverse slopes (dX/dY) for all edges // Fixed-point division is performed using 64-bit integer casting to prevent // overflow com_fix_t dx02 = (v2.a[1] != v0.a[1]) ? (com_fix_t)((int64_t)(v2.a[0] - v0.a[0]) << COM_FIX_FRACBITS / (v2.a[1] - v0.a[1])) : 0; com_fix_t dx01 = (v1.a[1] != v0.a[1]) ? (com_fix_t)((int64_t)(v1.a[0] - v0.a[0]) << COM_FIX_FRACBITS / (v1.a[1] - v0.a[1])) : 0; com_fix_t dx12 = (v2.a[1] != v1.a[1]) ? (com_fix_t)((int64_t)(v2.a[0] - v1.a[0]) << COM_FIX_FRACBITS / (v2.a[1] - v1.a[1])) : 0; // Determine the scanline limits (pixel discrete boundaries) int y_start = com_fix_int(v0.a[1]); int y_mid = com_fix_int(v1.a[1]); int y_end = com_fix_int(v2.a[1]); // Initialize edge walkers starting at the top vertex (v0) com_fix_t edge1 = v0.a[0]; // Follows the long edge (v0 -> v2) com_fix_t edge2 = v0.a[0]; // Follows short edges (v0 -> v1, then v1 -> v2) // 3. Top Half Scan: Walk from top vertex to middle vertex breakpoint for (int y = y_start; y < y_mid; y++) { draw_span(edge1, edge2, y, out, tex); edge1 += dx02; edge2 += dx01; } // Adjust the short edge walker directly to the middle vertex position edge2 = v1.a[0]; // 4. Bottom Half Scan: Walk from middle vertex to bottom vertex for (int y = y_mid; y < y_end; y++) { draw_span(edge1, edge2, y, out, tex); edge1 += dx02; edge2 += dx12; } } #endif