#include #include "sm64.h" #include "game_init.h" #include "mario.h" #include "memory.h" #include "save_file.h" #include "engine/surface_collision.h" #include "engine/graph_node.h" #include "geo_misc.h" #include "area.h" #include "segment2.h" #include "paintings.h" /** * @file paintings.c * * Implements the rippling painting effect. Paintings are GraphNodes that exist without being connected * to any particular object. * * Paintings are defined in level data. Look at levels/castle_inside/painting.inc.c for examples. * * The ripple effect uses data that is split into several parts: * The mesh positions are generated from a base mesh. See seg2_painting_triangle_mesh near the * bottom of bin/segment2.c * * The lighting for the ripple is also generated from a base table, seg2_painting_mesh_neighbor_tris * in bin/segment2.c * * Each painting's texture uses yet another table to map its texture to the mesh. * These maps are in level data, see levels/castle_inside/painting.inc.c for example. * * Finally, each painting has two display lists, normal and rippling, which are defined in the same * level data file as the Painting itself. See levels/castle_inside/painting.inc.c. * * * Painting state machine: * Paintings spawn in the PAINTING_IDLE state * From IDLE, paintings can change to PAINTING_RIPPLE or PAINTING_ENTERED * - This state checks for ENTERED because if mario waits long enough, a PROXIMITY painting could * reset to IDLE * * Paintings in the PAINTING_RIPPLE state are passively rippling. * For RIPPLE_TRIGGER_PROXIMITY paintings, this means mario bumped the wall in front of the * painting. * * Paintings that use RIPPLE_TRIGGER_CONTINUOUS try to transition to this state as soon as possible, * usually when mario enters the room. * * A PROXIMITY painting will automatically reset to IDLE if its ripple magnitude becomes small * enough. * * Paintings in the PAINTING_ENTERED state have been entered by mario. * A CONTINUOUS painting will automatically reset to RIPPLE if its ripple magnitude becomes small * enough. */ /** * Triggers a passive ripple on the left side of the painting. */ #define RIPPLE_LEFT 0x20 /** * Triggers a passive ripple in the middle the painting. */ #define RIPPLE_MIDDLE 0x10 /** * Triggers a passive ripple on the right side of the painting. */ #define RIPPLE_RIGHT 0x8 /** * Triggers an entry ripple on the left side of the painting. */ #define ENTER_LEFT 0x4 /** * Triggers an entry ripple in the middle of the painting. */ #define ENTER_MIDDLE 0x2 /** * Triggers an entry ripple on the right side of the painting. */ #define ENTER_RIGHT 0x1 /** * Use the 1/4th part of the painting that is nearest to mario's current floor. */ #define NEAREST_4TH 30 /** * Use mario's relative x position. * @see painting_mario_x */ #define MARIO_X 40 /** * Use the x center of the painting. */ #define MIDDLE_X 50 /** * Use mario's relative y position. * @see painting_mario_y */ #define MARIO_Y 60 /** * Use mario's relative z position. * @see painting_mario_z */ #define MARIO_Z 70 /** * Use the y center of the painting. */ #define MIDDLE_Y 80 /** * Does nothing to the timer. * Why -56 instead of false? Who knows. */ #define DONT_RESET -56 /** * Reset the timer to 0. */ #define RESET_TIMER 100 /// A copy of the type of floor mario is standing on. s16 gPaintingMarioFloorType; // A copy of mario's position f32 gPaintingMarioXPos, gPaintingMarioYPos, gPaintingMarioZPos; /** * When a painting is rippling, this mesh is generated each frame using the Painting's parameters. * * This mesh only contains the vertex positions and normals. * Paintings use an additional array to map textures to the mesh. */ struct PaintingMeshVertex *gPaintingMesh; /** * The painting's surface normals, used to approximate each of the vertex normals (for gouraud shading). */ Vec3f *gPaintingTriNorms; /** * The painting that is currently rippling. Only one painting can be rippling at once. */ struct Painting *gRipplingPainting; /** * Whether the DDD painting is moved forward, should being moving backwards, or has already moved backwards. */ s8 gDddPaintingStatus; struct Painting *sHmcPaintings[] = { &cotmc_painting, NULL, }; struct Painting *sInsideCastlePaintings[] = { &bob_painting, &ccm_painting, &wf_painting, &jrb_painting, &lll_painting, &ssl_painting, &hmc_painting, &ddd_painting, &wdw_painting, &thi_tiny_painting, &ttm_painting, &ttc_painting, &sl_painting, &thi_huge_painting, NULL, }; struct Painting *sTtmPaintings[] = { &ttm_slide_painting, NULL, }; struct Painting **sPaintingGroups[] = { sHmcPaintings, sInsideCastlePaintings, sTtmPaintings, }; s16 gPaintingUpdateCounter = 1; s16 gLastPaintingUpdateCounter = 0; /** * Stop paintings in paintingGroup from rippling if their id is different from *idptr. */ void stop_other_paintings(s16 *idptr, struct Painting *paintingGroup[]) { s16 index; s16 id = *idptr; index = 0; while (paintingGroup[index] != NULL) { struct Painting *painting = segmented_to_virtual(paintingGroup[index]); // stop all rippling except for the selected painting if (painting->id != id) { painting->state = 0; } index++; } } /** * @return mario's y position inside the painting (bounded). */ f32 painting_mario_y(struct Painting *painting) { //! Unnecessary use of double constants // Add 50 to make the ripple closer to mario's center of mass. f32 relY = gPaintingMarioYPos - painting->posY + 50.0; if (relY < 0.0) { relY = 0.0; } else if (relY > painting->size) { relY = painting->size; } return relY; } /** * @return mario's z position inside the painting (bounded). */ f32 painting_mario_z(struct Painting *painting) { f32 relZ = painting->posZ - gPaintingMarioZPos; if (relZ < 0.0) { relZ = 0.0; } else if (relZ > painting->size) { relZ = painting->size; } return relZ; } /** * @return The y origin for the ripple, based on ySource. * For floor paintings, the z-axis is treated as y. */ f32 painting_ripple_y(struct Painting *painting, s8 ySource) { switch (ySource) { case MARIO_Y: return painting_mario_y(painting); // normal wall paintings break; case MARIO_Z: return painting_mario_z(painting); // floor paintings use X and Z break; case MIDDLE_Y: return painting->size / 2.0; // some concentric ripples don't care about Mario break; } } /** * Return the quarter of the painting that is closest to the floor mario entered. */ f32 painting_nearest_4th(struct Painting *painting) { f32 firstQuarter = painting->size / 4.0; // 1/4 of the way across the painting f32 secondQuarter = painting->size / 2.0; // 1/2 of the way across the painting f32 thirdQuarter = painting->size * 3.0 / 4.0; // 3/4 of the way across the painting if (painting->floorEntered & RIPPLE_LEFT) { return firstQuarter; } else if (painting->floorEntered & RIPPLE_MIDDLE) { return secondQuarter; } else if (painting->floorEntered & RIPPLE_RIGHT) { return thirdQuarter; // Same as ripple floors. } else if (painting->floorEntered & ENTER_LEFT) { return firstQuarter; } else if (painting->floorEntered & ENTER_MIDDLE) { return secondQuarter; } else if (painting->floorEntered & ENTER_RIGHT) { return thirdQuarter; } } /** * @return mario's x position inside the painting (bounded). */ f32 painting_mario_x(struct Painting *painting) { f32 relX = gPaintingMarioXPos - painting->posX; if (relX < 0.0) { relX = 0.0; } else if (relX > painting->size) { relX = painting->size; } return relX; } /** * @return The x origin for the ripple, based on xSource. */ f32 painting_ripple_x(struct Painting *painting, s8 xSource) { switch (xSource) { case NEAREST_4TH: // normal wall paintings return painting_nearest_4th(painting); break; case MARIO_X: // horizontally placed paintings use X and Z return painting_mario_x(painting); break; case MIDDLE_X: // concentric rippling may not care about Mario return painting->size / 2.0; break; } } /** * Set the painting's state, causing it to start a passive ripple or a ripple from mario entering. * * @param state The state to enter * @param painting,paintingGroup identifies the painting that is changing state * @param xSource,ySource what to use for the x and y origin of the ripple * @param resetTimer if 100, set the timer to 0 */ void painting_state(s8 state, struct Painting *painting, struct Painting *paintingGroup[], s8 xSource, s8 ySource, s8 resetTimer) { // make sure no other paintings are rippling stop_other_paintings(&painting->id, paintingGroup); // use a different set of variables depending on the state switch (state) { case PAINTING_RIPPLE: painting->currRippleMag = painting->passiveRippleMag; painting->rippleDecay = painting->passiveRippleDecay; painting->currRippleRate = painting->passiveRippleRate; painting->dispersionFactor = painting->passiveDispersionFactor; break; case PAINTING_ENTERED: painting->currRippleMag = painting->entryRippleMag; painting->rippleDecay = painting->entryRippleDecay; painting->currRippleRate = painting->entryRippleRate; painting->dispersionFactor = painting->entryDispersionFactor; break; } painting->state = state; painting->rippleX = painting_ripple_x(painting, xSource); painting->rippleY = painting_ripple_y(painting, ySource); gPaintingMarioYEntry = gPaintingMarioYPos; // Because true or false would be too simple... if (resetTimer == RESET_TIMER) { painting->rippleTimer = 0.0f; } gRipplingPainting = painting; } /** * Idle update function for wall paintings that use RIPPLE_TRIGGER_PROXIMITY. */ void wall_painting_proximity_idle(struct Painting *painting, struct Painting *paintingGroup[]) { // Check for mario triggering a ripple if (painting->floorEntered & RIPPLE_LEFT) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } else if (painting->floorEntered & RIPPLE_MIDDLE) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } else if (painting->floorEntered & RIPPLE_RIGHT) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); // Check for mario entering } else if (painting->floorEntered & ENTER_LEFT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } else if (painting->floorEntered & ENTER_MIDDLE) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } else if (painting->floorEntered & ENTER_RIGHT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } } /** * Rippling update function for wall paintings that use RIPPLE_TRIGGER_PROXIMITY. */ void wall_painting_proximity_rippling(struct Painting *painting, struct Painting *paintingGroup[]) { if (painting->floorEntered & ENTER_LEFT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } else if (painting->floorEntered & ENTER_MIDDLE) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } else if (painting->floorEntered & ENTER_RIGHT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } } /** * Idle update function for wall paintings that use RIPPLE_TRIGGER_CONTINUOUS. */ void wall_painting_continuous_idle(struct Painting *painting, struct Painting *paintingGroup[]) { // Check for mario triggering a ripple if (painting->floorEntered & RIPPLE_LEFT) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER); } else if (painting->floorEntered & RIPPLE_MIDDLE) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER); } else if (painting->floorEntered & RIPPLE_RIGHT) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER); // Check for mario entering } else if (painting->floorEntered & ENTER_LEFT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } else if (painting->floorEntered & ENTER_MIDDLE) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } else if (painting->floorEntered & ENTER_RIGHT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER); } } /** * Rippling update function for wall paintings that use RIPPLE_TRIGGER_CONTINUOUS. */ void wall_painting_continuous_rippling(struct Painting *painting, struct Painting *paintingGroup[]) { if (painting->floorEntered & ENTER_LEFT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, DONT_RESET); } else if (painting->floorEntered & ENTER_MIDDLE) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, DONT_RESET); } else if (painting->floorEntered & ENTER_RIGHT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, DONT_RESET); } } /** * Idle update function for floor paintings that use RIPPLE_TRIGGER_PROXIMITY. * * No floor paintings use RIPPLE_TRIGGER_PROXIMITY in the game. */ void floor_painting_proximity_idle(struct Painting *painting, struct Painting *paintingGroup[]) { // Check for mario triggering a ripple if (painting->floorEntered & RIPPLE_LEFT) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } else if (painting->floorEntered & RIPPLE_MIDDLE) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } else if (painting->floorEntered & RIPPLE_RIGHT) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); // Only check for mario entering if he jumped below the surface } else if (painting->marioWentUnder) { if (painting->currFloor & ENTER_LEFT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } else if (painting->currFloor & ENTER_MIDDLE) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } else if (painting->currFloor & ENTER_RIGHT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } } } /** * Rippling update function for floor paintings that use RIPPLE_TRIGGER_PROXIMITY. * * No floor paintings use RIPPLE_TRIGGER_PROXIMITY in the game. */ void floor_painting_proximity_rippling(struct Painting *painting, struct Painting *paintingGroup[]) { if (painting->marioWentUnder) { if (painting->currFloor & ENTER_LEFT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } else if (painting->currFloor & ENTER_MIDDLE) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } else if (painting->currFloor & ENTER_RIGHT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } } } /** * Idle update function for floor paintings that use RIPPLE_TRIGGER_CONTINUOUS. * * Both floor paintings (HMC and CotMC) are hidden behind a door, which hides the ripple's start up. * The floor just inside the doorway is RIPPLE_LEFT, so the painting starts rippling as soon as mario * enters the room. */ void floor_painting_continuous_idle(struct Painting *painting, struct Painting *paintingGroup[]) { // Check for mario triggering a ripple if (painting->floorEntered & RIPPLE_LEFT) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER); } else if (painting->floorEntered & RIPPLE_MIDDLE) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER); } else if (painting->floorEntered & RIPPLE_RIGHT) { painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER); // Check for mario entering } else if (painting->currFloor & ENTER_LEFT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } else if (painting->currFloor & ENTER_MIDDLE) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } else if (painting->currFloor & ENTER_RIGHT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER); } } /** * Rippling update function for floor paintings that use RIPPLE_TRIGGER_CONTINUOUS. */ void floor_painting_continuous_rippling(struct Painting *painting, struct Painting *paintingGroup[]) { if (painting->marioWentUnder) { if (painting->currFloor & ENTER_LEFT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, DONT_RESET); } else if (painting->currFloor & ENTER_MIDDLE) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, DONT_RESET); } else if (painting->currFloor & ENTER_RIGHT) { painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, DONT_RESET); } } } /** * Check for mario entering one of the special floors associated with the painting. */ void painting_update_floors(struct Painting *painting) { s16 paintingId = painting->id; s8 rippleLeft = 0; s8 rippleMiddle = 0; s8 rippleRight = 0; s8 enterLeft = 0; s8 enterMiddle = 0; s8 enterRight = 0; /* The area in front of every painting in the game (except HMC and CotMC, which *\ |* act a little differently) is made up of 3 special floor triangles with special *| |* (unique) surface types. This code checks which surface Mario is currently on *| \* and sets a bitfield accordingly. */ // check if Mario's current floor is one of the special floors if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WOBBLE_A6) { rippleLeft = RIPPLE_LEFT; } if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WOBBLE_A7) { rippleMiddle = RIPPLE_MIDDLE; } if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WOBBLE_A8) { rippleRight = RIPPLE_RIGHT; } if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WARP_D3) { enterLeft = ENTER_LEFT; } if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WARP_D4) { enterMiddle = ENTER_MIDDLE; } if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WARP_D5) { enterRight = ENTER_RIGHT; } painting->lastFloor = painting->currFloor; // at most 1 of these will be nonzero; painting->currFloor = rippleLeft + rippleMiddle + rippleRight + enterLeft + enterMiddle + enterRight; // floorEntered is true iff currFloor is true and lastFloor is false // (Mario just entered the floor on this frame) painting->floorEntered = (painting->lastFloor ^ painting->currFloor) & painting->currFloor; painting->marioWasUnder = painting->marioIsUnder; // Check if mario has fallen below the painting (used for floor paintings) if (gPaintingMarioYPos < painting->posY) { painting->marioIsUnder = TRUE; } else { painting->marioIsUnder = FALSE; } // mario "went under" if he was not under last frame, but is under now painting->marioWentUnder = (painting->marioWasUnder ^ painting->marioIsUnder) & painting->marioIsUnder; } /** * Update the ripple's timer and magnitude, making it propagate outwards. * * Automatically changes the painting back to IDLE state (or RIPPLE for continuous paintings) if the * ripple's magnitude becomes small enough. */ void painting_update_ripple_state(struct Painting *painting) { if (gPaintingUpdateCounter != gLastPaintingUpdateCounter) { painting->currRippleMag *= painting->rippleDecay; //! After ~6.47 days, paintings with RIPPLE_TRIGGER_CONTINUOUS will increment this to //! 16777216 (1 << 24), at which point it will freeze (due to floating-point //! imprecision?) and the painting will stop rippling. This happens to HMC, DDD, and //! CotMC. This happens on Wii VC. Untested on N64 and Wii U VC. painting->rippleTimer += 1.0; } if (painting->rippleTrigger == RIPPLE_TRIGGER_PROXIMITY) { // if the painting is barely rippling, make it stop rippling if (painting->currRippleMag <= 1.0) { painting->state = PAINTING_IDLE; gRipplingPainting = NULL; } } else if (painting->rippleTrigger == RIPPLE_TRIGGER_CONTINUOUS) { // if the painting is doing the entry ripple but the ripples are as small as those from the // passive ripple, make it do a passive ripple // If mario goes below the surface but doesn't warp, the painting will eventually reset. if (painting->state == PAINTING_ENTERED && painting->currRippleMag <= painting->passiveRippleMag) { painting->state = PAINTING_RIPPLE; painting->currRippleMag = painting->passiveRippleMag; painting->rippleDecay = painting->passiveRippleDecay; painting->currRippleRate = painting->passiveRippleRate; painting->dispersionFactor = painting->passiveDispersionFactor; } } } /** * @return the ripple function at posX, posY * note that posX and posY correspond to a point on the face of the painting, not actual axes */ s16 calculate_ripple_at_point(struct Painting *painting, f32 posX, f32 posY) { /// Controls the peaks of the ripple. f32 rippleMag = painting->currRippleMag; /// Controls the ripple's frequency f32 rippleRate = painting->currRippleRate; /// Controls how fast the ripple spreads f32 dispersionFactor = painting->dispersionFactor; /// How far the ripple has spread f32 rippleTimer = painting->rippleTimer; /// x and y ripple origin f32 rippleX = painting->rippleX; f32 rippleY = painting->rippleY; f32 distanceToOrigin; f32 rippleDistance; posX *= painting->size / PAINTING_SIZE; posY *= painting->size / PAINTING_SIZE; distanceToOrigin = sqrtf((posX - rippleX) * (posX - rippleX) + (posY - rippleY) * (posY - rippleY)); // A larger dispersionFactor makes the ripple spread slower rippleDistance = distanceToOrigin / dispersionFactor; if (rippleTimer < rippleDistance) { // if the ripple hasn't reached the point yet, make the point magnitude 0 return 0; } else { // use a cosine wave to make the ripple go up and down, // scaled by the painting's ripple magnitude f32 rippleZ = rippleMag * cosf(rippleRate * (2 * M_PI) * (rippleTimer - rippleDistance)); // round it to an int and return it return round_float(rippleZ); } } /** * If movable, return the ripple function at (posX, posY) * else return 0 */ s16 ripple_if_movable(struct Painting *painting, s16 movable, s16 posX, s16 posY) { s16 rippleZ = 0; if (movable) { rippleZ = calculate_ripple_at_point(painting, posX, posY); } return rippleZ; } /** * Allocates and generates a mesh for the rippling painting effect by modifying the passed in `mesh` * based on the painting's current ripple state. * * The `mesh` table describes the location of mesh vertices, whether they move when rippling, and what * triangles they belong to. * * The static mesh passed in is organized into two lists. This function only uses the first list, * painting_calculate_triangle_normals below uses the second one. * * The first list describes the vertices in this format: * numVertices * v0 x, v0 y, movable * ... * vN x, vN y, movable * Where x and y are from 0 to PAINTING_SIZE, movable is 0 or 1. * * The mesh used in game, seg2_painting_triangle_mesh, is in bin/segment2.c. */ void painting_generate_mesh(struct Painting *painting, s16 *mesh, s16 numTris) { s16 i; gPaintingMesh = mem_pool_alloc(gEffectsMemoryPool, numTris * sizeof(struct PaintingMeshVertex)); if (gPaintingMesh == NULL) { } // accesses are off by 1 since the first entry is the number of vertices for (i = 0; i < numTris; i++) { gPaintingMesh[i].pos[0] = mesh[i * 3 + 1]; gPaintingMesh[i].pos[1] = mesh[i * 3 + 2]; // The "z coordinate" of each vertex in the mesh is either 1 or 0. Instead of being an // actual coordinate, it just determines whether the vertex moves gPaintingMesh[i].pos[2] = ripple_if_movable(painting, mesh[i * 3 + 3], gPaintingMesh[i].pos[0], gPaintingMesh[i].pos[1]); } } /** * Calculate the surface normals of each triangle in the generated ripple mesh. * * The static mesh passed in is organized into two lists. This function uses the second list, * painting_generate_mesh above uses the first one. * * The second list in `mesh` describes the mesh's triangles in this format: * numTris * tri0 v0, tri0 v1, tri0 v2 * ... * triN v0, triN v1, triN v2 * Where each v0, v1, v2 is an index into the first list in `mesh`. * * The mesh used in game, seg2_painting_triangle_mesh, is in bin/segment2.c. */ void painting_calculate_triangle_normals(s16 *mesh, s16 numVtx, s16 numTris) { s16 i; gPaintingTriNorms = mem_pool_alloc(gEffectsMemoryPool, numTris * sizeof(Vec3f)); if (gPaintingTriNorms == NULL) { } for (i = 0; i < numTris; i++) { s16 tri = numVtx * 3 + i * 3 + 2; // Add 2 because of the 2 length entries preceding the list s16 v0 = mesh[tri]; s16 v1 = mesh[tri + 1]; s16 v2 = mesh[tri + 2]; f32 x0 = gPaintingMesh[v0].pos[0]; f32 y0 = gPaintingMesh[v0].pos[1]; f32 z0 = gPaintingMesh[v0].pos[2]; f32 x1 = gPaintingMesh[v1].pos[0]; f32 y1 = gPaintingMesh[v1].pos[1]; f32 z1 = gPaintingMesh[v1].pos[2]; f32 x2 = gPaintingMesh[v2].pos[0]; f32 y2 = gPaintingMesh[v2].pos[1]; f32 z2 = gPaintingMesh[v2].pos[2]; // Cross product to find each triangle's normal vector gPaintingTriNorms[i][0] = (y1 - y0) * (z2 - z1) - (z1 - z0) * (y2 - y1); gPaintingTriNorms[i][1] = (z1 - z0) * (x2 - x1) - (x1 - x0) * (z2 - z1); gPaintingTriNorms[i][2] = (x1 - x0) * (y2 - y1) - (y1 - y0) * (x2 - x1); } } /** * Rounds a floating-point component of a normal vector to an s8 by multiplying it by 127 or 128 and * rounding away from 0. */ s8 normalize_component(f32 comp) { s8 rounded; if (comp > 0.0) { rounded = comp * 127.0 + 0.5; // round up } else if (comp < 0.0) { rounded = comp * 128.0 - 0.5; // round down } else { rounded = 0; // don't round 0 } return rounded; } /** * Approximates the painting mesh's vertex normals by averaging the normals of all triangles sharing a * vertex. Used for gouraud lighting. * * After each triangle's surface normal is calculated, the `neighborTris` table describes which triangles * each vertex should use when calculating the average normal vector. * * The table is a list of entries in this format: * numNeighbors, tri0, tri1, ..., triN * * Where each 'tri' is an index into gPaintingTriNorms. * Entry i in `neighborTris` corresponds to the vertex at gPaintingMesh[i] * * The table used in game, seg2_painting_mesh_neighbor_tris, is in bin/segment2.c. */ void painting_average_vertex_normals(s16 *neighborTris, s16 numVtx) { UNUSED s16 unused; s16 tri; s16 i; s16 j; s16 neighbors; s16 entry = 0; for (i = 0; i < numVtx; i++) { f32 nx = 0.0f; f32 ny = 0.0f; f32 nz = 0.0f; f32 nlen; // The first number of each entry is the number of adjacent tris neighbors = neighborTris[entry]; for (j = 0; j < neighbors; j++) { tri = neighborTris[entry + j + 1]; nx += gPaintingTriNorms[tri][0]; ny += gPaintingTriNorms[tri][1]; nz += gPaintingTriNorms[tri][2]; } // Move to the next vertex's entry entry += neighbors + 1; // average the surface normals from each neighboring tri nx /= neighbors; ny /= neighbors; nz /= neighbors; nlen = sqrtf(nx * nx + ny * ny + nz * nz); if (nlen == 0.0) { gPaintingMesh[i].norm[0] = 0; gPaintingMesh[i].norm[1] = 0; gPaintingMesh[i].norm[2] = 0; } else { gPaintingMesh[i].norm[0] = normalize_component(nx / nlen); gPaintingMesh[i].norm[1] = normalize_component(ny / nlen); gPaintingMesh[i].norm[2] = normalize_component(nz / nlen); } } } /** * Creates a display list that draws the rippling painting, with 'img' mapped to the painting's mesh, * using 'textureMap'. * * If the textureMap doesn't describe the whole mesh, then multiple calls are needed to draw the whole * painting. */ Gfx *render_painting(u8 *img, s16 tWidth, s16 tHeight, s16 *textureMap, s16 mapVerts, s16 mapTris, u8 alpha) { s16 group; s16 map; s16 triGroup; s16 mapping; s16 meshVtx; s16 tx; s16 ty; // We can fit 15 (16 / 3) vertices in the RSP's vertex buffer. // Group triangles by 5, with one remainder group. s16 triGroups = mapTris / 5; s16 remGroupTris = mapTris % 5; s16 numVtx = mapTris * 3; s16 commands = triGroups * 2 + remGroupTris + 7; Vtx *verts = alloc_display_list(numVtx * sizeof(Vtx)); Gfx *dlist = alloc_display_list(commands * sizeof(Gfx)); Gfx *gfx = dlist; if (verts == NULL || dlist == NULL) { } gLoadBlockTexture(gfx++, tWidth, tHeight, G_IM_FMT_RGBA, img); // Draw the groups of 5 first for (group = 0; group < triGroups; group++) { // The triangle groups are the second part of the texture map. // Each group is a list of 15 mappings triGroup = mapVerts * 3 + group * 15 + 2; for (map = 0; map < 15; map++) { // The mapping is just an index into the earlier part of the textureMap // Some mappings are repeated, for example, when multiple triangles share a vertex mapping = textureMap[triGroup + map]; // The first entry is the ID of the vertex in the mesh meshVtx = textureMap[mapping * 3 + 1]; // The next two are the texture coordinates for that vertex tx = textureMap[mapping * 3 + 2]; ty = textureMap[mapping * 3 + 3]; // Map the texture and place it in the verts array make_vertex(verts, group * 15 + map, gPaintingMesh[meshVtx].pos[0], gPaintingMesh[meshVtx].pos[1], gPaintingMesh[meshVtx].pos[2], tx, ty, gPaintingMesh[meshVtx].norm[0], gPaintingMesh[meshVtx].norm[1], gPaintingMesh[meshVtx].norm[2], alpha); } // Load the vertices and draw the 5 triangles gSPVertex(gfx++, VIRTUAL_TO_PHYSICAL(verts + group * 15), 15, 0); gSPDisplayList(gfx++, dl_paintings_draw_ripples); } // One group left with < 5 triangles triGroup = mapVerts * 3 + triGroups * 15 + 2; // Map the texture to the triangles for (map = 0; map < remGroupTris * 3; map++) { mapping = textureMap[triGroup + map]; meshVtx = textureMap[mapping * 3 + 1]; tx = textureMap[mapping * 3 + 2]; ty = textureMap[mapping * 3 + 3]; make_vertex(verts, triGroups * 15 + map, gPaintingMesh[meshVtx].pos[0], gPaintingMesh[meshVtx].pos[1], gPaintingMesh[meshVtx].pos[2], tx, ty, gPaintingMesh[meshVtx].norm[0], gPaintingMesh[meshVtx].norm[1], gPaintingMesh[meshVtx].norm[2], alpha); } // Draw the triangles individually gSPVertex(gfx++, VIRTUAL_TO_PHYSICAL(verts + triGroups * 15), remGroupTris * 3, 0); for (group = 0; group < remGroupTris; group++) { gSP1Triangle(gfx++, group * 3, group * 3 + 1, group * 3 + 2, 0); } gSPEndDisplayList(gfx); return dlist; } /** * Orient the painting mesh for rendering. */ Gfx *painting_model_view_transform(struct Painting *painting) { f32 sizeRatio = painting->size / PAINTING_SIZE; Mtx *rotX = alloc_display_list(sizeof(Mtx)); Mtx *rotY = alloc_display_list(sizeof(Mtx)); Mtx *translate = alloc_display_list(sizeof(Mtx)); Mtx *scale = alloc_display_list(sizeof(Mtx)); Gfx *dlist = alloc_display_list(5 * sizeof(Gfx)); Gfx *gfx = dlist; if (rotX == NULL || rotY == NULL || translate == NULL || dlist == NULL) { } guTranslate(translate, painting->posX, painting->posY, painting->posZ); guRotate(rotX, painting->pitch, 1.0f, 0.0f, 0.0f); guRotate(rotY, painting->yaw, 0.0f, 1.0f, 0.0f); guScale(scale, sizeRatio, sizeRatio, sizeRatio); gSPMatrix(gfx++, translate, G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_PUSH); gSPMatrix(gfx++, rotX, G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH); gSPMatrix(gfx++, rotY, G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH); gSPMatrix(gfx++, scale, G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH); gSPEndDisplayList(gfx); return dlist; } /** * Ripple a painting that has 1 or more images that need to be mapped */ Gfx *painting_ripple_image(struct Painting *painting) { s16 meshVerts; s16 meshTris; s16 i; s16 *textureMap; s16 imageCount = painting->imageCount; s16 tWidth = painting->textureWidth; s16 tHeight = painting->textureHeight; s16 **textureMaps = segmented_to_virtual(painting->textureMaps); u8 **textures = segmented_to_virtual(painting->textureArray); Gfx *dlist = alloc_display_list((imageCount + 6) * sizeof(Gfx)); Gfx *gfx = dlist; if (dlist == NULL) { return dlist; } gSPDisplayList(gfx++, painting_model_view_transform(painting)); gSPDisplayList(gfx++, dl_paintings_rippling_begin); gSPDisplayList(gfx++, painting->rippleDisplayList); // Map each image to the mesh's vertices for (i = 0; i < imageCount; i++) { textureMap = segmented_to_virtual(textureMaps[i]); meshVerts = textureMap[0]; meshTris = textureMap[meshVerts * 3 + 1]; gSPDisplayList(gfx++, render_painting(textures[i], tWidth, tHeight, textureMap, meshVerts, meshTris, painting->alpha)); } // Update the ripple, may automatically reset the painting's state. painting_update_ripple_state(painting); gSPPopMatrix(gfx++, G_MTX_MODELVIEW); gSPDisplayList(gfx++, dl_paintings_rippling_end); gSPEndDisplayList(gfx); return dlist; } /** * Ripple a painting that has 1 "environment map" texture. */ Gfx *painting_ripple_env_mapped(struct Painting *painting) { s16 meshVerts; s16 meshTris; s16 *textureMap; s16 tWidth = painting->textureWidth; s16 tHeight = painting->textureHeight; s16 **textureMaps = segmented_to_virtual(painting->textureMaps); u8 **tArray = segmented_to_virtual(painting->textureArray); Gfx *dlist = alloc_display_list(7 * sizeof(Gfx)); Gfx *gfx = dlist; if (dlist == NULL) { return dlist; } gSPDisplayList(gfx++, painting_model_view_transform(painting)); gSPDisplayList(gfx++, dl_paintings_env_mapped_begin); gSPDisplayList(gfx++, painting->rippleDisplayList); // Map the image to the mesh's vertices textureMap = segmented_to_virtual(textureMaps[0]); meshVerts = textureMap[0]; meshTris = textureMap[meshVerts * 3 + 1]; gSPDisplayList(gfx++, render_painting(tArray[0], tWidth, tHeight, textureMap, meshVerts, meshTris, painting->alpha)); // Update the ripple, may automatically reset the painting's state. painting_update_ripple_state(painting); gSPPopMatrix(gfx++, G_MTX_MODELVIEW); gSPDisplayList(gfx++, dl_paintings_env_mapped_end); gSPEndDisplayList(gfx); return dlist; } /** * Generates a mesh, calculates vertex normals for lighting, and renders a rippling painting. * The mesh and vertex normals are regenerated and freed every frame. */ Gfx *display_painting_rippling(struct Painting *painting) { s16 *mesh = segmented_to_virtual(seg2_painting_triangle_mesh); s16 *neighborTris = segmented_to_virtual(seg2_painting_mesh_neighbor_tris); s16 numVtx = mesh[0]; s16 numTris = mesh[numVtx * 3 + 1]; Gfx *dlist; // Generate the mesh and its lighting data painting_generate_mesh(painting, mesh, numVtx); painting_calculate_triangle_normals(mesh, numVtx, numTris); painting_average_vertex_normals(neighborTris, numVtx); // Map the painting's texture depending on the painting's texture type. switch (painting->textureType) { case PAINTING_IMAGE: dlist = painting_ripple_image(painting); break; case PAINTING_ENV_MAP: dlist = painting_ripple_env_mapped(painting); break; } // The mesh data is freed every frame. mem_pool_free(gEffectsMemoryPool, gPaintingMesh); mem_pool_free(gEffectsMemoryPool, gPaintingTriNorms); return dlist; } /** * Render a normal painting. */ Gfx *display_painting_not_rippling(struct Painting *painting) { Gfx *dlist = alloc_display_list(4 * sizeof(Gfx)); Gfx *gfx = dlist; if (dlist == NULL) { return dlist; } gSPDisplayList(gfx++, painting_model_view_transform(painting)); gSPDisplayList(gfx++, painting->normalDisplayList); gSPPopMatrix(gfx++, G_MTX_MODELVIEW); gSPEndDisplayList(gfx); return dlist; } /** * Clear mario-related state and clear gRipplingPainting. */ void reset_painting(struct Painting *painting) { painting->lastFloor = 0; painting->currFloor = 0; painting->floorEntered = 0; painting->marioWasUnder = 0; painting->marioIsUnder = 0; painting->marioWentUnder = 0; gRipplingPainting = NULL; } /** * Controls the x coordinate of the DDD painting. * * Before mario gets the "Board Bowser's Sub" star in DDD, the painting spawns at frontPos. * * If mario just got the star, the painting's x coordinate moves to backPos at a rate of `speed` units. * * When the painting reaches backPos, a save flag is set so that the painting will spawn at backPos * whenever it loads. * * This function also sets gDddPaintingStatus, which controls the warp: * 0 (0b00): set x coordinate to frontPos * 2 (0b10): set x coordinate to backPos * 3 (0b11): same as 2. Bit 0 is ignored */ void move_ddd_painting(struct Painting *painting, f32 frontPos, f32 backPos, f32 speed) { // Obtain the DDD star flags u32 dddFlags = save_file_get_star_flags(gCurrSaveFileNum - 1, COURSE_DDD - 1); // Get the other save file flags u32 saveFileFlags = save_file_get_flags(); // Find out whether Board Bowser's Sub was collected u32 bowsersSubBeaten = dddFlags & BOARD_BOWSERS_SUB; // Check whether DDD has already moved back u32 dddBack = saveFileFlags & SAVE_FLAG_DDD_MOVED_BACK; if (!bowsersSubBeaten && !dddBack) { // If we haven't collected the star or moved the painting, put the painting at the front painting->posX = frontPos; gDddPaintingStatus = 0; } else if (bowsersSubBeaten && !dddBack) { // If we've collected the star but not moved the painting back, // Each frame, move the painting by a certain speed towards the back area. painting->posX += speed; gDddPaintingStatus = BOWSERS_SUB_BEATEN; if (painting->posX >= backPos) { painting->posX = backPos; // Tell the save file that we've moved DDD back. save_file_set_flags(SAVE_FLAG_DDD_MOVED_BACK); } } else if (bowsersSubBeaten && dddBack) { // If the painting has already moved back, place it in the back position. painting->posX = backPos; gDddPaintingStatus = BOWSERS_SUB_BEATEN | DDD_BACK; } } /** * Set the painting's node's layer based on its alpha */ void set_painting_layer(struct GraphNodeGenerated *gen, struct Painting *painting) { switch (painting->alpha) { case 0xFF: // Opaque gen->fnNode.node.flags = (gen->fnNode.node.flags & 0xFF) | (LAYER_OPAQUE << 8); break; default: gen->fnNode.node.flags = (gen->fnNode.node.flags & 0xFF) | (LAYER_TRANSPARENT << 8); break; } } /** * Display either a normal painting or a rippling one depending on the painting's ripple status */ Gfx *display_painting(struct Painting *painting) { switch (painting->state) { case PAINTING_IDLE: return display_painting_not_rippling(painting); break; default: return display_painting_rippling(painting); break; } } /** * Update function for wall paintings. * Calls a different update function depending on the painting's ripple trigger and current state. */ void wall_painting_update(struct Painting *painting, struct Painting *paintingGroup[]) { if (painting->rippleTrigger == RIPPLE_TRIGGER_PROXIMITY) { switch (painting->state) { case PAINTING_IDLE: wall_painting_proximity_idle(painting, paintingGroup); break; case PAINTING_RIPPLE: wall_painting_proximity_rippling(painting, paintingGroup); break; } } else if (painting->rippleTrigger == RIPPLE_TRIGGER_CONTINUOUS) { switch (painting->state) { case PAINTING_IDLE: wall_painting_continuous_idle(painting, paintingGroup); break; case PAINTING_RIPPLE: wall_painting_continuous_rippling(painting, paintingGroup); break; } } } /** * Update function for floor paintings (HMC and CotMC) * Calls a different update function depending on the painting's ripple trigger and current state. * * No floor paintings use RIPPLE_TRIGGER_PROXIMITY in the game. */ void floor_painting_update(struct Painting *painting, struct Painting *paintingGroup[]) { if (painting->rippleTrigger == RIPPLE_TRIGGER_PROXIMITY) { switch (painting->state) { case PAINTING_IDLE: floor_painting_proximity_idle(painting, paintingGroup); break; case PAINTING_RIPPLE: floor_painting_proximity_rippling(painting, paintingGroup); break; } } else if (painting->rippleTrigger == RIPPLE_TRIGGER_CONTINUOUS) { switch (painting->state) { case PAINTING_IDLE: floor_painting_continuous_idle(painting, paintingGroup); break; case PAINTING_RIPPLE: floor_painting_continuous_rippling(painting, paintingGroup); break; } } } /** * Render and update the painting whose id and group matches the values in the GraphNode's parameter. * Use PAINTING_ID(id, group) to set the right parameter in a level's geo layout. */ Gfx *geo_painting_draw(s32 callContext, struct GraphNode *node, UNUSED void *context) { struct GraphNodeGenerated *gen = (struct GraphNodeGenerated *) node; s32 group = (gen->parameter >> 8) & 0xFF; s32 id = gen->parameter & 0xFF; Gfx *paintingDlist = NULL; struct Painting **paintingGroup = sPaintingGroups[group]; struct Painting *painting = segmented_to_virtual(paintingGroup[id]); if (callContext != GEO_CONTEXT_RENDER) { reset_painting(painting); } else if (callContext == GEO_CONTEXT_RENDER) { // Update the ddd painting before drawing if (group == 1 && id == PAINTING_ID_DDD) { move_ddd_painting(painting, 3456.0f, 5529.6f, 20.0f); } // Determine if the painting is transparent set_painting_layer(gen, painting); // Draw before updating paintingDlist = display_painting(painting); // Update the painting painting_update_floors(painting); switch ((s16) painting->pitch) { // only paintings with 0 pitch are treated as walls case 0: wall_painting_update(painting, paintingGroup); break; default: floor_painting_update(painting, paintingGroup); break; } } return paintingDlist; } /** * Update the painting system's local copy of mario's current floor and position. */ Gfx *geo_painting_update(s32 callContext, UNUSED struct GraphNode *node, UNUSED f32 c[4][4]) { struct Surface *surface; // Reset the update counter if (callContext != GEO_CONTEXT_RENDER) { gLastPaintingUpdateCounter = gAreaUpdateCounter - 1; gPaintingUpdateCounter = gAreaUpdateCounter; } else { gLastPaintingUpdateCounter = gPaintingUpdateCounter; gPaintingUpdateCounter = gAreaUpdateCounter; // Store mario's floor and position find_floor(gMarioObject->oPosX, gMarioObject->oPosY, gMarioObject->oPosZ, &surface); gPaintingMarioFloorType = surface->type; gPaintingMarioXPos = gMarioObject->oPosX; gPaintingMarioYPos = gMarioObject->oPosY; gPaintingMarioZPos = gMarioObject->oPosZ; } return NULL; }