wipeout-rewrite icon indicating copy to clipboard operation
wipeout-rewrite copied to clipboard

Collisions not accurate to original Wipeout game, missing behavior and much more punishing

Open cshonegger opened this issue 10 months ago • 15 comments

First of all, awesome work on this. Been playing your recreation a ton the last month.

The rewrite is not correctly replicating Wipeout's track collision behavior - it is missing the way the game bounces the ship back from the wall or changes its direction based on the type of collision. Some animation is missing as well.

In the original Wipeout, if wing collides with track wall:

  1. Ship slows significantly, less or more based on the angle and speed of collision.
  2. Ship bounces towards the middle of the track away from wall, proportional to the speed and angle of the hit. Grazing hits have very little bounce, harder hits at sharper angles (more magnitude) bounce back more.
  3. Ship animates a roll in the opposite direction from the wall, between 0 or 90 degrees - or a quick 360 degree roll - according to how hard the hit was. Anecdotally, when the ship rolls 360, the ship bounces back from the wall less. This video has good examples at 0:55 (grazing), 1:19, 1:25, 1:42 and 1:45 (harder hits with varying bounce), and 2:02 (360 roll). Note the slowdowns on 0:55, 1:42, and 1:45 are less than the slowdowns on 1:19, 1:25 and 2:02.

In the original, if nose collides with track wall:

  1. Ship slows significantly.
  2. Ship turns (yaw) away from the wall a fixed amount. Seems to be around 15-20 degrees. This happens every collision, and there is no bounce out from the wall, only turn. (Open Altima VII and drive straight without turning into the first corner, on the original and the rewrite, you will see the difference.)

This rewrite is only performing the first step in both situations, and for wing collisions, is not varying the slowdown based on angle of collision / magnitude. This unintentionally makes collisions much more punishing than the original. Wing collisions result in the ship being stranded at low speed on the outside of a corner, and nose collisions cause the player to get stuck in the wall, colliding repeatedly instead of turning out.

I've searched through the original source to find what did not get preserved. There seems to be code near the end of the original COLLIDE.C within voids Reflection, WingCollision, and NoseCollision that I don't see replicated here. May be related to the numbers you have in the comments. I am not an expert on this though!

This isn't a request to backport WOXL/97 collision into the game, to be clear!

cshonegger avatar Feb 27 '25 18:02 cshonegger

This bothered me enough to look into it, so I went back to the original leaked source to reference the original code.

The translation/refactoring in wipeout-rewrite looks just about identical, but there are two constants I think got messed up along the way.

One issue is it seems like grazing wing collisions just kill all momentum, and they shouldn't. I think some wing collisions are being treated as nose collisions, and this is why:

In ship.c , the code for vec3_is_on_face. See the leaked source COLLIDE.C for reference, particularly the original version of this function CheckPolygonCollision.

I am almost 100% certain that the translation of angle > 30000 to a floating point comparison should be ((float)30000/32768)*2*PI , which would be 0.91 * 2 * PI . rewrite is using 2*PI - 0.01 which is 0.99 * 2 * PI. Using 0.91 gives a behavior much closer to what I observe playing the PSX version.

Another issue is not getting pushed back out toward the middle of the track on nose collisions. See ship.c for ship_resolve_nose_collision and reference COLLIDE.C for NoseCollision.

Here's the original magnitude calculation:

magnitude = (playerShip->speed>>4) + 400;	
if(direction > 0) playerShip->vhdg += magnitude;	
else playerShip->vhdg -= magnitude;

rewrite has:

float magnitude = ((self->speed * 0.0625) + 400) * M_PI / (4096.0 * 64.0);
if (direction > 0)  self->angular_velocity.y += magnitude;
else self->angular_velocity.y -= magnitude;

If you look at the scaling factor PI / (4096*64) , there's nothing that corresponds to a >>6 or / 64 in the original code. The >>4 is the * 0.0625 (/ 16). I think that was just some oversight that slipped in, an honest mistake.

Get rid of the * 64 and now if you crash the nose into the wall, the ship swings back out in a way that looks and feels right if you're checking the PSX version side-by-side.

jnmartin84 avatar Apr 12 '25 13:04 jnmartin84

That's a lot of words to say "I think the nose vs wing check is off by about 29 degrees in the worst case" in rewrite , and "the nose swing magnitude is 1/64th the amount it should be when smashing the nose into the wall"

jnmartin84 avatar Apr 12 '25 13:04 jnmartin84

I managed to reproduce the roll behavior. This was another scaling issue.

Looking at ship.c , ship_resolve_wing_collision : https://github.com/phoboslab/wipeout-rewrite/blob/05e9c2d3a1272e631e256a76b89aca235b92e4a9/src/wipeout/ship.c#L644

Then look at original source COLLIDE.C , WING_COLLISION : magnitude = (abs(angle)*playerShip->speed) >> 16;

I don't have a good justification for why this is the way it is that isn't really hand-wavy and "just try it" but if you change the wipeout-rewrite code to the following: float magnitude = (abs(angle) * self->speed) * ((2.0 * M_PI) / 4096.0);

You will see the increasing roll based on how hard you collide wing to wall, eventually leading to a full 360 degree roll.

I'm not sure where the division by 16 (well, its really 2/32 instead of /16, but they're the same thing) came into play. Another error going fixed to float, they happen.

jnmartin84 avatar Apr 12 '25 16:04 jnmartin84

I'm not sure if that 2pi/4096 is totally correct, someone more familiar with the original might need to see if the roll behavior really lines up or if it is too sensitive. I know that if I do 2pi/2048, it seems to roll too much. If I do 2pi/8192, I can't seem to reproduce a full 360 roll.

jnmartin84 avatar Apr 12 '25 16:04 jnmartin84

https://youtu.be/5sc_9MjTeJk

this is an example of using 2pi / 4096. Maybe the divisor needs to be something in between 4k and 8k? I was testing with powers of 2

jnmartin84 avatar Apr 12 '25 16:04 jnmartin84

6144 seems less aggressive but still does a full roll at times: https://youtu.be/EwEhb8xuVOI

jnmartin84 avatar Apr 12 '25 16:04 jnmartin84

I am almost 100% certain that the translation of angle > 30000 to a floating point comparison should be ((float)30000/32768)*2*PI , which would be 0.91 * 2 * PI . rewrite is using 2*PI - 0.01 which is 0.99 * 2 * PI. Using 0.91 gives a behavior much closer to what I observe playing the PSX version.

Looking at the original source again: I agree. I believe I used the smaller epsilon out of a misguided desire to "do it right", technically...

If you look at the scaling factor PI / (4096*64) , there's nothing that corresponds to a >>6 or / 64 in the original code. The >>4 is the * 0.0625 (/ 16). I think that was just some oversight that slipped in, an honest mistake.

Yep. No idea where the * 64 came from.

I don't have a good justification for why this is the way it is that isn't really hand-wavy and "just try it" but if you change the wipeout-rewrite code to the following: float magnitude = (abs(angle) * self->speed) * ((2.0 * M_PI) / 4096.0);

I believe you're right. float radians = fixed_point_degrees * 2 * M_PI / 4096 is correct. 4096 fixed point corresponds to 360 degree, as helpfully stated in the original Standard.h.

As for why 6144 might be a better value: I suspect that there's another conversion error when applying self->angular_acceleration.z and self->angular_velocity.z in ship_player_update_race() (formerly shipTrkReaction()). I need to go through this again when I have some more time.

Thanks for investigating this! These things happen easily and are hard to spot.

Later during the rewrite I introduced some macros FIXED_TO_FLOAT(), ANGLE_NORM_TO_RADIAN(), NTSC_STEP_TO_RATE_PER_SECOND() etc. Using these macros from the beginning instead of sprinkling / 4096 everywhere would have probably prevented some of these mistakes...

Anyway, please feel free to submit a PR or I can implement these fixes myself early next week.

phoboslab avatar Apr 13 '25 09:04 phoboslab

Glad to see I'm on the right track.

I was starting to get bothered by seeing comments about "must have changed the logic" (and my apologies, I said the same thing at least once).

After comparing the code it was obvious everyone was wrong about that, names changed but it was doing the same thing so it had to be math.

I'll put a PR together today

jnmartin84 avatar Apr 13 '25 11:04 jnmartin84

Submitted a PR for this and some for a few other minor issues. I'm looking into the ship update code you mentioned to see if I can find anything else.

jnmartin84 avatar Apr 13 '25 12:04 jnmartin84

Another inconsistency, this time regarding collision with floor.

There are two lines that add or subtract normal from velocity. One just uses face->normal raw, the other scales it.

raw

https://github.com/phoboslab/wipeout-rewrite/blob/05e9c2d3a1272e631e256a76b89aca235b92e4a9/src/wipeout/ship_player.c#L350

vs

scaled

https://github.com/phoboslab/wipeout-rewrite/blob/05e9c2d3a1272e631e256a76b89aca235b92e4a9/src/wipeout/ship_player.c#L353

Should they both be scaled?

jnmartin84 avatar Apr 13 '25 15:04 jnmartin84

There's a contribution of vhdg/32 to vroll at two points in DYNAM.C that I can't really figure out if they're applied in ship_player.c or not. I don't think so.

playerShip->vroll +=    sar(playerShip->vhdg,5);
playerShip->vroll -=    sar(playerShip->vroll,1);

I tried several things and they were all very wrong.

jnmartin84 avatar Apr 13 '25 19:04 jnmartin84

There's a contribution of vhdg/32 to vroll at two points in DYNAM.C...

One place in the original is in the if "Ship is held by the track", the other is in the else "Ship is flying", so it's always applied in the rewrite here: https://github.com/phoboslab/wipeout-rewrite/blob/9052cfa76362ef34453e2b641565a25320514a33/src/wipeout/ship_player.c#L437

It's not changing angular_velocity directly, but rather just adding to angular_acceleration, which is later applied to angular_velocity in one go. But I can't remember or trace why the / 32 was removed from self->angular_velocity.y in the above line... it might just be correct, because of some scaling things elsewhere. I'm sorry, this whole thing is a mess :(

phoboslab avatar Apr 13 '25 20:04 phoboslab

There's a contribution of vhdg/32 to vroll at two points in DYNAM.C...

One place in the original is in the if "Ship is held by the track", the other is in the else "Ship is flying", so it's always applied in the rewrite here:

wipeout-rewrite/src/wipeout/ship_player.c

Line 437 in 9052cfa

self->angular_acceleration.z += (self->angular_velocity.y - 0.5 * self->angular_velocity.z) * 30; It's not changing angular_velocity directly, but rather just adding to angular_acceleration, which is later applied to angular_velocity in one go. But I can't remember or trace why the / 32 was removed from self->angular_velocity.y in the above line... it might just be correct, because of some scaling things elsewhere. I'm sorry, this whole thing is a mess :(

One of my commits I had to back out was on that line. I tried doing the / 32 there and it makes the ship fly flat to the track no matter how fast you are going or how hard you turn. Even then, the rolling when you hit the wall didn't seem to change, which was more confusing.

jnmartin84 avatar Apr 13 '25 20:04 jnmartin84

There are two lines that add or subtract normal from velocity. One just uses face->normal raw, the other scales it. Should they both be scaled?

Good question. The unscaled if (alpha <= 0) case only happens when the ship is through the floor. The velocity is reflected and an "impulse" is added to move the ship out immediately. The other case (close to floor, but not touching) happens continuously and more slowly adds an acceleration to nudge the ship upwards.

Though track face normals are already normalized in the rewrite from FP.12 (/4096), and so is velocity but from FP.6 (/64), which leaves us with another FP.6 (/64). So I guess both cases should be just this!? self->velocity = vec3_add(self->velocity, vec3_mulf(face->normal, 64.0 * system_tick()));

Very good brain teasers...

Edit: actually 64.0 * 30.0 * system_tick() because the original assumed 30 FPS and we're multiplying the tick!?

phoboslab avatar Apr 13 '25 21:04 phoboslab

64.0 * 30.0 * system_tick()

Just tested this for the two cases, nothing seems to be "off." Not hanging up in the air or slamming through the floor or anything weird like that.

jnmartin84 avatar Apr 14 '25 02:04 jnmartin84