diff --git a/CMakeLists.txt b/CMakeLists.txt index 665ce22..1efc087 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,8 +19,9 @@ set(SOURCES src/level.cpp src/log.cpp src/main.cpp - src/render.cpp src/menu.cpp + src/ptrace.cpp + src/render.cpp src/util.cpp) set(ASSETS diff --git a/src/fsblend.cpp b/src/fsblend.cpp index ff4185e..a738f51 100644 --- a/src/fsblend.cpp +++ b/src/fsblend.cpp @@ -32,8 +32,8 @@ void shader_fsblend(uint16_t color, int alpha) { prof_enter(azrp_perf_cmdgen); - struct command *cmd = - (struct command *)azrp_new_command(sizeof *cmd, 0, azrp_frag_count); + struct command *cmd = (struct command *) + azrp_new_command(sizeof *cmd, 0, azrp_frag_count); if(cmd) { cmd->shader_id = BOSONX_SHADER_FSBLEND; cmd->alpha = (alpha & 31); diff --git a/src/main.cpp b/src/main.cpp index 838219a..b845e2d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -294,6 +294,8 @@ int play_level(int level_id) bool death_2 = (player->height < LEVEL_RADIUS - KILL_PLANE_RADIUS); if(!game.debug.invincible && (death_1 || death_2)) { + if(player->stance != player::Collided) + shader_ptrace_reset(); player->stance = player::Collided; } else if(standing_on && standing_on->type == PLATFORM_BLUE) { diff --git a/src/ptrace.cpp b/src/ptrace.cpp new file mode 100644 index 0000000..0bbac75 --- /dev/null +++ b/src/ptrace.cpp @@ -0,0 +1,357 @@ +#include +#include +#include +#include +#include +#include +#include +#include "settings.h" +#include "render.h" +#include "util.h" +using namespace libnum; + +uint8_t BOSONX_SHADER_PTRACE = -1; +static azrp_shader_t bosonx_shader_ptrace; + +__attribute__((constructor)) +static void register_shader(void) +{ + BOSONX_SHADER_PTRACE = azrp_register_shader(bosonx_shader_ptrace, NULL); +} + +//--- + +struct command { + uint8_t shader_id; + uint8_t y; + int16_t x; + int16_t pty, height; + uint16_t bgcolor; + num t; +}; + +/* Nested circles */ +#define CLIGHT_R1 10 +#define CLIGHT_R2 15 +#define CDARK_R1 36 +#define CBEAMS1_R 41 +#define CDARK_R2 45 +#define CMAIN_R 48 +#define CBEAMS2_R 52 +#define CMAX_R 55 + +/* Ticks */ +#define TICK_SIZE 7 +#define TICK_COUNT 17 /* > 1 */ + +/* Traces */ +#define TRACES_R CDARK_R1 +#define TRACES_NUM_THREADS 24 /* < 255*/ +#define TRACES_PALETTE_SIZE (TRACES_NUM_THREADS+1) +#define TRACES_TURN_MIN num(0.02) +#define TRACES_TURN_RATIO 5 +#define TRACES_TURN_MAX (TRACES_TURN_RATIO * TRACES_TURN_MIN) + +/* Precomputed and preallocated data */ + +/* Concentric circles pattern */ +static uint8_t circles[CMAIN_R+1][CMAIN_R+1]; +/* Trace image palette */ +static uint16_t palette[TRACES_PALETTE_SIZE] = { 0 }; +/* Trace turn vectors */ +static vec2 trace_turns[TRACES_TURN_RATIO * TRACES_R]; +/* Ticks tileset */ +static image_t *img_ticks = NULL; +/* Trace image (regenerated every frame */ +static image_t *img_trace = NULL; +/* Random seed for the entire animation */ +static int seed = 0; + +static void add_circle(int r, int value) +{ + int x = -r; + int y = 0; + int err = 2 - 2 * r; + + do { + for(int i = x + CMAIN_R; i <= CMAIN_R; i++) + circles[CMAIN_R - y][i] = value; + r = err; + if(r <= y) + err += ++y * 2 + 1; + if(r > x || err > y) + err += ++x * 2 + 1; + } + while(x <= 0); +} + +__attribute__((constructor)) +static void precompute_circles(void) +{ + memset(circles, 0, sizeof circles); + add_circle(CMAIN_R, 1); + add_circle(CDARK_R2, 2); + add_circle(CDARK_R1, 1); + add_circle(CLIGHT_R2, 3); + add_circle(CLIGHT_R1, 1); +} + +__attribute__((constructor)) +static void precompute_ticks(void) +{ + img_ticks = image_alloc(TICK_SIZE*TICK_COUNT, TICK_SIZE, IMAGE_P8_RGB565A); + if(!img_ticks) + return; + + static uint16_t img_ticks_palette[2] = { 0x5555, C_WHITE }; + image_set_palette(img_ticks, img_ticks_palette, 2, false); + + num angle_step = num(1.57080) / (TICK_COUNT - 1); + + for(int i = 0; i < TICK_COUNT; i++) { + num alpha = i * angle_step; + vec2 D(num_cos(alpha), num_sin(alpha)); + + /* num_cos / num_sin rounding... */ + if(i == 0) + D = vec2(1, 0); + else if(i == TICK_COUNT - 1) + D = vec2(0, 1); + + vec2 norm_D(-D.y, D.x); + + /* Render the segment that goes through the center with slope alpha, + rendering with width 1 and length TICK_SIZE / 2 on both sides. */ + + for(int y = 0; y < TICK_SIZE; y++) + for(int x = 0; x < TICK_SIZE; x++) { + vec2 p(+num(x) - num(TICK_SIZE - 1) / 2, + -num(y) + num(TICK_SIZE - 1) / 2); + + if(D.dot(p).abs() <= TICK_SIZE / 2 && norm_D.dot(p).abs() <= 1) + image_set_pixel(img_ticks, i * TICK_SIZE + x, y, 0x81); + else + image_set_pixel(img_ticks, i * TICK_SIZE + x, y, 0x80); + } + } +} + +static void render_tick(int cx, int cy, int radius, num angle) +{ + num c = num_cos(angle); + num s = num_sin(angle); + + bool flip = false; + + /* Normalize to 0..π/2 */ + if(angle < 0) { + angle = -angle; + flip = !flip; + } + angle = angle % num(3.14159); + if(angle > num(1.57080)) { + angle = num(3.14159) - angle; + flip = !flip; + } + + int n = (angle * TICK_COUNT / num(1.57080)).ifloor(); + if(n < 0) + n = 0; + if(n >= TICK_COUNT) + n = TICK_COUNT - 1; + azrp_subimage( + cx + int(c * radius) - TICK_SIZE / 2, + cy - int(s * radius) - TICK_SIZE / 2, + img_ticks, n * TICK_SIZE, 0, TICK_SIZE, TICK_SIZE, + flip ? IMAGE_HFLIP : 0); +} + +static void bosonx_shader_ptrace(void *, void *cmd0, void *frag0) +{ + struct command *cmd = (struct command *)cmd0; + uint16_t *frag = (uint16_t *)frag0; + frag += azrp_width * cmd->y + cmd->x; + + int pty = cmd->pty; + int h = azrp_frag_height - cmd->y; + if(h > cmd->height) + h = cmd->height; + + cmd->height -= h; + cmd->y = 0; + cmd->pty += h; + + uint16_t palette[4] = { 0, + render_blend(cmd->bgcolor, C_BLACK, 10), + render_blend(cmd->bgcolor, C_BLACK, 16), + render_blend(cmd->bgcolor, C_BLACK, 4), + }; + + do { + int ly = pty - CMAX_R; + uint16_t *fragc = frag + CMAX_R; + + if(ly >= -CMAIN_R && ly <= CMAIN_R) { + int data_y = (ly <= 0) ? ly + CMAIN_R : CMAIN_R - ly; + + for(int lx = -CMAIN_R; lx <= 0; lx++) { + int8_t c = circles[data_y][lx + CMAIN_R]; + if(c) + fragc[lx] = palette[c]; + } + for(int lx = 1; lx <= CMAIN_R; lx++) { + int8_t c = circles[data_y][CMAIN_R - lx]; + if(c) + fragc[lx] = palette[c]; + } + } + + frag += azrp_width; + pty++; + } + while(--h > 0); +} + +/* Cannot use a constructor here because compiler schedules the global object + constructor (which sets everything to 0) to run after global constructors */ +static void precompute_turn_vectors(void) +{ + static bool computed = false; + if(computed) + return; + + for(int i = 0; i < TRACES_TURN_RATIO * TRACES_R; i++) { + trace_turns[i].x = num_cos(i * TRACES_TURN_MIN); + trace_turns[i].y = num_sin(i * TRACES_TURN_MIN); + } + computed = true; +} + +void generate_traces(uint16_t color, bool successful, num t) +{ + int const N = 2 * TRACES_R; + precompute_turn_vectors(); + + if(!img_trace) { + img_trace = image_alloc(N, N, IMAGE_P8_RGB565A); + if(!img_trace) + return; + image_set_palette(img_trace, palette, TRACES_PALETTE_SIZE, false); + } + + /* Clear out the entire image */ + uint8_t *px = (uint8_t *)img_trace->data; + memset(px, 0x80, N * N); + + /* Generate the palette */ + int R5 = (color >> 11); + int G6 = (color >> 5) & 0x3f; + int B5 = (color & 0x1f); + + for(int i = 1; i < TRACES_PALETTE_SIZE; i++) { + /* alpha=31: color, alpha=0: white */ + int alpha = (i >= TRACES_NUM_THREADS / 4) ? 0 : + 31 - (i * 31) / (TRACES_NUM_THREADS / 4); + int R = ((alpha * R5) + (31 - alpha) * 0x1f) / 31; + int G = ((alpha * G6) + (31 - alpha) * 0x3f) / 31; + int B = ((alpha * B5) + (31 - alpha) * 0x1f) / 31; + palette[i] = (R << 11) | (G << 5) | B; + } + + std::minstd_rand r(seed); + std::minstd_rand r2(seed+1); + + int steps = (t * TRACES_R / TIME_PTRACE_ANIMATION).ifloor(); + if(steps > TRACES_R) + steps = TRACES_R; + + for(int i = 0; i < TRACES_NUM_THREADS; i++) { + vec2 dir = num_rand_vec2_std(r); + int turn_factor = 1 + (r2() % 4); + + for(int s = 0; s < steps; s++) { + vec2 straight = dir * s; + vec2 rotated = straight; + + if(successful) { + vec2 const &turn = trace_turns[s * turn_factor]; + rotated.x = straight.x * turn.x - straight.y * turn.y; + rotated.y = straight.x * turn.y + straight.y * turn.x; + } + + int x = (N / 2 + rotated.x.ifloor()); + int y = (N / 2 - rotated.y.ifloor()); + + /* "Increase" the color of the pixel. The palette is a gradient so + this will imitate an overlay effect */ + if((unsigned)y < N) { + if((unsigned)x < N) + px[N * y + x]++; + if((unsigned)x+1 < N) + px[N * y + x+1]++; + } + if((unsigned)y+1 < N) { + if((unsigned)x < N) + px[N * (y+1) + x]++; + if((unsigned)x+1 < N) + px[N * (y+1) + x+1]++; + } + } + } + + /* Make sure every value is within the palette range */ + for(int i = 0; i < N * N; i++) { + if((uint8_t)(px[i] - 0x80) > TRACES_PALETTE_SIZE) + px[i] = 0x80 + TRACES_PALETTE_SIZE - 1; + } +} + +void shader_ptrace_reset(void) +{ + seed = rtc_ticks(); +} + +void shader_ptrace(int x, int y, uint16_t color, uint16_t bgcolor, + bool successful, num t) +{ + prof_enter(azrp_perf_cmdgen); + + t = num_clamp(t, 0, TIME_PTRACE_ANIMATION); + + int frag_first, frag_offset, frag_count; + int height = 2 * CMAX_R + 1; + azrp_config_get_lines(y, height, &frag_first, &frag_offset, &frag_count); + + struct command *cmd = (struct command *) + azrp_new_command(sizeof *cmd, frag_first, frag_count); + if(cmd) { + cmd->shader_id = BOSONX_SHADER_PTRACE; + cmd->y = frag_offset; + cmd->x = x; + cmd->pty = 0; + cmd->height = height; + cmd->bgcolor = bgcolor; + cmd->t = t; + } + + prof_leave(azrp_perf_cmdgen); + + /* Start with 4/8 ticks and go up to 12/20 if un/successful */ + int N1 = successful ? 8 : 4, N2 = successful ? 20 : 12; + int N = N1 +(N2 * t / TIME_PTRACE_ANIMATION).ifloor(); + + std::minstd_rand r(seed); + for(int i = 0; i < N; i++) { + num speed = num_rand_between_std(r, -1, 1); + num orig = num_rand_between_std(r, 0, num(6.28)); + render_tick(x + CMAX_R, y + CMAX_R, CBEAMS1_R, orig + t * speed); + render_tick(x + CMAX_R, y + CMAX_R, CBEAMS2_R, -orig - t * speed); + } + + generate_traces(color, successful, t); + if(img_trace) { + azrp_image(x + CMAX_R - img_trace->width / 2, + y + CMAX_R - img_trace->height / 2, + img_trace); + } +} diff --git a/src/render.cpp b/src/render.cpp index e8cc20c..66a3c30 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -348,7 +348,6 @@ static int cubic(int start, int end, num t, num tmax) void render_game(struct game &game, prof_t *perf_comp) { azrp_perf_clear(); - azrp_clear(game.level.bgcolor); game.perf.effect_bpp = prof_make(); struct player *player = &game.player; @@ -359,6 +358,12 @@ void render_game(struct game &game, prof_t *perf_comp) char energy_str[32]; sprintf(energy_str, "%.2f%%", (float)player->energy_percent); + bool bg_done = false; + if(alive || game.t_death < num(1.25)) { + azrp_clear(game.level.bgcolor); + bg_done = true; + } + /* Standard gameplay before collision */ if(alive) { prof_enter_norec(*perf_comp); @@ -393,9 +398,14 @@ void render_game(struct game &game, prof_t *perf_comp) } /* Background fading to end screen */ if(game.t_death > num(0.75)) { - num t_fadeout = (game.t_death - num(0.75)) * num(2.0); - int alpha = (int)(20 * min(t_fadeout, num(1.0))); - shader_fsblend(C_BLACK, alpha); + if(bg_done) { + num t_fadeout = (game.t_death - num(0.75)) * num(2.0); + int alpha = (int)(16 * min(t_fadeout, num(1.0))); + shader_fsblend(C_BLACK, alpha); + } + else { + azrp_clear(render_blend(game.level.bgcolor, C_BLACK, 16)); + } } /* Particle explosion */ @@ -453,7 +463,9 @@ void render_game(struct game &game, prof_t *perf_comp) } if(t_endscreen >= num(0.6)) { ye += 30; - // TODO: Crazy particle trace animation + // TODO: Level-specific particle trace animation color? + shader_ptrace(xe, ye, C_RGB(24, 24, 8), game.level.bgcolor, + game.player.energy_percent >= 100, t_endscreen - num(0.6)); } } diff --git a/src/render.h b/src/render.h index 70ff0be..134235e 100644 --- a/src/render.h +++ b/src/render.h @@ -118,4 +118,10 @@ void render_game(struct game &game, prof_t *perf_comp); /* Alpha blend an opaque color over the full screen (alpha = 0..31) */ void shader_fsblend(uint16_t color, int alpha); +/* "Particule trace" graphics on end screen */ +void shader_ptrace(int x, int y, uint16_t color, uint16_t bgcolor, + bool successful, num t); +/* Reset the shader (to be called once per death, for randomness and stuff) */ +void shader_ptrace_reset(void); + #endif /* __RENDERH__ */ diff --git a/src/settings.h b/src/settings.h index a6e6994..40a50ad 100644 --- a/src/settings.h +++ b/src/settings.h @@ -35,6 +35,8 @@ using namespace libnum; #define TIME_JUMP_THRUST_MIN num(0.25) /* Duration of the death animation (s). */ #define TIME_DEATH_ANIMATION num(2.0) +/* Duration of the particle trace animation. */ +#define TIME_PTRACE_ANIMATION num(2.0) /* Vertical FOV, in degrees */ #define RENDER_FOV 120.0 diff --git a/src/util.cpp b/src/util.cpp index 737dd67..b1b8da7 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -41,6 +41,17 @@ num num_rand(void) return r; } +num num_rand_between(num low, num high) +{ + num width = high - low; + if(width <= 0) + return low; + + num r; + r.v = rand() % width.v; + return r + low; +} + vec3 vec_rotate_around_z(vec3 v, vec2 rotator) { num c = rotator.x; diff --git a/src/util.h b/src/util.h index 386e274..7cce9c6 100644 --- a/src/util.h +++ b/src/util.h @@ -3,6 +3,7 @@ #include #include +#include using namespace libnum; /* A platform rectangle, aligned with the camera's view */ @@ -19,9 +20,44 @@ num num_sin(num a); /* Clamp a num between two bounds. */ num num_clamp(num t, num lower_bound, num upper_bound); -/* Random num between 0 and 1. */ +/* Uniform random num between 0 and 1. */ num num_rand(void); +/* Uniform random num between low and high. */ +num num_rand_between(num low, num high); + +/* Random number functions based on C++ standard PRNGs. */ + +template requires(std::uniform_random_bit_generator) +num num_rand_std(T &engine) +{ + num r; + std::uniform_int_distribution<> d(0, 0xffff); + r.v = d(engine); + return r; +} + +template requires(std::uniform_random_bit_generator) +num num_rand_between_std(T &engine, num low, num high) +{ + num r; + std::uniform_int_distribution<> d(low.v, high.v); + r.v = d(engine); + return r; +} + +template requires(std::uniform_random_bit_generator) +vec2 num_rand_vec2_std(T &engine) +{ + vec2 r; + do { + r.x = num_rand_between_std(engine, -1, 1); + r.y = num_rand_between_std(engine, -1, 1); + } + while(r.x * r.x + r.y * r.y > 1); + return r.normalize(); +} + /* String representation of various objects (static strings rotating; can use up to 8 of them at once). */ char const *str(num x);