diff --git a/src/game/artifact.c b/src/game/artifact.c index 390c161a67..3d151bfe4e 100644 --- a/src/game/artifact.c +++ b/src/game/artifact.c @@ -744,3 +744,130 @@ Gfx *artifactsRenderGlaresForRoom(Gfx *gdl, s32 roomnum) return gdl; } + +#ifndef PLATFORM_N64 + +bool artifactTestDir(struct model *model, struct coord *dir, struct coord *bounds, f32 *lowest_dist) +{ + /** + * Test whether a line-of-sight direction originating from the player camera + * intersects the weapon model. + * + * Arguments: + * model (model): The model object to test + * dir (coord): The line-of-sight direction to test + * bounds (coord *): Array of {min, max} coordinates defining the model bounding box + * lowest_dist (f32 *): Pointer to lowest hit distance + */ + s32 i; + const struct coord origin = {{0., 0., 0.}}; // Camera origin + struct coord end = {{dir->x * 32767.0f, dir->y * 32767.0f, dir->z * 32767.0f}}; // Extrapolate direction to the farthest possible end point + + struct modelnode *node = model->definition->rootnode; + struct modelnode *endnode = model->definition->rootnode; + + Gfx *opagdl = NULL; + Vtx *vertices = NULL; + + bool hit = false; + + // Start by testing bounding box + if (bgTestLineIntersectsBbox(&origin, dir, bounds, bounds + 1) == false) + return hit; + + /** + * Loop through model nodes to retrieve graphics display list commands + * for the opaque parts of weapons that should block light. This + * seems like the easiest way to account for animation information. + */ + while (node) { + u32 type = node->type & 0xff; + + if (type == MODELNODETYPE_GUNDL) { + struct modelrodata_gundl *rodata = &node->rodata->gundl; + if (rodata->opagdl != NULL) { + opagdl = (Gfx *)((uintptr_t)rodata->baseaddr + ((uintptr_t)UNSEGADDR(rodata->opagdl) & 0xffffff)); + vertices = (void *)(uintptr_t)rodata->baseaddr; + /** + * Test for intersection with the weapon model + */ + if (bgTestHitOnWeapon(model, &origin, &end, dir, opagdl, NULL, vertices, bounds, lowest_dist)) + hit = true; + } + } + + if (node->child) { + node = node->child; + } else { + while (node) { + if (node == endnode) { + node = NULL; + break; + } + + if (node->next) { + node = node->next; + break; + } + + node = node->parent; + } + } + } + + return hit; +} + +void artifactsUpdateGlaresForPlayer(struct model *gunmodel, struct model *handmodel, bool hand, f32 znear, f32 zfar) +{ + /** + * Update light artifacts to account for the player weapon + * position on the screen. This must be called before weapon + * matrices are wiped at the end of bgunRender(). + */ + if ((g_Vars.currentplayer->devicesactive & (DEVICE_CLOAKRCP120 | DEVICE_CLOAKDEVICE)) || + (g_Vars.currentplayer->prop->chr->cloakfadefrac > 0)) return; + + s32 i, j; + struct coord gundir2d; + struct artifact *artifacts = schedGetWriteArtifacts(); + + const f32 max = 1e6; + + f32 lowest_dist = max; + + struct coord gunbounds[2] = {{max, max, max}, {-max, -max, -max}}; + struct coord handbounds[2] = {{max, max, max}, {-max, -max, -max}}; + + for (i = 0; i < MAX_ARTIFACTS; i++) { + struct artifact *artifact = &artifacts[i]; + + if (artifact->type != ARTIFACTTYPE_FREE && artifact->visiblelos) { + // Get the direction of this light artifact from the player's perspective + f32 crosspos[2] = { (f32)artifact->screenx, (f32)artifact->screeny }; + cam0f0b4c3c(crosspos, &gundir2d, 1.0f); + /** + * Test whether this direction intersects the gun or hand model + * + * Note: We use an OR for speed, but it would be more accurate to + * always test both the gun and hand models to check for a closer + * lowest_dist. It's probably fine in most cases. + */ + if (artifactTestDir(gunmodel, &gundir2d, gunbounds, &lowest_dist) || (hand && artifactTestDir(handmodel, &gundir2d, handbounds, &lowest_dist))) { + /** + * Compute N64 depth value for comparison with the artifact's expected depth. + * This is needed to account for the fact that the weapon draw uses different + * znear/zfar settings compared to the global settings from vi. + */ + f32 z = -gundir2d.z * lowest_dist; + f32 znorm = (1/z - 1/znear) / (1/zfar - 1/znear); + u16 actualdepth = floatToN64Depth(32704.0f * znorm) >> 2; + + artifact->visiblelos = actualdepth > artifact->expecteddepth; + } + } + + } +} + +#endif diff --git a/src/game/bg.c b/src/game/bg.c index ea4d08784f..e60a7951e8 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -44,6 +44,7 @@ #include "gbiex.h" #include "types.h" #ifndef PLATFORM_N64 +#include "math.h" #include "preprocess.h" #include "system.h" #include "video.h" @@ -3632,6 +3633,337 @@ bool bgTestLineIntersectsBbox(struct coord *arg0, struct coord *arg1, struct coo return true; } +#ifndef PLATFORM_N64 + +bool bgTestLineIntersectsTriangle(struct coord *origin, struct coord *dir, struct coord *point1, struct coord *point2, struct coord *point3, f32 *dist) +{ + /** + * Implementation of Moller-Trumbore triangle intersection test + * without backface culling (assumes triangles in forward direction) + * + * See: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm + */ + const f32 epsilon = 1.19e-7; + + struct coord edge1 = { + point2->x - point1->x, + point2->y - point1->y, + point2->z - point1->z}; + + struct coord edge2 = { + point3->x - point1->x, + point3->y - point1->y, + point3->z - point1->z}; + + struct coord dir_cross_e2 = { + dir->y * edge2.z - dir->z * edge2.y, + dir->z * edge2.x - dir->x * edge2.z, + dir->x * edge2.y - dir->y * edge2.x}; + + f32 det = + edge1.x * dir_cross_e2.x + + edge1.y * dir_cross_e2.y + + edge1.z * dir_cross_e2.z; + + if (fabsf(det) < epsilon) return false; // Direction is parallel to triangle + + f32 inv_det = 1.0 / det; + + struct coord s = { + origin->x - point1->x, + origin->y - point1->y, + origin->z - point1->z}; + + f32 u = inv_det * ( + s.x * dir_cross_e2.x + + s.y * dir_cross_e2.y + + s.z * dir_cross_e2.z); + + if (u < -epsilon || u - 1 > epsilon) return false; // Direction passes outside edge2's bounds + + struct coord s_cross_e1 = { + s.y * edge1.z - s.z * edge1.y, + s.z * edge1.x - s.x * edge1.z, + s.x * edge1.y - s.y * edge1.x}; + + f32 v = inv_det * ( + dir->x * s_cross_e1.x + + dir->y * s_cross_e1.y + + dir->z * s_cross_e1.z); + + if (v < -epsilon || u + v - 1 > epsilon) return false; // Direction passes outside edge1's bounds + + f32 t = inv_det * ( + edge2.x * s_cross_e1.x + + edge2.y * s_cross_e1.y + + edge2.z * s_cross_e1.z); + + // Direction intersects triangle + if (t > epsilon) { + *dist = t; + return true; + } + return false; +} + +bool bgTestHitOnWeapon(struct model *model, struct coord *origin, struct coord *end, struct coord *dir, Gfx *gdl, Gfx *gdl2, Vtx *vertices, struct coord *bounds, f32 *lowest_dist) +{ + s16 triref; + s32 i; + bool intersectsbbox; + s32 count; + s32 istart; + s32 istop; + s32 numvertices; + f32 *ptr; + Vtx *vtx; + struct coord *point1; + struct coord *point2; + struct coord *point3; + u32 word; + Gfx *tri4gdl; + Mtxf *mtx; + struct coord min; + struct coord max; + bool hit; + f32 dist; + s32 points[3]; + + istart = 16; + istop = 0; + hit = false; + + while (true) { + if (gdl->dma.cmd == G_ENDDL) { + if (gdl2 != NULL) { + gdl = gdl2; + gdl2 = NULL; + continue; + } + break; + } else if (gdl->dma.cmd == G_MTX) { + word = UNSEGADDR(gdl->words.w1) & 0xffffff; + i = word / sizeof(Mtxf); + mtx = &model->matrices[i]; + } else if (gdl->dma.cmd == G_VTX) { + count = (gdl->bytes[GFX_W0_BYTE(1)] & 0xf); + word = UNSEGADDR(gdl->words.w1) & 0xffffff; + numvertices = ((u32) gdl->bytes[GFX_W0_BYTE(1)] >> 4) + 1; + vtx = (Vtx *)((uintptr_t)vertices + word); + + if (count < istart) { + istart = count; + } + + if (numvertices + count > istop) { + istop = numvertices + count; + } + + ptr = &var800a6470[count * 3]; + + while (numvertices > 0) { + ptr[0] = vtx->x; + ptr[1] = vtx->y; + ptr[2] = vtx->z; + + mtx4TransformVecInPlace(mtx, (struct coord *) ptr); + + numvertices--; + ptr += 3; + vtx++; + } + + ptr = &var800a6470[istart]; + + min.x = ptr[0]; + max.x = ptr[0]; + min.y = ptr[1]; + max.y = ptr[1]; + min.z = ptr[2]; + max.z = ptr[2]; + + for (i = istart; i < istop; i++) { + if (ptr[0] < min.x) { + min.x = ptr[0]; + } + + if (ptr[1] < min.y) { + min.y = ptr[1]; + } + + if (ptr[2] < min.z) { + min.z = ptr[2]; + } + + if (ptr[0] > max.x) { + max.x = ptr[0]; + } + + if (ptr[1] > max.y) { + max.y = ptr[1]; + } + + if (ptr[2] > max.z) { + max.z = ptr[2]; + } + + ptr += 3; + } + + // Update external min, max bounds when present + if (bounds != NULL) { + if (bounds[0].x > min.x) bounds[0].x = min.x; + if (bounds[0].y > min.y) bounds[0].y = min.y; + if (bounds[0].z > min.z) bounds[0].z = min.z; + if (bounds[1].x < max.x) bounds[1].x = max.x; + if (bounds[1].y < max.y) bounds[1].y = max.y; + if (bounds[1].z < max.z) bounds[1].z = max.z; + } + + if ((origin->x < min.x && end->x < min.x) + || (origin->x > max.x && end->x > max.x) + || (origin->y < min.y && end->y < min.y) + || (origin->y > max.y && end->y > max.y) + || (origin->z < min.z && end->z < min.z) + || (origin->z > max.z && end->z > max.z)) { + intersectsbbox = false; + } else { + intersectsbbox = bgTestLineIntersectsBbox(origin, dir, &min, &max); + } + } else { + if (!intersectsbbox) { + gdl++; + continue; + } + + if ((gdl->dma.cmd != G_TRI1 && gdl->dma.cmd != G_TRI4)) { + gdl++; + continue; + } + + if (gdl->dma.cmd == G_TRI1) { + i = 0; + triref = 0; + points[0] = gdl->tri.tri.v[GFX_TRI_VTX(0)] / 10; + points[1] = gdl->tri.tri.v[GFX_TRI_VTX(1)] / 10; + points[2] = gdl->tri.tri.v[GFX_TRI_VTX(2)] / 10; + } else if (gdl->dma.cmd == G_TRI4) { + tri4gdl = gdl; + i = 3; + triref = 1; + points[0] = gdl->tri4.x1; + points[1] = gdl->tri4.y1; + points[2] = gdl->tri4.z1; + } + + do { + if (points[0] == 0 && points[1] == 0 && points[2] == 0) { + break; + } + + point1 = (struct coord *) (var800a6470 + points[0] * 3); + point2 = (struct coord *) (var800a6470 + points[1] * 3); + point3 = (struct coord *) (var800a6470 + points[2] * 3); + + min.x = point1->x; + max.x = point1->x; + + if (point2->x < min.x) { + min.x = point2->x; + } + + if (point2->x > max.x) { + max.x = point2->x; + } + + if (point3->x < min.x) { + min.x = point3->x; + } + + if (point3->x > max.x) { + max.x = point3->x; + } + + if (!(origin->x < min.x && end->x < min.x) && !(origin->x > max.x && end->x > max.x)) { + min.z = point1->z; + max.z = point1->z; + + if (point2->z < min.z) { + min.z = point2->z; + } + + if (point2->z > max.z) { + max.z = point2->z; + } + + if (point3->z < min.z) { + min.z = point3->z; + } + + if (point3->z > max.z) { + max.z = point3->z; + } + + if (!(origin->z < min.z && end->z < min.z) && !(origin->z > max.z && end->z > max.z)) { + min.y = point1->y; + max.y = point1->y; + + if (point2->y < min.y) { + min.y = point2->y; + } + + if (point2->y > max.y) { + max.y = point2->y; + } + + if (point3->y < min.y) { + min.y = point3->y; + } + + if (point3->y > max.y) { + max.y = point3->y; + } + + if (!(origin->y < min.y && end->y < min.y) && !(origin->y > max.y && end->y > max.y)) { + if (bgTestLineIntersectsBbox(origin, dir, &min, &max) + && bgTestLineIntersectsTriangle(origin, dir, point1, point2, point3, &dist)) { + hit = true; + if (dist < *lowest_dist) { + *lowest_dist = dist; + } + } + } + } + } + + i--; + + if (i == 2) { + points[0] = tri4gdl->tri4.x2; + points[1] = tri4gdl->tri4.y2; + points[2] = tri4gdl->tri4.z2; + triref = 2; + } else if (i == 1) { + points[0] = tri4gdl->tri4.x3; + points[1] = tri4gdl->tri4.y3; + points[2] = tri4gdl->tri4.z3; + triref = 3; + } else if (i == 0) { + points[0] = tri4gdl->tri4.x4; + points[1] = tri4gdl->tri4.y4; + points[2] = tri4gdl->tri4.z4; + triref = 1; + } + } while (i >= 0); + } + gdl++; + } + + return hit; +} + +#endif + bool bgTestHitOnObj(struct coord *arg0, struct coord *arg1, struct coord *arg2, Gfx *gdl, Gfx *gdl2, Vtx *vertices, struct hitthing *hitthing) { @@ -4029,6 +4361,15 @@ bool bgTestHitOnChr(struct model *model, struct coord *arg1, struct coord *arg2, min.z = ptr[2]; max.z = ptr[2]; + /** + * @bug: Advancing the pointer before the next loop + * results in overflow into empty memory on the last + * iteration. This produces incorrect min/max values + * for the box intersection test, but luckily it usually + * gets corrected by the detailed triangle test so the + * net impact on N64 is extra CPU usage. We could skip + * this on PC to obtain the correct min/max values. + */ ptr += 3; for (i = spdc; i < spd8; i++) { diff --git a/src/game/bondgun.c b/src/game/bondgun.c index 7415bf8512..d4e0303404 100644 --- a/src/game/bondgun.c +++ b/src/game/bondgun.c @@ -54,6 +54,7 @@ #include "data.h" #include "types.h" #ifndef PLATFORM_N64 +#include "game/artifact.h" #include "game/stagetable.h" #include "video.h" #include "platform.h" @@ -11167,7 +11168,10 @@ void bgunRender(Gfx **gdlptr) modelRender(&renderdata, &hand->handmodel); renderdata.envcolour = prevcolour; } - +#ifndef PLATFORM_N64 + // Update light artifacts to account for gun and hand models + artifactsUpdateGlaresForPlayer(&hand->gunmodel, &hand->handmodel, player->gunctrl.handmodeldef && renderhand, 1.5, 1000); +#endif // Clean up gdl = renderdata.gdl; diff --git a/src/include/game/artifact.h b/src/include/game/artifact.h index 9feae2a184..d1cce0be84 100644 --- a/src/include/game/artifact.h +++ b/src/include/game/artifact.h @@ -15,6 +15,7 @@ Gfx *artifactsUnconfigureForGlares(Gfx *gdl); Gfx *artifactsRenderGlaresForRoom(Gfx *gdl, s32 roomnum); #ifndef PLATFORM_N64 bool artifactTestLos(struct coord *spec, struct coord *roompos, s32 xi, s32 yi); +void artifactsUpdateGlaresForPlayer(struct model *gunmodel, struct model *handmodel, bool hand, f32 znear, f32 zfar); #endif #endif diff --git a/src/include/game/bg.h b/src/include/game/bg.h index 89b6c7550c..73a4e3e514 100644 --- a/src/include/game/bg.h +++ b/src/include/game/bg.h @@ -104,6 +104,7 @@ void bgCalculatePortalBbox(s32 portalnum, struct coord *bbmin, struct coord *bbm void bgFindEnteredRooms(struct coord *bbmin, struct coord *upper, RoomNum *rooms, s32 maxlen, bool arg4); #ifndef PLATFORM_N64 void bgCalculateGlaresForVisibleRooms(void); +bool bgTestHitOnWeapon(struct model *model, struct coord *origin, struct coord *end, struct coord *dir, Gfx *gdl, Gfx *gdl2, Vtx *vertices, struct coord *bounds, f32 *lowest_dist); #endif #endif