particle trace animation on death screen

This commit is contained in:
Lephenixnoir 2023-07-23 11:39:07 +02:00
parent 3daf3240be
commit 4de6f917c1
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
9 changed files with 436 additions and 9 deletions

View File

@ -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

View File

@ -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);

View File

@ -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) {

357
src/ptrace.cpp Normal file
View File

@ -0,0 +1,357 @@
#include <azur/gint/render.h>
#include <gint/rtc.h>
#include <num/num.h>
#include <num/vec.h>
#include <random>
#include <string.h>
#include <stdlib.h>
#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);
}
}

View File

@ -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));
}
}

View File

@ -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__ */

View File

@ -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

View File

@ -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;

View File

@ -3,6 +3,7 @@
#include <num/num.h>
#include <num/vec.h>
#include <random>
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<typename T> requires(std::uniform_random_bit_generator<T>)
num num_rand_std(T &engine)
{
num r;
std::uniform_int_distribution<> d(0, 0xffff);
r.v = d(engine);
return r;
}
template<typename T> requires(std::uniform_random_bit_generator<T>)
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<typename T> requires(std::uniform_random_bit_generator<T>)
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);