208 lines
7.8 KiB
C++
208 lines
7.8 KiB
C++
#include "chaos-drop.h"
|
|
#include <stdio.h>
|
|
#include <math.h>
|
|
|
|
/* 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<num16,3>;
|
|
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;
|
|
}
|
|
}
|