diff --git a/include/config/config_graphics.h b/include/config/config_graphics.h index 4c64c597a..bf1aed2ec 100644 --- a/include/config/config_graphics.h +++ b/include/config/config_graphics.h @@ -133,3 +133,20 @@ * NOTE: When this is enabled, The 49th hardcoded rectangle shadow will act as a regular circular shadow, due to Mario's shadow ID being 99 in vanilla. */ #define LEGACY_SHADOW_IDS + + +/** + * Limits the horizontal fov on emulator like on console. May break viewport widescreen hacks. + */ +//#define HORIZONTAL_CULLING_ON_EMULATOR + +/** + * Makes objects bellow the screen be culled. + */ +#define VERTICAL_CULLING + +/** + * If the first command of an object´s geolayout is not GEO_CULLING_RADIUS, DEFAULT_CULLING_RADIUS + * will be used instead. + */ +#define DEFAULT_CULLING_RADIUS 300 diff --git a/src/engine/geo_layout.c b/src/engine/geo_layout.c index 15172300b..d91dac3d9 100644 --- a/src/engine/geo_layout.c +++ b/src/engine/geo_layout.c @@ -261,7 +261,7 @@ void geo_layout_cmd_node_perspective(void) { gGeoLayoutCommand += 4 << CMD_SIZE_SHIFT; } - graphNode = init_graph_node_perspective(gGraphNodePool, NULL, (f32) fov, near, far, frustumFunc, 0); + graphNode = init_graph_node_perspective(gGraphNodePool, NULL, (f32) fov, near, far, frustumFunc); register_scene_graph_node(&graphNode->fnNode.node); diff --git a/src/engine/graph_node.c b/src/engine/graph_node.c index e20cd9c0e..62fccb881 100644 --- a/src/engine/graph_node.c +++ b/src/engine/graph_node.c @@ -71,7 +71,7 @@ init_graph_node_ortho_projection(struct AllocOnlyPool *pool, struct GraphNodeOrt struct GraphNodePerspective *init_graph_node_perspective(struct AllocOnlyPool *pool, struct GraphNodePerspective *graphNode, f32 fov, u16 near, u16 far, - GraphNodeFunc nodeFunc, s32 unused) { + GraphNodeFunc nodeFunc) { if (pool != NULL) { graphNode = alloc_only_pool_alloc(pool, sizeof(struct GraphNodePerspective)); } @@ -83,7 +83,6 @@ struct GraphNodePerspective *init_graph_node_perspective(struct AllocOnlyPool *p graphNode->near = near; graphNode->far = far; graphNode->fnNode.func = nodeFunc; - graphNode->unused = unused; if (nodeFunc != NULL) { nodeFunc(GEO_CONTEXT_CREATE, &graphNode->fnNode.node, pool); diff --git a/src/engine/graph_node.h b/src/engine/graph_node.h index 857272ddd..4d0d4d2a9 100644 --- a/src/engine/graph_node.h +++ b/src/engine/graph_node.h @@ -131,10 +131,13 @@ struct GraphNodeOrthoProjection { */ struct GraphNodePerspective { /*0x00*/ struct FnGraphNode fnNode; - /*0x18*/ s32 unused; - /*0x1C*/ f32 fov; // horizontal field of view in degrees - /*0x20*/ u16 near; // near clipping plane - /*0x22*/ u16 far; // far clipping plane + /*0x18*/ f32 fov; // horizontal field of view in degrees + /*0x1C*/ u16 near; // near clipping plane + /*0x1E*/ u16 far; // far clipping plane + /*0x20*/ f32 halfFovHorizontal; +#ifdef VERTICAL_CULLING + /*0x24*/ f32 halfFovVertical; +#endif }; /** An entry in the master list. It is a linked list of display lists @@ -369,7 +372,7 @@ void init_scene_graph_node_links(struct GraphNode *graphNode, s32 type); struct GraphNodeRoot *init_graph_node_root (struct AllocOnlyPool *pool, struct GraphNodeRoot *graphNode, s16 areaIndex, s16 x, s16 y, s16 width, s16 height); struct GraphNodeOrthoProjection *init_graph_node_ortho_projection (struct AllocOnlyPool *pool, struct GraphNodeOrthoProjection *graphNode, f32 scale); -struct GraphNodePerspective *init_graph_node_perspective (struct AllocOnlyPool *pool, struct GraphNodePerspective *graphNode, f32 fov, u16 near, u16 far, GraphNodeFunc nodeFunc, s32 unused); +struct GraphNodePerspective *init_graph_node_perspective (struct AllocOnlyPool *pool, struct GraphNodePerspective *graphNode, f32 fov, u16 near, u16 far, GraphNodeFunc nodeFunc); struct GraphNodeStart *init_graph_node_start (struct AllocOnlyPool *pool, struct GraphNodeStart *graphNode); struct GraphNodeMasterList *init_graph_node_master_list (struct AllocOnlyPool *pool, struct GraphNodeMasterList *graphNode, s16 on); struct GraphNodeLevelOfDetail *init_graph_node_render_range (struct AllocOnlyPool *pool, struct GraphNodeLevelOfDetail *graphNode, s16 minDistance, s16 maxDistance); diff --git a/src/game/rendering_graph_node.c b/src/game/rendering_graph_node.c index 174c069f1..1c2a4e2e0 100644 --- a/src/game/rendering_graph_node.c +++ b/src/game/rendering_graph_node.c @@ -566,6 +566,28 @@ void geo_process_perspective(struct GraphNodePerspective *node) { sAspectRatio = 4.0f / 3.0f; // 1.33333f #endif + // The reason this is not divided as an integer is to prevent an integer division. + f32 vHalfFov = ( (f32) ((node->fov * 4096) + 8192) ) / 45.f; + + // We need to account for aspect ratio changes by multiplying by the widescreen horizontal stretch + // (normally 1.775). + f32 hHalfFov = vHalfFov * sAspectRatio; + + node->halfFovHorizontal = tans(hHalfFov); + +#ifdef VERTICAL_CULLING + node->halfFovVertical = tans(vHalfFov); +#endif + +#ifndef HORIZONTAL_CULLING_ON_EMULATOR + // If an emulator is detected, use a large value for the half fov + // horizontal value to account for viewport widescreen hacks. + + if(!gIsConsole){ + node->halfFovHorizontal = 9999.0f; + } +#endif + guPerspective(mtx, &perspNorm, node->fov, sAspectRatio, node->near / WORLD_SCALE, node->far / WORLD_SCALE, 1.0f); gSPPerspNormalize(gDisplayListHead++, perspNorm); @@ -1015,23 +1037,24 @@ void geo_process_shadow(struct GraphNodeShadow *node) { } /** - * Check whether an object is in view to determine whether it should be drawn. + * Check whether an object is in view to determine whether if it should be drawn. * This is known as frustum culling. - * It checks whether the object is far away, very close / behind the camera, - * or horizontally out of view. It does not check whether it is vertically - * out of view. It assumes a sphere of 300 units around the object's position - * unless the object has a culling radius node that specifies otherwise. - * + * It checks whether the object is far away, very close or behind the camera and + * vertically or horizontally out of view. + * The radius used is specified in DEFAULT_CULLING_RADIUS unless the object + * has a culling radius node that specifies another value. + * * The matrix parameter should be the top of the matrix stack, which is the * object's transformation matrix times the camera 'look-at' matrix. The math * is counter-intuitive, but it checks column 3 (translation vector) of this * matrix to determine where the origin (0,0,0) in object space will be once * transformed to camera space (x+ = right, y+ = up, z = 'coming out the screen'). + * * In 3D graphics, you typically model the world as being moved in front of a * static camera instead of a moving camera through a static world, which in * this case simplifies calculations. Note that the perspective matrix is not * on the matrix stack, so there are still calculations with the fov to compute - * the slope of the lines of the frustum. + * the slope of the lines of the frustum, these are done once during geo_process_perspective. * * z- * @@ -1046,10 +1069,6 @@ void geo_process_shadow(struct GraphNodeShadow *node) { * Since (0,0,0) is unaffected by rotation, columns 0, 1 and 2 are ignored. */ s32 obj_is_in_view(struct GraphNodeObject *node) { - if (node->node.flags & GRAPH_RENDER_INVISIBLE) { - return FALSE; - } - struct GraphNode *geo = node->sharedChild; s16 cullingRadius; @@ -1057,43 +1076,38 @@ s32 obj_is_in_view(struct GraphNodeObject *node) { if (geo != NULL && geo->type == GRAPH_NODE_TYPE_CULLING_RADIUS) { cullingRadius = ((struct GraphNodeCullingRadius *) geo)->cullingRadius; } else { - cullingRadius = 300; + cullingRadius = DEFAULT_CULLING_RADIUS; } - // Don't render if the object is close to or behind the camera - if (node->cameraToObject[2] > -100.0f + cullingRadius) { + // Check whether the object is not too far away or too close / behind the camera. + // This makes the HOLP not update when the camera is far away, and it + // makes PU travel safe when the camera is locked on the main map. + // If Mario were rendered with a depth over 65536 it would cause overflow + // when converting the transformation matrix to a fixed point matrix. + f32 cameraToObjectDepth = node->cameraToObject[2]; + + #define VALID_DEPTH_MIDDLE (-20100.f / 2.f) + #define VALID_DEPTH_RANGE (19900 / 2.f) + if (absf(cameraToObjectDepth - VALID_DEPTH_MIDDLE) >= VALID_DEPTH_RANGE + cullingRadius) { return FALSE; } - //! This makes the HOLP not update when the camera is far away, and it - // makes PU travel safe when the camera is locked on the main map. - // If Mario were rendered with a depth over 65536 it would cause overflow - // when converting the transformation matrix to a fixed point matrix. - if (node->cameraToObject[2] < -20000.0f - cullingRadius) { +#ifdef VERTICAL_CULLING + f32 vScreenEdge = -cameraToObjectDepth * gCurGraphNodeCamFrustum->halfFovVertical; + + // Unlike with horizontal culling, we only check if the object is bellow the screen + // to prevent shadows from being culled. + if (node->cameraToObject[1] < -vScreenEdge - cullingRadius) { return FALSE; } - // half of the fov in in-game angle units instead of degrees - s16 halfFov = (((((gCurGraphNodeCamFrustum->fov * sAspectRatio) / 2.0f) + 1.0f) * 32768.0f) / 180.0f) + 0.5f; +#endif + + f32 hScreenEdge = -cameraToObjectDepth * gCurGraphNodeCamFrustum->halfFovHorizontal; - f32 hScreenEdge = -node->cameraToObject[2] * tans(halfFov); - // -matrix[3][2] is the depth, which gets multiplied by tan(halfFov) to get - // the amount of units between the center of the screen and the horizontal edge - // given the distance from the object to the camera. - - // This multiplication should really be performed on 4:3 as well, - // but the issue will be more apparent on widescreen. - // HackerSM64: This multiplication is done regardless of aspect ratio to fix object pop-in on the edges of the screen (which happens at 4:3 too) - // hScreenEdge *= GFX_DIMENSIONS_ASPECT_RATIO; - - // Check whether the object is horizontally in view - if (node->cameraToObject[0] > hScreenEdge + cullingRadius) { + if (absf(node->cameraToObject[0]) > hScreenEdge + cullingRadius) { return FALSE; } - if (node->cameraToObject[0] < -hScreenEdge - cullingRadius) { - return FALSE; - } - return TRUE; } @@ -1132,14 +1146,25 @@ void visualise_object_hitbox(struct Object *node) { */ void geo_process_object(struct Object *node) { if (node->header.gfx.areaIndex == gCurGraphNodeRoot->areaIndex) { - if (node->header.gfx.throwMatrix != NULL) { - mtxf_scale_vec3f(gMatStack[gMatStackIndex + 1], *node->header.gfx.throwMatrix, node->header.gfx.scale); - } else if (node->header.gfx.node.flags & GRAPH_RENDER_BILLBOARD) { - mtxf_billboard(gMatStack[gMatStackIndex + 1], gMatStack[gMatStackIndex], - node->header.gfx.pos, node->header.gfx.scale, gCurGraphNodeCamera->roll); - } else { - mtxf_rotate_zxy_and_translate(gMatStack[gMatStackIndex + 1], node->header.gfx.pos, node->header.gfx.angle); - mtxf_scale_vec3f(gMatStack[gMatStackIndex + 1], gMatStack[gMatStackIndex + 1], node->header.gfx.scale); + s32 isInvisible = (node->header.gfx.node.flags & GRAPH_RENDER_INVISIBLE); + s32 noThrowMatrix = (node->header.gfx.throwMatrix == NULL); + + // If the throw matrix is null and the object is invisible, there is no need + // to update billboarding, scale, rotation, etc. + // This still updates translation since it is needed for sound. + if (isInvisible && noThrowMatrix) { + mtxf_translate(gMatStack[gMatStackIndex + 1], node->header.gfx.pos); + } + else{ + if (!noThrowMatrix) { + mtxf_scale_vec3f(gMatStack[gMatStackIndex + 1], *node->header.gfx.throwMatrix, node->header.gfx.scale); + } else if (node->header.gfx.node.flags & GRAPH_RENDER_BILLBOARD) { + mtxf_billboard(gMatStack[gMatStackIndex + 1], gMatStack[gMatStackIndex], + node->header.gfx.pos, node->header.gfx.scale, gCurGraphNodeCamera->roll); + } else { + mtxf_rotate_zxy_and_translate(gMatStack[gMatStackIndex + 1], node->header.gfx.pos, node->header.gfx.angle); + mtxf_scale_vec3f(gMatStack[gMatStackIndex + 1], gMatStack[gMatStackIndex + 1], node->header.gfx.scale); + } } node->header.gfx.throwMatrix = &gMatStack[++gMatStackIndex]; @@ -1149,7 +1174,8 @@ void geo_process_object(struct Object *node) { if (node->header.gfx.animInfo.curAnim != NULL) { geo_set_animation_globals(&node->header.gfx.animInfo, (node->header.gfx.node.flags & GRAPH_RENDER_HAS_ANIMATION) != 0); } - if (obj_is_in_view(&node->header.gfx)) { + + if (!isInvisible && obj_is_in_view(&node->header.gfx)) { gMatStackIndex--; inc_mat_stack();