taisei/src/cutscenes/cutscene.c
2023-04-29 20:01:50 +02:00

478 lines
12 KiB
C

/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
*/
#include "taisei.h"
#include "cutscene.h"
#include "scene_impl.h"
#include "scenes.h"
#include "audio/audio.h"
#include "color.h"
#include "global.h"
#include "progress.h"
#include "renderer/api.h"
#include "util/fbmgr.h"
#include "util/glm.h"
#include "video.h"
#include "eventloop/eventloop.h"
#define SKIP_DELAY 3
#define AUTO_ADVANCE_TIME_BEFORE_TEXT FPS * 2
#define AUTO_ADVANCE_TIME_MID_SCENE FPS * 20
#define AUTO_ADVANCE_TIME_CROSS_SCENE FPS * 180
// TODO maybe make transitions configurable?
#define CUTSCENE_FADE_OUT 200
typedef struct CutsceneBGState {
Texture *scene;
Texture *next_scene;
float alpha;
float transition_rate;
bool fade_out;
} CutsceneBGState;
typedef struct CutsceneTextVisual {
LIST_INTERFACE(struct CutsceneTextVisual);
const CutscenePhaseTextEntry *entry;
float alpha;
float target_alpha;
} CutsceneTextVisual;
typedef struct CutsceneState {
const CutscenePhase *phase;
const CutscenePhaseTextEntry *text_entry;
CallChain cc;
ResourceGroup rg;
CutsceneBGState bg_state;
LIST_ANCHOR(CutsceneTextVisual) text_visuals;
ManagedFramebufferGroup *mfb_group;
Framebuffer *text_fb;
FBPair erase_mask_fbpair;
int skip_timer;
int advance_timer;
int fadeout_timer;
} CutsceneState;
static void clear_text(CutsceneState *st) {
for(CutsceneTextVisual *tv = st->text_visuals.first; tv; tv = tv->next) {
tv->target_alpha = 0;
}
}
static void switch_bg(CutsceneState *st, const char *texture) {
Texture *scene = *texture ? res_texture(texture) : NULL;
if(st->bg_state.scene == NULL) {
st->bg_state.scene = scene;
st->bg_state.fade_out = false;
} else {
st->bg_state.next_scene = scene;
if(st->bg_state.scene == st->bg_state.next_scene) {
st->bg_state.next_scene = NULL;
st->bg_state.fade_out = false;
} else {
st->bg_state.fade_out = true;
}
}
}
static void reset_timers(CutsceneState *st) {
st->skip_timer = SKIP_DELAY;
if(st->text_entry) {
if(st->text_entry[1].text) {
st->advance_timer = AUTO_ADVANCE_TIME_MID_SCENE;
} else {
st->advance_timer = AUTO_ADVANCE_TIME_CROSS_SCENE;
}
} else {
st->advance_timer = AUTO_ADVANCE_TIME_BEFORE_TEXT;
}
log_debug("st->advance_timer = %i", st->advance_timer);
}
static bool skip_text_animation(CutsceneState *st) {
bool animation_skipped = false;
for(CutsceneTextVisual *tv = st->text_visuals.first; tv; tv = tv->next) {
if(tv->target_alpha > 0 && tv->alpha < tv->target_alpha * 0.6) {
tv->alpha = tv->target_alpha;
animation_skipped = true;
}
}
return animation_skipped;
}
static void begin_fadeout(CutsceneState *st) {
const int fade_frames = CUTSCENE_FADE_OUT;
audio_bgm_stop((FPS * fade_frames) / 4000.0);
set_transition(TransFadeBlack, fade_frames, fade_frames, NO_CALLCHAIN);
st->fadeout_timer = fade_frames;
st->bg_state.next_scene = NULL;
st->bg_state.fade_out = true;
st->bg_state.transition_rate = 1.0f / fade_frames;
}
static void cutscene_advance(CutsceneState *st) {
if(st->skip_timer > 0) {
log_debug("Skip too soon");
return;
}
if(st->phase) {
if(st->text_entry == NULL) {
st->text_entry = &st->phase->text_entries[0];
} else if((++st->text_entry)->text == NULL) {
if(skip_text_animation(st)) {
--st->text_entry;
return;
}
st->text_entry = NULL;
clear_text(st);
if((++st->phase)->background == NULL) {
st->phase = NULL;
begin_fadeout(st);
} else {
switch_bg(st, st->phase->background);
}
}
if(st->text_entry && st->text_entry->text) {
alist_append(&st->text_visuals, ALLOC(CutsceneTextVisual, {
.alpha = 0.1,
.target_alpha = 1,
.entry = st->text_entry,
}));
}
}
reset_timers(st);
}
static bool cutscene_event(SDL_Event *evt, void *ctx) {
CutsceneState *st = ctx;
if(evt->type == MAKE_TAISEI_EVENT(TE_MENU_ACCEPT)) {
cutscene_advance(st);
}
return false;
}
static LogicFrameAction cutscene_logic_frame(void *ctx) {
CutsceneState *st = ctx;
update_transition();
events_poll((EventHandler[]) {
{ .proc = cutscene_event, .arg = st, .priority = EPRIO_NORMAL },
{ NULL },
}, EFLAG_MENU);
if(st->skip_timer > 0) {
st->skip_timer--;
}
if(st->advance_timer > 0) {
st->advance_timer--;
}
if(st->advance_timer == 0 || gamekeypressed(KEY_SKIP)) {
cutscene_advance(st);
}
if(st->bg_state.fade_out) {
if(fapproach_p(&st->bg_state.alpha, 0, st->bg_state.transition_rate) == 0) {
st->bg_state.scene = st->bg_state.next_scene;
st->bg_state.next_scene = NULL;
st->bg_state.fade_out = false;
}
} else if(st->bg_state.scene != NULL) {
fapproach_p(&st->bg_state.alpha, 1, st->bg_state.transition_rate);
}
for(CutsceneTextVisual *tv = st->text_visuals.first, *next; tv; tv = next) {
next = tv->next;
float rate = 1/120.0f;
if(tv->target_alpha > 0 && st->text_entry != tv->entry) {
// if we skipped past this one and it's still fading in, speed it up
rate *= 5;
}
if(fapproach_p(&tv->alpha, tv->target_alpha, rate) == 0) {
mem_free(alist_unlink(&st->text_visuals, tv));
}
}
if(st->fadeout_timer > 0) {
if(!(--st->fadeout_timer)) {
return LFRAME_STOP;
}
}
return LFRAME_WAIT;
}
static void draw_background(CutsceneState *st, Texture *erase_mask) {
r_state_push();
r_blend(BLEND_NONE);
r_mat_mv_push();
r_mat_mv_scale(SCREEN_W, SCREEN_H, 1);
r_mat_mv_translate(0.5, 0.5, 0);
float th0, th1, x;
x = 1.0f - st->bg_state.alpha;
th0 = x;
th1 = x * (x + 1.5f * (1.0f - x));
if(st->bg_state.fade_out) {
th0 = 1 - th0;
th1 = 1 - th1;
}
r_shader("cutscene");
r_uniform_vec2("thresholds", th0, th1);
if(st->bg_state.scene) {
r_uniform_sampler("tex", st->bg_state.scene);
}
r_uniform_sampler("noise_tex", "cell_noise");
r_uniform_sampler("paper_tex", "cutscenes/paper");
r_uniform_sampler("erase_mask_tex", erase_mask);
r_uniform_float("distort_strength", 0.01);
r_draw_quad();
r_mat_mv_pop();
r_state_pop();
}
static void draw_center_text(
const CutsceneTextVisual *tv,
const CutscenePhaseTextEntry *e,
TextParams p
) {
p.font_ptr = res_font("big");
p.align = ALIGN_CENTER;
p.pos.x = SCREEN_W/2;
p.pos.y = SCREEN_H/2 - font_get_lineskip(p.font_ptr);
text_draw(e->header, &p);
p.pos.y += font_get_lineskip(p.font_ptr) * 1.2;
p.font_ptr = res_font("standard");
text_draw(e->text, &p);
}
static void draw_text(CutsceneState *st) {
Font *font = res_font("standard");
const float lines = 7;
const float offset_x = 16;
const float offset_y = 24;
float width = SCREEN_W - 2 * offset_x;
float speaker_width = width * 0.1;
float height = lines * font_get_lineskip(font);
float x = offset_x, y = offset_y;
FloatRect textbox = {
.extent = { width + offset_x * 2, height + offset_y * 2 },
.offset = { x + width/2, y + height/2 - 2*offset_y/3 },
};
r_state_push();
ShaderCustomParams cparams = { 0 };
TextParams p = {
.shader_ptr = res_shader("text_cutscene"),
.aux_textures = { res_texture("cell_noise") },
.shader_params = &cparams,
.pos = { x, y },
.align = ALIGN_LEFT,
.font_ptr = font,
.overlay_projection = &textbox,
};
for(CutsceneTextVisual *tv = st->text_visuals.first; tv; tv = tv->next) {
const CutscenePhaseTextEntry *e = tv->entry;
p.color = &e->color;
cparams.vector[0] = tv->alpha;
if(e->type == CTT_CENTERED) {
draw_center_text(tv, e, p);
continue;
}
float w = width;
p.pos.x = x;
p.pos.y = y;
if(e->header != NULL) {
float ofs = 8;
p.pos.x += speaker_width - ofs;
w -= speaker_width;
p.align = ALIGN_RIGHT;
text_draw(e->header, &p);
p.align = ALIGN_LEFT;
p.pos.x += ofs;
}
char buf[strlen(e->text) * 2 + 1];
text_wrap(font, e->text, w, buf, sizeof(buf));
text_draw(buf, &p);
y += text_height(font, buf, 0) * glm_ease_quad_in(fminf(3 * tv->alpha, 1));
}
r_state_pop();
}
static RenderFrameAction cutscene_render_frame(void *ctx) {
CutsceneState *st = ctx;
r_clear(BUFFER_ALL, RGBA(0, 0, 0, 1), 1);
set_ortho(SCREEN_W, SCREEN_H);
r_state_push();
r_framebuffer(st->text_fb);
r_clear(BUFFER_ALL, RGBA(0, 0, 0, 0), 1);
draw_text(st);
r_shader_standard();
r_blend(BLEND_NONE);
r_framebuffer(st->erase_mask_fbpair.back);
r_clear(BUFFER_ALL, RGBA(0, 0, 0, 0), 1);
draw_framebuffer_tex(st->text_fb, SCREEN_W, SCREEN_H);
fbpair_swap(&st->erase_mask_fbpair);
FloatRect mask_vp;
r_framebuffer_viewport_current(st->erase_mask_fbpair.back, &mask_vp);
r_shader("blur9");
r_uniform_vec2("blur_resolution", mask_vp.w, mask_vp.h);
r_framebuffer(st->erase_mask_fbpair.back);
r_clear(BUFFER_ALL, RGBA(0, 0, 0, 0), 1);
r_uniform_vec2("blur_direction", 1, 0);
draw_framebuffer_tex(st->erase_mask_fbpair.front, SCREEN_W, SCREEN_H);
fbpair_swap(&st->erase_mask_fbpair);
r_framebuffer(st->erase_mask_fbpair.back);
r_clear(BUFFER_ALL, RGBA(0, 0, 0, 0), 1);
r_uniform_vec2("blur_direction", 0, 1);
draw_framebuffer_tex(st->erase_mask_fbpair.front, SCREEN_W, SCREEN_H);
fbpair_swap(&st->erase_mask_fbpair);
r_state_pop();
draw_background(st, r_framebuffer_get_attachment(st->erase_mask_fbpair.front, FRAMEBUFFER_ATTACH_COLOR0));
draw_framebuffer_tex(st->text_fb, SCREEN_W, SCREEN_H);
draw_transition();
return RFRAME_SWAP;
}
static void cutscene_end_loop(void *ctx) {
CutsceneState *st = ctx;
res_group_release(&st->rg);
for(CutsceneTextVisual *tv = st->text_visuals.first, *next; tv; tv = next) {
next = tv->next;
mem_free(tv);
}
fbmgr_group_destroy(st->mfb_group);
CallChain cc = st->cc;
mem_free(st);
run_call_chain(&cc, NULL);
}
static void cutscene_preload(const CutscenePhase phases[], ResourceGroup *rg) {
for(const CutscenePhase *p = phases; p->background; ++p) {
if(*p->background) {
res_group_preload(rg, RES_TEXTURE, RESF_DEFAULT, p->background, NULL);
}
}
}
static CutsceneState *cutscene_state_new(const CutscenePhase phases[]) {
auto st = ALLOC(CutsceneState, {
.phase = &phases[0],
.mfb_group = fbmgr_group_create(),
});
res_group_init(&st->rg);
cutscene_preload(phases, &st->rg);
switch_bg(st, st->phase->background);
reset_timers(st);
FBAttachmentConfig a = { 0 };
a.attachment = FRAMEBUFFER_ATTACH_COLOR0;
a.tex_params.type = TEX_TYPE_RGBA_8;
a.tex_params.filter.min = TEX_FILTER_LINEAR;
a.tex_params.filter.mag = TEX_FILTER_LINEAR;
a.tex_params.wrap.s = TEX_WRAP_MIRROR;
a.tex_params.wrap.s = TEX_WRAP_MIRROR;
FramebufferConfig fbconf = { 0 };
fbconf.attachments = &a;
fbconf.num_attachments = 1;
fbconf.resize_strategy.resize_func = fbmgr_resize_strategy_screensized;
st->text_fb = fbmgr_group_framebuffer_create(st->mfb_group, "Cutscene text", &fbconf);
fbconf.resize_strategy.resize_func = NULL;
a.tex_params.width = SCREEN_W / 4;
a.tex_params.height= SCREEN_H / 4;
a.tex_params.type = TEX_TYPE_RGBA_8;
fbmgr_group_fbpair_create(st->mfb_group, "Cutscene erase mask", &fbconf, &st->erase_mask_fbpair);
return st;
}
void cutscene_enter(CallChain next, CutsceneID id) {
assert((uint)id < NUM_CUTSCENE_IDS);
progress_unlock_cutscene(id);
const Cutscene *cs = g_cutscenes + id;
if(cs->phases == NULL) {
log_error("Cutscene %i not implemented!", id);
run_call_chain(&next, NULL);
return;
}
log_info("Playing cutscene ID: #%i", id);
CutsceneState *st = cutscene_state_new(cs->phases);
st->cc = next;
st->bg_state.transition_rate = 1/80.0f;
audio_bgm_play(res_bgm(cs->bgm), true, 0, 1);
eventloop_enter(st, cutscene_logic_frame, cutscene_render_frame, cutscene_end_loop, FPS);
}
const char *cutscene_get_name(CutsceneID id) {
assert((uint)id < NUM_CUTSCENE_IDS);
return g_cutscenes[id].name;
}