I wanted to document a VR paddling system that I designed a few years ago for a VR game called The Grand Canyon VR Experience. The goal was to simulate a standing paddle board experience in VR and the end result was designed and implemented in a couple days.
I felt particularly equipped to build this system since I had recently gone paddle-boarding with some family members.
I have this odd childhood habit of timing things in my head. If I see a stop light turn red, I’ll start counting up in my head until it turns green. I do this for more things than I care to admit… So during the first few minutes of our paddle-boarding experience, I found myself counting how long it took for the drag of the water to stop the rotation of my board after swiping horizontally across the bow at different strengths.
I remember being initially surprised at how high the drag felt, my board stopped rotating more quickly than I expected. By the end of the day, I had a good feel and intuition for these physics. So when one of my contracts said that they wanted a paddle-boarding experience, I felt confident that I could reproduce this experience in VR.
Breaking the Problem Down
For context, this was a single player VR game. So the sky was the limit as far as how I wanted to implement this. I didn’t have to worry about having a bunch of sub-components attached to an actor and how that might affect a multiplayer world with dozens of these actors moving around etc.
I wanted the final implementation to be a simplistic physics system. The paddle should apply a force on the vehicle at the location where the paddle was dragged through the water.
My thought process goes something like this. Ok, this is easy, all I need to do is apply a force to the vehicle based on how fast the paddle is moving. Yes, yes, so we need to calculate the velocity of the paddle every frame. Hmm, we should probably only apply these forces when the paddle is touching the water. So we need to detect when the paddle is in the water. Hmm, I bet it’d be cool if we also calculated a resistance value for the paddle so that slicing the paddle through the water would react as expected – less surface area on the paddle displacing less water.
I broke the problem down into 2 categories:
- Detect when the paddle is in the water
- Calculate the velocity of the paddle
- Calculate the resistance of the paddle
- Calculate the force that the paddle should apply to the vehicle
- Apply forces to the vehicle based on the paddle
- Apply linear and rotational drag on the vehicle to simulate the drag of the water
I came up with a simple mathematical model that I felt would allow for a handful of easy to tune parameters. The final force would be:
Force = Map(Paddle Velocity onto Force) * Paddle Resistance
I’ll break this down further.
Water Plane Detection
How do we know when the paddle is in the water? I made a BP_WaterPlane and instructed everyone to use this as the new water plane. It contained an infinite water plane for visuals as well as a box component that I could use to trigger overlaps on the paddle.
On the paddle itself, I arranged an array of 17 sphere overlaps. This number was completely arbitrary and was just based on how well I could pack spheres onto the paddle surface for the mesh I was given. I could have used just 1 large sphere, but I thought, why not get fancy with this? This way I can calculate the percentage of the paddle that’s submerged.
My first test was simply turning my sphere overlaps green when they were in the water and red when they were in the air. Somehow this was incredibly satisfying to see come together.
Every frame, I would cast a sphere down the shaft of the paddle to determine what surface the paddle was touching. I wanted to prevent paddling if the paddle was hitting rocks or the surface of the paddle-board.
Evidently, this was too realistic for my product manager who came to me frustrated one day “Zach, I like the paddling, but when I paddle like this <arms waving> the board doesn’t move.”
“Right, well you can’t row through the surface of the board… I mean, in real life the board would stop your paddle” I replied.
“Yes, well, I think it would feel better” he stated plainly.
So this wasn’t used in the end for locomotion but I believe the audio dude used the data for rock and dirt thumping sounds.
I calculate the instantaneous velocity of the paddle head every frame (not the motion controller, not the center of the paddle, the actual paddle head). This is nothing more than:
(CurrentLocation - PreviousLocation) / DeltaTime; // Units of m/s
Using instantaneous velocity can be very spikey in nature and I found that it didn’t work that great used alone. When this happens it’s often the case that I just calculate an Exponential Moving Average (EMA), that’s what I did here. So, calculate an EMA of the instantaneous paddle velocity and have that waiting for our final force application.
Part of the mathematical model that I brainstormed involved a parameter that I called paddle resistance. The idea was two-fold:
- Calculate a resistance value between [0,1] where 0 is no resistance and 1 is maximum resistance.
- Calculate this value for both air and water (this way you can do swishing sounds when people slice their paddle through the air).
When in the air, this value is the similarity between the velocity of the paddle and the direction of the paddle’s normal vector. I call this the paddle’s Air Resistance. In the water, the final resistance is:
Resistance_Water = Resistance_Air * Percent of Paddle Submerged
So you can see that by using the dot product we can determine how similar the normalized paddle velocity is to the paddle normal vector. Air Resistance should be 1 when moving parallel with the paddle normal (green axis) and 0 if moving parallel with the blue or red axis.
My first thought at calculating force was that I was going start with F=m*a and work my way from paddle acceleration to my final force. In the end I just took a huge shortcut.
I started by defining 2 ranges:
- Velocity Range [0, Maximum allowed Paddle Velocity]
- Force Range [0, Maximum allowed Paddle Force]
I then clamped Paddle Velocity EMA to my Velocity Range above and calculated the final force as:
Force = (Map Paddle EMA onto Force Range) * Water Resistance
So this is just mapping paddle velocity to a range of forces and then damping by the amount of water resistance on the paddle. Now print those values out, map my VR touchpads to adjust the values up and down and play around in VR until I get good feeling numbers.
The locomotion is performed by getting the current location of the paddle and applying the Force calculated above at the paddle position onto the vehicle.
I did a similar exercise with locomotion where I setup Vehicle Linear and Angular physics damping parameters and tuned those until it felt like the same water drag experience that I had in real life.
It’s not often that you get your paddle-board stuck on a rock and then find yourself suddenly traveling at 10,000 mph over the surface of the water. So, I also found it helpful to create parameters for Maximum Linear/Angular Velocity. This way physics de-penetration events don’t ruin everything.
My final table of parameters was fairly modest
|Paddle Max Force||This will directly affect how hard the paddle can push a vehicle. Remember that forces are applied every frame that the paddle is in the water. It’s the accumulation of all these tiny forces that end up moving the vehicle.|
|Vehicle Linear Damping||This affects how quickly the velocity will get dampened back to zero. This is a very important quantity for water. A value of 0 would be an infinitely smooth surface where objects slide forever.|
|Vehicle Angular Damping||This affects how quickly the angular velocity gets dampened back to zero. Again very critical to make it feel like you’re on water.|
|Vehicle Max Linear Velocity||What it sounds like. This is probably a least significant tunable since having correct values for the above 3 will make it so this never gets hit. This is mostly here to prevent depenetration events from making the system unstable.|
|Vehicle Max Angular Velocity||Prevents the vehicle from spinning too quickly. This is again mostly tuned by angular damping value but this is here mostly to prevent physics depenetration events from making the system unstable.|
The simulation worked, was delivered on time and everyone was pleased. I thought it was a very faithful recreation of the real thing. The major limitation in VR was, of course, the lack of any actual resistance when paddling.
The final implementation put a paddle into each hand, so it felt more like skiing than paddle boarding. I was also sad to see my original parameters tuned to feel more arcade-like, but that’s the gig right? I really enjoyed working on this system.