raytrace2.c
C source for Real-time ray tracing on a round ESP32 display, 2061 lines. Raw file
#include "raytrace2.h"
#include <math.h>
#include <string.h>
#include "esp_attr.h"
#include "esp_heap_caps.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define PI 3.14159265358979323846f
#define CX (RT2_W / 2)
#define CY (RT2_H / 2)
#define RT_EPS 0.004f
#define T_MAX 80.0f
typedef struct {
float x, y, z;
} vec3;
typedef struct {
vec3 c;
float r;
uint8_t cr, cg, cb;
uint8_t metal;
} sphere_t;
typedef struct {
int x0, y0, x1, y1;
} rect_t;
typedef struct {
uint8_t hit;
uint8_t flags;
uint8_t cr, cg, cb;
vec3 p;
vec3 rorig;
vec3 rdir;
} corner_t;
typedef struct {
vec3 eye, fwd, right, up, light;
float focal;
float floor_lit;
sphere_t sph[RT2_NS];
rect_t rect[RT2_NS];
rect_t refl[RT2_NS][RT2_NS]; /* cell bbox: j visible in i's reflection */
rect_t shad[RT2_NS]; /* cell bbox: i's shadow on the floor */
float ssx[RT2_NS], ssy[RT2_NS], ssr[RT2_NS]; /* screen disk, no pad */
} scene_t;
#define PH_MARK 0
#define PH_PREP 1
#define PH_CORNERS 2
#define PH_FILL 3
#define PH_BG 4
#define RT2_PIVOT_X 0.0f
#define RT2_PIVOT_Y 0.38f
#define RT2_PIVOT_Z 0.15f
#define RT2_EYE0_Y 1.22f
#define RT2_EYE0_Z (-3.55f)
#define RT2_BG_FOG 0.22f
static TaskHandle_t s_worker;
static TaskHandle_t s_waiter;
static raytrace2_t *s_job;
static scene_t *s_scene;
static int s_phase;
static int s_half_s;
static int s_half_sc;
static int s_half_sh;
static uint16_t s_svis[RT2_GW * RT2_GH];
static uint16_t s_shvis[RT2_GW * RT2_GH];
static uint16_t s_scvis[RT2_CW * RT2_CH];
static uint8_t s_need[RT2_CW * RT2_CH];
static uint8_t s_dirty[RT2_GW * RT2_GH];
static int s_svis_n;
static int s_shvis_n;
static int s_scvis_n;
static rect_t s_last_obj[RT2_NS];
static rect_t s_cur[RT2_NS];
static uint32_t s_fps_shown;
static int s_bg_write_px;
static int s_skip_restore;
static float s_cam_r;
static float s_cam_p0;
static int cell_under_sphere_disk(const scene_t *s, int gx, int gy);
static inline vec3 v3(float x, float y, float z)
{
vec3 r = { x, y, z };
return r;
}
static inline vec3 vadd(vec3 a, vec3 b) { return v3(a.x + b.x, a.y + b.y, a.z + b.z); }
static inline vec3 vsub(vec3 a, vec3 b) { return v3(a.x - b.x, a.y - b.y, a.z - b.z); }
static inline vec3 vmul(vec3 a, float s) { return v3(a.x * s, a.y * s, a.z * s); }
static inline float vdot(vec3 a, vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
static inline vec3 vcross(vec3 a, vec3 b)
{
return v3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
}
static inline vec3 vnorm(vec3 a)
{
float l = sqrtf(vdot(a, a));
return (l > 1e-8f) ? vmul(a, 1.0f / l) : a;
}
static inline vec3 vlerp(vec3 a, vec3 b, float t)
{
return vadd(vmul(a, 1.0f - t), vmul(b, t));
}
static inline vec3 vbilerp(vec3 a, vec3 b, vec3 c, vec3 d, float u, float v)
{
return vlerp(vlerp(a, b, u), vlerp(c, d, u), v);
}
static inline corner_t *corn(raytrace2_t *t, int cx, int cy)
{
return &((corner_t *)t->corners)[cy * RT2_CW + cx];
}
static inline int clamp8(int v)
{
if (v < 0) {
return 0;
}
if (v > 255) {
return 255;
}
return v;
}
static IRAM_ATTR uint16_t rgb565_pack(int r, int g, int b)
{
uint16_t c = (uint16_t)(((r & 0xf8) << 8) | ((g & 0xfc) << 3) | (b >> 3));
return (uint16_t)((c << 8) | (c >> 8));
}
static IRAM_ATTR uint16_t rgb565(int r, int g, int b)
{
if (r < 0) {
r = 0;
} else if (r > 255) {
r = 255;
}
if (g < 0) {
g = 0;
} else if (g > 255) {
g = 255;
}
if (b < 0) {
b = 0;
} else if (b > 255) {
b = 255;
}
return rgb565_pack(r, g, b);
}
static int clampi(int v, int lo, int hi)
{
if (v < lo) {
return lo;
}
if (v > hi) {
return hi;
}
return v;
}
static void pix_of_corner(int cx, int cy, int *x, int *y)
{
*x = clampi(cx * RT2_CELL, 0, RT2_W - 1);
*y = clampi(cy * RT2_CELL, 0, RT2_H - 1);
}
static int cell_on_screen(int gx, int gy)
{
int x0 = gx * RT2_CELL;
int y0 = gy * RT2_CELL;
int x1 = x0 + RT2_CELL;
int y1 = y0 + RT2_CELL;
if (x0 >= RT2_W || y0 >= RT2_H) {
return 0;
}
if (x1 > RT2_W) {
x1 = RT2_W;
}
if (y1 > RT2_H) {
y1 = RT2_H;
}
int qx = (CX < x0) ? x0 : ((CX >= x1) ? x1 - 1 : CX);
int qy = (CY < y0) ? y0 : ((CY >= y1) ? y1 - 1 : CY);
int dx = qx - CX;
int dy = qy - CY;
return dx * dx + dy * dy <= CX * CX;
}
/* Unnormalized camera ray. Floor hit is t = -eye.y / d.y, p = eye + t*d. */
static IRAM_ATTR vec3 ray_raw(const scene_t *s, float px, float py)
{
return vadd(vadd(vmul(s->fwd, s->focal), vmul(s->right, px - (float)CX)),
vmul(s->up, (float)CY - py));
}
/* Normalized. tmin on closest_sphere is in world units. */
static IRAM_ATTR vec3 ray_dir(const scene_t *s, float px, float py)
{
return vnorm(ray_raw(s, px, py));
}
/* Works for unnormalized rd: a t^2 + 2 b t + c = 0. No normalize. */
static IRAM_ATTR int hit_sphere(vec3 ro, vec3 rd, vec3 c, float r, float tmin, float tmax, float *t)
{
vec3 oc = vsub(ro, c);
float a = vdot(rd, rd);
if (a < 1e-12f) {
return 0;
}
float b = vdot(oc, rd);
float disc = b * b - a * (vdot(oc, oc) - r * r);
if (disc < 0.0f) {
return 0;
}
float sd = sqrtf(disc);
float h = (-b - sd) / a;
if (h < tmin || h > tmax) {
h = (-b + sd) / a;
if (h < tmin || h > tmax) {
return 0;
}
}
*t = h;
return 1;
}
static void want_at_px(const scene_t *s, int x, int y, uint8_t want[RT2_NS])
{
int gx = clampi(x / RT2_CELL, 0, RT2_GW - 1);
int gy = clampi(y / RT2_CELL, 0, RT2_GH - 1);
for (int i = 0; i < RT2_NS; i++) {
const rect_t *rc = &s->rect[i];
want[i] = (gx >= rc->x0 && gx < rc->x1 && gy >= rc->y0 && gy < rc->y1) ? 1 : 0;
}
}
static IRAM_ATTR uint8_t closest_sphere(const scene_t *s, vec3 ro, vec3 rd, float tmin,
const uint8_t *want, uint8_t skip, float *t_out, vec3 *n_out)
{
float best = T_MAX;
uint8_t id = RT2_HIT_NONE;
vec3 n = v3(0, 1, 0);
for (int i = 0; i < RT2_NS; i++) {
uint8_t sid = (uint8_t)(RT2_HIT_SPHERE + i);
if (sid == skip || (want && !want[i])) {
continue;
}
float t;
if (!hit_sphere(ro, rd, s->sph[i].c, s->sph[i].r, tmin, best, &t)) {
continue;
}
best = t;
id = sid;
n = vnorm(vsub(vadd(ro, vmul(rd, t)), s->sph[i].c));
}
*t_out = best;
*n_out = n;
return id;
}
static vec3 normal_of(const scene_t *s, uint8_t id, vec3 p)
{
if (id < RT2_HIT_SPHERE) {
return v3(0, 1, 0);
}
return vnorm(vsub(p, s->sph[id - RT2_HIT_SPHERE].c));
}
static void snap_sphere(const scene_t *s, uint8_t id, vec3 *p)
{
if (id >= RT2_HIT_SPHERE) {
int i = id - RT2_HIT_SPHERE;
*p = vadd(s->sph[i].c, vmul(vnorm(vsub(*p, s->sph[i].c)), s->sph[i].r));
}
}
static IRAM_ATTR int in_shadow(const scene_t *s, vec3 p, vec3 n, uint8_t skip)
{
vec3 ro = vadd(p, vmul(n, RT_EPS * 4.0f));
for (int i = 0; i < RT2_NS; i++) {
if ((uint8_t)(RT2_HIT_SPHERE + i) == skip) {
continue;
}
float t;
if (hit_sphere(ro, s->light, s->sph[i].c, s->sph[i].r, RT_EPS, T_MAX, &t)) {
return 1;
}
}
return 0;
}
static void project_spheres(scene_t *s)
{
for (int i = 0; i < RT2_NS; i++) {
vec3 rel = vsub(s->sph[i].c, s->eye);
float z = vdot(rel, s->fwd);
rect_t *rc = &s->rect[i];
if (z < 0.12f) {
rc->x0 = rc->y0 = rc->x1 = rc->y1 = 0;
s->ssr[i] = 0.0f;
continue;
}
float x = vdot(rel, s->right);
float y = vdot(rel, s->up);
float sx = (float)CX + (x / z) * s->focal;
float sy = (float)CY - (y / z) * s->focal;
s->ssx[i] = sx;
s->ssy[i] = sy;
s->ssr[i] = (s->sph[i].r / z) * s->focal;
float sr = s->ssr[i] + (float)RT2_CELL;
rc->x0 = clampi((int)floorf((sx - sr) / (float)RT2_CELL) - 1, 0, RT2_GW);
rc->y0 = clampi((int)floorf((sy - sr) / (float)RT2_CELL) - 1, 0, RT2_GH);
rc->x1 = clampi((int)ceilf((sx + sr) / (float)RT2_CELL) + 1, 0, RT2_GW);
rc->y1 = clampi((int)ceilf((sy + sr) / (float)RT2_CELL) + 1, 0, RT2_GH);
}
}
static void rect_clear(rect_t *r)
{
r->x0 = r->y0 = r->x1 = r->y1 = 0;
}
static int rect_empty(const rect_t *r)
{
return r->x1 <= r->x0 || r->y1 <= r->y0;
}
static void rect_intersect(rect_t *r, const rect_t *a)
{
if (r->x0 < a->x0) {
r->x0 = a->x0;
}
if (r->y0 < a->y0) {
r->y0 = a->y0;
}
if (r->x1 > a->x1) {
r->x1 = a->x1;
}
if (r->y1 > a->y1) {
r->y1 = a->y1;
}
if (rect_empty(r)) {
rect_clear(r);
}
}
static void rect_union(rect_t *a, const rect_t *b)
{
if (rect_empty(b)) {
return;
}
if (rect_empty(a)) {
*a = *b;
return;
}
if (b->x0 < a->x0) {
a->x0 = b->x0;
}
if (b->y0 < a->y0) {
a->y0 = b->y0;
}
if (b->x1 > a->x1) {
a->x1 = b->x1;
}
if (b->y1 > a->y1) {
a->y1 = b->y1;
}
}
static void obj_dirty_cells(const scene_t *s, int i, rect_t *d)
{
*d = s->rect[i];
rect_union(d, &s->shad[i]);
if (!rect_empty(d)) {
d->x0 = clampi(d->x0 - 1, 0, RT2_GW);
d->y0 = clampi(d->y0 - 1, 0, RT2_GH);
d->x1 = clampi(d->x1 + 1, 0, RT2_GW);
d->y1 = clampi(d->y1 + 1, 0, RT2_GH);
}
}
static void add_flush(raytrace2_t *t, int x0, int y0, int x1, int y1)
{
if (t->flush_n >= RT2_FLUSH_MAX || x0 >= x1 || y0 >= y1) {
return;
}
if (x0 < 0) {
x0 = 0;
}
if (y0 < 0) {
y0 = 0;
}
if (x1 > RT2_W) {
x1 = RT2_W;
}
if (y1 > RT2_H) {
y1 = RT2_H;
}
rt2_flush_t *f = &t->flush[t->flush_n++];
f->x0 = (int16_t)x0;
f->y0 = (int16_t)y0;
f->x1 = (int16_t)x1;
f->y1 = (int16_t)y1;
}
static void add_flush_cells(raytrace2_t *t, const rect_t *c)
{
if (rect_empty(c)) {
return;
}
int x1 = c->x1 * RT2_CELL;
int y1 = c->y1 * RT2_CELL;
if (x1 > RT2_W) {
x1 = RT2_W;
}
if (y1 > RT2_H) {
y1 = RT2_H;
}
add_flush(t, c->x0 * RT2_CELL, c->y0 * RT2_CELL, x1, y1);
}
static void restore_cells(raytrace2_t *t, const rect_t *d)
{
if (rect_empty(d)) {
return;
}
int x0 = d->x0 * RT2_CELL;
int y0 = d->y0 * RT2_CELL;
int x1 = d->x1 * RT2_CELL;
int y1 = d->y1 * RT2_CELL;
if (x1 > RT2_W) {
x1 = RT2_W;
}
if (y1 > RT2_H) {
y1 = RT2_H;
}
if (x0 >= x1 || y0 >= y1) {
return;
}
size_t nbytes = (size_t)(x1 - x0) * 2u;
for (int y = y0; y < y1; y++) {
memcpy(t->pixels + y * RT2_W + x0, t->bg + y * RT2_W + x0, nbytes);
}
}
static void mark_rect_dirty(const rect_t *r)
{
if (rect_empty(r)) {
return;
}
for (int y = r->y0; y < r->y1; y++) {
memset(s_dirty + y * RT2_GW + r->x0, 1, (size_t)(r->x1 - r->x0));
}
}
static int rects_overlap(const rect_t *a, const rect_t *b)
{
return a->x0 < b->x1 && b->x0 < a->x1 && a->y0 < b->y1 && b->y0 < a->y1;
}
static int merge_overlap_rects(rect_t *b, int n)
{
int merged = 1;
while (merged) {
merged = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (!rects_overlap(&b[i], &b[j])) {
continue;
}
rect_union(&b[i], &b[j]);
b[j] = b[n - 1];
n--;
merged = 1;
break;
}
if (merged) {
break;
}
}
}
return n;
}
static void restore_dirty_stride(raytrace2_t *t, int gy0, int step)
{
const scene_t *s = s_scene;
for (int gy = gy0; gy < RT2_GH; gy += step) {
const uint8_t *row = s_dirty + gy * RT2_GW;
int gx = 0;
while (gx < RT2_GW) {
while (gx < RT2_GW &&
(!row[gx] || (s && cell_under_sphere_disk(s, gx, gy)))) {
gx++;
}
if (gx >= RT2_GW) {
break;
}
int gx0 = gx;
while (gx < RT2_GW && row[gx] &&
!(s && cell_under_sphere_disk(s, gx, gy))) {
gx++;
}
rect_t d = { gx0, gy, gx, gy + 1 };
restore_cells(t, &d);
}
}
}
static void rect_union_disk(rect_t *r, float sx, float sy, float rad, int pad)
{
int x0 = clampi((int)floorf((sx - rad) / (float)RT2_CELL) - pad, 0, RT2_GW);
int y0 = clampi((int)floorf((sy - rad) / (float)RT2_CELL) - pad, 0, RT2_GH);
int x1 = clampi((int)ceilf((sx + rad) / (float)RT2_CELL) + pad, 0, RT2_GW);
int y1 = clampi((int)ceilf((sy + rad) / (float)RT2_CELL) + pad, 0, RT2_GH);
if (rect_empty(r)) {
r->x0 = x0;
r->y0 = y0;
r->x1 = x1;
r->y1 = y1;
return;
}
if (x0 < r->x0) {
r->x0 = x0;
}
if (y0 < r->y0) {
r->y0 = y0;
}
if (x1 > r->x1) {
r->x1 = x1;
}
if (y1 > r->y1) {
r->y1 = y1;
}
}
static int project_world(const scene_t *s, vec3 p, float *sx, float *sy, float *z)
{
vec3 rel = vsub(p, s->eye);
*z = vdot(rel, s->fwd);
if (*z < 0.12f) {
return 0;
}
*sx = (float)CX + (vdot(rel, s->right) / *z) * s->focal;
*sy = (float)CY - (vdot(rel, s->up) / *z) * s->focal;
return 1;
}
/*
* Conservative screen box for "sphere j reflected in i".
* Union of: Blinn bisector blob, inverted-j-in-i, and the limb if the
* hit sits on the back hemisphere. Always clipped to i's disk, padded.
*/
static void project_refl_boxes(scene_t *s)
{
for (int i = 0; i < RT2_NS; i++) {
const rect_t *host = &s->rect[i];
for (int j = 0; j < RT2_NS; j++) {
rect_t *rb = &s->refl[i][j];
rect_clear(rb);
if (i == j || rect_empty(host)) {
continue;
}
vec3 C = s->sph[i].c;
float ar = s->sph[i].r;
vec3 Q = s->sph[j].c;
float br = s->sph[j].r;
vec3 toB = vsub(Q, C);
float d2 = vdot(toB, toB);
vec3 toE = vsub(s->eye, C);
float e2 = vdot(toE, toE);
if (d2 < 1e-6f || e2 < 1e-6f) {
*rb = *host;
continue;
}
float d = sqrtf(d2);
float el = sqrtf(e2);
vec3 u = vmul(toB, 1.0f / d);
vec3 v = vmul(toE, 1.0f / el);
float zA, sxA, syA;
if (!project_world(s, C, &sxA, &syA, &zA)) {
*rb = *host;
continue;
}
float srA = (ar / zA) * s->focal;
float ang = br / (d - ar > 0.08f ? (d - ar) : 0.08f);
if (ang > 1.8f) {
ang = 1.8f;
}
float blob = srA * ang * 3.2f + (float)RT2_CELL * 2.0f;
vec3 bis = vadd(v, u);
float bl2 = vdot(bis, bis);
if (bl2 > 0.04f) {
vec3 n = vmul(bis, 1.0f / sqrtf(bl2));
vec3 P = vadd(C, vmul(n, ar));
float sx, sy, z;
if (project_world(s, P, &sx, &sy, &z)) {
float grow = blob;
float facing = vdot(n, v);
if (facing < 0.35f) {
grow += srA * 0.55f;
}
rect_union_disk(rb, sx, sy, grow, 2);
}
}
/* Sphere inversion of j through i — extra coverage, viewer-independent. */
float L = d2 - br * br;
if (L > 0.08f || L < -0.08f) {
float k = (ar * ar) / L;
vec3 Qp = vadd(C, vmul(toB, k));
float sp = fabsf((ar * ar) * br / L);
float sx, sy, z;
if (sp < ar * 2.5f && project_world(s, Qp, &sx, &sy, &z)) {
float zq = z > 0.12f ? z : 0.12f;
float spr = (sp / zq) * s->focal + blob * 0.35f;
rect_union_disk(rb, sx, sy, spr, 2);
}
} else {
*rb = *host;
continue;
}
/* Back-hemisphere / opposite: image piles on the limb toward B. */
if (bl2 <= 0.04f || vdot(u, v) < -0.15f) {
float dx = 0.0f, dy = 0.0f, zz;
float sxB, syB;
if (project_world(s, Q, &sxB, &syB, &zz)) {
dx = sxB - sxA;
dy = syB - syA;
} else {
dx = vdot(toB, s->right);
dy = -vdot(toB, s->up);
}
float len = sqrtf(dx * dx + dy * dy);
if (len > 1.0f) {
float lx = sxA + srA * (dx / len);
float ly = syA + srA * (dy / len);
rect_union_disk(rb, lx, ly, srA * 0.75f + blob, 2);
} else {
*rb = *host;
continue;
}
}
if (rect_empty(rb)) {
*rb = *host;
} else {
rect_t clip = *host;
clip.x0 = clampi(clip.x0 - 1, 0, RT2_GW);
clip.y0 = clampi(clip.y0 - 1, 0, RT2_GH);
clip.x1 = clampi(clip.x1 + 1, 0, RT2_GW);
clip.y1 = clampi(clip.y1 + 1, 0, RT2_GH);
rect_intersect(rb, &clip);
if (rect_empty(rb)) {
*rb = *host;
}
}
}
}
}
/* Directional light: sphere ∩ plane y=0 is an ellipse centred on the
* anti-light projection of C, contained in a circle of radius r / L.y. */
static void union_floor_disk(const scene_t *s, rect_t *r, float px, float pz, float rad)
{
float sx, sy, z;
if (project_world(s, v3(px, 0.0f, pz), &sx, &sy, &z)) {
float sr = (rad / z) * s->focal + (float)RT2_CELL * 1.5f;
rect_union_disk(r, sx, sy, sr, 1);
}
const float ox[4] = { rad, -rad, 0.0f, 0.0f };
const float oz[4] = { 0.0f, 0.0f, rad, -rad };
for (int k = 0; k < 4; k++) {
if (project_world(s, v3(px + ox[k], 0.0f, pz + oz[k]), &sx, &sy, &z)) {
rect_union_disk(r, sx, sy, (float)RT2_CELL * 1.5f, 1);
}
}
}
static void project_shadow_boxes(scene_t *s)
{
vec3 L = s->light;
for (int i = 0; i < RT2_NS; i++) {
rect_t *rb = &s->shad[i];
rect_clear(rb);
if (L.y < 0.08f) {
continue;
}
vec3 C = s->sph[i].c;
float r = s->sph[i].r;
if (C.y + r < 0.0f) {
continue;
}
float t = C.y / L.y;
vec3 P0 = vsub(C, vmul(L, t));
float grad = r / L.y * 1.25f;
union_floor_disk(s, rb, P0.x, P0.z, grad);
union_floor_disk(s, rb, C.x, C.z, r + 0.04f);
}
}
static int cell_in_rect(const rect_t *r, int gx, int gy)
{
return gx >= r->x0 && gx < r->x1 && gy >= r->y0 && gy < r->y1;
}
static int cell_in_shadow_rect(const scene_t *s, int gx, int gy)
{
for (int i = 0; i < RT2_NS; i++) {
if (cell_in_rect(&s->shad[i], gx, gy)) {
return 1;
}
}
return 0;
}
static int refl_box_overlaps(const rect_t *r, int x0, int y0, int x1, int y1)
{
if (rect_empty(r)) {
return 0;
}
int rx0 = r->x0 * RT2_CELL;
int ry0 = r->y0 * RT2_CELL;
int rx1 = r->x1 * RT2_CELL;
int ry1 = r->y1 * RT2_CELL;
return x0 < rx1 && x1 > rx0 && y0 < ry1 && y1 > ry0;
}
static void cam_const_init(void)
{
if (s_cam_r > 0.0f) {
return;
}
float dy = RT2_EYE0_Y - RT2_PIVOT_Y;
float dz = RT2_EYE0_Z - RT2_PIVOT_Z;
s_cam_r = sqrtf(dy * dy + dz * dz);
s_cam_p0 = atan2f(dy, -dz);
}
static float cam_radius(void)
{
cam_const_init();
return s_cam_r;
}
static float cam_pitch0(void)
{
cam_const_init();
return s_cam_p0;
}
static void apply_camera(scene_t *s, float yaw, float pitch)
{
vec3 pivot = v3(RT2_PIVOT_X, RT2_PIVOT_Y, RT2_PIVOT_Z);
float r = cam_radius();
float cp = cosf(pitch);
float sp = sinf(pitch);
float cy = cosf(yaw);
float sy = sinf(yaw);
s->eye = v3(pivot.x + r * cp * sy, pivot.y + r * sp, pivot.z - r * cp * cy);
s->fwd = vnorm(vsub(pivot, s->eye));
vec3 world_up = v3(0, 1, 0);
s->right = vnorm(vcross(s->fwd, world_up));
if (vdot(s->right, s->right) < 1e-8f) {
s->right = v3(1, 0, 0);
}
s->up = vcross(s->right, s->fwd);
s->focal = (float)CX / tanf(52.0f * PI / 180.0f * 0.5f);
}
static void setup_scene(scene_t *s, uint32_t frames, float yaw, float pitch)
{
float a = frames * 0.018f;
apply_camera(s, yaw, pitch);
s->light = vnorm(v3(0.45f, 0.88f, -0.30f));
float ndl = s->light.y;
if (ndl < 0.0f) {
ndl = 0.0f;
}
s->floor_lit = 0.28f + 0.72f * ndl;
s->sph[0].c = v3(sinf(a) * 0.95f, 0.58f, cosf(a) * 0.55f);
s->sph[0].r = 0.56f;
s->sph[0].cr = 255;
s->sph[0].cg = 36;
s->sph[0].cb = 28;
s->sph[0].metal = 160;
s->sph[1].c = v3(-1.15f, 0.42f, 0.15f + sinf(a * 0.7f) * 0.2f);
s->sph[1].r = 0.42f;
s->sph[1].cr = 16;
s->sph[1].cg = 196;
s->sph[1].cb = 230;
s->sph[1].metal = 150;
s->sph[2].c = v3(1.05f, 0.32f + fabsf(sinf(a * 1.6f)) * 0.45f, -0.15f);
s->sph[2].r = 0.32f;
s->sph[2].cr = 255;
s->sph[2].cg = 200;
s->sph[2].cb = 24;
s->sph[2].metal = 80;
s->sph[3].c = v3(0.15f, 0.28f, 1.15f);
s->sph[3].r = 0.28f;
s->sph[3].cr = 236;
s->sph[3].cg = 236;
s->sph[3].cb = 242;
s->sph[3].metal = 230;
}
static IRAM_ATTR void mix_fog(int *r, int *g, int *b, float f)
{
if (f <= 0.0f) {
return;
}
if (f > 1.0f) {
f = 1.0f;
}
const int fr = 118, fg = 126, fb = 148;
*r += (int)((fr - *r) * f);
*g += (int)((fg - *g) * f);
*b += (int)((fb - *b) * f);
}
static inline int ifloor1p6(float x)
{
float f = x * 1.6f;
int i = (int)f;
if (f < 0.0f && f != (float)i) {
i--;
}
return i;
}
static inline int ifloorf(float x)
{
int i = (int)x;
if (x < 0.0f && x != (float)i) {
i--;
}
return i;
}
static void checker_rgb(vec3 p, int *r, int *g, int *b)
{
int ix = ifloor1p6(p.x);
int iz = ifloor1p6(p.z);
if (((ix + iz) & 1) == 0) {
*r = 248;
*g = 236;
*b = 214;
} else {
*r = 28;
*g = 26;
*b = 32;
}
}
static IRAM_ATTR void shade_sky(vec3 d, float fog_k, int *r, int *g, int *b)
{
float len2 = vdot(d, d);
float ny = 0.0f;
if (d.y > 0.0f && len2 > 1e-8f) {
ny = (d.y * d.y) / len2;
}
*r = (int)(18 + ny * 70);
*g = (int)(28 + ny * 90);
*b = (int)(70 + ny * 120);
float fog = 1.0f - ny;
mix_fog(r, g, b, fog * fog * fog_k);
}
/* Analytic floor or sky from an unnormalized ray. fog_k 0..1.
* Floor n=(0,1,0): rd·n < 0 and origin above y=0, else sky. */
static IRAM_ATTR void shade_env(vec3 ro, vec3 rd, float floor_lit, float fog_k, int *r, int *g, int *b)
{
if (rd.y >= 0.0f || ro.y <= 0.0f) {
shade_sky(rd, fog_k, r, g, b);
return;
}
float t = -ro.y / rd.y;
vec3 p = vadd(ro, vmul(rd, t));
checker_rgb(p, r, g, b);
*r = (int)(*r * floor_lit);
*g = (int)(*g * floor_lit);
*b = (int)(*b * floor_lit);
float dist2 = p.x * p.x + p.z * p.z;
float fog = (dist2 - 16.0f) * (1.0f / 90.0f);
float len2 = vdot(rd, rd);
float down = (len2 > 1e-8f) ? ((rd.y * rd.y) / len2) : 0.0f;
float horiz = (1.0f - down);
horiz = horiz * horiz;
if (horiz > fog) {
fog = horiz;
}
mix_fog(r, g, b, fog * fog_k);
}
static void shade_sphere_lit(const scene_t *s, uint8_t id, vec3 n, int *r, int *g, int *b)
{
int i = id - RT2_HIT_SPHERE;
float ndl = vdot(n, s->light);
if (ndl < 0.0f) {
ndl = 0.0f;
}
float lit = 0.28f + 0.72f * ndl;
*r = clamp8((int)(s->sph[i].cr * lit));
*g = clamp8((int)(s->sph[i].cg * lit));
*b = clamp8((int)(s->sph[i].cb * lit));
}
static int floor_point(const scene_t *s, float px, float py, vec3 *p)
{
vec3 d = ray_raw(s, px, py);
if (d.y >= -1e-6f) {
return 0;
}
float t = -s->eye.y / d.y;
if (t <= 0.0f) {
return 0;
}
*p = vadd(s->eye, vmul(d, t));
p->y = 0.0f;
return 1;
}
static int cell_in_sphere_rect(const scene_t *s, int gx, int gy, int pad)
{
for (int k = 0; k < RT2_NS; k++) {
const rect_t *rc = &s->rect[k];
if (gx >= rc->x0 - pad && gx < rc->x1 + pad &&
gy >= rc->y0 - pad && gy < rc->y1 + pad) {
return 1;
}
}
return 0;
}
static void mark_sphere_cells(const scene_t *s, const raytrace2_t *t)
{
memset(s_need, 0, sizeof(s_need));
s_svis_n = 0;
for (int i = 0; i < t->vis_n; i++) {
int gx = t->vis[i] & 0xff;
int gy = t->vis[i] >> 8;
if (!cell_in_sphere_rect(s, gx, gy, 0)) {
continue;
}
s_svis[s_svis_n++] = t->vis[i];
s_need[gy * RT2_CW + gx] = 1;
s_need[gy * RT2_CW + gx + 1] = 1;
s_need[(gy + 1) * RT2_CW + gx] = 1;
s_need[(gy + 1) * RT2_CW + gx + 1] = 1;
}
s_scvis_n = 0;
for (int i = 0; i < RT2_CW * RT2_CH; i++) {
if (s_need[i]) {
s_scvis[s_scvis_n++] = (uint16_t)((i % RT2_CW) | ((i / RT2_CW) << 8));
}
}
s_half_s = s_svis_n / 2;
s_half_sc = s_scvis_n / 2;
}
/* True only if the whole 8×8 sits inside a projected sphere disk. */
static int cell_under_sphere_disk(const scene_t *s, int gx, int gy)
{
int x0 = gx * RT2_CELL;
int y0 = gy * RT2_CELL;
int x1 = x0 + RT2_CELL - 1;
int y1 = y0 + RT2_CELL - 1;
for (int k = 0; k < RT2_NS; k++) {
float sr = s->ssr[k] - 1.5f;
if (sr < (float)RT2_CELL) {
continue;
}
float sx = s->ssx[k];
float sy = s->ssy[k];
float fx = (fabsf((float)x0 - sx) > fabsf((float)x1 - sx)) ? (float)x0 : (float)x1;
float fy = (fabsf((float)y0 - sy) > fabsf((float)y1 - sy)) ? (float)y0 : (float)y1;
float dx = fx - sx;
float dy = fy - sy;
if (dx * dx + dy * dy < sr * sr) {
return 1;
}
}
return 0;
}
static void mark_shadow_cells(const scene_t *s, const raytrace2_t *t)
{
s_shvis_n = 0;
for (int i = 0; i < t->vis_n; i++) {
int gx = t->vis[i] & 0xff;
int gy = t->vis[i] >> 8;
if (cell_in_shadow_rect(s, gx, gy) && !cell_under_sphere_disk(s, gx, gy)) {
s_shvis[s_shvis_n++] = t->vis[i];
}
}
s_half_sh = s_shvis_n / 2;
}
static void mark_object_dirty(const scene_t *s, int i)
{
obj_dirty_cells(s, i, &s_cur[i]);
mark_rect_dirty(&s_last_obj[i]);
mark_rect_dirty(&s_cur[i]);
}
static void cache_primary(const scene_t *s, corner_t *c)
{
if (c->hit < RT2_HIT_SPHERE) {
c->cr = c->cg = c->cb = 0;
return;
}
int i = c->hit - RT2_HIT_SPHERE;
vec3 n = normal_of(s, c->hit, c->p);
float ndl = vdot(n, s->light);
if (ndl < 0.0f) {
ndl = 0.0f;
}
float lit = 0.28f + ((c->flags & RT2_SHADOW_P) ? 0.0f : 0.72f * ndl);
c->cr = (uint8_t)clamp8((int)(s->sph[i].cr * lit));
c->cg = (uint8_t)clamp8((int)(s->sph[i].cg * lit));
c->cb = (uint8_t)clamp8((int)(s->sph[i].cb * lit));
}
static void set_refl_unnorm(const scene_t *s, corner_t *c, vec3 view)
{
vec3 n = normal_of(s, c->hit, c->p);
/* I - 2(I·N)N, left unnormalized so interpolation has no sqrt. */
c->rdir = vsub(view, vmul(n, 2.0f * vdot(view, n)));
c->rorig = vadd(c->p, vmul(n, RT_EPS * 4.0f));
}
static void trace_full(const scene_t *s, int x, int y, corner_t *c)
{
c->hit = 0;
c->flags = 0;
vec3 rd = ray_dir(s, (float)x, (float)y);
uint8_t want[RT2_NS];
want_at_px(s, x, y, want);
float t;
vec3 n;
c->hit = closest_sphere(s, s->eye, rd, 0.02f, want, RT2_HIT_NONE, &t, &n);
if (c->hit == RT2_HIT_NONE) {
return;
}
c->p = vadd(s->eye, vmul(rd, t));
set_refl_unnorm(s, c, rd);
if (in_shadow(s, c->p, n, c->hit)) {
c->flags |= RT2_SHADOW_P;
}
cache_primary(s, c);
}
static void sample_mid(const scene_t *s, int x, int y,
const corner_t *c00, const corner_t *c10,
const corner_t *c01, const corner_t *c11,
float u, float v, int same_hit, int same_sp,
corner_t *out)
{
out->hit = 0;
out->flags = 0;
if (same_hit) {
out->hit = c00->hit;
if (out->hit >= RT2_HIT_SPHERE) {
out->p = vbilerp(c00->p, c10->p, c01->p, c11->p, u, v);
snap_sphere(s, out->hit, &out->p);
out->rorig = vbilerp(c00->rorig, c10->rorig, c01->rorig, c11->rorig, u, v);
out->rdir = vbilerp(c00->rdir, c10->rdir, c01->rdir, c11->rdir, u, v);
}
} else {
vec3 view = ray_dir(s, (float)x, (float)y);
uint8_t want[RT2_NS];
want_at_px(s, x, y, want);
float t;
vec3 n;
out->hit = closest_sphere(s, s->eye, view, 0.02f, want, RT2_HIT_NONE, &t, &n);
if (out->hit >= RT2_HIT_SPHERE) {
out->p = vadd(s->eye, vmul(view, t));
set_refl_unnorm(s, out, view);
}
}
if (out->hit < RT2_HIT_SPHERE) {
return;
}
if (same_sp) {
out->flags |= (uint8_t)(c00->flags & RT2_SHADOW_P);
} else if (in_shadow(s, out->p, normal_of(s, out->hit, out->p), out->hit)) {
out->flags |= RT2_SHADOW_P;
}
cache_primary(s, out);
}
static int all_eq4(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
{
return a == b && a == c && a == d;
}
static IRAM_ATTR void fill_sphere(raytrace2_t *t, const scene_t *s,
int x0, int y0, int x1, int y1,
const corner_t *c00, const corner_t *c10,
const corner_t *c01, const corner_t *c11)
{
int w = x1 - x0;
int h = y1 - y0;
if (w <= 0 || h <= 0) {
return;
}
uint8_t metal = 0;
uint8_t want[RT2_NS];
int any_refl = 0;
int si = 0;
if (c00->hit >= RT2_HIT_SPHERE) {
si = c00->hit - RT2_HIT_SPHERE;
metal = s->sph[si].metal;
for (int j = 0; j < RT2_NS; j++) {
want[j] = 0;
if (j == si) {
continue;
}
if (refl_box_overlaps(&s->refl[si][j], x0, y0, x1, y1)) {
want[j] = 1;
any_refl = 1;
}
}
}
float invw = 1.0f / (float)w;
float invh = 1.0f / (float)h;
int cr_l = (int)c00->cr << 8, cr_r = (int)c10->cr << 8;
int cg_l = (int)c00->cg << 8, cg_r = (int)c10->cg << 8;
int cb_l = (int)c00->cb << 8, cb_r = (int)c10->cb << 8;
int dcr_l = (((int)c01->cr - (int)c00->cr) << 8) / h;
int dcr_r = (((int)c11->cr - (int)c10->cr) << 8) / h;
int dcg_l = (((int)c01->cg - (int)c00->cg) << 8) / h;
int dcg_r = (((int)c11->cg - (int)c10->cg) << 8) / h;
int dcb_l = (((int)c01->cb - (int)c00->cb) << 8) / h;
int dcb_r = (((int)c11->cb - (int)c10->cb) << 8) / h;
int nm = 256 - (int)metal;
float lit = s->floor_lit;
int nsx = w / 2 + 1;
int nsy = h / 2 + 1;
int env_r[36], env_g[36], env_b[36];
if (metal && nsx * nsy <= 36) {
for (int iy = 0; iy < nsy; iy++) {
int py = y0 + iy * 2;
if (py >= y1) {
py = y1 - 1;
}
float vf = ((float)(py - y0) + 0.5f) * invh;
for (int ix = 0; ix < nsx; ix++) {
int px = x0 + ix * 2;
if (px >= x1) {
px = x1 - 1;
}
float uf = ((float)(px - x0) + 0.5f) * invw;
vec3 ro = vbilerp(c00->rorig, c10->rorig, c01->rorig, c11->rorig, uf, vf);
vec3 rd = vbilerp(c00->rdir, c10->rdir, c01->rdir, c11->rdir, uf, vf);
int er, eg, eb;
uint8_t hid = RT2_HIT_NONE;
if (any_refl) {
uint8_t swant[RT2_NS];
int gx = px / RT2_CELL;
int gy = py / RT2_CELL;
int any = 0;
if (gx < 0) {
gx = 0;
} else if (gx >= RT2_GW) {
gx = RT2_GW - 1;
}
if (gy < 0) {
gy = 0;
} else if (gy >= RT2_GH) {
gy = RT2_GH - 1;
}
for (int j = 0; j < RT2_NS; j++) {
swant[j] = (want[j] && cell_in_rect(&s->refl[si][j], gx, gy)) ? 1 : 0;
any |= swant[j];
}
if (any) {
float ht;
vec3 n;
hid = closest_sphere(s, ro, rd, RT_EPS, swant, c00->hit, &ht, &n);
if (hid >= RT2_HIT_SPHERE) {
shade_sphere_lit(s, hid, n, &er, &eg, &eb);
}
}
}
if (hid < RT2_HIT_SPHERE) {
shade_env(ro, rd, lit, 0.70f, &er, &eg, &eb);
}
int i = iy * nsx + ix;
env_r[i] = er;
env_g[i] = eg;
env_b[i] = eb;
}
}
}
for (int y = y0; y < y1; y++) {
uint16_t *row = t->pixels + y * RT2_W;
int cr = cr_l, cg = cg_l, cb = cb_l;
int dcr = (cr_r - cr_l) / w;
int dcg = (cg_r - cg_l) / w;
int dcb = (cb_r - cb_l) / w;
int oy = y - y0;
int iy = oy >> 1;
int iy1 = iy + 1;
if (iy1 >= nsy) {
iy1 = nsy - 1;
}
unsigned fy = (unsigned)(oy & 1) << 7;
for (int x = x0; x < x1; x++) {
int r = cr >> 8;
int g = cg >> 8;
int b = cb >> 8;
if (metal) {
int ox = x - x0;
int ix = ox >> 1;
int ix1 = ix + 1;
if (ix1 >= nsx) {
ix1 = nsx - 1;
}
unsigned fx = (unsigned)(ox & 1) << 7;
int i00 = iy * nsx + ix;
int i10 = iy * nsx + ix1;
int i01 = iy1 * nsx + ix;
int i11 = iy1 * nsx + ix1;
int top_r = env_r[i00] + (((env_r[i10] - env_r[i00]) * (int)fx) >> 8);
int bot_r = env_r[i01] + (((env_r[i11] - env_r[i01]) * (int)fx) >> 8);
int top_g = env_g[i00] + (((env_g[i10] - env_g[i00]) * (int)fx) >> 8);
int bot_g = env_g[i01] + (((env_g[i11] - env_g[i01]) * (int)fx) >> 8);
int top_b = env_b[i00] + (((env_b[i10] - env_b[i00]) * (int)fx) >> 8);
int bot_b = env_b[i01] + (((env_b[i11] - env_b[i01]) * (int)fx) >> 8);
int er = top_r + (((bot_r - top_r) * (int)fy) >> 8);
int eg = top_g + (((bot_g - top_g) * (int)fy) >> 8);
int eb = top_b + (((bot_b - top_b) * (int)fy) >> 8);
r = (r * nm + er * (int)metal) >> 8;
g = (g * nm + eg * (int)metal) >> 8;
b = (b * nm + eb * (int)metal) >> 8;
}
row[x] = rgb565_pack(r, g, b);
cr += dcr;
cg += dcg;
cb += dcb;
}
cr_l += dcr_l;
cr_r += dcr_r;
cg_l += dcg_l;
cg_r += dcg_r;
cb_l += dcb_l;
cb_r += dcb_r;
}
}
static void fill_quad(raytrace2_t *t, const scene_t *s,
int x0, int y0, int x1, int y1,
const corner_t *c00, const corner_t *c10,
const corner_t *c01, const corner_t *c11)
{
if (x0 >= x1 || y0 >= y1) {
return;
}
int same_hit = all_eq4(c00->hit, c10->hit, c01->hit, c11->hit);
int w = x1 - x0;
int h = y1 - y0;
if (same_hit && c00->hit == RT2_HIT_NONE) {
return;
}
if (same_hit && c00->hit >= RT2_HIT_SPHERE) {
fill_sphere(t, s, x0, y0, x1, y1, c00, c10, c01, c11);
return;
}
if (w == 1 && h == 1) {
if (c00->hit >= RT2_HIT_SPHERE) {
fill_sphere(t, s, x0, y0, x1, y1, c00, c10, c01, c11);
}
return;
}
int mx = x0 + w / 2;
int my = y0 + h / 2;
if (mx == x0) {
mx = x1;
}
if (my == y0) {
my = y1;
}
float uw = (w > 0) ? (float)(mx - x0) / (float)w : 0.0f;
float vh = (h > 0) ? (float)(my - y0) / (float)h : 0.0f;
corner_t cM0, cM1, c0M, c1M, cMM;
if (mx < x1) {
sample_mid(s, mx, y0, c00, c10, c01, c11, uw, 0.0f, same_hit, 0, &cM0);
sample_mid(s, mx, y1, c00, c10, c01, c11, uw, 1.0f, same_hit, 0, &cM1);
} else {
cM0 = *c10;
cM1 = *c11;
}
if (my < y1) {
sample_mid(s, x0, my, c00, c10, c01, c11, 0.0f, vh, same_hit, 0, &c0M);
sample_mid(s, x1, my, c00, c10, c01, c11, 1.0f, vh, same_hit, 0, &c1M);
} else {
c0M = *c01;
c1M = *c11;
}
if (mx < x1 && my < y1) {
sample_mid(s, mx, my, c00, c10, c01, c11, uw, vh, same_hit, 0, &cMM);
} else {
cMM = cM0;
}
fill_quad(t, s, x0, y0, mx, my, c00, &cM0, &c0M, &cMM);
if (mx < x1) {
fill_quad(t, s, mx, y0, x1, my, &cM0, c10, &cMM, &c1M);
}
if (my < y1) {
fill_quad(t, s, x0, my, mx, y1, &c0M, &cMM, c01, &cM1);
}
if (mx < x1 && my < y1) {
fill_quad(t, s, mx, my, x1, y1, &cMM, &c1M, &cM1, c11);
}
}
static uint16_t darken_565(uint16_t c, unsigned k)
{
c = (uint16_t)((c << 8) | (c >> 8));
unsigned r5 = ((c >> 11) & 31u) * k >> 8;
unsigned g6 = ((c >> 5) & 63u) * k >> 8;
unsigned b5 = (c & 31u) * k >> 8;
c = (uint16_t)((r5 << 11) | (g6 << 5) | b5);
return (uint16_t)((c << 8) | (c >> 8));
}
static void darken_floor(raytrace2_t *t, const scene_t *s,
int x0, int y0, int x1, int y1, int test_floor)
{
unsigned k = (unsigned)(0.28f / s->floor_lit * 256.0f);
if (k > 256u) {
k = 256u;
}
for (int y = y0; y < y1; y++) {
uint16_t *row = t->pixels + y * RT2_W;
const uint16_t *src = t->bg + y * RT2_W;
for (int x = x0; x < x1; x++) {
if (test_floor) {
vec3 p;
if (!floor_point(s, (float)x + 0.5f, (float)y + 0.5f, &p)) {
continue;
}
}
row[x] = darken_565(src[x], k);
}
}
}
/* Directional umbra on y=0 is the ellipse (cylinder ∩ plane). Same as a
* unit-L ray-sphere test, no sqrt. c<=0: floor point inside the ball. */
static IRAM_ATTR int ellipse_shadows_floor(vec3 C, float r, vec3 L, vec3 p)
{
float ocx = p.x - C.x;
float ocy = -C.y;
float ocz = p.z - C.z;
float b = ocx * L.x + ocy * L.y + ocz * L.z;
float c = ocx * ocx + ocy * ocy + ocz * ocz - r * r;
if (c <= 0.0f) {
return 1;
}
return b < 0.0f && (b * b >= c);
}
static int in_floor_shadow(const scene_t *s, vec3 p, int x, int y)
{
int gx = x / RT2_CELL;
int gy = y / RT2_CELL;
if (gx < 0) {
gx = 0;
} else if (gx >= RT2_GW) {
gx = RT2_GW - 1;
}
if (gy < 0) {
gy = 0;
} else if (gy >= RT2_GH) {
gy = RT2_GH - 1;
}
vec3 L = s->light;
for (int i = 0; i < RT2_NS; i++) {
if (!cell_in_rect(&s->shad[i], gx, gy)) {
continue;
}
if (ellipse_shadows_floor(s->sph[i].c, s->sph[i].r, L, p)) {
return 1;
}
}
return 0;
}
static int corner_floor_shadow(const scene_t *s, int x, int y)
{
vec3 p;
if (!floor_point(s, (float)x, (float)y, &p)) {
return 0;
}
return in_floor_shadow(s, p, x, y);
}
static void fill_bg_quad(raytrace2_t *t, const scene_t *s,
int x0, int y0, int x1, int y1,
int sh00, int sh10, int sh01, int sh11)
{
int w = x1 - x0;
int h = y1 - y0;
if (sh00 == sh10 && sh00 == sh01 && sh00 == sh11) {
if (sh00) {
darken_floor(t, s, x0, y0, x1, y1, 0);
}
return;
}
if (w == 1 && h == 1) {
if (sh00) {
darken_floor(t, s, x0, y0, x1, y1, 1);
}
return;
}
int mx = x0 + w / 2;
int my = y0 + h / 2;
if (mx == x0) {
mx = x1;
}
if (my == y0) {
my = y1;
}
int shM0 = (mx < x1) ? corner_floor_shadow(s, mx, y0) : sh10;
int shM1 = (mx < x1) ? corner_floor_shadow(s, mx, y1) : sh11;
int sh0M = (my < y1) ? corner_floor_shadow(s, x0, my) : sh01;
int sh1M = (my < y1) ? corner_floor_shadow(s, x1, my) : sh11;
int shMM = (mx < x1 && my < y1) ? corner_floor_shadow(s, mx, my) : shM0;
fill_bg_quad(t, s, x0, y0, mx, my, sh00, shM0, sh0M, shMM);
if (mx < x1) {
fill_bg_quad(t, s, mx, y0, x1, my, shM0, sh10, shMM, sh1M);
}
if (my < y1) {
fill_bg_quad(t, s, x0, my, mx, y1, sh0M, shMM, sh01, shM1);
}
if (mx < x1 && my < y1) {
fill_bg_quad(t, s, mx, my, x1, y1, shMM, sh1M, shM1, sh11);
}
}
static int floor_uv_at(const scene_t *s, float px, float py, float *u, float *v)
{
vec3 d = ray_raw(s, px, py);
if (d.y >= 0.0f || s->eye.y <= 0.0f) {
return 0;
}
float t = -s->eye.y / d.y;
*u = (s->eye.x + d.x * t) * 1.6f;
*v = (s->eye.z + d.z * t) * 1.6f;
return 1;
}
static uint16_t sky_color(const scene_t *s, float px, float py)
{
vec3 d = ray_raw(s, px, py);
int r, g, b;
shade_sky(d, RT2_BG_FOG, &r, &g, &b);
return rgb565(r, g, b);
}
static void checker_pair(const scene_t *s, float fog, uint16_t *c0, uint16_t *c1)
{
int r0 = (int)(248 * s->floor_lit);
int g0 = (int)(236 * s->floor_lit);
int b0 = (int)(214 * s->floor_lit);
int r1 = (int)(28 * s->floor_lit);
int g1 = (int)(26 * s->floor_lit);
int b1 = (int)(32 * s->floor_lit);
mix_fog(&r0, &g0, &b0, fog);
mix_fog(&r1, &g1, &b1, fog);
*c0 = rgb565(r0, g0, b0);
*c1 = rgb565(r1, g1, b1);
}
static float scanline_fog(const scene_t *s, float px, float py)
{
vec3 d = ray_raw(s, px, py);
if (d.y >= -1e-6f || s->eye.y <= 0.0f) {
return 0.0f;
}
float t = -s->eye.y / d.y;
float x = s->eye.x + d.x * t;
float z = s->eye.z + d.z * t;
float dist2 = x * x + z * z;
float fog = (dist2 - 16.0f) * (1.0f / 90.0f);
float len2 = vdot(d, d);
float down = (len2 > 1e-8f) ? ((d.y * d.y) / len2) : 0.0f;
float horiz = (1.0f - down);
horiz = horiz * horiz;
if (horiz > fog) {
fog = horiz;
}
return fog * RT2_BG_FOG;
}
/* Plane UV at the visible left/right of the scanline, lerp across. One fog. */
static void fill_bg_row(raytrace2_t *t, const scene_t *s, int y, int write_px)
{
int dy = y - CY;
int rem = CX * CX - dy * dy;
if (rem <= 0) {
return;
}
int rx = (int)sqrtf((float)rem);
int xa = CX - rx;
int xb = CX + rx + 1;
if (xa < 0) {
xa = 0;
}
if (xb > RT2_W) {
xb = RT2_W;
}
if (xa >= xb) {
return;
}
float py = (float)y + 0.5f;
float xl = (float)xa + 0.5f;
float xr = (float)(xb - 1) + 0.5f;
vec3 dL = ray_raw(s, xl, py);
vec3 dR = ray_raw(s, xr, py);
int flL = (dL.y < 0.0f && s->eye.y > 0.0f);
int flR = (dR.y < 0.0f && s->eye.y > 0.0f);
uint16_t *bg = t->bg + y * RT2_W;
uint16_t *px = t->pixels + y * RT2_W;
int x_floor0 = xa;
int x_floor1 = xb;
if (!flL && !flR) {
uint16_t c = sky_color(s, (xl + xr) * 0.5f, py);
for (int x = xa; x < xb; x++) {
bg[x] = c;
if (write_px) {
px[x] = c;
}
}
return;
}
if (flL != flR) {
float ddy = dR.y - dL.y;
float u = (fabsf(ddy) > 1e-8f) ? (-dL.y / ddy) : 0.5f;
if (u < 0.0f) {
u = 0.0f;
}
if (u > 1.0f) {
u = 1.0f;
}
int xh = xa + (int)(u * (float)(xb - xa) + 0.5f);
if (xh < xa) {
xh = xa;
}
if (xh > xb) {
xh = xb;
}
if (flL) {
x_floor0 = xa;
x_floor1 = xh;
} else {
x_floor0 = xh;
x_floor1 = xb;
}
int xs0 = flL ? xh : xa;
int xs1 = flL ? xb : xh;
if (xs0 < xs1) {
uint16_t c = sky_color(s, (float)(xs0 + xs1) * 0.5f, py);
for (int x = xs0; x < xs1; x++) {
bg[x] = c;
if (write_px) {
px[x] = c;
}
}
}
}
if (x_floor1 <= x_floor0) {
return;
}
float u0, v0, u1, v1;
float xf0 = (float)x_floor0 + 0.5f;
float xf1 = (float)(x_floor1 - 1) + 0.5f;
if (!floor_uv_at(s, xf0, py, &u0, &v0) || !floor_uv_at(s, xf1, py, &u1, &v1)) {
uint16_t c = sky_color(s, (xf0 + xf1) * 0.5f, py);
for (int x = x_floor0; x < x_floor1; x++) {
bg[x] = c;
if (write_px) {
px[x] = c;
}
}
return;
}
int n = x_floor1 - x_floor0;
float du = (u1 - u0) / (float)n;
float dv = (v1 - v0) / (float)n;
float fog = scanline_fog(s, (xf0 + xf1) * 0.5f, py);
uint16_t c0, c1;
checker_pair(s, fog, &c0, &c1);
float u = u0;
float v = v0;
for (int x = x_floor0; x < x_floor1; x++) {
uint16_t c = (((ifloorf(u) + ifloorf(v)) & 1) == 0) ? c0 : c1;
bg[x] = c;
if (write_px) {
px[x] = c;
}
u += du;
v += dv;
}
}
static void fill_bg_scanlines(raytrace2_t *t, const scene_t *s, int y0, int step, int write_px)
{
for (int y = y0; y < RT2_H; y += step) {
fill_bg_row(t, s, y, write_px);
}
}
static void prerender_background(raytrace2_t *t)
{
static scene_t scene;
setup_scene(&scene, 0, t->yaw, t->pitch);
fill_bg_scanlines(t, &scene, 0, 1, 0);
}
static void fill_background(raytrace2_t *t, const scene_t *s, int start, int end)
{
for (int i = start; i < end; i++) {
int gx = s_shvis[i] & 0xff;
int gy = s_shvis[i] >> 8;
int x0 = gx * RT2_CELL;
int y0 = gy * RT2_CELL;
int x1 = x0 + RT2_CELL;
int y1 = y0 + RT2_CELL;
if (x1 > RT2_W) {
x1 = RT2_W;
}
if (y1 > RT2_H) {
y1 = RT2_H;
}
int sh00 = corner_floor_shadow(s, x0, y0);
int sh10 = corner_floor_shadow(s, x1, y0);
int sh01 = corner_floor_shadow(s, x0, y1);
int sh11 = corner_floor_shadow(s, x1, y1);
fill_bg_quad(t, s, x0, y0, x1, y1, sh00, sh10, sh01, sh11);
}
}
static void trace_corners(raytrace2_t *t, const scene_t *s, int start, int end)
{
for (int i = start; i < end; i++) {
uint16_t v = s_scvis[i];
int cx = v & 0xff;
int cy = v >> 8;
int x, y;
pix_of_corner(cx, cy, &x, &y);
trace_full(s, x, y, corn(t, cx, cy));
}
}
static void fill_spheres(raytrace2_t *t, const scene_t *s, int start, int end)
{
for (int i = start; i < end; i++) {
uint16_t v = s_svis[i];
int gx = v & 0xff;
int gy = v >> 8;
int x0 = gx * RT2_CELL;
int y0 = gy * RT2_CELL;
int x1 = x0 + RT2_CELL;
int y1 = y0 + RT2_CELL;
if (x1 > RT2_W) {
x1 = RT2_W;
}
if (y1 > RT2_H) {
y1 = RT2_H;
}
fill_quad(t, s, x0, y0, x1, y1,
corn(t, gx, gy), corn(t, gx + 1, gy),
corn(t, gx, gy + 1), corn(t, gx + 1, gy + 1));
}
}
static void worker_task(void *arg)
{
(void)arg;
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if (s_phase == PH_MARK) {
mark_shadow_cells(s_scene, s_job);
mark_object_dirty(s_scene, 2);
mark_object_dirty(s_scene, 3);
} else if (s_phase == PH_PREP) {
if (!s_skip_restore) {
restore_dirty_stride(s_job, 1, 2);
}
fill_background(s_job, s_scene, s_half_sh, s_shvis_n);
} else if (s_phase == PH_CORNERS) {
trace_corners(s_job, s_scene, s_half_sc, s_scvis_n);
} else if (s_phase == PH_BG) {
fill_bg_scanlines(s_job, s_scene, 1, 2, s_bg_write_px);
} else {
fill_spheres(s_job, s_scene, s_half_s, s_svis_n);
}
xTaskNotifyGive(s_waiter);
}
}
int raytrace2_init(raytrace2_t *t)
{
memset(t, 0, sizeof(*t));
t->pixels = heap_caps_aligned_alloc(64, (size_t)RT2_W * RT2_H * 2,
MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
t->bg = heap_caps_malloc((size_t)RT2_W * RT2_H * 2,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
t->corners = heap_caps_malloc((size_t)RT2_CW * RT2_CH * sizeof(corner_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
t->vis = heap_caps_malloc((size_t)RT2_GW * RT2_GH * sizeof(uint16_t),
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
t->cvis = heap_caps_malloc((size_t)RT2_CW * RT2_CH * sizeof(uint16_t),
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
if (!t->vis) {
t->vis = heap_caps_malloc((size_t)RT2_GW * RT2_GH * sizeof(uint16_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
if (!t->cvis) {
t->cvis = heap_caps_malloc((size_t)RT2_CW * RT2_CH * sizeof(uint16_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
if (!t->pixels || !t->bg || !t->corners || !t->vis || !t->cvis) {
raytrace2_free(t);
return -1;
}
memset(t->pixels, 0, (size_t)RT2_W * RT2_H * 2);
memset(t->bg, 0, (size_t)RT2_W * RT2_H * 2);
t->vis_n = 0;
for (int gy = 0; gy < RT2_GH; gy++) {
for (int gx = 0; gx < RT2_GW; gx++) {
if (cell_on_screen(gx, gy)) {
t->vis[t->vis_n++] = (uint16_t)(gx | (gy << 8));
}
}
}
t->cvis_n = 0;
if (s_worker == NULL) {
if (xTaskCreatePinnedToCore(worker_task, "rt2", 8192, NULL, 5, &s_worker, 1) != pdPASS) {
raytrace2_free(t);
return -1;
}
}
t->yaw = 0.0f;
t->pitch = cam_pitch0();
t->view_dirty = 0;
t->show_boxes = 0;
t->force_flush = 0;
prerender_background(t);
memcpy(t->pixels, t->bg, (size_t)RT2_W * RT2_H * 2);
for (int i = 0; i < RT2_NS; i++) {
rect_clear(&s_last_obj[i]);
}
t->flush_n = 0;
add_flush(t, 0, 0, RT2_W, RT2_H);
s_fps_shown = 0;
t->fps_t0 = esp_timer_get_time();
return 0;
}
void raytrace2_orbit(raytrace2_t *t, float dx, float dy)
{
if (dx * dx + dy * dy < 4.0f) {
return;
}
float r = cam_radius();
float ny = t->yaw + dx * 0.006f;
float np = t->pitch + dy * 0.005f;
float smin = (0.12f - RT2_PIVOT_Y) / r;
if (smin < -0.99f) {
smin = -0.99f;
}
if (smin > 0.99f) {
smin = 0.99f;
}
float pmin = asinf(smin);
const float pmax = 1.35f;
if (np < pmin) {
np = pmin;
}
if (np > pmax) {
np = pmax;
}
if (ny == t->yaw && np == t->pitch) {
return;
}
t->yaw = ny;
t->pitch = np;
t->view_dirty = 1;
}
void raytrace2_toggle_boxes(raytrace2_t *t)
{
t->show_boxes ^= 1;
t->force_flush = 1;
}
void raytrace2_free(raytrace2_t *t)
{
heap_caps_free(t->pixels);
heap_caps_free(t->bg);
heap_caps_free(t->corners);
heap_caps_free(t->vis);
heap_caps_free(t->cvis);
memset(t, 0, sizeof(*t));
}
static void draw_hline(uint16_t *fb, int y, int x0, int x1, uint16_t c)
{
if (y < 0 || y >= RT2_H) {
return;
}
if (x0 < 0) {
x0 = 0;
}
if (x1 > RT2_W) {
x1 = RT2_W;
}
uint16_t *row = fb + y * RT2_W;
for (int x = x0; x < x1; x++) {
row[x] = c;
}
}
static void draw_vline(uint16_t *fb, int x, int y0, int y1, uint16_t c)
{
if (x < 0 || x >= RT2_W) {
return;
}
if (y0 < 0) {
y0 = 0;
}
if (y1 > RT2_H) {
y1 = RT2_H;
}
for (int y = y0; y < y1; y++) {
fb[y * RT2_W + x] = c;
}
}
static void outline_cell_rect(uint16_t *fb, const rect_t *r, uint16_t c)
{
if (rect_empty(r)) {
return;
}
int x0 = r->x0 * RT2_CELL;
int y0 = r->y0 * RT2_CELL;
int x1 = r->x1 * RT2_CELL;
int y1 = r->y1 * RT2_CELL;
if (x1 > RT2_W) {
x1 = RT2_W;
}
if (y1 > RT2_H) {
y1 = RT2_H;
}
if (x0 >= x1 || y0 >= y1) {
return;
}
draw_hline(fb, y0, x0, x1, c);
draw_hline(fb, y1 - 1, x0, x1, c);
draw_vline(fb, x0, y0, y1, c);
draw_vline(fb, x1 - 1, y0, y1, c);
}
static void draw_debug_boxes(raytrace2_t *t, const scene_t *s)
{
uint16_t *fb = t->pixels;
uint16_t cref = rgb565(255, 70, 230);
uint16_t csh = rgb565(255, 220, 40);
uint16_t csph = rgb565(50, 255, 90);
for (int i = 0; i < RT2_NS; i++) {
for (int j = 0; j < RT2_NS; j++) {
outline_cell_rect(fb, &s->refl[i][j], cref);
}
outline_cell_rect(fb, &s->shad[i], csh);
outline_cell_rect(fb, &s->rect[i], csph);
}
}
void raytrace2_frame(raytrace2_t *t)
{
static scene_t scene;
setup_scene(&scene, t->frames, t->yaw, t->pitch);
memset(s_dirty, 0, sizeof(s_dirty));
s_job = t;
s_scene = &scene;
s_waiter = xTaskGetCurrentTaskHandle();
int view_changed = t->view_dirty;
if (view_changed) {
s_bg_write_px = 1;
s_phase = PH_BG;
xTaskNotifyGive(s_worker);
fill_bg_scanlines(t, &scene, 0, 2, 1);
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
t->view_dirty = 0;
for (int i = 0; i < RT2_NS; i++) {
rect_clear(&s_last_obj[i]);
}
}
project_spheres(&scene);
project_refl_boxes(&scene);
project_shadow_boxes(&scene);
s_phase = PH_MARK;
xTaskNotifyGive(s_worker);
mark_sphere_cells(&scene, t);
mark_object_dirty(&scene, 0);
mark_object_dirty(&scene, 1);
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
t->flush_n = 0;
if (t->frames < 2 || view_changed || t->force_flush || t->show_boxes) {
add_flush(t, 0, 0, RT2_W, RT2_H);
t->force_flush = 0;
} else {
rect_t boxes[RT2_NS];
int nb = 0;
for (int i = 0; i < RT2_NS; i++) {
rect_t uni = s_last_obj[i];
rect_union(&uni, &s_cur[i]);
if (!rect_empty(&uni)) {
boxes[nb++] = uni;
}
}
nb = merge_overlap_rects(boxes, nb);
for (int i = 0; i < nb; i++) {
add_flush_cells(t, &boxes[i]);
}
}
for (int i = 0; i < RT2_NS; i++) {
s_last_obj[i] = s_cur[i];
}
s_skip_restore = view_changed;
s_phase = PH_PREP;
xTaskNotifyGive(s_worker);
if (!s_skip_restore) {
restore_dirty_stride(t, 0, 2);
}
fill_background(t, &scene, 0, s_half_sh);
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
s_phase = PH_CORNERS;
xTaskNotifyGive(s_worker);
trace_corners(t, &scene, 0, s_half_sc);
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
s_phase = PH_FILL;
xTaskNotifyGive(s_worker);
fill_spheres(t, &scene, 0, s_half_s);
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if (t->show_boxes) {
draw_debug_boxes(t, &scene);
}
t->frames++;
t->fps_acc++;
int64_t now = esp_timer_get_time();
if (now - t->fps_t0 >= 1000000) {
t->fps = t->fps_acc;
t->fps_acc = 0;
t->fps_t0 = now;
}
if (t->frames >= 2) {
int hit_fps = (t->fps != s_fps_shown);
for (int i = 0; i < t->flush_n && !hit_fps; i++) {
if (t->flush[i].y1 > RT2_H - 40) {
hit_fps = 1;
}
}
if (hit_fps) {
add_flush(t, 0, RT2_H - 40, RT2_W, RT2_H);
s_fps_shown = t->fps;
}
}
}
static const uint16_t k_digits[10] = {
0x7b6f, 0x2c97, 0x73e7, 0x73cf, 0x5bc9,
0x79cf, 0x79ef, 0x7249, 0x7bef, 0x7bcf,
};
static void blit_digit(uint16_t *fb, int ox, int oy, int digit, uint16_t on)
{
uint16_t bits = k_digits[digit];
for (int y = 0; y < 5; y++) {
for (int x = 0; x < 3; x++) {
if (bits & (1 << (14 - (y * 3 + x)))) {
int px = ox + x * 4;
int py = oy + y * 4;
for (int dy = 0; dy < 3; dy++) {
for (int dx = 0; dx < 3; dx++) {
fb[(py + dy) * RT2_W + (px + dx)] = on;
}
}
}
}
}
}
void raytrace2_draw_fps(raytrace2_t *t)
{
char buf[16];
int n = 0;
unsigned v = (unsigned)t->fps;
if (v == 0) {
buf[n++] = '0';
} else {
char tmp[8];
int m = 0;
while (v && m < 8) {
tmp[m++] = (char)('0' + (v % 10));
v /= 10;
}
while (m--) {
buf[n++] = tmp[m];
}
}
const int scale_w = 3 * 4 + 4;
int x = (RT2_W - n * scale_w) / 2;
int y = RT2_H - 36;
uint16_t on = rgb565(255, 255, 255);
for (int i = 0; i < n; i++) {
blit_digit(t->pixels, x + i * scale_w, y, buf[i] - '0', on);
}
}