Well, after many weeks of trial and error, experimentation and frustration, I think I finally have a solution for the gear physics! To recap, the problem with my old implementation was that it was only considering relative angular motion between the gears, and not linear motion as well. This made the gears behave incorrectly if they had any relative linear velocity.
Need for a new solution
My old gear physics implementation was inspired by the one in Box2D, so I looked there again for ideas. It turns out they work around this problem by also taking into account the angular velocity of the rigidbodies that the gears are attached to. I played around with this idea for a while but I found it didn’t really extend well to my more general 3D case.
There were also other things to consider, like supporting other types of gear such as rack and pinion. Also, the fact that my gear physics was only updated once per FixedUpdate (and not as part of the main PhysX solver update) caused lag, or “wind-up” between the gears under high torque loads. A compromise I was living with, but was never really happy about.
Using configurable joints
So I decided to throw out my old approach and instead make use of the built in PhysX constraints (or “joints”), as exposed by Unity’s ConfigurableJoint. The idea was to position the joint anchors at the current contact point between the two gears (right at the edge of the gears where they touch), and then lock the motion only along the tangent at that contact point. Then every FixedUpdate, the joint anchors are re-positioned to the current contact point (as the gears will have moved slightly). By carefully positioning the anchors like this, the gear ratio (relative torque transfer), and the relative angular and linear motion are all taken into account automatically. This approach also makes it relatively easy to add support for rack and pinion gears. And…by using built-in joints, the gear constraints are solved along with everything else, so there is minimal wind-up.
Sounds good in principle, right? However the re-positioning of the joint anchors is where things get tricky. If every fixed update, they are simply re-positioned exactly to the new contact point, the gears will gradually slip when under load. This is because a joint doesn’t perfectly rigidly lock its anchors together, and there will usually be a small difference in position between them, even after the physics solve. This error gets fed back into the new joint anchor positions and the error accumulates over time.
Correcting the joint anchors
So I tried correcting for this error by figuring out what the anchor position delta was vs. what it should have been. This is not easy because there is not really an absolute angle for a rigidbody, only an absolute position. The orientation angles of a rigidbody wrap every 360 degrees of rotation, so you’ve no idea how many of these rotations have happened since the last update. You end up having to integrate velocity over time and accumulate the positional delta as found at the contact point between the two gears. There are two problems with this. First, I found that when comparing the contact point velocities between the two gears as reported by Rigidbody.GetPointVelocity, they seemed slightly off if the gears were moving linearly relative to one another. Close, but not spot on. This small error was compounded by the second problem, which is that by accumulating the contact position delta over time, any errors also accumulate – quickly. It creates a feedback loop, the error feeds into the joint anchors, which causes the joint to pull the rigidbodies around too much, causing more positional error, which feeds into the new anchor positions in the next update, and so on. This leads to unstable physics behaviour – high impulses, oscillations, and your machines exploding like crazy.
At this point I got pretty desperate, thinking I might have to abandon gears altogether, maybe even the whole game. I even tried mocking up a solution using physics colliders (one BoxCollider for each gear tooth!), and using the collisions between the teeth to provide the gear behaviour. Er yeah – that didn’t work unsurprisingly.
Predefined anchor positions
Then I came up with the idea for the solution I have now. Re-positioning the joint anchors exactly to the gear’s contact point leads to accumulated error and gear slip. So instead, each gear is given predefined positions at equal intervals around its circumference, and every update the one closest to the current contact point is found, and that’s the position used for the joint anchor. As long as the distance between these predefined positions is greater than the distance between the joint anchors after the physics solve, you get no gear slip.
However, there is a problem with this if the gears are different sizes, as shown above. The next / previous predefined anchor positions as measured along the contact tangent don’t quite line up due to the differing radii. I tried correcting for this with a fun bit of trig, the maths worked out beautifully, but in practice it led me right back to another feedback loop, the joint and its corrected anchor positions ended up fighting each other, causing oscillations, exploding machines and much cursing. In the end I just went with a simpler approach of just upping the number of predefined anchor positions, so that any differences along the contact tangent were minimised. However, this of course means the distance between each predefined position is smaller, so increasing the chance of gears slipping under load. In practice it’s actually not too bad though, it takes quite a high torque load to cause slip (to the point where axle hinge joints are also being noticeably pulled apart).
This whole endeavour has been very frustrating, as the ideas I tried often worked fine most of the time, only to fail in particular use cases (certain combinations of bevel gears seemed to be particularly sensitive). I spent a lot of time switching back and forth between my various approaches, trying different modifications, until homing in on what I have now. It was so time consuming as I had to do lots of testing every time I made a change to check that I hadn’t broken one of my many test cases.
Well, hopefully the implementation I ended up with will see me through. I’ve tested a lot of situations (complex gear trains, high torque loads, high rpm, etc.) but the open ended nature of this game means I might have missed a case where things go bad, let’s hope not. Next up I’m going to work on implementing rack and pinion gears, and probably worm gears too, I wanna really be sure these will actually work with no problems, before going ahead with any other part of the game.