diff --git a/idf_zacus/components/display_ui/display_ui.cpp b/idf_zacus/components/display_ui/display_ui.cpp index 50bea49..d9b2492 100644 --- a/idf_zacus/components/display_ui/display_ui.cpp +++ b/idf_zacus/components/display_ui/display_ui.cpp @@ -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); } diff --git a/idf_zacus/components/display_ui/include/intro_fx3d.h b/idf_zacus/components/display_ui/include/intro_fx3d.h index d36c57b..cf40b52 100644 --- a/idf_zacus/components/display_ui/include/intro_fx3d.h +++ b/idf_zacus/components/display_ui/include/intro_fx3d.h @@ -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. diff --git a/idf_zacus/components/display_ui/intro_fx3d.cpp b/idf_zacus/components/display_ui/intro_fx3d.cpp index c156dd2..c66a534 100644 --- a/idf_zacus/components/display_ui/intro_fx3d.cpp +++ b/idf_zacus/components/display_ui/intro_fx3d.cpp @@ -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(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(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((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.