#include "chaos-drop.h" #include #include /* 5-bit channels to RGB565 */ #define RGB(R, G, B) ((R << 11) | (G << 6) | (B)) /* Color palette for world objects */ static uint16_t const object_colors[] = { RGB(3, 4, 5), /* Void/background */ RGB(3, 4, 5), /* Left wall (-X) */ RGB(5, 6, 7), /* Top wall (+Z) */ RGB(7, 8, 9), /* Right wall (+X) */ RGB(5, 6, 7), /* Bottom wall (-Z) */ RGB(31, 0, 0), /* ELEMENT_PLANE_DAMAGE */ RGB(0, 31, 0), /* ELEMENT_PLANE_ARROW */ RGB(15, 0, 0), /* ELEMENT_PLANE_INVIS */ }; /* Objects hit by a ray */ enum { VOID = 0, /* Background (no hit) */ LEFT, TOP, RIGHT, BOTTOM, /* Wall sides */ OBJECT_PLANE, /* Object plane (from level) */ }; /* "Short" vec3: 16-bit-based vectors for faster raytracing */ using svec3 = vec; svec3 svec3_of_vec3(vec3 v) { return svec3(num16(v.x), num16(v.y), num16(v.z)); } /* Cast a ray from `origin` with direction `rayDir`. Stores the distance to the collision point in `*t` and the full-precision y-coordinate of that point in `*collision_y`. Returns the identifier of the object type hit by the ray. If `secondary` is true, invisible planes are not ignored. */ static int cast_ray(struct game const *g, svec3 const &origin, svec3 const &rayDir, num16 *t, num *collision_y, bool secondary) { /* We directly compute intersections between rays and walls by exploiting the very regular structure of the walls. To find out whether the x or z wall is hit first, we compute tx and tz (travel distances of the ray before hitting the x/z walls) and check which is smaller. The thing is, tx is Δx / rayDir.x and tz is Δz / rayDir.z. We don't want to compute both divisions because it's expensive. So instead of tx < tz we check tx * rayDir.x * rayDir.z < tz * rayDir.x * rayDir.z and only divide once, when we know which is smaller. */ num16 tx_rx_rz, tz_rx_rz; /* Object hit for each wall direction */ int hitx = 0, hitz = 0; /* Computing with num16 comes with range issues. Rays that go very straight hit the x and z walls at more than 128 units away, causing an overflow on the computation of the y coordinate. We hack around this by registering background hits if the ray's x/z is smaller than epsilon. */ static num16 const epsilon = 0.1; /* Determine which wall is hit first. */ if(rayDir.x > epsilon) { tx_rx_rz = (num16(WORLD_SIZE) - origin.x) * rayDir.z; hitx = RIGHT; } else if(__builtin_expect(rayDir.x < -epsilon, 1)) { tx_rx_rz = (num16(-WORLD_SIZE) - origin.x) * rayDir.z; hitx = LEFT; } if(rayDir.z > epsilon) { tz_rx_rz = (num16(WORLD_SIZE) - origin.z) * rayDir.x; hitz = TOP; } else if(__builtin_expect(rayDir.z < -epsilon, 1)) { tz_rx_rz = (num16(-WORLD_SIZE) - origin.z) * rayDir.x; hitz = BOTTOM; } int rx_rz_sign = (rayDir.x < 0) ^ (rayDir.z < 0); int hit = 0; if(hitz && (!hitx || (tz_rx_rz < tx_rx_rz) ^ rx_rz_sign)) { if(hitz == TOP) *t = (num16(WORLD_SIZE) - origin.z) / rayDir.z; else *t = (origin.z + num16(WORLD_SIZE)) / -rayDir.z; hit = hitz; } else if(__builtin_expect(hitx, 1)) { if(hitx == RIGHT) *t = (num16(WORLD_SIZE) - origin.x) / rayDir.x; else *t = (origin.x + num16(WORLD_SIZE)) / -rayDir.x; hit = hitx; } /* Compute collision point's y-coordinate in full precision. This only half works because *t can be incorrect due to an overflow. Computing a more precise version of *t would be too expensive though, so we settle for this. */ *collision_y = hit ? num(origin.y) + num(*t) * num(rayDir.y) : num(192); /* Determine if there is an intersection with the object plane */ num pc = g->plane_collision[secondary]; if(__builtin_expect(*collision_y > pc, 0)) { num16 plane_y = num16(pc); num16 plane_t = (plane_y - origin.y) / rayDir.y; num16 plane_x = origin.x + plane_t * rayDir.x; num16 plane_z = origin.z + plane_t * rayDir.z; if(level_plane_collides(plane_x, plane_z, g->plane->shape)) { *t = plane_t; *collision_y = pc; return OBJECT_PLANE; } } return hit; } void render_fragment(struct game const *g, uint16_t *fragment, int y_start, int y_height) { struct camera const *camera = g->camera; svec3 origin = svec3_of_vec3(camera->pos); svec3 forward = svec3_of_vec3(camera->forward); svec3 right = svec3_of_vec3(camera->right); svec3 up = svec3_of_vec3(camera->up); /* Screen center (in front of the camera) */ svec3 screen_center = origin + num16(camera->screen_distance) * forward; /* Unitary movements, in world coordinates, on screen placed in world, corresponding to individual pixels */ svec3 pixel_dy = num16(HALF_SCREEN_HEIGHT) * up / num16(VHEIGHT / 2); svec3 pixel_dx = num16(HALF_SCREEN_HEIGHT) * right / num16(VHEIGHT / 2); /* Ray direction at the start of current row */ svec3 rayDir_row = screen_center - origin + num16(VHEIGHT/2 - y_start) * pixel_dy + num16(0 - VWIDTH/2) * pixel_dx; for(int y = y_start; y < y_start + y_height; y++) { svec3 rayDir = rayDir_row; for(int x = 0; x < VWIDTH; x++) { num16 t; num coll_y; int obj = cast_ray(g, origin, rayDir, &t, &coll_y, false); bool darken = false; if(__builtin_expect(g->mirror && (obj==LEFT || obj==RIGHT), 0)) { svec3 collision; collision.z = origin.z + t * rayDir.z; /* Mirror covers only part of width of wall */ bool ok_z = collision.z >= num16(-WORLD_SIZE / 2) && collision.z < num16(WORLD_SIZE / 2); /* Mirror also does not go on forever */ bool ok_y = g->mirror && coll_y >= (g->mirror->begin - g->depth) && coll_y < (g->mirror->end - g->depth); if(ok_z && ok_y) { collision.x = origin.x + t * rayDir.x; collision.y = num16(coll_y); rayDir.x = -rayDir.x; obj = cast_ray(g, collision, rayDir, &t, &coll_y,true); darken = true; rayDir.x = -rayDir.x; } } uint16_t color = object_colors[ obj + (obj == OBJECT_PLANE ? g->plane->type : 0)]; /* Don't show neons that are too far to avoid flickering */ if(coll_y < 64 && obj != OBJECT_PLANE) { num16 neon_pos = g->neon_position; if(obj == TOP || obj == BOTTOM) neon_pos += g->neon_period * num16(0.5); num16 neon = num16(coll_y) - neon_pos; neon.v = neon.v & (g->neon_period.v - 1); /* Also make neons larger when they're far to further avoid flickering */ num16 neon_size = 1; if(coll_y > 20) neon_size += ((num16(coll_y)-num16(20)) * num16(1.0/4)); if(neon <= neon_size) color = g->text ? RGB(15,15,15) : 0xffff; } else if(__builtin_expect(coll_y >= 64, 0)) { if(coll_y > 128) color = (color & 0xc718) >> 3; else if(coll_y > 96) color = (color & 0xe79c) >> 2; else color = (color & 0xf7de) >> 1; } if(darken) color = (color & 0xf7de) >> 1; *fragment++ = color; rayDir += pixel_dx; } rayDir_row -= pixel_dy; } }