chaos-drop/src/raytracing.cc

209 lines
7.9 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 */
RGB(31, 31, 31), /* ELEMENT_PLANE_DEATH */
};
/* 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;
}
}