/* * This software is licensed under the terms of the MIT License. * See COPYING for further information. * --- * Copyright (c) 2011-2024, Lukas Weber . * Copyright (c) 2012-2024, Andrei Alexeyev . */ #include "stagedraw.h" #include "entity.h" #include "events.h" #include "global.h" #include "replay/struct.h" #include "resource/postprocess.h" #include "stageobjects.h" #include "stagetext.h" #include "util/env.h" #include "util/fbmgr.h" #include "util/graphics.h" #include "video.h" #ifdef DEBUG #define GRAPHS_DEFAULT 1 #define OBJPOOLSTATS_DEFAULT 0 #else #define GRAPHS_DEFAULT 0 #define OBJPOOLSTATS_DEFAULT 0 #endif #define SPELL_INTRO_DURATION 120 #define SPELL_INTRO_TIME_FACTOR 0.8 static struct { struct { ShaderProgram *shader; Font *font; struct { Color active; Color inactive; Color dark; Color label; Color label_power; Color label_value; Color label_graze; Color label_voltage; } color; } hud_text; struct { ShaderProgram *fxaa; } shaders; PostprocessShader *viewport_pp; FBPair fb_pairs[NUM_FBPAIRS]; FBPair powersurge_fbpair; FBPair *current_postprocess_fbpair; ManagedFramebufferGroup *mfb_group; StageDrawEvents events; struct { float alpha; float target_alpha; } clear_screen; bool framerate_graphs; bool objpool_stats; #ifdef DEBUG Sprite dummy; #endif } stagedraw = { .hud_text.color = { // NOTE: premultiplied alpha .active = { 1.00, 1.00, 1.00, 1.00 }, .inactive = { 0.50, 0.50, 0.50, 0.70 }, .dark = { 0.30, 0.30, 0.30, 0.70 }, .label = { 1.00, 1.00, 1.00, 1.00 }, .label_power = { 1.00, 0.50, 0.50, 1.00 }, .label_value = { 0.30, 0.60, 1.00, 1.00 }, .label_graze = { 0.50, 1.00, 0.50, 1.00 }, .label_voltage = { 0.80, 0.50, 1.00, 1.00 }, } }; bool stage_draw_is_initialized(void) { return stagedraw.mfb_group; } static double fb_scale(void) { float vp_width, vp_height; video_get_viewport_size(&vp_width, &vp_height); return (double)vp_height / SCREEN_H; } static void set_fb_size(StageFBPair fb_id, int *w, int *h, float scale_worst, float scale_best) { double scale = fb_scale(); if(fb_id == FBPAIR_FG_AUX || fb_id == FBPAIR_BG_AUX) { scale_worst *= 0.5; } int pp_qual = config_get_int(CONFIG_POSTPROCESS); // We might lerp between these in the future if(pp_qual < 2) { scale *= scale_worst; } else { scale *= scale_best; } log_debug("%u %f %f %f %i", fb_id, scale, scale_worst, scale_best, pp_qual); switch(fb_id) { case FBPAIR_BG: scale *= config_get_float(CONFIG_BG_QUALITY); // fallthrough default: scale *= config_get_float(CONFIG_FG_QUALITY); break; } scale = max(0.1, scale); *w = round(VIEWPORT_W * scale); *h = round(VIEWPORT_H * scale); } typedef struct StageFramebufferResizeParams { struct { float worst, best; } scale; StageFBPair scaling_base; int refs; } StageFramebufferResizeParams; static void stage_framebuffer_resize_strategy(void *userdata, IntExtent *out_dimensions, FloatRect *out_viewport) { StageFramebufferResizeParams *rp = userdata; set_fb_size(rp->scaling_base, &out_dimensions->w, &out_dimensions->h, rp->scale.worst, rp->scale.best); *out_viewport = (FloatRect) { 0, 0, out_dimensions->w, out_dimensions->h }; } static void stage_framebuffer_resize_strategy_cleanup(void *userdata) { StageFramebufferResizeParams *rp = userdata; if(--rp->refs <= 0) { mem_free(rp); } } static bool stage_draw_event(SDL_Event *e, void *arg) { if(!IS_TAISEI_EVENT(e->type)) { return false; } switch(TAISEI_EVENT(e->type)) { case TE_FRAME: { fapproach_p(&stagedraw.clear_screen.alpha, stagedraw.clear_screen.target_alpha, 0.01); break; } } return false; } static void stage_draw_fbpair_create( FBPair *pair, int num_attachments, FBAttachmentConfig *attachments, const StageFramebufferResizeParams *resize_params, const char *name ) { StageFramebufferResizeParams *rp = memdup(resize_params, sizeof(*resize_params)); rp->refs = 2; FramebufferConfig fbconf = { 0 }; fbconf.attachments = attachments; fbconf.num_attachments = num_attachments; fbconf.resize_strategy.resize_func = stage_framebuffer_resize_strategy; fbconf.resize_strategy.userdata = rp; fbconf.resize_strategy.cleanup_func = stage_framebuffer_resize_strategy_cleanup; fbmgr_group_fbpair_create(stagedraw.mfb_group, name, &fbconf, pair); } static void stage_draw_setup_framebuffers(void) { FBAttachmentConfig a[2], *a_color, *a_depth; memset(a, 0, sizeof(a)); a_color = &a[0]; a_depth = &a[1]; a_color->attachment = FRAMEBUFFER_ATTACH_COLOR0; a_depth->attachment = FRAMEBUFFER_ATTACH_DEPTH; StageFramebufferResizeParams rp_fg = { .scaling_base = FBPAIR_FG, .scale.best = 1, .scale.worst = 1 }; StageFramebufferResizeParams rp_fg_aux = { .scaling_base = FBPAIR_FG_AUX, .scale.best = 1, .scale.worst = 1 }; StageFramebufferResizeParams rp_bg = { .scaling_base = FBPAIR_BG, .scale.best = 1, .scale.worst = 1 }; StageFramebufferResizeParams rp_bg_aux = { .scaling_base = FBPAIR_BG_AUX, .scale.best = 1, .scale.worst = 1 }; // Set up some parameters shared by all attachments TextureParams tex_common = { .filter.min = TEX_FILTER_LINEAR, .filter.mag = TEX_FILTER_LINEAR, .wrap.s = TEX_WRAP_MIRROR, .wrap.t = TEX_WRAP_MIRROR, }; memcpy(&a_color->tex_params, &tex_common, sizeof(tex_common)); memcpy(&a_depth->tex_params, &tex_common, sizeof(tex_common)); a_depth->tex_params.type = TEX_TYPE_DEPTH; // Foreground: 1 RGB texture per FB a_color->tex_params.type = TEX_TYPE_RGB_16; stage_draw_fbpair_create(stagedraw.fb_pairs + FBPAIR_FG, 1, a, &rp_fg, "Stage FG"); // Foreground auxiliary: 1 RGBA texture per FB a_color->tex_params.type = TEX_TYPE_RGBA_8; stage_draw_fbpair_create(stagedraw.fb_pairs + FBPAIR_FG_AUX, 1, a, &rp_fg_aux, "Stage FG AUX"); // Background: 1 HDR RGBA texture + depth per FB a_color->tex_params.type = TEX_TYPE_RGBA_16_FLOAT; stage_draw_fbpair_create(stagedraw.fb_pairs + FBPAIR_BG, 2, a, &rp_bg, "Stage BG"); // Background auxiliary: 1 RGBA texture per FB a_color->tex_params.type = TEX_TYPE_RGBA_8; stage_draw_fbpair_create(stagedraw.fb_pairs + FBPAIR_BG_AUX, 1, a, &rp_bg_aux, "Stage BG AUX"); // CAUTION: should be at least 16-bit, lest the feedback shader do an oopsie! a_color->tex_params.type = TEX_TYPE_RGBA_16; stagedraw.powersurge_fbpair.front = stage_add_background_framebuffer("Powersurge effect FB 1", 0.125, 0.25, 1, a); stagedraw.powersurge_fbpair.back = stage_add_background_framebuffer("Powersurge effect FB 2", 0.125, 0.25, 1, a); } static Framebuffer *add_custom_framebuffer( const char *label, StageFBPair fbtype, float scale_worst, float scale_best, uint num_attachments, FBAttachmentConfig attachments[num_attachments] ) { StageFramebufferResizeParams rp = { 0 }; rp.scaling_base = fbtype; rp.scale.worst = scale_worst; rp.scale.best = scale_best; FramebufferConfig fbconf = { 0 }; fbconf.attachments = attachments; fbconf.num_attachments = num_attachments; fbconf.resize_strategy.resize_func = stage_framebuffer_resize_strategy; fbconf.resize_strategy.userdata = memdup(&rp, sizeof(rp)); fbconf.resize_strategy.cleanup_func = stage_framebuffer_resize_strategy_cleanup; return fbmgr_group_framebuffer_create(stagedraw.mfb_group, label, &fbconf); } Framebuffer *stage_add_foreground_framebuffer(const char *label, float scale_worst, float scale_best, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) { return add_custom_framebuffer(label, FBPAIR_FG, scale_worst, scale_best, num_attachments, attachments); } Framebuffer *stage_add_background_framebuffer(const char *label, float scale_worst, float scale_best, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) { return add_custom_framebuffer(label, FBPAIR_BG, scale_worst, scale_best, num_attachments, attachments); } Framebuffer *stage_add_static_framebuffer(const char *label, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) { FramebufferConfig fbconf = { 0 }; fbconf.attachments = attachments; fbconf.num_attachments = num_attachments; return fbmgr_group_framebuffer_create(stagedraw.mfb_group, label, &fbconf); } static void stage_draw_destroy_framebuffers(void) { fbmgr_group_destroy(stagedraw.mfb_group); stagedraw.mfb_group = NULL; } void stage_draw_preload(ResourceGroup *rg) { res_group_preload(rg, RES_POSTPROCESS, RESF_OPTIONAL, "viewport", NULL); res_group_preload(rg, RES_SPRITE, RESF_DEFAULT, "hud/heart", "hud/star", "star", NULL); res_group_preload(rg, RES_TEXTURE, RESF_DEFAULT, "powersurge_flow", "titletransition", "hud", NULL); res_group_preload(rg, RES_MODEL, RESF_DEFAULT, "hud", NULL); res_group_preload(rg, RES_SHADER_PROGRAM, RESF_DEFAULT, "ingame_menu", "powersurge_effect", "powersurge_feedback", "sprite_circleclipped_indicator", "text_hud", "text_stagetext", // TODO: Maybe don't preload this if FXAA is disabled. // But then we also have to handle the edge case of it being enabled later mid-game. "fxaa", #ifdef DEBUG "sprite_filled_circle", #endif NULL); res_group_preload(rg, RES_FONT, RESF_DEFAULT, "mono", "small", "monosmall", NULL); if(stage_is_demo_mode()) { res_group_preload(rg, RES_SHADER_PROGRAM, RESF_DEFAULT, "text_demo", NULL); } stagedraw.framerate_graphs = env_get("TAISEI_FRAMERATE_GRAPHS", GRAPHS_DEFAULT); stagedraw.objpool_stats = env_get("TAISEI_OBJPOOL_STATS", OBJPOOLSTATS_DEFAULT); if(stagedraw.framerate_graphs) { res_group_preload(rg, RES_SHADER_PROGRAM, RESF_DEFAULT, "graph", NULL); } if(stagedraw.objpool_stats) { res_group_preload(rg, RES_FONT, RESF_DEFAULT, "monotiny", NULL); } } void stage_draw_init(void) { stagedraw.mfb_group = fbmgr_group_create(); stagedraw.viewport_pp = res_get_data(RES_POSTPROCESS, "viewport", RESF_OPTIONAL); stagedraw.hud_text.shader = res_shader("text_hud"); stagedraw.hud_text.font = res_font("standard"); stagedraw.shaders.fxaa = res_shader("fxaa"); r_shader_standard(); #ifdef DEBUG stagedraw.dummy.tex = res_sprite("star")->tex; stagedraw.dummy.w = 1; stagedraw.dummy.h = 1; #endif stage_draw_setup_framebuffers(); stagedraw.clear_screen.alpha = 0; stagedraw.clear_screen.target_alpha = 0; events_register_handler(&(EventHandler) { stage_draw_event, NULL, EPRIO_SYSTEM, }); COEVENT_INIT_ARRAY(stagedraw.events); } void stage_draw_shutdown(void) { COEVENT_CANCEL_ARRAY(stagedraw.events); events_unregister_handler(stage_draw_event); stage_draw_destroy_framebuffers(); } FBPair *stage_get_fbpair(StageFBPair id) { assert(id >= 0 && id < NUM_FBPAIRS); return stagedraw.fb_pairs + id; } FBPair *stage_get_postprocess_fbpair(void) { return NOT_NULL(stagedraw.current_postprocess_fbpair); } static void stage_draw_collision_areas(void) { #ifdef DEBUG static bool enabled, keystate_saved; bool keystate = gamekeypressed(KEY_HITAREAS); if(keystate ^ keystate_saved) { if(keystate) { enabled = !enabled; } keystate_saved = keystate; } if(!enabled) { return; } r_shader("sprite_filled_circle"); r_uniform_vec4("color_inner", 0, 0, 0, 1); r_uniform_vec4("color_outer", 1, 1, 1, 0.1); for(Projectile *p = global.projs.first; p; p = p->next) { cmplx gsize = projectile_graze_size(p); if(re(gsize)) { r_draw_sprite(&(SpriteParams) { .color = RGB(0, 0.5, 0.5), .sprite_ptr = &stagedraw.dummy, .pos = { re(p->pos), im(p->pos) }, .rotation.angle = p->angle + M_PI/2, .scale = { .x = re(gsize), .y = im(gsize) }, .blend = BLEND_SUB, }); } } r_flush_sprites(); r_uniform_vec4("color_inner", 0.0, 1.0, 0.0, 0.75); r_uniform_vec4("color_outer", 0.0, 0.5, 0.5, 0.75); for(Projectile *p = global.projs.first; p; p = p->next) { r_draw_sprite(&(SpriteParams) { .sprite_ptr = &stagedraw.dummy, .pos = { re(p->pos), im(p->pos) }, .rotation.angle = p->angle + M_PI/2, .scale = { .x = re(p->collision_size), .y = im(p->collision_size) }, .blend = BLEND_ALPHA, }); } for(Enemy *e = global.enemies.first; e; e = e->next) { float hurt_radius = enemy_get_hurt_radius(e); if(hurt_radius > 0) { r_draw_sprite(&(SpriteParams) { .sprite_ptr = &stagedraw.dummy, .pos = { re(e->pos), im(e->pos) }, .scale = { .x = hurt_radius * 2, .y = hurt_radius * 2 }, .blend = BLEND_ALPHA, }); } } if(global.boss && boss_is_player_collision_active(global.boss)) { r_draw_sprite(&(SpriteParams) { .sprite_ptr = &stagedraw.dummy, .pos = { re(global.boss->pos), im(global.boss->pos) }, .scale = { .x = BOSS_HURT_RADIUS * 2, .y = BOSS_HURT_RADIUS * 2 }, .blend = BLEND_ALPHA, }); } r_draw_sprite(&(SpriteParams) { .sprite_ptr = &stagedraw.dummy, .pos = { re(global.plr.pos), im(global.plr.pos) }, .scale.both = 2, // NOTE: actual player is a singular point }); // TODO: perhaps handle lasers somehow r_flush_sprites(); #endif } static void apply_shader_rules(ShaderRule *shaderrules, FBPair *fbos) { if(!shaderrules) { return; } for(ShaderRule *rule = shaderrules; *rule; ++rule) { r_framebuffer(fbos->back); if((*rule)(fbos->front)) { fbpair_swap(fbos); } } return; } static void draw_wall_of_text(float f, const char *txt) { Sprite spr; TextBBox bbox; char buf[strlen(txt) + 4]; memcpy(buf, txt, sizeof(buf) - 4); memcpy(buf + sizeof(buf) - 4, " ~ ", 4); text_render(buf, res_font("standard"), &spr, &bbox); // FIXME: The shader currently assumes that the sprite takes up the entire texture. // If it could handle any arbitrary sprite, then text_render wouldn't have to resize // the texture per every new string of text. float w = VIEWPORT_W; float h = VIEWPORT_H; r_mat_mv_push(); r_mat_mv_translate(w/2, h/2, 0); r_mat_mv_scale(w, h, 1.0); uint tw, th; r_texture_get_size(spr.tex, 0, &tw, &th); r_shader("spellcard_walloftext"); r_uniform_float("w", spr.tex_area.w); r_uniform_float("h", spr.tex_area.h); r_uniform_float("ratio", h/w); r_uniform_vec2("origin", re(global.boss->pos)/h, im(global.boss->pos)/w); // what the fuck? r_uniform_float("t", f); r_uniform_sampler("tex", spr.tex); r_draw_quad(); r_shader_standard(); r_mat_mv_pop(); } static void draw_spellbg(int t) { Boss *b = global.boss; r_state_push(); b->current->draw_rule(b, t); r_state_pop(); if(b->current->type == AT_ExtraSpell) { draw_extraspell_bg(b, t); } float scale = 1; if(t < 0) { scale = 1.0 - t/attacktype_start_delay(b->current->type); } r_draw_sprite(&(SpriteParams) { .sprite_ptr = res_sprite("boss_spellcircle0"), .shader_ptr = res_shader("sprite_default"), .pos = { re(b->pos), im(b->pos) }, .rotation.angle = global.frames * 7.0 * DEG2RAD, .rotation.vector = { 0, 0, -1 }, .scale.both = scale, }); } static void draw_spellbg_overlay(int t) { Boss *b = global.boss; float delay = attacktype_start_delay(b->current->type); float f = (ATTACK_START_DELAY - t) / (delay + ATTACK_START_DELAY); if(f > 0) { draw_wall_of_text(f, b->current->name); } } static inline bool should_draw_stage_bg(void) { if(!global.boss || !global.boss->current) return true; return ( !global.boss->current->draw_rule || !attack_is_active(global.boss->current) || global.frames - global.boss->current->starttime < SPELL_INTRO_DURATION ); } static bool fxaa_rule(Framebuffer *fb) { r_state_push(); r_enable(RCAP_DEPTH_TEST); r_depth_func(DEPTH_ALWAYS); r_blend(BLEND_NONE); r_shader_ptr(stagedraw.shaders.fxaa); Texture *tex = r_framebuffer_get_attachment(fb, FRAMEBUFFER_ATTACH_COLOR0); uint w, h; r_texture_get_size(tex, 0, &w, &h); r_uniform_vec2("tex_size", w, h); r_uniform_sampler("depth", r_framebuffer_get_attachment(fb, FRAMEBUFFER_ATTACH_DEPTH)); draw_framebuffer_tex(fb, VIEWPORT_W, VIEWPORT_H); r_state_pop(); return true; } static bool copydepth_rule(Framebuffer *fb) { Framebuffer *target_fb = r_framebuffer_current(); r_framebuffer_copy(target_fb, fb, BUFFER_DEPTH); return false; } static bool powersurge_draw_predicate(EntityInterface *ent) { uint layer = ent->draw_layer & ~LAYER_LOW_MASK; if(player_is_powersurge_active(&global.plr)) { switch(layer) { case LAYER_PLAYER_SLAVE: case LAYER_PLAYER_SHOT: case LAYER_PLAYER_SHOT_HIGH: case LAYER_PLAYER_FOCUS: case LAYER_PLAYER: return true; } if(ent->type == ENT_TYPE_ID(Projectile)) { Projectile *p = ENT_CAST(ent, Projectile); return p->flags & PFLAG_PLRSPECIALPARTICLE; } } if(ent->type == ENT_TYPE_ID(Item)) { Item *i = ENT_CAST(ent, Item); return i->type == ITEM_VOLTAGE; } return false; } static bool draw_powersurge_effect(Framebuffer *target_fb, BlendMode blend) { r_state_push(); r_blend(BLEND_NONE); r_disable(RCAP_DEPTH_TEST); r_disable(RCAP_CULL_FACE); // if(player_is_powersurge_active(&global.plr)) { r_state_push(); r_framebuffer(stagedraw.powersurge_fbpair.front); r_blend(BLEND_PREMUL_ALPHA); r_shader("sprite_default"); ent_draw(powersurge_draw_predicate); r_state_pop(); // } // TODO: Add heuristic to not run the effect if the buffer can be reasonably assumed to be empty. r_shader("powersurge_feedback"); r_uniform_vec2("blur_resolution", 0.5*VIEWPORT_W, 0.5*VIEWPORT_H); r_framebuffer(stagedraw.powersurge_fbpair.back); r_uniform_vec2("blur_direction", 1, 0); r_uniform_vec4("fade", 1, 1, 1, 1); draw_framebuffer_tex(stagedraw.powersurge_fbpair.front, VIEWPORT_W, VIEWPORT_H); fbpair_swap(&stagedraw.powersurge_fbpair); r_framebuffer(stagedraw.powersurge_fbpair.back); r_uniform_vec2("blur_direction", 0, 1); r_uniform_vec4("fade", 0.9, 0.9, 0.9, 0.9); draw_framebuffer_tex(stagedraw.powersurge_fbpair.front, VIEWPORT_W, VIEWPORT_H); r_framebuffer(target_fb); r_shader("powersurge_effect"); r_uniform_sampler("shotlayer", r_framebuffer_get_attachment(stagedraw.powersurge_fbpair.back, FRAMEBUFFER_ATTACH_COLOR0)); r_uniform_sampler("flowlayer", "powersurge_flow"); r_uniform_float("time", global.frames/60.0); r_blend(blend); r_cull(CULL_BACK); r_mat_mv_push(); r_mat_mv_scale(VIEWPORT_W, VIEWPORT_H, 1); r_mat_mv_translate(0.5, 0.5, 0); r_draw_quad(); r_mat_mv_pop(); fbpair_swap(&stagedraw.powersurge_fbpair); r_state_pop(); return true; } static bool boss_distortion_rule(Framebuffer *fb) { if(global.boss == NULL) { return false; } r_state_push(); r_blend(BLEND_NONE); r_disable(RCAP_DEPTH_TEST); r_disable(RCAP_CULL_FACE); cmplx fpos = global.boss->pos;; cmplx pos = fpos; r_shader("boss_zoom"); r_uniform_vec2("blur_orig", re(pos) / VIEWPORT_W, 1-im(pos) / VIEWPORT_H); r_uniform_vec2("fix_orig", re(fpos) / VIEWPORT_W, 1-im(fpos) / VIEWPORT_H); r_uniform_float("blur_rad", 1.5*(0.2+0.025*sin(global.frames/15.0))); r_uniform_float("rad", 0.24); r_uniform_float("ratio", (float)VIEWPORT_H/VIEWPORT_W); r_uniform_vec4_rgba("color", &global.boss->zoomcolor); draw_framebuffer_tex(fb, VIEWPORT_W, VIEWPORT_H); r_state_pop(); return true; } static void finish_3d_scene(FBPair *fbpair) { // Here we synchronize the depth buffers of both framebuffers in the pair. // The FXAA shader has this built-in, so we don't need to do the copy_depth // pass in that case. // The reason for this is that, normally, depth testing is disabled during // the stage-specific post-processing ping-pong. This applies to depth writes // as well. Therefore, only one of the framebuffers will get a depth write in // a given frame. In the worst case, this will be the same framebuffer every // time, leaving the other one's depth buffer undefined. In the best case, // they will alternate. If a shader down the post-processing pipeline happens // to sample the "unlucky" buffer, it'll probably either completely destroy // the background rendering, or the sample will lag 1 frame behind. // This flew past the radar for a long time. Now that I actually had to waste // my evening debugging this peculiarity, I figured I might as well document // it here. It is possible to solve this problem without an additional pass, // but that would require a bit of refactoring. This is the simplest solution // as far as I can tell. apply_shader_rules((ShaderRule[]) { /* config_get_int(CONFIG_FXAA) ? fxaa_rule : copydepth_rule, NULL */ copydepth_rule, NULL }, fbpair); } static void draw_full_spellbg(int t, FBPair *fbos) { BlendMode blend_old = r_blend_current(); r_framebuffer(fbos->back); r_blend(BLEND_PREMUL_ALPHA); draw_spellbg(t); fbpair_swap(fbos); r_blend(BLEND_NONE); apply_shader_rules((ShaderRule[]) { boss_distortion_rule, NULL }, fbos); r_blend(BLEND_PREMUL_ALPHA); draw_spellbg_overlay(t); r_blend(blend_old); } static void apply_bg_shaders(ShaderRule *shaderrules, FBPair *fbos) { Boss *b = global.boss; r_state_push(); r_blend(BLEND_NONE); if(should_draw_stage_bg()) { finish_3d_scene(fbos); apply_shader_rules(shaderrules, fbos); // anti-aliasing if(config_get_int(CONFIG_FXAA)) { apply_shader_rules((ShaderRule[]) { fxaa_rule, NULL } , fbos); } } if(b && b->current && b->current->draw_rule && b->current->starttime > 0) { int t = global.frames - b->current->starttime; int delay = attacktype_start_delay(b->current->type); bool trans_intro = t + delay < SPELL_INTRO_DURATION; bool trans_outro = attack_has_finished(b->current); if(!trans_intro && !trans_outro) { draw_full_spellbg(t, fbos); } else { FBPair *aux = stage_get_fbpair(FBPAIR_BG_AUX); draw_full_spellbg(t, aux); apply_shader_rules((ShaderRule[]) { boss_distortion_rule, NULL }, fbos); fbpair_swap(fbos); r_framebuffer(fbos->back); cmplx pos = b->pos; float ratio = (float)VIEWPORT_H/VIEWPORT_W; if(trans_intro) { r_shader("spellcard_intro"); r_uniform_float("ratio", ratio); r_uniform_vec2("origin", re(pos) / VIEWPORT_W, 1 - im(pos) / VIEWPORT_H); r_uniform_float("t", SPELL_INTRO_TIME_FACTOR * (t + delay) / (float)SPELL_INTRO_DURATION); } else { int tn = global.frames - b->current->endtime; delay = b->current->endtime - b->current->endtime_undelayed; r_shader("spellcard_outro"); r_uniform_float("ratio", ratio); r_uniform_vec2("origin", re(pos) / VIEWPORT_W, 1 - im(pos) / VIEWPORT_H); r_uniform_float("t", max(0, tn / (float)delay + 1)); } r_blend(BLEND_PREMUL_ALPHA); draw_framebuffer_tex(aux->front, VIEWPORT_W, VIEWPORT_H); r_blend(BLEND_NONE); fbpair_swap(fbos); } } else { apply_shader_rules((ShaderRule[]) { boss_distortion_rule, NULL }, fbos); } r_state_pop(); } static void stage_render_bg(StageInfo *stage) { FBPair *background = stage_get_fbpair(FBPAIR_BG); r_framebuffer(background->back); r_clear(BUFFER_ALL, RGBA(0, 0, 0, 1), 1); if(should_draw_stage_bg()) { r_mat_mv_push(); r_enable(RCAP_DEPTH_TEST); r_enable(RCAP_CULL_FACE); stage->procs->draw(); r_mat_mv_pop(); fbpair_swap(background); } set_ortho(VIEWPORT_W, VIEWPORT_H); r_disable(RCAP_DEPTH_TEST); r_disable(RCAP_CULL_FACE); apply_bg_shaders(stage->procs->shader_rules, background); int pp = config_get_int(CONFIG_POSTPROCESS); if(pp > 1) { draw_powersurge_effect(background->front, BLEND_PREMUL_ALPHA); } else if(pp > 0) { Framebuffer *staging = stage_get_fbpair(FBPAIR_BG_AUX)->back; r_state_push(); r_framebuffer_clear(staging, BUFFER_COLOR, RGBA(0, 0, 0, 0), 1); draw_powersurge_effect(staging, BLEND_NONE); r_shader_standard(); r_framebuffer(background->front); r_blend(BLEND_PREMUL_ALPHA); draw_framebuffer_tex(staging, VIEWPORT_W, VIEWPORT_H); r_state_pop(); } } bool stage_should_draw_particle(Projectile *p) { return (p->flags & PFLAG_REQUIREDPARTICLE) || config_get_int(CONFIG_PARTICLES); } static bool stage_draw_predicate(EntityInterface *ent) { if(ent->type == ENT_TYPE_ID(Projectile)) { Projectile *p = ENT_CAST(ent, Projectile); if(p->type == PROJ_PARTICLE) { return stage_should_draw_particle(p); } } return true; } static void stage_draw_objects(void) { r_shader("sprite_default"); if(global.boss) { draw_boss_background(global.boss); } ent_draw( config_get_int(CONFIG_PARTICLES) ? NULL : stage_draw_predicate ); if(global.boss) { draw_boss_fake_overlay(global.boss); } stage_draw_collision_areas(); r_shader_standard(); } void stage_draw_overlay(void) { r_state_push(); r_blend(BLEND_PREMUL_ALPHA); r_disable(RCAP_CULL_FACE); if(global.boss) { draw_boss_overlay(global.boss); } player_draw_overlay(&global.plr); if(stagedraw.clear_screen.alpha > 0) { fade_out(stagedraw.clear_screen.alpha * 0.5); } stagetext_draw(); r_state_pop(); } static void postprocess_prepare(Framebuffer *fb, ShaderProgram *s, void *arg) { r_uniform_int("frames", global.frames); r_uniform_vec2("viewport", VIEWPORT_W, VIEWPORT_H); r_uniform_vec2("player", re(global.plr.pos), VIEWPORT_H - im(global.plr.pos)); } static inline void begin_viewport_shake(void) { float s = stage_get_view_shake_strength(); if(s > 0) { // s = log1pf(s); s = sqrtf(s); r_mat_mv_push(); r_mat_mv_translate( s * sin(global.frames), s * sin(global.frames * 1.1 + 3), 0 ); r_mat_mv_scale( 1 + 2 * s / VIEWPORT_W, 1 + 2 * s / VIEWPORT_H, 1 ); r_mat_mv_translate( -s, -s, 0 ); } } static inline void end_viewport_shake(void) { if(stage_get_view_shake_strength() > 0) { r_mat_mv_pop(); } } /* * Small helpers for entities draw code that might want to suppress viewport shake temporarily. * This is mostly useful when multiple framebuffers are involved. */ static int shake_suppressed = 0; void stage_draw_begin_noshake(void) { assert(!shake_suppressed); shake_suppressed = 1; if(stage_get_view_shake_strength() > 0) { shake_suppressed = 2; r_mat_mv_push_identity(); } } void stage_draw_end_noshake(void) { assert(shake_suppressed); if(stage_get_view_shake_strength() > 0) { // make sure shake_view doesn't change in-between the begin/end calls; // that would've been *really* nasty to debug. assert(shake_suppressed == 2); r_mat_mv_pop(); } shake_suppressed = 0; } void stage_draw_viewport(void) { FloatRect dest_vp; r_framebuffer_viewport_current(r_framebuffer_current(), &dest_vp); r_uniform_sampler("tex", r_framebuffer_get_attachment(stagedraw.fb_pairs[FBPAIR_FG].front, FRAMEBUFFER_ATTACH_COLOR0)); // CAUTION: Very intricate pixel perfect scaling that will ruin your day. float facw = dest_vp.w / SCREEN_W; float fach = dest_vp.h / SCREEN_H; // fach is equal to facw up to roundoff error. float scale = fach; r_mat_mv_push(); r_mat_mv_scale(1/facw, 1/fach, 1); r_mat_mv_translate(roundf(facw * VIEWPORT_X), roundf(fach * VIEWPORT_Y), 0); r_mat_mv_scale(roundf(scale * VIEWPORT_W), roundf(scale * VIEWPORT_H), 1); r_mat_mv_translate(0.5, 0.5, 0); r_draw_quad(); r_mat_mv_pop(); } void stage_draw_scene(StageInfo *stage) { #ifdef DEBUG bool key_nobg = gamekeypressed(KEY_NOBACKGROUND); #else bool key_nobg = false; #endif FBPair *background = stage_get_fbpair(FBPAIR_BG); FBPair *foreground = stage_get_fbpair(FBPAIR_FG); bool draw_bg = !config_get_int(CONFIG_NO_STAGEBG) && !key_nobg; if(draw_bg) { stage_render_bg(stage); } // prepare for 2D rendering into the game viewport framebuffer r_framebuffer(foreground->back); set_ortho(VIEWPORT_W, VIEWPORT_H); r_disable(RCAP_DEPTH_TEST); r_blend(BLEND_PREMUL_ALPHA); r_cull(CULL_BACK); r_shader_standard(); begin_viewport_shake(); if(draw_bg) { // blit the background r_state_push(); r_blend(BLEND_NONE); draw_framebuffer_tex(background->front, VIEWPORT_W, VIEWPORT_H); r_state_pop(); coevent_signal(&stagedraw.events.background_drawn); } else if(!key_nobg) { r_clear(BUFFER_COLOR, RGBA(0, 0, 0, 1), 1); } // draw the 2D objects stage_draw_objects(); end_viewport_shake(); // prepare to apply postprocessing fbpair_swap(foreground); r_blend(BLEND_NONE); stagedraw.current_postprocess_fbpair = foreground; coevent_signal(&stagedraw.events.postprocess_before_overlay); // draw overlay: in-viewport text and HUD elements, etc. // this stuff is not affected by the screen shake effect stage_draw_overlay(); coevent_signal(&stagedraw.events.postprocess_after_overlay); // stage postprocessing apply_shader_rules(global.stage->procs->postprocess_rules, foreground); // custom postprocessing postprocess( stagedraw.viewport_pp, foreground, postprocess_prepare, draw_framebuffer_tex, VIEWPORT_W, VIEWPORT_H, NULL ); stagedraw.current_postprocess_fbpair = NULL; // prepare for 2D rendering into the main framebuffer (actual screen) r_framebuffer(video_get_screen_framebuffer()); set_ortho(SCREEN_W, SCREEN_H); // draw viewport contents stage_draw_viewport(); // draw HUD stage_draw_hud(); // draw dialog dialog_draw(global.dialog); // draw "bottom text" (FPS, replay info, etc.) stage_draw_bottom_text(); } #define HUD_X_PADDING 16 #define HUD_X_OFFSET (VIEWPORT_W + VIEWPORT_X) #define HUD_WIDTH (SCREEN_W - HUD_X_OFFSET) #define HUD_EFFECTIVE_WIDTH (HUD_WIDTH - HUD_X_PADDING * 2) #define HUD_X_SECONDARY_OFS_ICON 18 #define HUD_X_SECONDARY_OFS_LABEL (HUD_X_SECONDARY_OFS_ICON + 12) #define HUD_X_SECONDARY_OFS_VALUE (HUD_X_SECONDARY_OFS_LABEL + 60) struct glyphcb_state { Color *color1, *color2; }; static void draw_numeric_callback(Font *font, charcode_t charcode, SpriteInstanceAttribs *spr_attribs, void *userdata) { struct glyphcb_state *st = userdata; if(charcode != '0' && charcode != ',') { st->color1 = st->color2; } spr_attribs->rgba = *st->color1; } static inline void stage_draw_hud_power_value(float xpos, float ypos) { Font *fnt_int = res_font("standard"); Font *fnt_fract = res_font("small"); int pw = global.plr.power_stored; Color *c_whole, c_whole_buf, *c_fract, c_fract_buf; Color *c_op_mod = RGBA(1, 0.2 + 0.3 * psin(global.frames / 10.0), 0.2, 1.0); if(pw <= PLR_MAX_POWER_EFFECTIVE) { c_whole = &stagedraw.hud_text.color.active; c_fract = &stagedraw.hud_text.color.inactive; } else if(pw - PLR_MAX_POWER_EFFECTIVE < 100) { c_whole = &stagedraw.hud_text.color.active; c_fract = color_mul(color_copy(&c_fract_buf, &stagedraw.hud_text.color.inactive), c_op_mod); } else { c_whole = color_mul(color_copy(&c_whole_buf, &stagedraw.hud_text.color.active), c_op_mod); c_fract = color_mul(color_copy(&c_fract_buf, &stagedraw.hud_text.color.inactive), c_op_mod); } xpos = draw_fraction( pw / 100.0, ALIGN_LEFT, xpos, ypos, fnt_int, fnt_fract, c_whole, c_fract, false ); xpos += text_draw(" / ", &(TextParams) { .pos = { xpos, ypos }, .color = &stagedraw.hud_text.color.active, .align = ALIGN_LEFT, .font_ptr = fnt_int, }); draw_fraction( PLR_MAX_POWER_EFFECTIVE / 100.0, ALIGN_LEFT, xpos, ypos, fnt_int, fnt_fract, &stagedraw.hud_text.color.active, &stagedraw.hud_text.color.inactive, false ); } static void stage_draw_hud_score(Alignment a, float xpos, float ypos, char *buf, size_t bufsize, uint64_t score) { format_huge_num(10, score, bufsize, buf); Font *fnt = res_font("standard"); bool kern_saved = font_get_kerning_enabled(fnt); font_set_kerning_enabled(fnt, false); text_draw(buf, &(TextParams) { .pos = { xpos, ypos }, .font = "standard", .align = ALIGN_RIGHT, .glyph_callback = { draw_numeric_callback, &(struct glyphcb_state) { &stagedraw.hud_text.color.inactive, &stagedraw.hud_text.color.active }, } }); font_set_kerning_enabled(fnt, kern_saved); } static void stage_draw_hud_scores(float ypos_hiscore, float ypos_score, char *buf, size_t bufsize) { stage_draw_hud_score(ALIGN_RIGHT, HUD_EFFECTIVE_WIDTH, ypos_hiscore, buf, bufsize, progress.hiscore); stage_draw_hud_score(ALIGN_RIGHT, HUD_EFFECTIVE_WIDTH, ypos_score, buf, bufsize, global.plr.points); } static void stage_draw_hud_objpool_stats(float x, float y, float width) { char buf[128]; auto objpools = &stage_objects.pools.as_array; MemArena *arena = &stage_objects.arena; Font *font = res_font("monotiny"); ShaderProgram *sh_prev = r_shader_current(); r_shader("text_hud"); float lineskip = font_get_lineskip(font); text_draw("Arena memory:", &(TextParams) { .pos = { x, y }, .font_ptr = font, .align = ALIGN_LEFT, }); snprintf(buf, sizeof(buf), "%zukb | %5zukb", arena->total_used / 1024, arena->total_allocated / 1024 ); text_draw(buf, &(TextParams) { .pos = { x + width, y }, .font_ptr = font, .align = ALIGN_RIGHT, }); y += lineskip * 1.5; const char *const names[] = { #define GET_POOL_NAME(t, n) #t, OBJECT_POOLS(GET_POOL_NAME) #undef GET_POOL_NAME }; static_assert(ARRAY_SIZE(*objpools) == ARRAY_SIZE(names)); for(int i = 0; i < ARRAY_SIZE(*objpools); ++i) { MemPool *p = &(*objpools)[i]; snprintf(buf, sizeof(buf), "%u | %7u", p->num_used, p->num_allocated); text_draw(names[i], &(TextParams) { .pos = { x, y }, .font_ptr = font, .align = ALIGN_LEFT, }); text_draw(buf, &(TextParams) { .pos = { x + width, y }, .font_ptr = font, .align = ALIGN_RIGHT, }); y += lineskip; } r_shader_ptr(sh_prev); } struct labels_s { struct { float next_life; float next_bomb; } x; struct { float hiscore; float score; float lives; float bombs; float power; float value; float voltage; float graze; } y; struct { float lives_display; float lives_text; float bombs_display; float bombs_text; } y_ofs; Color lb_baseclr; }; static void draw_graph(float x, float y, float w, float h) { r_mat_mv_push(); r_mat_mv_translate(x + w/2, y + h/2, 0); r_mat_mv_scale(w, h, 1); r_draw_quad(); r_mat_mv_pop(); } static void draw_label(const char *label_str, double y_ofs, struct labels_s* labels, Color *clr) { text_draw(label_str, &(TextParams) { .font_ptr = stagedraw.hud_text.font, .shader_ptr = stagedraw.hud_text.shader, .pos = { 0, y_ofs }, .color = clr, }); } static void stage_draw_hud_text(struct labels_s* labels) { char buf[64]; Font *font; bool kern_saved; r_shader_ptr(stagedraw.hud_text.shader); Color *lb_label_clr = color_mul(COLOR_COPY(&labels->lb_baseclr), &stagedraw.hud_text.color.label); // Labels draw_label("Hi-Score:", labels->y.hiscore, labels, &stagedraw.hud_text.color.label); draw_label("Score:", labels->y.score, labels, &stagedraw.hud_text.color.label); draw_label("Lives:", labels->y.lives, labels, lb_label_clr); draw_label("Spell Cards:", labels->y.bombs, labels, lb_label_clr); r_mat_mv_push(); r_mat_mv_translate(HUD_X_SECONDARY_OFS_LABEL, 0, 0); draw_label("Power:", labels->y.power, labels, &stagedraw.hud_text.color.label_power); draw_label("Value:", labels->y.value, labels, &stagedraw.hud_text.color.label_value); draw_label("Volts:", labels->y.voltage, labels, &stagedraw.hud_text.color.label_voltage); draw_label("Graze:", labels->y.graze, labels, &stagedraw.hud_text.color.label_graze); r_mat_mv_pop(); // Score/Hi-Score values stage_draw_hud_scores(labels->y.hiscore, labels->y.score, buf, sizeof(buf)); // Lives and Bombs (N/A) if(global.stage->type == STAGE_SPELL) { r_color(color_mul_scalar(COLOR_COPY(&labels->lb_baseclr), 0.7)); text_draw("N/A", &(TextParams) { .pos = { HUD_EFFECTIVE_WIDTH, labels->y.lives }, .font_ptr = stagedraw.hud_text.font, .align = ALIGN_RIGHT }); text_draw("N/A", &(TextParams) { .pos = { HUD_EFFECTIVE_WIDTH, labels->y.bombs }, .font_ptr = stagedraw.hud_text.font, .align = ALIGN_RIGHT }); r_color4(1, 1, 1, 1.0); } const float res_text_padding = 4; // Score left to next extra life if(labels->x.next_life > 0) { Color *next_clr = color_mul(RGBA(0.5, 0.3, 0.4, 0.5), &labels->lb_baseclr); format_huge_num(0, global.plr.extralife_threshold - global.plr.points, sizeof(buf), buf); font = res_font("small"); text_draw("Next:", &(TextParams) { .pos = { labels->x.next_life + res_text_padding, labels->y.lives + labels->y_ofs.lives_text }, .font_ptr = font, .align = ALIGN_LEFT, .color = next_clr, }); kern_saved = font_get_kerning_enabled(font); font_set_kerning_enabled(font, false); text_draw(buf, &(TextParams) { .pos = { HUD_EFFECTIVE_WIDTH - res_text_padding, labels->y.lives + labels->y_ofs.lives_text }, .font_ptr = font, .align = ALIGN_RIGHT, .color = next_clr, }); font_set_kerning_enabled(font, kern_saved); } // Bomb fragments (numeric) if(labels->x.next_bomb > 0) { Color *next_clr = color_mul(RGBA(0.3, 0.5, 0.3, 0.5), &labels->lb_baseclr); snprintf(buf, sizeof(buf), "%d / %d", global.plr.bomb_fragments, PLR_MAX_BOMB_FRAGMENTS); font = res_font("small"); text_draw("Fragments:", &(TextParams) { .pos = { labels->x.next_bomb + res_text_padding, labels->y.bombs + labels->y_ofs.bombs_text }, .font_ptr = font, .align = ALIGN_LEFT, .color = next_clr, }); kern_saved = font_get_kerning_enabled(font); font_set_kerning_enabled(font, false); text_draw(buf, &(TextParams) { .pos = { HUD_EFFECTIVE_WIDTH - res_text_padding, labels->y.bombs + labels->y_ofs.bombs_text }, .font_ptr = font, .align = ALIGN_RIGHT, .color = next_clr, }); font_set_kerning_enabled(font, kern_saved); } r_mat_mv_push(); r_mat_mv_translate(HUD_X_SECONDARY_OFS_VALUE, 0, 0); // Power value stage_draw_hud_power_value(0, labels->y.power); font = res_font("standard"); kern_saved = font_get_kerning_enabled(font); font_set_kerning_enabled(font, false); // Point Item Value... value format_huge_num(6, global.plr.point_item_value, sizeof(buf), buf); text_draw(buf, &(TextParams) { .pos = { 0, labels->y.value }, .shader_ptr = stagedraw.hud_text.shader, .font_ptr = font, .glyph_callback = { draw_numeric_callback, &(struct glyphcb_state) { &stagedraw.hud_text.color.inactive, &stagedraw.hud_text.color.active }, } }); // Voltage value format_huge_num(4, global.plr.voltage, sizeof(buf), buf); float volts_x = 0; Color *voltage_tint = global.plr.voltage >= global.voltage_threshold ? RGB(1.0, 0.9, 0.7) // RGB(0.9, 0.7, 1.0) : RGB(1.0, 1.0, 1.0); volts_x += text_draw(buf, &(TextParams) { .pos = { volts_x, labels->y.voltage }, .shader_ptr = stagedraw.hud_text.shader, .font_ptr = font, .glyph_callback = { draw_numeric_callback, &(struct glyphcb_state) { color_mul(COLOR_COPY(&stagedraw.hud_text.color.inactive), voltage_tint), color_mul(COLOR_COPY(&stagedraw.hud_text.color.active), voltage_tint), }, } }); volts_x += text_draw("V", &(TextParams) { .pos = { volts_x, labels->y.voltage }, .shader_ptr = stagedraw.hud_text.shader, .font_ptr = font, .color = &stagedraw.hud_text.color.dark, }); volts_x += text_draw(" / ", &(TextParams) { .pos = { volts_x, labels->y.voltage }, .shader_ptr = stagedraw.hud_text.shader, .font_ptr = font, .color = &stagedraw.hud_text.color.active, }); format_huge_num(4, global.voltage_threshold, sizeof(buf), buf); volts_x += text_draw(buf, &(TextParams) { .pos = { volts_x, labels->y.voltage }, .shader_ptr = stagedraw.hud_text.shader, .font_ptr = font, .glyph_callback = { draw_numeric_callback, &(struct glyphcb_state) { &stagedraw.hud_text.color.inactive, &stagedraw.hud_text.color.active }, } }); volts_x += text_draw("V", &(TextParams) { .pos = { volts_x, labels->y.voltage }, .shader_ptr = stagedraw.hud_text.shader, .font_ptr = font, .color = &stagedraw.hud_text.color.dark, }); // Graze value format_huge_num(6, global.plr.graze, sizeof(buf), buf); text_draw(buf, &(TextParams) { .pos = { 0, labels->y.graze }, .shader_ptr = stagedraw.hud_text.shader, .font_ptr = font, .glyph_callback = { draw_numeric_callback, &(struct glyphcb_state) { &stagedraw.hud_text.color.inactive, &stagedraw.hud_text.color.active }, } }); font_set_kerning_enabled(font, kern_saved); r_mat_mv_pop(); // God Mode indicator if(global.plr.iddqd) { text_draw("God Mode is enabled!", &(TextParams) { .pos = { HUD_EFFECTIVE_WIDTH * 0.5, 450 }, .font_ptr = font, .shader_ptr = stagedraw.hud_text.shader, .align = ALIGN_CENTER, .color = RGB(1.0, 0.5, 0.2), }); } if(stagedraw.objpool_stats) { stage_draw_hud_objpool_stats(0, 440, HUD_EFFECTIVE_WIDTH); } } void stage_draw_bottom_text(void) { char buf[64]; Font *font; #ifdef DEBUG snprintf(buf, sizeof(buf), "%.2f lfps, %.2f rfps, frames: %d (%d:%02d) ", global.fps.logic.fps, global.fps.render.fps, global.frames, global.frames / 3600, (global.frames % 3600) / 60 ); #else if(get_effective_frameskip() > 1) { snprintf(buf, sizeof(buf), "%.2f lfps, %.2f rfps", global.fps.logic.fps, global.fps.render.fps ); } else { snprintf(buf, sizeof(buf), "%.2f fps", global.fps.logic.fps ); } #endif font = res_font("monosmall"); text_draw(buf, &(TextParams) { .align = ALIGN_RIGHT, .pos = { SCREEN_W, SCREEN_H - 0.5 * text_height(font, buf, 0) }, .font_ptr = font, }); ReplayState *rst = &global.replay.input; if(rst->replay != NULL && (!stage_is_demo_mode() || rst->play.desync_frame >= 0)) { r_shader_ptr(stagedraw.hud_text.shader); // XXX: does it make sense to use the monospace font here? snprintf(buf, sizeof(buf), "Replay: %s (%i fps)", rst->replay->playername, rst->play.fps); int x = 0, y = SCREEN_H - 0.5 * text_height(font, buf, 0); x += text_draw(buf, &(TextParams) { .pos = { x, y }, .font_ptr = font, .color = &stagedraw.hud_text.color.inactive, }); if(rst->play.desync_frame >= 0) { snprintf(buf, sizeof(buf), " (DESYNCED near frame #%i)", rst->play.desync_frame); text_draw(buf, &(TextParams) { .pos = { x, y }, .font_ptr = font, .color = RGBA_MUL_ALPHA(1.00, 0.20, 0.20, 0.60), }); } } #ifdef PLR_DPS_STATS else if(global.frames) { int totaldmg = 0; int framespan = sizeof(global.plr.dmglog)/sizeof(*global.plr.dmglog); int graphspan = framespan; static int max = 0; float graph[framespan]; if(graphspan > 120) { // shader limitation graphspan = 120; } // hack to update the graph every frame player_register_damage(&global.plr, NULL, &(DamageInfo) { .amount = 0, .type = DMG_PLAYER_SHOT }); for(int i = 0; i < framespan; ++i) { totaldmg += global.plr.dmglog[i]; if(global.plr.dmglog[i] > max) { max = global.plr.dmglog[i]; } } for(int i = 0; i < graphspan; ++i) { if(max > 0) { graph[i] = (float)global.plr.dmglog[i] / max; } else { graph[i] = 0; } } snprintf(buf, sizeof(buf), "%.02f", totaldmg / (framespan / (double)FPS)); double text_h = text_height(font, buf, 0); double x = 0, y = SCREEN_H - 0.5 * text_h; x += text_draw("Avg DPS: ", &(TextParams) { .pos = { x, y }, .font_ptr = font, .color = &stagedraw.hud_text.color.inactive, }); text_draw(buf, &(TextParams) { .pos = { x, y }, .font_ptr = font, .color = &stagedraw.hud_text.color.active, }); r_shader("graph"); r_uniform_vec3("color_low", 1.0, 0.0, 0.0); r_uniform_vec3("color_mid", 1.0, 1.0, 0.0); r_uniform_vec3("color_high", 0.0, 1.0, 0.0); r_uniform_float_array("points", 0, graphspan, graph); draw_graph(142, SCREEN_H - text_h, graphspan, text_h); } #endif r_shader_standard(); } static void fill_graph(int num_samples, float *samples, FPSCounter *fps) { for(int i = 0; i < num_samples; ++i) { samples[i] = fps->frametimes[i] / (2.0 * (HRTIME_RESOLUTION / (long double)FPS)); if(samples[i] > 1.0) { samples[i] = 1.0; } } } static void stage_draw_framerate_graphs(float x, float y, float w, float h) { #define NUM_SAMPLES (sizeof(((FPSCounter){{0}}).frametimes) / sizeof(((FPSCounter){{0}}).frametimes[0])) static float samples[NUM_SAMPLES]; r_state_push(); r_shader("graph"); fill_graph(NUM_SAMPLES, samples, &global.fps.logic); r_uniform_vec3("color_low", 0.0, 1.0, 1.0); r_uniform_vec3("color_mid", 1.0, 1.0, 0.0); r_uniform_vec3("color_high", 1.0, 0.0, 0.0); r_uniform_float_array("points", 0, NUM_SAMPLES, samples); draw_graph(x, y, w, h); // x -= w * 1.1; y += h + 1; fill_graph(NUM_SAMPLES, samples, &global.fps.busy); r_uniform_vec3("color_low", 0.0, 1.0, 0.0); r_uniform_vec3("color_mid", 1.0, 0.0, 0.0); r_uniform_vec3("color_high", 1.0, 0.0, 0.5); r_uniform_float_array("points", 0, NUM_SAMPLES, samples); draw_graph(x, y, w, h); r_state_pop(); } void stage_draw_hud(void) { // Background r_mat_mv_push(); r_mat_mv_translate(SCREEN_W * 0.5, SCREEN_H * 0.5, 0); r_mat_mv_scale(SCREEN_W, SCREEN_W, 1); r_shader_standard(); r_uniform_sampler("tex", "hud"); r_draw_model("hud"); r_mat_mv_pop(); r_blend(BLEND_PREMUL_ALPHA); // Set up positions of most HUD elements struct labels_s labels = { 0 }; const float label_spacing = 32; float label_ypos = 0; label_ypos = 16; labels.y.hiscore = label_ypos += label_spacing; labels.y.score = label_ypos += label_spacing; label_ypos = 108; labels.y.lives = label_ypos += label_spacing; labels.y.bombs = label_ypos += label_spacing * 1.25; label_ypos = 210; labels.y.power = label_ypos += label_spacing; labels.y.value = label_ypos += label_spacing; labels.y.voltage = label_ypos += label_spacing; labels.y.graze = label_ypos += label_spacing; r_mat_mv_push(); r_mat_mv_translate(HUD_X_OFFSET + HUD_X_PADDING, 0, 0); // Set up Extra Spell indicator opacity early; some other elements depend on it float extraspell_alpha = 0; float extraspell_fadein = 1; if(global.boss && global.boss->current && global.boss->current->type == AT_ExtraSpell) { extraspell_fadein = min(1, -min(0, global.frames - global.boss->current->starttime) / (float)ATTACK_START_DELAY); float fadeout; if(attack_has_finished(global.boss->current)) { fadeout = (1 - (global.boss->current->endtime - global.frames) / (float)ATTACK_END_DELAY_EXTRA) / 0.74; } else { fadeout = 0; } float fade = max(extraspell_fadein, fadeout); extraspell_alpha = 1 - fade; } labels.lb_baseclr.r = 1 - extraspell_alpha; labels.lb_baseclr.g = 1 - extraspell_alpha; labels.lb_baseclr.b = 1 - extraspell_alpha; labels.lb_baseclr.a = 1 - extraspell_alpha * 0.5; // Lives and Bombs if(global.stage->type != STAGE_SPELL) { r_mat_mv_push(); r_mat_mv_translate(0, font_get_descent(res_font("standard")), 0); Sprite *spr_life = res_sprite("hud/heart"); Sprite *spr_bomb = res_sprite("hud/star"); float spacing = 1; float pos_lives = HUD_EFFECTIVE_WIDTH - spr_life->w * (PLR_MAX_LIVES - 0.5) - spacing * (PLR_MAX_LIVES - 1); float pos_bombs = HUD_EFFECTIVE_WIDTH - spr_bomb->w * (PLR_MAX_BOMBS - 0.5) - spacing * (PLR_MAX_BOMBS - 1); labels.y_ofs.lives_display = 0 /* spr_life->h * -0.25 */; labels.y_ofs.bombs_display = 0 /* spr_bomb->h * -0.25 */; labels.y_ofs.lives_text = labels.y_ofs.lives_display + spr_life->h; labels.y_ofs.bombs_text = labels.y_ofs.bombs_display + spr_bomb->h; labels.x.next_life = pos_lives - spr_life->w * 0.5; labels.x.next_bomb = pos_bombs - spr_bomb->w * 0.5; draw_fragments(&(DrawFragmentsParams) { .fill = spr_life, .pos = { pos_lives, labels.y.lives + labels.y_ofs.lives_display }, .origin_offset = { 0, 0 }, .limits = { PLR_MAX_LIVES, PLR_MAX_LIFE_FRAGMENTS }, .filled = { global.plr.lives, global.plr.life_fragments }, .alpha = 1, .spacing = spacing, .color = { .fill = color_mul(RGBA(1, 1, 1, 1), &labels.lb_baseclr), .back = RGBA(0, 0, 0, 0.5), .frag = RGBA(0.5, 0.5, 0.6, 0.5), } }); draw_fragments(&(DrawFragmentsParams) { .fill = spr_bomb, .pos = { pos_bombs, labels.y.bombs + labels.y_ofs.bombs_display }, .origin_offset = { 0, 0.05 }, .limits = { PLR_MAX_BOMBS, PLR_MAX_BOMB_FRAGMENTS }, .filled = { global.plr.bombs, global.plr.bomb_fragments }, .alpha = 1, .spacing = spacing, .color = { .fill = color_mul(RGBA(1, 1, 1, 1), &labels.lb_baseclr), .back = color_mul(RGBA(0, 0, 0, 0.5), &labels.lb_baseclr), .frag = color_mul(RGBA(0.5, 0.5, 0.6, 0.5), &labels.lb_baseclr), } }); r_mat_mv_pop(); } // Difficulty indicator r_draw_sprite(&(SpriteParams) { .sprite_ptr = res_sprite(difficulty_sprite_name(global.diff)), .pos = { HUD_EFFECTIVE_WIDTH * 0.5, 400 }, .scale.both = 0.6, .shader_ptr = res_shader("sprite_default"), }); // Power/Item/Voltage icons r_mat_mv_push(); r_mat_mv_translate(HUD_X_SECONDARY_OFS_ICON, font_get_descent(res_font("standard")) * 0.5 - 1, 0); r_draw_sprite(&(SpriteParams) { .pos = { 2, labels.y.power + 2 }, .sprite_ptr = res_sprite("item/power"), .shader_ptr = res_shader("sprite_default"), .color = RGBA(0, 0, 0, 0.5), }); r_draw_sprite(&(SpriteParams) { .pos = { 0, labels.y.power }, .sprite_ptr = res_sprite("item/power"), .shader_ptr = res_shader("sprite_default"), }); r_draw_sprite(&(SpriteParams) { .pos = { 2, labels.y.value + 2 }, .sprite_ptr = res_sprite("item/point"), .shader_ptr = res_shader("sprite_default"), .color = RGBA(0, 0, 0, 0.5), }); r_draw_sprite(&(SpriteParams) { .pos = { 0, labels.y.value }, .sprite_ptr = res_sprite("item/point"), .shader_ptr = res_shader("sprite_default"), }); r_draw_sprite(&(SpriteParams) { .pos = { 2, labels.y.voltage + 2 }, .sprite_ptr = res_sprite("item/voltage"), .shader_ptr = res_shader("sprite_default"), .color = RGBA(0, 0, 0, 0.5), }); r_draw_sprite(&(SpriteParams) { .pos = { 0, labels.y.voltage }, .sprite_ptr = res_sprite("item/voltage"), .shader_ptr = res_shader("sprite_default"), }); r_mat_mv_pop(); stage_draw_hud_text(&labels); // Extra Spell indicator if(extraspell_alpha > 0) { float s2 = max(0, swing(extraspell_alpha, 3)); r_state_push(); r_shader("text_default"); r_mat_mv_push(); r_mat_mv_translate(lerp(-HUD_X_OFFSET - HUD_X_PADDING, HUD_EFFECTIVE_WIDTH * 0.5, pow(2*extraspell_fadein-1, 2)), 128, 0); r_mat_mv_rotate((360 * (1-s2) - 25) * DEG2RAD, 0, 0, 1); r_mat_mv_scale(s2, s2, 0); r_color(RGBA_MUL_ALPHA(0.3, 0.6, 0.7, 0.7 * extraspell_alpha)); Font *font = res_font("big"); // TODO: replace this with a shader text_draw("Voltage \n Overdrive!", &(TextParams) { .pos = { 1, 1 }, .font_ptr = font, .align = ALIGN_CENTER }); text_draw("Voltage \n Overdrive!", &(TextParams) { .pos = { -1, -1 }, .font_ptr = font, .align = ALIGN_CENTER }); r_color4(extraspell_alpha, extraspell_alpha, extraspell_alpha, extraspell_alpha); text_draw("Voltage \n Overdrive!", &(TextParams) { .pos = { 0, 0 }, .font_ptr = font, .align = ALIGN_CENTER }); r_mat_mv_pop(); r_state_pop(); } if(stagedraw.framerate_graphs) { stage_draw_framerate_graphs(0, 360, HUD_EFFECTIVE_WIDTH, 30); } r_mat_mv_pop(); // Boss indicator ("Enemy") if(global.boss) { float red = 0.5*exp(-0.5*(global.frames-global.boss->lastdamageframe)); // hit indicator if(red > 1) red = 0; r_draw_sprite(&(SpriteParams) { .sprite_ptr = res_sprite("boss_indicator"), .shader_ptr = res_shader("sprite_default"), .pos = { VIEWPORT_X+re(global.boss->pos), 590 }, .color = RGBA(1 - red, 1 - red, 1 - red, 1 - red), }); } // Demo indicator if(stage_is_demo_mode()) { cmplxf pos = CMPLXF( VIEWPORT_X + VIEWPORT_W * 0.5f, VIEWPORT_Y + VIEWPORT_H * 0.5f - 75 ); float bg_width = 250; float bg_height = 60; Sprite *bg = res_sprite("part/smoke"); SpriteParams sp = { .sprite_ptr = bg, .shader_ptr = res_shader("sprite_default"), .color = RGBA(0.1, 0.1, 0.2, 0.07), }; r_mat_mv_push(); r_mat_mv_translate(re(pos), im(pos), 0); r_mat_mv_scale(bg_width / bg->w, bg_height / bg->h, 1); r_mat_mv_push(); r_mat_mv_rotate(global.frames * 0.5 * DEG2RAD, 0, 0, 1); r_draw_sprite(&sp); r_mat_mv_pop(); sp.color = RGBA(0.2, 0.1, 0.1, 0.07); r_mat_mv_push(); r_mat_mv_rotate(global.frames * -0.93 * DEG2RAD, 0, 0, 1); r_draw_sprite(&sp); r_mat_mv_pop(); r_mat_mv_pop(); text_draw("Demo", &(TextParams) { .align = ALIGN_CENTER, .font = "big", .shader_ptr = res_shader("text_demo"), .shader_params = &(ShaderCustomParams) { global.frames / 60.0f }, .pos.as_cmplx = pos, }); } } void stage_display_clear_screen(const StageClearBonus *bonus) { StageTextTable tbl; bool all_clear = bonus->all_clear.base; const char *title = all_clear ? "All Clear!" : "Stage Clear!"; stagetext_begin_table(&tbl, title, RGB(1, 1, 1), RGB(1, 1, 1), 2*VIEWPORT_W/3, 20, 5184000, 60, 60); stagetext_table_add_numeric_nonzero(&tbl, "Stage Clear bonus", bonus->base); stagetext_table_add_numeric_nonzero(&tbl, "Life bonus", bonus->lives); stagetext_table_add_numeric_nonzero(&tbl, "Voltage bonus", bonus->voltage); stagetext_table_add_numeric_nonzero(&tbl, "Graze bonus", bonus->graze); if(all_clear) { stagetext_table_add_separator(&tbl); stagetext_table_add_numeric_nonzero(&tbl, "All Clear bonus", bonus->all_clear.base); if(bonus->all_clear.diff_bonus) { char tmp[128]; int percent = (bonus->all_clear.diff_multiplier - 1.0) * 100; snprintf(tmp, sizeof(tmp), "Difficulty bonus (+%i%%)", percent); stagetext_table_add_numeric_nonzero(&tbl, tmp, bonus->all_clear.diff_bonus); } } stagetext_table_add_separator(&tbl); stagetext_table_add_numeric(&tbl, "Total", bonus->total); stagetext_end_table(&tbl); stagetext_add( "Press Fire to continue", VIEWPORT_W/2 + VIEWPORT_H*0.7*I, ALIGN_CENTER, res_font("standard"), RGB(1, 0.5, 0), tbl.delay, tbl.lifetime, tbl.fadeintime, tbl.fadeouttime ); stagedraw.clear_screen.target_alpha = 1; } StageDrawEvents *stage_get_draw_events(void) { return &stagedraw.events; }