#include #include "sm64.h" #include "gfx_dimensions.h" #include "engine/math_util.h" #include "memory.h" #include "area.h" #include "save_file.h" #include "segment2.h" #include "level_update.h" #include "geo_misc.h" /** * @file skybox.c * * Implements the skybox background. * * It's not exactly a sky"box": it's more of a sky tilemap. It renders a 3x3 grid of 32x32 pieces of the * whole skybox image, starting from the top left based on the camera's rotation. A skybox image has 64 * unique 32x32 tiles, with the first two columns duplicated for a total of 80. * * The tiles are mapped to world space such that 2 full tiles fit on the screen, for a total of * 8 tiles around the full 360 degrees. Each tile takes up 45 degrees of the camera's field of view, and * the code draws 3 tiles or 135 degrees of the skybox in a frame. But only 2 tiles, or 90 degrees, can * fit on-screen at a time. * * @bug FOV is handled strangely by the code. It is used to scale and rotate the skybox, when really it * should probably only be used to calculate the distance drawn from the center of the looked-at tile. * But since the game always sets it to 90 degrees, the skybox always scales and rotates the same, * regardless of the camera's actual FOV. So even if the camera's FOV is 10 degrees the game draws a * full 90 degrees of the skybox, which makes the sky look really far away. * * @bug Skyboxes unnecessarily repeat the first 2 columns when they could just wrap the col index. * Although, the wasted space is only about 128 bytes for each image. */ /** * Describes the position, tiles, and orientation of the skybox image. * * Describes the scaled x and y offset into the tilemap, based on the yaw and pitch. Computes the * upperLeftTile index into the skybox's tile list using scaledX and scaledY. See get_top_left_tile_idx. * * The skybox is always drawn behind everything, because in the level's geo script, the skybox is drawn * first, in a display list with the Z buffer disabled */ struct Skybox { /// The camera's yaw, from 0 to 65536, which maps to 0 to 360 degrees u16 yaw; /// The camera's pitch, which is bounded by +-16384, which maps to -90 to 90 degrees s16 pitch; /// The skybox's X position in world space s32 scaledX; /// The skybox's Y position in world space s32 scaledY; /// The index of the upper-left tile in the 3x3 grid that gets drawn s32 upperLeftTile; }; struct Skybox sSkyBoxInfo[2]; typedef const u8 *const SkyboxTexture[80]; extern SkyboxTexture bbh_skybox_ptrlist; extern SkyboxTexture bidw_skybox_ptrlist; extern SkyboxTexture bitfs_skybox_ptrlist; extern SkyboxTexture bits_skybox_ptrlist; extern SkyboxTexture ccm_skybox_ptrlist; extern SkyboxTexture cloud_floor_skybox_ptrlist; extern SkyboxTexture clouds_skybox_ptrlist; extern SkyboxTexture ssl_skybox_ptrlist; extern SkyboxTexture water_skybox_ptrlist; extern SkyboxTexture wdw_skybox_ptrlist; SkyboxTexture *sSkyboxTextures[10] = { &water_skybox_ptrlist, &bitfs_skybox_ptrlist, &wdw_skybox_ptrlist, &cloud_floor_skybox_ptrlist, &ccm_skybox_ptrlist, &ssl_skybox_ptrlist, &bbh_skybox_ptrlist, &bidw_skybox_ptrlist, &clouds_skybox_ptrlist, &bits_skybox_ptrlist, }; /** * The skybox color mask. * The final color of each pixel is computed from the bitwise AND of the color and the texture. */ u8 sSkyboxColors[][3] = { { 0x50, 0x64, 0x5A }, { 0xFF, 0xFF, 0xFF }, }; /** * Constant used to scale the skybox horizontally to a multiple of the screen's width */ #define SKYBOX_WIDTH (4 * SCREEN_WIDTH) /** * Constant used to scale the skybox vertically to a multiple of the screen's height */ #define SKYBOX_HEIGHT (4 * SCREEN_HEIGHT) /** * The tile's width in world space. * By default, two full tiles can fit in the screen. */ #define SKYBOX_TILE_WIDTH (SCREEN_WIDTH / 2) /** * The tile's height in world space. * By default, two full tiles can fit in the screen. */ #define SKYBOX_TILE_HEIGHT (SCREEN_HEIGHT / 2) /** * The horizontal length of the skybox tilemap in tiles. */ #define SKYBOX_COLS (10) /** * The vertical length of the skybox tilemap in tiles. */ #define SKYBOX_ROWS (8) /** * Convert the camera's yaw into an x position into the scaled skybox image. * * fov is always 90 degrees, set in draw_skybox_facing_camera. * * The calculation performed is equivalent to (360 / fov) * (yaw / 65536) * SCREEN_WIDTH * in other words: (the number of fov-sized parts of the circle there are) * * (how far is the camera rotated from 0, scaled 0 to 1) * * (the screen width) */ int calculate_skybox_scaled_x(s8 player, f32 fov) { f32 yaw = sSkyBoxInfo[player].yaw; //! double literals are used instead of floats f32 yawScaled = SCREEN_WIDTH * 360.0 * yaw / (fov * 65536.0); // Round the scaled yaw. Since yaw is a u16, it doesn't need to check for < 0 s32 scaledX = yawScaled + 0.5; if (scaledX > SKYBOX_WIDTH) { scaledX -= scaledX / SKYBOX_WIDTH * SKYBOX_WIDTH; } return SKYBOX_WIDTH - scaledX; } /** * Convert the camera's pitch into a y position in the scaled skybox image. * * fov may have been used in an earlier version, but the developers changed the function to always use * 90 degrees. */ int calculate_skybox_scaled_y(s8 player, UNUSED f32 fov) { // Convert pitch to degrees. Pitch is bounded between -90 (looking down) and 90 (looking up). f32 pitchInDegrees = (f32) sSkyBoxInfo[player].pitch * 360.0 / 65535.0; // Scale by 360 / fov f32 degreesToScale = 360.0f * pitchInDegrees / 90.0; s32 roundedY = round_float(degreesToScale); // Since pitch can be negative, and the tile grid starts 1 octant above the camera's focus, add // 5 octants to the y position s32 scaledY = roundedY + 5 * SKYBOX_TILE_HEIGHT; if (scaledY > SKYBOX_HEIGHT) { scaledY = SKYBOX_HEIGHT; } if (scaledY < SCREEN_HEIGHT) { scaledY = SCREEN_HEIGHT; } return scaledY; } /** * Converts the upper left xPos and yPos to the index of the upper left tile in the skybox. */ static int get_top_left_tile_idx(s8 player) { s32 tileCol = sSkyBoxInfo[player].scaledX / SKYBOX_TILE_WIDTH; s32 tileRow = (SKYBOX_HEIGHT - sSkyBoxInfo[player].scaledY) / SKYBOX_TILE_HEIGHT; return tileRow * SKYBOX_COLS + tileCol; } /** * Generates vertices for the skybox tile. * * @param tileIndex The index into the 32x32 sections of the whole skybox image. The index is converted * into an x and y by modulus and division by SKYBOX_COLS. x and y are then scaled by * SKYBOX_TILE_WIDTH to get a point in world space. */ Vtx *make_skybox_rect(s32 tileIndex, s8 colorIndex) { Vtx *verts = alloc_display_list(4 * sizeof(*verts)); s16 x = tileIndex % SKYBOX_COLS * SKYBOX_TILE_WIDTH; s16 y = SKYBOX_HEIGHT - tileIndex / SKYBOX_COLS * SKYBOX_TILE_HEIGHT; if (verts != NULL) { make_vertex(verts, 0, x, y, -1, 0, 0, sSkyboxColors[colorIndex][0], sSkyboxColors[colorIndex][1], sSkyboxColors[colorIndex][2], 255); make_vertex(verts, 1, x, y - SKYBOX_TILE_HEIGHT, -1, 0, 31 << 5, sSkyboxColors[colorIndex][0], sSkyboxColors[colorIndex][1], sSkyboxColors[colorIndex][2], 255); make_vertex(verts, 2, x + SKYBOX_TILE_WIDTH, y - SKYBOX_TILE_HEIGHT, -1, 31 << 5, 31 << 5, sSkyboxColors[colorIndex][0], sSkyboxColors[colorIndex][1], sSkyboxColors[colorIndex][2], 255); make_vertex(verts, 3, x + SKYBOX_TILE_WIDTH, y, -1, 31 << 5, 0, sSkyboxColors[colorIndex][0], sSkyboxColors[colorIndex][1], sSkyboxColors[colorIndex][2], 255); } else { } return verts; } /** * Draws a 3x3 grid of 32x32 sections of the original skybox image. * The row and column are converted into an index into the skybox's tile list, which is then drawn in * world space so that the tiles will rotate with the camera. */ void draw_skybox_tile_grid(Gfx **dlist, s8 background, s8 player, s8 colorIndex) { s32 row; s32 col; for (row = 0; row < 3; row++) { for (col = 0; col < 3; col++) { s32 tileIndex = sSkyBoxInfo[player].upperLeftTile + row * SKYBOX_COLS + col; const u8 *const texture = (*(SkyboxTexture *) segmented_to_virtual(sSkyboxTextures[background]))[tileIndex]; Vtx *vertices = make_skybox_rect(tileIndex, colorIndex); gLoadBlockTexture((*dlist)++, 32, 32, G_IM_FMT_RGBA, texture); gSPVertex((*dlist)++, VIRTUAL_TO_PHYSICAL(vertices), 4, 0); gSPDisplayList((*dlist)++, dl_draw_quad_verts_0123); } } } void *create_skybox_ortho_matrix(s8 player) { f32 left = sSkyBoxInfo[player].scaledX; f32 right = sSkyBoxInfo[player].scaledX + SCREEN_WIDTH; f32 bottom = sSkyBoxInfo[player].scaledY - SCREEN_HEIGHT; f32 top = sSkyBoxInfo[player].scaledY; Mtx *mtx = alloc_display_list(sizeof(*mtx)); #ifndef TARGET_N64 f32 half_width = (4.0f / 3.0f) / GFX_DIMENSIONS_ASPECT_RATIO * SCREEN_WIDTH / 2; f32 center = (sSkyBoxInfo[player].scaledX + SCREEN_WIDTH / 2); if (half_width < SCREEN_WIDTH / 2) { // A wider screen than 4:3 left = center - half_width; right = center + half_width; } #endif if (mtx != NULL) { guOrtho(mtx, left, right, bottom, top, 0.0f, 3.0f, 1.0f); } else { } return mtx; } /** * Creates the skybox's display list, then draws the 3x3 grid of tiles. */ Gfx *init_skybox_display_list(s8 player, s8 background, s8 colorIndex) { s32 dlCommandCount = 5 + (3 * 3) * 7; // 5 for the start and end, plus 9 skybox tiles void *skybox = alloc_display_list(dlCommandCount * sizeof(Gfx)); Gfx *dlist = skybox; if (skybox == NULL) { return NULL; } else { Mtx *ortho = create_skybox_ortho_matrix(player); gSPDisplayList(dlist++, dl_skybox_begin); gSPMatrix(dlist++, VIRTUAL_TO_PHYSICAL(ortho), G_MTX_PROJECTION | G_MTX_MUL | G_MTX_NOPUSH); gSPDisplayList(dlist++, dl_skybox_tile_tex_settings); draw_skybox_tile_grid(&dlist, background, player, colorIndex); gSPDisplayList(dlist++, dl_skybox_end); gSPEndDisplayList(dlist); } return skybox; } /** * Draw a skybox facing the direction from pos to foc. * * @param player Unused, determines which orientation info struct to update * @param background The skybox image to use * @param fov Unused. It SHOULD control how much the skybox is scaled, but the way it's coded it just * controls how fast the skybox rotates. The given value is replaced with 90 right before the * dl is created * @param posX,posY,posZ The camera's position * @param focX,focY,focZ The camera's focus. */ Gfx *create_skybox_facing_camera(s8 player, s8 background, f32 fov, f32 posX, f32 posY, f32 posZ, f32 focX, f32 focY, f32 focZ) { f32 cameraFaceX = focX - posX; f32 cameraFaceY = focY - posY; f32 cameraFaceZ = focZ - posZ; s8 colorIndex = 1; // If the first star is collected in JRB, make the sky darker and slightly green if (background == 8 && !(save_file_get_star_flags(gCurrSaveFileNum - 1, COURSE_JRB - 1) & 1)) { colorIndex = 0; } //! fov is always set to 90.0f. If this line is removed, then the game crashes because fov is 0 on //! the first frame, which causes a floating point divide by 0 fov = 90.0f; sSkyBoxInfo[player].yaw = atan2s(cameraFaceZ, cameraFaceX); sSkyBoxInfo[player].pitch = atan2s(sqrtf(cameraFaceX * cameraFaceX + cameraFaceZ * cameraFaceZ), cameraFaceY); sSkyBoxInfo[player].scaledX = calculate_skybox_scaled_x(player, fov); sSkyBoxInfo[player].scaledY = calculate_skybox_scaled_y(player, fov); sSkyBoxInfo[player].upperLeftTile = get_top_left_tile_idx(player); return init_skybox_display_list(player, background, colorIndex); }