Pool Ball Spin Physics

Although Unity has a wonderful set of physics solutions out of the box, it misses a key physics phenomenon that is absolutely essential to many sports: spin. Golf, soccer (football), bowling, billiards and many other sports rely on spins to create unexpected and exciting game plays. Given this circumstance, it would be almost tragic if such a feature is not implemented in virtual ball games. Compounding this sentiment is the general anxiety I face when Unity does not natively support a feature I need.

Fortunately, the solution to ball spin effect was derived rather easily and it takes only one line of code:

rb.AddForce(Vector3.Cross(rb.velocity, rb.angularVelocity)*0.01f);

It’s almost too good to be true, but the explanation is simple. When a ball is spinning clockwise or counter-clockwise relative to a flat ground surface, the angular velocity vector is directed either up or down (majority of the code below is for visualizing that). When we take a cross product between this vertical vector and the forward movement vector, the resulting vector when a ball has a side spin would be perpendicular to the ball’s trajectory. By adding a small force in that perpendicular direction, we could finally simulate spins. The constant at the end could be adjusted according to needs.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PoolBallSpin : MonoBehaviour
{
    Rigidbody rb;
    GameObject spinIndicator;
    GameObject EndIndicator;
    void Awake()
    {
        rb = GetComponent<Rigidbody>();

        // For visualization of angular velocity vector
        spinIndicator = GameObject.CreatePrimitive(PrimitiveType.Cube);
        spinIndicator.name = "SpinIndicator";
        spinIndicator.layer = LayerMask.NameToLayer("YourNonPhysicsLayer");
        spinIndicator.transform.localScale = new Vector3(0.01f,0.01f,0f);

        EndIndicator = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        EndIndicator.name = "EndIndicator";
        EndIndicator.layer = LayerMask.NameToLayer("YourNonPhysicsLayer");
        EndIndicator.transform.localScale = new Vector3(0.02f,0.02f,0.02f);
    }

    void Update()
    {
        // Scale angular velocity visualizer based on magnitude
        ScaledPosition(spinIndicator, transform.position, transform.position + rb.angularVelocity*0.25f);
        Quaternion rotation = Quaternion.LookRotation(rb.angularVelocity, Vector3.up);
        spinIndicator.transform.rotation = rotation;

        // "spin force" or curve force. A.K.A Magnus effect: https://en.wikipedia.org/wiki/Magnus_effect
        rb.AddForce(Vector3.Cross(rb.velocity, rb.angularVelocity)*0.01f);
    }

    // Assuming mesh spans -0.5 to 0.5 along z-axis at size 1f
    void ScaledPosition(GameObject go, Vector3 Start, Vector3 End)
    {
        float Length = (End - Start).magnitude;
        go.transform.localScale = new Vector3(go.transform.localScale.x, go.transform.localScale.y, Length);
        go.transform.position = (Start + End) / 2;
        EndIndicator.transform.position = End;
    }
}

Jay Oh – 01/25/2024

Leave a comment