#define __BSD_VISIBLE 1 #include #include "render.h" #include "game.h" #include "util.h" #include #include void camera::set_fov(num fov) { this->fov = fov; float fov_radians = (float)fov * 3.14159 / 180; float sd = 1 / tanf(fov_radians / 2); this->_sd = num(sd); } void camera_track(struct camera *camera, struct player const *player) { camera->pos = player->pos() + player->up() * RENDER_EYE_HEIGHT; camera->pos.z -= RENDER_CAMERA_BACK_DISTANCE; camera->platform = player->platform; if(player->jump_dir && player->jump_t >= TIME_ROTATION / 2) camera->platform += player->jump_dir; num angle = player->world_angle(); camera->angle_vector = vec2(num_cos(-angle), num_sin(-angle)); } vec3 camera_project_point(struct camera *camera, vec3 u) { if(u.z < camera->pos.z + camera->near_plane()) return u; u = vec_rotate_around_z(u - camera->pos, camera->angle_vector); num f = camera->screen_distance() * (camera->screen_size.y / 2) / u.z; u.x = u.x * f + camera->screen_size.x / 2; u.y = -u.y * f + camera->screen_size.y / 2 ; return u; } void camera_project_prect(struct camera *camera, struct prect *p) { /* Require the rectangle to be already clipped */ if(p->nl.z < camera->pos.z + camera->near_plane()) return; vec2 half_screen(camera->screen_size.x / 2, camera->screen_size.y / 2); p->nl = vec_rotate_around_z(p->nl - camera->pos, camera->angle_vector); p->nr = vec_rotate_around_z(p->nr - camera->pos, camera->angle_vector); p->fl = vec_rotate_around_z(p->fl - camera->pos, camera->angle_vector); p->fr = vec_rotate_around_z(p->fr - camera->pos, camera->angle_vector); /* We assume nl/nr have the same z, and so do fl/fr */ num f = camera->screen_distance() * half_screen.y; num near_f = f / p->nl.z; num far_f = f / p->fl.z; p->nl.x = p->nl.x * near_f + half_screen.x; p->nl.y = -p->nl.y * near_f + half_screen.y; p->nr.x = p->nr.x * near_f + half_screen.x; p->nr.y = -p->nr.y * near_f + half_screen.y; p->fl.x = p->fl.x * far_f + half_screen.x; p->fl.y = -p->fl.y * far_f + half_screen.y; p->fr.x = p->fr.x * far_f + half_screen.x; p->fr.y = -p->fr.y * far_f + half_screen.y; } void camera_project_frect(struct camera *camera, struct frect *f) { f->tr = camera_project_point(camera, f->tr); f->tl = camera_project_point(camera, f->tl); f->br = camera_project_point(camera, f->br); f->bl = camera_project_point(camera, f->bl); } void camera_project_srect(struct camera *camera, struct srect *s) { s->nt = camera_project_point(camera, s->nt); s->nb = camera_project_point(camera, s->nb); s->ft = camera_project_point(camera, s->ft); s->fb = camera_project_point(camera, s->fb); } int camera_platform_color(struct camera const *camera, int platform_id) { /* Accounting for the full angle is too precise and results in weird color jumps at different times across platforms, instead switch at half movement. */ int dist = platform_id - camera->platform; if(dist < 0) dist += PLATFORM_COUNT; dist = std::min(dist, PLATFORM_COUNT - dist); int gray = 31 - 2 * dist; return C_RGB(gray, gray, gray); } //======= Rendering tools =======// static uint16_t blue_ramp[32]; static void split_RGB(uint32_t RGB, int *R, int *G, int *B) { *R = (RGB >> 16) & 0xff; *G = (RGB >> 8) & 0xff; *B = (RGB >> 0) & 0xff; } static uint32_t make_RGB(int R, int G, int B) { return ((R & 0xff) << 16) | ((G & 0xff) << 8) | (B & 0xff); } static void gradient(uint32_t RGB1, uint32_t RGB2, uint16_t *gradient, int N) { int R1, G1, B1, R2, G2, B2; split_RGB(RGB1, &R1, &G1, &B1); split_RGB(RGB2, &R2, &G2, &B2); for(int i = 0; i < N; i++) { int R = ((N-1-i) * R1 + i * R2) / (N-1); int G = ((N-1-i) * G1 + i * G2) / (N-1); int B = ((N-1-i) * B1 + i * B2) / (N-1); gradient[i] = RGB24(make_RGB(R, G, B)); } } void render_init(void) { /* Generate a gradient ramp for blue platforms */ gradient(0x56a3ef, 0x25c5ba, blue_ramp, 32); } uint16_t render_color_blue_platform(num t) { int step = (int)(32 * t.frac()) & 31; int phase = (int)t & 1; return phase ? blue_ramp[31 - step] : blue_ramp[step]; } uint16_t render_platform_color(struct platform const *p, struct camera const *camera, num t) { if(p->type == PLATFORM_WHITE) return camera_platform_color(camera, p->face); if(p->type == PLATFORM_BLUE) return render_color_blue_platform(t); if(p->type == PLATFORM_SEPARATING_WALL) return RGB24(0x909090); if(p->type == PLATFORM_BLOCK_TOP) return RGB24(0xa0a0a0); if(p->type == PLATFORM_BLOCK) return RGB24(0x808080); if(p->type == PLATFORM_RED) { return t.frac() >= num(0.6) ? camera_platform_color(camera, p->face) : RGB24(0xd06060); } return RGB24(0xff00ff); } void render_triangle(vec3 *p1, vec3 *p2, vec3 *p3, int color) { azrp_triangle( (int)p1->x, (int)p1->y, (int)p2->x, (int)p2->y, (int)p3->x, (int)p3->y, color); } void render_dots(std::initializer_list const &&points) { extern image_t img_dot; for(auto p: points) azrp_image((int)p.x - 2, (int)p.y - 2, &img_dot); } void render_anim_frame(int x, int y, struct anim *anim, int frame) { azrp_image( x + anim->x_offsets[frame] - anim->x_anchor, y + anim->y_offsets[frame] - anim->y_anchor, anim->frames[frame]); } void render_anim_frame_palette(int x, int y, struct anim *anim, int frame, uint16_t const *palette) { image_t img = *anim->frames[frame]; img.palette = (uint16_t *)palette; azrp_image( x + anim->x_offsets[frame] - anim->x_anchor, y + anim->y_offsets[frame] - anim->y_anchor, &img); } static bool render_energy_str_glyph_params(int size, int c, int *ix, int *w) { size = (size >= 1 && size <= 3) ? size-1 : 0; int dw[3] = { 11, 14, 18 }; int ow[3] = { 8, 11, 16 }; int dotx[3] = { 127, 157, 198 }; int dotw[3] = { 4, 5, 7 }; int perx[3] = { 132, 164, 207 }; int perw[3] = { 8, 13, 16 }; if(c >= '0' && c <= '9') { *ix = (dw[size] + 2) * (c-'0') - (c >= '2' ? (dw[size]-ow[size]) : 0); *w = (c == '1') ? ow[size] : dw[size]; return true; } if(c == '.') { *ix = dotx[size]; *w = dotw[size]; return true; } if(c == '%') { *ix = perx[size]; *w = perw[size]; return true; } return false; } void render_energy_str(int x, int y, int halign, int size, char const *str) { extern image_t img_font_energy, img_font_energy2, img_font_energy3; image_t const *img = (size == 3) ? &img_font_energy3 : (size == 2) ? &img_font_energy2 : &img_font_energy; int ix, w; /* First compute width of string */ int total_width = 0; for(int i = 0; str[i]; i++) { if(render_energy_str_glyph_params(size, str[i], &ix, &w)) total_width += w + 2; } total_width -= (total_width ? 2 : 0); /* Apply horizontal alignment */ if(halign == DTEXT_CENTER) x -= total_width >> 1; if(halign == DTEXT_RIGHT) x -= total_width - 1; /* Render string */ for(int i = 0; str[i]; i++) { if(!render_energy_str_glyph_params(size, str[i], &ix, &w)) continue; azrp_subimage(x, y, img, ix, 0, w, img->height, DIMAGE_NONE); x += w + 2; } } static void render_effect_bpp_internal(image_t **image, num16 t) { static uint16_t bpp_palette[] = { RGB24(0xff00ff), /* alpha */ C_WHITE, }; static int const bpp_W = 35; static int const bpp_H = 68; if(!*image) { *image = image_alloc(bpp_W, bpp_H, IMAGE_P8_RGB565A); if(!*image) return; image_set_palette(*image, bpp_palette, 2, false); } image_clear(*image); uint8_t *pixels = (uint8_t *)(*image)->data; for(int i = 0; i < 32; i++) { /* Particule #i is visible from t=i/32 until t=i/32+1. Local time variable is normalized to 0..1. */ num16 start = num16(i) / 32; if(t < start) continue; num16 local_time = (t - start) % num16(1); int x0_int = (bpp_W / 2 - 17) + (((967 * i) >> 4) & 31); /* 0+(...)&31 => [0,31] = [0,W-3] */ num16 x0 = num16(x0_int); num16 y0 = num16(bpp_H - 4 - ((37 * i) & 15)); /* [H-19, H-4] */ num16 vx = num16(bpp_W / 2 - x0_int) / 2; num16 x = x0 + vx * local_time; num16 y = y0 - (bpp_H - 19) * local_time; /* (0, H-4] */ int size = 4 - (y.ifloor() >> 4); for(int dy = 0; dy <= size; dy++) for(int dx = 0; dx <= size; dx++) { // if((unsigned)((int)y + dy) >= bpp_H) // abort(); // if((unsigned)((int)x + dx) >= bpp_W) // abort(); pixels[((int)y + dy) * (*image)->stride + (int)x + dx] = 0x81; } } } void render_effect_bpp(image_t **image, num global_t) { /* Time within the animation (loops every second). Two segments: - t = [0..1) for the animation startup (particles appear progressively) - t = [1..2) for the loop (all particles visible) */ num16 t = num16(global_t).frac() + num16(global_t >= 1); return render_effect_bpp_internal(image, t); } void render_effect_dp(int x, int y, int count, num t, int color) { for(int i = 0; i < count; i++) { num lifetime = (num(37 * i) / 16).frac() + num(0.75); if(t >= lifetime) continue; num v = (num(113 * i * i) / 24) % num(8); num t0 = v / 8; v = v * v + num(4); num alpha = num((163 * i) / 32) % num(6.28); num vx = v * num_cos(alpha); num vy = v * num_sin(alpha); num local_t = TIME_DEATH_ANIMATION * TIME_DEATH_ANIMATION - (TIME_DEATH_ANIMATION - t) * (TIME_DEATH_ANIMATION - t); int rx = x + (int)((t0 + local_t) * vx); int ry = y + (int)((t0 + local_t) * vy); azrp_rect(rx, ry, 3, 3, color); } } uint16_t render_blend(uint16_t c1, uint16_t c2, int t) { uint32_t o1 = ((c1 << 16) | c1) & 0x07e0f81f; uint32_t o2 = ((c2 << 16) | c2) & 0x07e0f81f; uint32_t r = (t * (o2 - o1) >> 5) + o1; r &= 0x07e0f81f; return (r >> 16) | r; } static void render_game_platforms(struct game &game) { num near = game.camera.pos.z + game.camera.near_plane(); for(auto it = game.level.platform_buffer.rbegin(); it != game.level.platform_buffer.rend(); ++it) { int color = render_platform_color(&*it, &game.camera, game.t); if(it->type == PLATFORM_SEPARATING_WALL) { struct srect side = space_wall_position(&*it); /* Check if the wall should be rendered at all; it might be on the side of a block that's hiding it from the player's view */ int d = (it->face - game.player.platform) % PLATFORM_COUNT; if(d > PLATFORM_COUNT / 2) d -= PLATFORM_COUNT; if(d <= -PLATFORM_COUNT / 2) d += PLATFORM_COUNT; if((d < 0 && !it->wall_left_sided) || (d > 0 && !it->wall_right_sided)) continue; if(side.ft.z > near) { if(side.nt.z <= near) side.nt.z = near; if(side.nb.z <= near) side.nb.z = near; camera_project_srect(&game.camera, &side); render_triangle(&side.ft, &side.fb, &side.nb, color); render_triangle(&side.ft, &side.nb, &side.nt, color); } } else if(it->type == PLATFORM_BLOCK) { struct frect front = space_block_position(&*it); if(front.tl.z > near) { camera_project_frect(&game.camera, &front); render_triangle(&front.tr, &front.tl, &front.bl, color); render_triangle(&front.tr, &front.bl, &front.br, color); } } else if(it->type == PLATFORM_BLOCK_TOP) { struct prect top = space_block_top_position(&*it); if(top.fl.z > near) { if(top.nl.z <= near) top.nl.z = near; if(top.nr.z <= near) top.nr.z = near; camera_project_prect(&game.camera, &top); render_triangle(&top.nl, &top.fr, &top.fl, color); render_triangle(&top.fr, &top.nl, &top.nr, color); } } /* Normal, flat platforms */ else { struct prect p = space_platform_position(&*it); /* Near plane clipping */ if(p.fl.z <= near) continue; if(p.nl.z <= near) p.nl.z = near; if(p.nr.z <= near) p.nr.z = near; camera_project_prect(&game.camera, &p); render_triangle(&p.nl, &p.fr, &p.fl, color); render_triangle(&p.fr, &p.nl, &p.nr, color); } } } /* Blue platform particle effect's buffer image */ static image_t *_effect_bpp = NULL; static int cubic(int start, int end, num t, num tmax) { if(t >= tmax) return end; t = t / tmax; num x = num(1.0) - (num(1.0) - t) * (num(1.0) - t) * (num(1.0) - t); return (int)(num(start) + num(end - start) * x + num(0.5)); } void render_game(struct game &game, prof_t *perf_comp) { azrp_perf_clear(); game.perf.effect_bpp = prof_make(); struct player *player = &game.player; vec3 player_dot = camera_project_point(&game.camera, player->pos()); bool alive = (player->stance != player::Collided); 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); render_game_platforms(game); prof_leave_norec(*perf_comp); struct anim *player_anim; int player_frame; player->get_anim(&player_anim, &player_frame); /* Make a bluer palette palette when running on blue platforms */ uint16_t palette[16]; int blue_tint_alpha = 0; if(player->blueanim > 0) { num t = (player->blueanim >= 2) ? (num(2.5) - player->blueanim) / 2 : min(player->blueanim, num(0.25)); blue_tint_alpha = (t * 96).ifloor(); } for(int i = 0; i < 16; i++) { palette[i] = render_blend( player_anim->frames[player_frame]->palette[i], RGB24(0xb0d0f0), blue_tint_alpha); } render_anim_frame_palette((int)player_dot.x, (int)player_dot.y, player_anim, player_frame, palette); if(player->blueanim > 0) { prof_enter(game.perf.effect_bpp); render_effect_bpp(&_effect_bpp, player->blueanim); azrp_image((int)player_dot.x - _effect_bpp->width / 2, (int)player_dot.y - _effect_bpp->height + 2, _effect_bpp); prof_leave(game.perf.effect_bpp); } /* Render energy score */ azrp_rect(DWIDTH-100, 4, 96, 20, AZRP_RECT_DARKEN); render_energy_str(DWIDTH-8, 8, DTEXT_RIGHT, 1, energy_str); } /* End transition and end screen */ else { /* Background flash of light */ if(game.t_death < num(0.25)) { int alpha = 31 - (int)(31 * (game.t_death / num(0.25))); shader_fsblend(C_WHITE, alpha); } /* Background fading to end screen */ if(game.t_death > num(0.75)) { 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 */ if(game.t_death < TIME_DEATH_ANIMATION) { int color_t = (int)(min(game.t_death, num(1.0)) * 32); render_effect_dp((int)player_dot.x, (int)player_dot.y - 40, 256, game.t_death, render_blend(C_WHITE, 0x5555, color_t)); } /* End screen elements appearing in order */ num t_endscreen = game.t_death - num(1.0); int xe = 40, ye0 = 20, ye = ye0; if(t_endscreen >= num(0.0)) { azrp_print(xe, ye, C_WHITE, "experiments %s", game.level.name); } if(t_endscreen >= num(0.1)) { ye += 15; azrp_text(xe, ye, C_WHITE, "UNDISCOVERED"); } if(t_endscreen >= num(0.2)) { ye += 20; azrp_text(xe, ye+2, C_WHITE, "energy"); azrp_text(xe, ye+11, C_WHITE, "reading"); render_energy_str(xe+65, ye, DTEXT_LEFT, 3, energy_str); azrp_text(xe+65, ye+22, RGB24(0x9cefe3), "new peak"); } if(t_endscreen >= num(0.3)) { ye += 40; azrp_text(xe, ye+1, C_WHITE, "old peak"); // TODO: Old energy str render_energy_str(xe+65, ye, DTEXT_LEFT, 1, "0.00%"); } if(t_endscreen >= num(0.4)) { ye += 20; azrp_text(xe, ye, C_WHITE, "100%"); azrp_rect(xe, ye+10, 160, 40, AZRP_RECT_WHITEN); azrp_text(xe, ye+49, C_WHITE, "0%"); int h_footer = 30; int y_footer = cubic(DHEIGHT, DHEIGHT - h_footer, t_endscreen - num(0.4), num(0.2)); azrp_rect(0, y_footer, DWIDTH, h_footer, C_WHITE); azrp_text_opt(8, y_footer + 14, NULL, RGB24(0x49759f), DTEXT_LEFT, DTEXT_MIDDLE, "exit: back", -1); azrp_text_opt(DWIDTH-8, y_footer + 14, NULL, RGB24(0x49759f), DTEXT_RIGHT, DTEXT_MIDDLE, "SHIFT: AGAIN",-1); } if(t_endscreen >= num(0.5)) { xe = 230; ye = ye0; // TODO: Run count azrp_text(xe, ye, C_WHITE, "run count 42"); azrp_text(xe, ye+10, C_WHITE, "particle trace"); } if(t_endscreen >= num(0.6)) { ye += 30; // 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)); /* // Debugging overflows on BPP effect static num16 t = 0; render_effect_bpp_internal(&_effect_bpp, t); t.v = (t.v + 1) & 511; azrp_image(azrp_width / 2 - _effect_bpp->width / 2, azrp_height / 2 - _effect_bpp->height + 2, _effect_bpp); */ } } azrp_update(); }