Files
uv-k1-k5v3-firmware-custom/App/apps/cube3d/cube3d_app.c
T

262 lines
9.6 KiB
C

/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Cube3D — overlay app. A real-time rotating solid on the 1-bit 128x64 LCD.
* Vertices are spun by three axis rotations in Q14 fixed point (Cortex-M0+ has
* no FPU and no hardware divide), then perspective-projected with a single
* divide per vertex. Two looks, toggled with F:
* - SOLID: hidden-line removal by back-face culling (a face is drawn only when
* the signed area of its projected polygon shows it facing us).
* - WIRE : every edge, with the far hemisphere dotted for a depth cue.
* Edges use a self-clipped Bresenham writing the full 64 rows directly, since the
* resident pixel helper does not bound-check. The solids, the sine table and the
* speeds are read-only assets (gen_assets.py; faces with a uniform outward
* winding from an offline convex-hull extractor). They are copied onto the stack
* (the sine once, the displayed solid every frame), so none of them occupies the
* 4 KiB overlay and a solid can be added without touching this file.
* Pure compute, no radio.
*
* Keys: UP/DOWN speed · 1-9 shape · STAR next shape · F solid/wire ·
* MENU pause · EXIT quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include "../app_api.h"
#include "cube3d_assets.h" /* generated by gen_assets.py */
#define W 128
#define H 64
#define CX 64 /* projection centre x */
#define CY 32 /* projection centre y */
#define DIST 150 /* camera distance along +z (keeps zc > 0) */
#define FOCAL 80 /* focal length / field-of-view scale */
#define MAXV SHAPE_MAXV /* largest vertex count across the solids */
static const app_api_t *A;
static const int16_t *sinq; /* Q14 sine quadrant, on app_main's stack */
static int sin8(uint8_t angle)
{
const uint8_t quadrant = angle >> 6;
uint8_t i = angle & 63u;
if (quadrant & 1u)
i = (uint8_t)(64u - i);
const int value = sinq[i];
return quadrant >= 2u ? -value : value;
}
/* Per-frame projected screen coords + rotated depth of each vertex. */
static int16_t px[MAXV], py[MAXV], pz[MAXV];
/* Set one pixel across the full 64 rows: 0..7 -> status line, 8..63 -> fb. */
static void set_pixel(int x, int y)
{
if ((unsigned)x >= W || (unsigned)y >= H)
return;
const uint8_t bit = (uint8_t)(1u << (y & 7));
if (y < 8)
A->status_line[x] |= bit;
else
A->fb[(y >> 3) - 1][x] |= bit;
}
/* Integer Bresenham; dotted skips every other step for the depth cue. */
static void draw_edge(int x0, int y0, int x1, int y1, bool dotted)
{
const int dx = (x1 > x0 ? x1 - x0 : x0 - x1);
const int dy = -(y1 > y0 ? y1 - y0 : y0 - y1);
const int sx = (x0 < x1 ? 1 : -1);
const int sy = (y0 < y1 ? 1 : -1);
int err = dx + dy;
unsigned step = 0;
for (;;) {
if (!dotted || (step & 1u) == 0u)
set_pixel(x0, y0);
if (x0 == x1 && y0 == y1)
break;
const int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
step++;
}
}
static void clear_screen(void)
{
for (uint8_t x = 0; x < W; x++) {
A->status_line[x] = 0;
for (uint8_t p = 0; p < 7u; p++)
A->fb[p][x] = 0;
}
}
/* Signed area of a packed face's projected polygon (<0 == facing us). */
static int face_area(const uint8_t *f)
{
const uint8_t n = f[0];
const uint8_t *v = f + 1;
int sa = 0;
for (uint8_t k = 0; k < n; k++) {
const uint8_t a = v[k];
const uint8_t b = v[(k + 1u == n) ? 0u : k + 1u];
sa += (int)px[a] * py[b] - (int)px[b] * py[a];
}
return sa;
}
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api)
{
A = api;
A->backlight_on();
A->status_clear();
/* Assets copied to the stack: the sine once, the displayed solid every
* frame (nv, nf, name + NUL, vertices, packed faces; see gen_assets.py). */
int16_t sin_q[SIN_Q_LEN / 2u];
uint8_t rec[SHAPE_REC_MAX];
uint8_t nshape;
A->asset_read(SIN_Q, sin_q, sizeof(sin_q));
sinq = sin_q;
if (A->asset_read(SHAPES, &nshape, 1) != 1u || nshape == 0u)
return;
uint16_t ax = 0, ay = 0, az = 0; /* Q2 half-units: 2048 = full turn */
uint8_t shape = 0;
uint8_t speed = 4; /* 1..16, shared by all three rotation axes */
bool paused = false;
bool wire = true; /* false = solid (hidden-line) */
bool running = true;
uint8_t prevKey = APP_KEY_INVALID;
while (running) {
uint8_t key = A->get_key();
if (key == APP_KEY_SAVER) {
prevKey = APP_KEY_INVALID;
A->delay_ms(10);
A->backlight_update();
continue;
}
if (key == APP_KEY_WAKE)
key = APP_KEY_INVALID;
if (key != prevKey && key != APP_KEY_INVALID) {
A->backlight_on();
switch (key) {
case APP_KEY_EXIT:
running = false;
break;
case APP_KEY_UP:
case APP_KEY_DOWN: {
const int8_t dir = A->nav_dir(key);
if (dir > 0 && speed < 16u) speed++;
if (dir < 0 && speed > 1u) speed--;
break;
}
case APP_KEY_MENU:
paused = !paused;
break;
case APP_KEY_STAR:
shape = (uint8_t)((shape + 1u) % nshape);
break;
case APP_KEY_F:
wire = !wire;
break;
case APP_KEY_1: case APP_KEY_2: case APP_KEY_3: case APP_KEY_4:
case APP_KEY_5: case APP_KEY_6: case APP_KEY_7: case APP_KEY_8:
case APP_KEY_9:
if ((uint8_t)(key - APP_KEY_1) < nshape)
shape = (uint8_t)(key - APP_KEY_1);
break;
default:
break;
}
}
prevKey = key;
if (!running)
break;
uint8_t entry[3]; /* directory: u16 record offset, u8 record length */
A->asset_read(SHAPES + 1u + 3u * shape, entry, sizeof(entry));
const uint8_t reclen = entry[2] < sizeof(rec) ? entry[2] : (uint8_t)sizeof(rec);
A->asset_read(SHAPES + (uint16_t)(entry[0] | (entry[1] << 8)), rec, reclen);
const uint8_t nv = rec[0] < MAXV ? rec[0] : MAXV;
const uint8_t nf = rec[1];
uint8_t namelen = 0;
while (2u + namelen < reclen && rec[2u + namelen])
namelen++;
const int8_t (*verts)[3] = (const int8_t (*)[3])&rec[3u + namelen];
const uint8_t ia = (uint8_t)(ax >> 3), ib = (uint8_t)(ay >> 3), ic = (uint8_t)(az >> 3);
const int cx = sin8((uint8_t)(ia + 64u)), sxr = sin8(ia);
const int cy = sin8((uint8_t)(ib + 64u)), syr = sin8(ib);
const int cz = sin8((uint8_t)(ic + 64u)), szr = sin8(ic);
for (uint8_t i = 0; i < nv; i++) {
int x = verts[i][0], y = verts[i][1], z = verts[i][2];
int ny = (y * cx - z * sxr) >> 14; /* Rx */
int nz = (y * sxr + z * cx) >> 14;
y = ny; z = nz;
int nx = (x * cy + z * syr) >> 14; /* Ry */
nz = (z * cy - x * syr) >> 14;
x = nx; z = nz;
nx = (x * cz - y * szr) >> 14; /* Rz */
ny = (x * szr + y * cz) >> 14;
x = nx; y = ny;
const int zc = z + DIST; /* always > 0 */
px[i] = (int16_t)(CX + (x * FOCAL) / zc);
py[i] = (int16_t)(CY + (y * FOCAL) / zc);
pz[i] = (int16_t)z;
}
clear_screen();
const uint8_t *f = &rec[3u + namelen + 3u * rec[0]]; /* packed faces */
for (uint8_t i = 0; i < nf; i++) {
const uint8_t n = f[0];
if (wire || face_area(f) < 0) { /* else hidden face culled */
for (uint8_t k = 0; k < n; k++) {
const uint8_t a = f[1u + k];
const uint8_t b = f[(k + 1u == n) ? 1u : 2u + k];
const bool dotted = wire && (pz[a] + pz[b] > 0); /* far half */
draw_edge(px[a], py[a], px[b], py[b], dotted);
}
}
f += 1u + n;
}
/* Shape name: inverse label, top-left of the status bar (scan-list look). */
const uint8_t end = (uint8_t)(2u + 4u * namelen);
for (uint8_t i = 0; i <= end; i++)
A->status_line[i] = 0;
A->print_inverse((const char *)&rec[2], 2, 0, true, true, end);
A->blit_status();
A->blit_full();
if (!paused) {
uint8_t rate;
A->asset_read(ROT_RATE + speed - 1u, &rate, 1);
ax += rate;
ay += (uint16_t)(rate + rate / 2u);
az += (uint16_t)((rate + 1u) / 2u);
}
A->backlight_update();
A->delay_ms((uint32_t)(32u - speed * 2u)); /* slow low end, no added delay at level 16 */
}
}