feat(intro): pool FX 3D complet + tirage par boot

Ajout des 3 dernieres scenes 3D de l'original : starfield 3D
(vol en z + trainees), voxel landscape (heightfield raycast) et
wirecube v9 (cube filaire Bresenham). L'intro tire 3 effets
distincts au hasard parmi les 6 a chaque boot (Fisher-Yates),
sans rallonger les 16 s. Build vert.
This commit is contained in:
Claude Worker claude2
2026-06-11 04:54:17 +02:00
parent 1faba591d5
commit 9c133842a4
3 changed files with 217 additions and 9 deletions
+11 -1
View File
@@ -277,6 +277,7 @@ static volatile bool s_intro_done = false;
static lv_obj_t *s_fx_canvas; // phases B/C/D render target
static lv_color_t *s_fx_canvas_buf; // 480x320 RGB565, PSRAM
static bool s_fx_tried, s_fx_ok; // lazy one-shot setup at phase B
static fx3d_mode_t s_fx_order[3]; // 3 distinct FX drawn per boot
// Original kIntro defaults: scroll 90 px/s, sine amp 14 px, period 120 px.
#define INTRO_SCROLL_PXS 90
#define INTRO_SINE_AMP 14
@@ -673,6 +674,15 @@ static void build_intro_screen(void) {
// intro simply keeps the starfield (s_fx_ok stays false). Display task only.
static bool intro_fx_setup(void) {
if (!fx3d_init()) return false;
// Fisher-Yates over the 6-FX pool, keep the first 3: each boot shows a
// different random selection/order without lengthening the intro.
fx3d_mode_t pool[FX3D_MODE_COUNT];
for (int i = 0; i < FX3D_MODE_COUNT; i++) pool[i] = (fx3d_mode_t) i;
for (int i = FX3D_MODE_COUNT - 1; i > 0; i--) {
const int j = (int) (esp_random() % (uint32_t) (i + 1));
const fx3d_mode_t tmp = pool[i]; pool[i] = pool[j]; pool[j] = tmp;
}
for (int i = 0; i < 3; i++) s_fx_order[i] = pool[i];
s_fx_canvas_buf = (lv_color_t *) heap_caps_malloc(
(size_t) DUI_HOR_RES * DUI_VER_RES * sizeof(lv_color_t),
MALLOC_CAP_SPIRAM);
@@ -707,7 +717,7 @@ static void update_intro(uint32_t dt_ms) {
static bool skip;
skip = !skip;
if (!skip) {
fx3d_render((fx3d_mode_t) fx, s_intro_t_ms,
fx3d_render(s_fx_order[fx], s_intro_t_ms,
(uint16_t *) s_fx_canvas_buf, DUI_HOR_RES, DUI_VER_RES);
lv_obj_invalidate(s_fx_canvas);
}
@@ -15,9 +15,13 @@ extern "C" {
#define FX3D_H 160
typedef enum {
FX3D_ROTOZOOM = 0,
FX3D_DOTSPHERE = 1,
FX3D_CORRIDOR = 2,
FX3D_ROTOZOOM = 0,
FX3D_DOTSPHERE = 1,
FX3D_CORRIDOR = 2,
FX3D_STARFIELD = 3, // renderStarfield3D — z-flight + rotation + trails
FX3D_VOXEL = 4, // renderVoxelLandscape — raycast heightfield
FX3D_WIRECUBE = 5, // v9 WireCubeFx — Bresenham wireframe cube
FX3D_MODE_COUNT = 6,
} fx3d_mode_t;
// Allocate the low-res buffer + LUTs/textures (PSRAM). Idempotent.
+199 -5
View File
@@ -18,19 +18,32 @@ constexpr int kRayTexSize = 64; // FxEngine kRayTexSize
constexpr int kDotCount = 360; // ~W*H/75 clamped (original formula)
constexpr int kDotRadius = 72; // min_dim/2 - 8 clamped to [24,72]
constexpr int kDotBlobR = 2;
constexpr int kStar3DCount = 400; // ~W*H/50, capped for the 10 ms tick
constexpr int kVoxelMaxDist = 96; // FxEngine voxel_max_dist_
struct DotPt { int16_t x, y, z; };
struct DotPt { int16_t x, y, z; };
struct Star3D { int16_t x, y; uint16_t z; };
uint16_t *s_lowres; // FX3D_W * FX3D_H
uint16_t *s_roto_tex; // 128 * 128
uint16_t *s_ray_tex; // 64 * 64
DotPt *s_dots;
Star3D *s_stars3d;
uint16_t s_dot_shade[256];
int8_t s_ray_col_off[FX3D_W];
uint16_t s_ray_floor_q12[FX3D_H];
int16_t s_sin_q15[256];
uint8_t s_voxel_height[256];
uint16_t s_voxel_pal[256];
uint16_t s_voxel_proj_q8[kVoxelMaxDist + 1];
uint32_t s_rng = 0x5EED1234u;
bool s_ready = false;
uint32_t next_rand(void) {
s_rng ^= s_rng << 13; s_rng ^= s_rng >> 17; s_rng ^= s_rng << 5;
return s_rng;
}
// ---- helpers (FxEngine::rgb565 / mul565_u8 / addSat565 / sin8) ----
uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) {
@@ -78,6 +91,27 @@ void add_pixel(int x, int y, uint16_t color) {
s_lowres[idx] = add_sat565(s_lowres[idx], color);
}
void set_pixel(int x, int y, uint16_t color) {
if (x < 0 || y < 0 || x >= FX3D_W || y >= FX3D_H) return;
s_lowres[(size_t) y * FX3D_W + (size_t) x] = color;
}
// Bresenham, additive (v9 WireCubeFx::line_, max-blend approximated by add).
void add_line(int x0, int y0, int x1, int y1, uint16_t color) {
int dx = (x1 > x0) ? (x1 - x0) : (x0 - x1);
const int sx = (x0 < x1) ? 1 : -1;
int dy = (y1 > y0) ? (y0 - y1) : (y1 - y0); // -abs
const int sy = (y0 < y1) ? 1 : -1;
int err = dx + dy;
for (;;) {
add_pixel(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
const int e2 = err << 1;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
void *psram_alloc(size_t bytes) {
void *p = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM);
if (!p) p = heap_caps_malloc(bytes, MALLOC_CAP_DEFAULT);
@@ -221,6 +255,141 @@ void render_corridor(uint32_t now_ms) {
}
}
// FxEngine::renderStarfield3D — z-flight starfield, slow roll, motion trails.
void render_starfield3d(uint32_t now_ms) {
fill_lowres(rgb565(2u, 4u, 10u));
const uint8_t angle = (uint8_t) (now_ms >> 4);
const int16_t cs = cos_q15(angle);
const int16_t sn = sin_q15(angle);
const int fov = ((FX3D_W < FX3D_H) ? FX3D_W : FX3D_H) + 24;
const uint16_t z_min = 32u;
const int dz = (int) (10u + ((now_ms >> 6) & 7u));
const int cx = FX3D_W / 2, cy = FX3D_H / 2;
const uint16_t base = rgb565(240u, 248u, 255u);
for (int i = 0; i < kStar3DCount; i++) {
Star3D &star = s_stars3d[i];
const uint16_t z_prev = star.z;
const int z_next = (int) star.z - dz;
if (z_next < (int) z_min) {
star.x = (int16_t) ((int32_t) (next_rand() & 511u) - 256);
star.y = (int16_t) ((int32_t) ((next_rand() >> 9) & 511u) - 256);
star.z = (uint16_t) (256u + (next_rand() % 768u));
continue;
}
star.z = (uint16_t) z_next;
const int xr = (int) (((int32_t) star.x * cs - (int32_t) star.y * sn) >> 15);
const int yr = (int) (((int32_t) star.x * sn + (int32_t) star.y * cs) >> 15);
const int sx = cx + (xr * fov) / (int) star.z;
const int sy = cy + (yr * fov) / (int) star.z;
const int sx0 = cx + (xr * fov) / (int) z_prev;
const int sy0 = cy + (yr * fov) / (int) z_prev;
if (sx < 0 || sy < 0 || sx >= FX3D_W || sy >= FX3D_H) continue;
uint8_t brightness = (uint8_t) (255u - (star.z >> 2));
if (brightness < 40u) brightness = 40u;
set_pixel(sx, sy, mul565_u8(base, brightness));
const int dx = sx - sx0, dy = sy - sy0;
int steps = ((dx < 0 ? -dx : dx) > (dy < 0 ? -dy : dy))
? (dx < 0 ? -dx : dx) : (dy < 0 ? -dy : dy);
steps = clampv<int>(steps, 0, 10);
for (int s = 1; s <= steps; s++) {
const int x = sx0 + (dx * s) / steps;
const int y = sy0 + (dy * s) / steps;
const uint8_t fade = (uint8_t) ((brightness * (steps - s)) / (steps + 1));
add_pixel(x, y, mul565_u8(base, fade));
}
}
}
// FxEngine::renderVoxelLandscape — per-column raycast heightfield over a
// vertical sky gradient, camera drifting with time.
void render_voxel(uint32_t now_ms) {
for (int y = 0; y < FX3D_H; y++) {
const uint8_t t = (uint8_t) ((y * 255) / FX3D_H);
const uint16_t color = rgb565((uint8_t) ((8 * (255 - t)) >> 8),
(uint8_t) ((12 * (255 - t)) >> 8),
(uint8_t) ((32 * (255 - t)) >> 8));
const size_t row = (size_t) y * FX3D_W;
for (int x = 0; x < FX3D_W; x++) s_lowres[row + x] = color;
}
const int horizon = FX3D_H / 2;
const uint8_t angle = (uint8_t) (now_ms >> 6);
const uint16_t cam_x = (uint16_t) ((now_ms >> 5) & 255u);
const uint16_t cam_y = (uint16_t) ((now_ms >> 6) & 255u);
const int half = FX3D_W / 2;
for (int x = 0; x < FX3D_W; x++) {
const int dx = x - half;
const uint8_t ray_angle = (uint8_t) (angle + (uint8_t) ((dx * 24) / half));
const int16_t dir_x = cos_q15(ray_angle);
const int16_t dir_y = sin_q15(ray_angle);
int max_y = FX3D_H - 1;
for (int z = 1; z <= kVoxelMaxDist; z++) {
const int map_x = ((int) cam_x + ((dir_x * z) >> 15)) & 255;
const int map_y = ((int) cam_y + ((dir_y * z) >> 15)) & 255;
const uint8_t hh = s_voxel_height[(uint8_t) ((map_x + map_y * 3) & 255)];
const uint16_t proj = s_voxel_proj_q8[z];
int y = horizon - (int) (((unsigned) hh * proj) >> 8);
if (y < 0) y = 0;
if (y > max_y) continue;
const uint8_t shade = (uint8_t) ((z * 3 < 255) ? (255 - z * 3) : 0);
const uint16_t color = s_voxel_pal[shade];
for (int yy = y; yy <= max_y; yy++)
s_lowres[(size_t) yy * FX3D_W + x] = color;
max_y = y - 1;
if (max_y < 0) break;
}
}
}
// v9 WireCubeFx — wireframe cube, float math (8 vertices/frame, FPU is fine),
// rotation speeds and projection from the v9 defaults feel.
void render_wirecube(uint32_t now_ms) {
fill_lowres(rgb565(2u, 3u, 8u));
static const float V[8][3] = {
{-1, -1, -1}, {+1, -1, -1}, {+1, +1, -1}, {-1, +1, -1},
{-1, -1, +1}, {+1, -1, +1}, {+1, +1, +1}, {-1, +1, +1},
};
static const uint8_t E[12][2] = {
{0, 1}, {1, 2}, {2, 3}, {3, 0},
{4, 5}, {5, 6}, {6, 7}, {7, 4},
{0, 4}, {1, 5}, {2, 6}, {3, 7},
};
const float t = (float) now_ms * 0.001f;
const float ax = t * 0.9f, ay = t * 1.3f, az = t * 0.5f;
const float sx = sinf(ax), cxr = cosf(ax);
const float sy = sinf(ay), cyr = cosf(ay);
const float sz = sinf(az), czr = cosf(az);
const int cx = FX3D_W / 2, cy = FX3D_H / 2;
const float pulse = 0.5f + 0.5f * sinf(t * 2.4f);
const float scale = (float) FX3D_H * 0.30f * (1.0f + 0.20f * pulse);
const float fov = 2.2f, z_offset = 3.0f;
int px[8], py[8];
for (int i = 0; i < 8; i++) {
const float x = V[i][0], y = V[i][1], z = V[i][2];
const float x1 = x * cyr + z * sy;
const float z1 = -x * sy + z * cyr;
const float y2 = y * cxr - z1 * sx;
const float z2 = y * sx + z1 * cxr;
const float x3 = x1 * czr - y2 * sz;
const float y3 = x1 * sz + y2 * czr;
float zz = z2 + z_offset;
if (zz < 0.3f) zz = 0.3f;
const float inv = fov / zz;
px[i] = cx + (int) lroundf(x3 * inv * scale);
py[i] = cy + (int) lroundf(y3 * inv * scale);
}
const uint16_t edge = mul565_u8(rgb565(120u, 255u, 220u),
(uint8_t) (200 + (int) (55.0f * pulse)));
for (int e = 0; e < 12; e++)
add_line(px[E[e][0]], py[E[e][0]], px[E[e][1]], py[E[e][1]], edge);
}
} // namespace
extern "C" bool fx3d_init(void) {
@@ -230,11 +399,13 @@ extern "C" bool fx3d_init(void) {
s_roto_tex = (uint16_t *) psram_alloc((size_t) kRotoTexSize * kRotoTexSize * sizeof(uint16_t));
s_ray_tex = (uint16_t *) psram_alloc((size_t) kRayTexSize * kRayTexSize * sizeof(uint16_t));
s_dots = (DotPt *) psram_alloc((size_t) kDotCount * sizeof(DotPt));
if (!s_lowres || !s_roto_tex || !s_ray_tex || !s_dots) {
s_stars3d = (Star3D *) psram_alloc((size_t) kStar3DCount * sizeof(Star3D));
if (!s_lowres || !s_roto_tex || !s_ray_tex || !s_dots || !s_stars3d) {
heap_caps_free(s_lowres); s_lowres = nullptr;
heap_caps_free(s_roto_tex); s_roto_tex = nullptr;
heap_caps_free(s_ray_tex); s_ray_tex = nullptr;
heap_caps_free(s_dots); s_dots = nullptr;
heap_caps_free(s_stars3d); s_stars3d = nullptr;
return false;
}
@@ -305,6 +476,26 @@ extern "C" bool fx3d_init(void) {
s_ray_floor_q12[y] = (uint16_t) value;
}
// Starfield 3D — initModeState kStarfield3D.
for (int i = 0; i < kStar3DCount; i++) {
s_stars3d[i].x = (int16_t) ((int32_t) (next_rand() & 511u) - 256);
s_stars3d[i].y = (int16_t) ((int32_t) ((next_rand() >> 9) & 511u) - 256);
s_stars3d[i].z = (uint16_t) (128u + (next_rand() % 896u));
}
// Voxel landscape — initModeState kVoxelLandscape (sine heightfield,
// shaded green palette, perspective projection table).
for (int i = 0; i < 256; i++) {
const int16_t s1 = sin_q15((uint8_t) i);
const int16_t s2 = sin_q15((uint8_t) (i * 3));
s_voxel_height[i] = (uint8_t) clampv<int>(s1 / 512 + s2 / 1024 + 128, 0, 255);
s_voxel_pal[i] = mul565_u8(rgb565(30u, 220u, 80u), (uint8_t) i);
}
for (int z = 1; z <= kVoxelMaxDist; z++) {
s_voxel_proj_q8[z] = (uint16_t) clampv<int>((70 * 256) / (z + 8), 0, 65535);
}
s_voxel_proj_q8[0] = 0u;
s_ready = true;
return true;
}
@@ -315,9 +506,12 @@ extern "C" void fx3d_render(fx3d_mode_t mode, uint32_t t_ms, uint16_t *dst,
return;
}
switch (mode) {
case FX3D_ROTOZOOM: render_rotozoom(t_ms); break;
case FX3D_DOTSPHERE: render_dotsphere(t_ms); break;
case FX3D_CORRIDOR: render_corridor(t_ms); break;
case FX3D_ROTOZOOM: render_rotozoom(t_ms); break;
case FX3D_DOTSPHERE: render_dotsphere(t_ms); break;
case FX3D_CORRIDOR: render_corridor(t_ms); break;
case FX3D_STARFIELD: render_starfield3d(t_ms); break;
case FX3D_VOXEL: render_voxel(t_ms); break;
case FX3D_WIRECUBE: render_wirecube(t_ms); break;
default: return;
}
// 2x pixel doubling, two 32-bit writes per source pixel, row duplicated.