Predicting Rolling Physics of a Ball

Hi I’m trying to figure out how to predict the motion of a regular ball part. I’ve gotten the prediction for the bouncing part of the ball (thanks to this post: Modeling a projectile's motion) but I’m not really sure how to predict the rolling motion of the ball.

This is what I have for my code so far:

local BallSimulation = {}
BallSimulation.__index = BallSimulation

local function reflect(velocity, normal)
	return -2 * velocity:Dot(normal) * normal + velocity
end

local function angularToLinear(w0, r)
	return w0:Cross(Vector3.new(0, r, 0))
end

local function linearToAngular(v0, r)
	return Vector3.new(0, r, 0):Cross(v0) / (r * r)
end

function BallSimulation.new(radius, gravity, raycastParams)
	local self = setmetatable({}, BallSimulation)

	self.gravity = gravity
	self.radius = radius
	self.timeStep = 1/240
	self.maxBounceInterval = 10
	self.raycastParams = raycastParams
	self.material = Enum.Material.Plastic
	
	return self
end

-- Rolling
function BallSimulation:getAcceleration(p0)
	local result = workspace:Raycast(p0, p0 - Vector3.new(0, self.radius, 0), self.raycastParams)
	if result then
		local normal = result.Normal
		local gravityComponent = self.gravity:Dot(normal)
		return self.gravity - normal * gravityComponent
	end
	return self.gravity
end

function BallSimulation:nextVelocity(v0, a)
	return a * self.timeStep + v0
end

function BallSimulation:nextPosition(p0, v0, a)
	return 0.5 * a * self.timeStep * self.timeStep + v0 * self.timeStep + p0
end

function BallSimulation:nextStep(p0, v0)
	local a = self:getAcceleration(p0)
	local p1 = self:nextPosition(p0, v0, a)
	local v1 = self:nextVelocity(v0, a)
	local direction = p1 - p0
	local result = workspace:Raycast(p0, direction + direction.Unit * self.radius, self.raycastParams)
	if result then
		local nextPosition = result.Position + result.Normal * self.radius
		
		local pA = PhysicalProperties.new(self.material) 
		local pB = PhysicalProperties.new(result.Material)
		local normal = result.Normal
		
		local elast = (pA.Elasticity*pA.ElasticityWeight + pB.Elasticity*pB.ElasticityWeight)/(pA.ElasticityWeight+pB.ElasticityWeight)
		local frict = (pA.Friction*pA.FrictionWeight + pB.Friction*pB.FrictionWeight)/(pA.FrictionWeight+pB.FrictionWeight)
		local dot = 1 - math.abs(v0.Unit:Dot(normal))
		
		local nextVelocity = reflect(v1, normal) * elast + v0 * frict * dot
		return nextPosition, nextVelocity
	end
	return p1, v1
end

-- Simulate
function BallSimulation:Simulate(p0, v0)
	local pA = PhysicalProperties.new(self.material) 
	local path = {}
	
	for i = 1, 1000 do
		local p1, v1 = self:nextStep(p0, v0)
		table.insert(path, {p0, v0})
		p0, v0 = p1, v1
	end
	
	for i = 1, 1000, 10 do
		local ball = Instance.new("Part")
		ball.Shape = Enum.PartType.Ball
		ball.Size = Vector3.one * 4
		ball.Transparency = 0.9
		ball.Color = Color3.fromRGB(255, 0, 0)
		ball.Material = Enum.Material.Neon
		ball.CanCollide = false
		ball.Anchored = true
		ball.Position = path[i][1]
		ball.Parent = workspace.Balls
	end

	return path
end

return BallSimulation

image
As you can see above, when the ball should be rolling, what happens instead is the ball just keeps bouncing up and down and losing speed due to the elasticity coefficient.

I appreciate any help!

3 Likes

My idea for where to go next is to somehow figure out the angular velocity generated every time the ball bounces with torque or something. Then on every frame where the ball is in contact with the floor add the distance traveled by the angular velocity.

1 Like

It seems to be that script is getting the balls actual physical propetries, try changing those using the properties?

That is intentional because I am getting the properties so I can calculate the friction/elasticity properly. Here is my revised code accounting for angular velocity but it doesn’t really help much.

local BallSimulation = {}
BallSimulation.__index = BallSimulation

local function reflect(velocity, normal)
	return -2 * velocity:Dot(normal) * normal + velocity
end

local function angularToLinear(w0, r)
	return w0:Cross(Vector3.new(0, r, 0))
end

local function linearToAngular(v0, r)
	return Vector3.new(0, r, 0):Cross(v0) / (r * r)
end

function BallSimulation.new(radius, gravity, raycastParams)
	local self = setmetatable({}, BallSimulation)

	self.gravity = gravity
	self.radius = radius
	self.timeStep = 1/240
	self.maxBounceInterval = 10
	self.raycastParams = raycastParams
	self.material = Enum.Material.Plastic
	
	return self
end

-- Linear
function BallSimulation:getAccelerationL(p0)
	local result = workspace:Raycast(p0, p0 - Vector3.new(0, self.radius, 0), self.raycastParams)
	if result then
		local normal = result.Normal
		local gravityComponent = self.gravity:Dot(normal)
		return self.gravity - normal * gravityComponent
	end
	return self.gravity
end

function BallSimulation:nextVelocityL(v0, a)
	return a * self.timeStep + v0
end

function BallSimulation:nextPositionL(p0, v0, a)
	return 0.5 * a * self.timeStep * self.timeStep + v0 * self.timeStep + p0
end

-- Angular
function BallSimulation:getAccelerationA(a, n)
	return (self.radius * n):Cross(a) / (self.radius * self.radius) * 5/2
end

function BallSimulation:nextPositionA(p0, w0, alpha)
	return self:nextPositionL(p0, w0 * self.radius, alpha * self.radius)
end

function BallSimulation:nextStep(p0, v0, w0)
	local a = self:getAccelerationL(p0)
	local p1 = self:nextPositionL(p0, v0, a)
	local v1 = self:nextVelocityL(v0, a)
	local direction = p1 - p0
	local result = workspace:Raycast(p0, direction + direction.Unit * self.radius, self.raycastParams)
	if result then		
		local pA = PhysicalProperties.new(self.material) 
		local pB = PhysicalProperties.new(result.Material)
		local normal = result.Normal
		
		local elast = (pA.Elasticity*pA.ElasticityWeight + pB.Elasticity*pB.ElasticityWeight)/(pA.ElasticityWeight+pB.ElasticityWeight)
		local frict = (pA.Friction*pA.FrictionWeight + pB.Friction*pB.FrictionWeight)/(pA.FrictionWeight+pB.FrictionWeight)
		local dot = 1 - math.abs(v0.Unit:Dot(normal))
		
		local nextVelocity = reflect(v1, normal) * elast + v0 * frict * dot
		
		local angularAcceleration = self:getAccelerationA(a, normal)
		local nextAngularVelocity = w0 + angularAcceleration * self.timeStep
		local nextPosition = self:nextPositionA(result.Position + result.Normal * self.radius, nextAngularVelocity, angularAcceleration)

		return nextPosition, nextVelocity, nextAngularVelocity
	end
	return p1, v1, w0
end

-- Simulate
function BallSimulation:Simulate(p0, v0, w0)
	local pA = PhysicalProperties.new(self.material) 
	local path = {}
	
	for i = 1, 1000 do
		local p1, v1, w1 = self:nextStep(p0, v0, w0)
		table.insert(path, {p0, v0})
		p0, v0, w0 = p1, v1, w1
	end
	
	for i = 1, 1000, 10 do
		local ball = Instance.new("Part")
		ball.Shape = Enum.PartType.Ball
		ball.Size = Vector3.one * 4
		ball.Transparency = 0.9
		ball.Color = Color3.fromRGB(255, 0, 0)
		ball.Material = Enum.Material.Neon
		ball.CanCollide = false
		ball.Anchored = true
		ball.Position = path[i][1]
		ball.Parent = workspace.Balls
	end

	return path
end

return BallSimulation

This is my calculation for how I derived the angular acceleration
Moment of inertia for solid sphere:

I = 2/5 * M * R^2

Equivalency:

R cross m * a = ⅖ * m * R^2 * alpha

Simplify:

R cross a = ⅖ * R^2 * alpha

Solve for alpha:

alpha = (R cross a)/(R*R) * 5/2

Ok I ended up fixing it by using the angular to linear function to convert the angular velocity and angular acceleration but the angular velocity increases by so little that its very inaccurate:

local BallSimulation = {}
BallSimulation.__index = BallSimulation

local function reflect(velocity, normal)
	return -2 * velocity:Dot(normal) * normal + velocity
end

local function angularToLinear(w0, r)
	return w0:Cross(Vector3.new(0, r, 0))
end

local function linearToAngular(v0, r)
	return Vector3.new(0, r, 0):Cross(v0) / (r * r)
end

function BallSimulation.new(radius, gravity, raycastParams)
	local self = setmetatable({}, BallSimulation)

	self.gravity = gravity
	self.radius = radius
	self.timeStep = 1/60
	self.maxBounceInterval = 10
	self.raycastParams = raycastParams
	self.material = Enum.Material.Plastic
	
	return self
end

-- Linear
function BallSimulation:getAccelerationL(p0)
	local result = workspace:Raycast(p0, p0 - Vector3.new(0, self.radius, 0), self.raycastParams)
	if result then
		local normal = result.Normal
		local gravityComponent = self.gravity:Dot(normal)
		return self.gravity - normal * gravityComponent
	end
	return self.gravity
end

function BallSimulation:nextVelocityL(v0, a)
	return a * self.timeStep + v0
end

function BallSimulation:nextPositionL(p0, v0, a)
	return 0.5 * a * self.timeStep * self.timeStep + v0 * self.timeStep + p0
end

-- Angular
function BallSimulation:getAccelerationA(a, n)
	return (self.radius * n):Cross(a) / (self.radius * self.radius) * 5/2
end

function BallSimulation:nextPositionA(p0, w0, alpha)
	return self:nextPositionL(p0, angularToLinear(w0, self.radius), angularToLinear(alpha, self.radius))
end

function BallSimulation:nextStep(p0, v0, w0)
	local a = self:getAccelerationL(p0)
	local p1 = self:nextPositionL(p0, v0, a)
	local v1 = self:nextVelocityL(v0, a)
	local direction = p1 - p0
	local result = workspace:Raycast(p0, direction + direction.Unit * self.radius, self.raycastParams)
	if result then		
		local pA = PhysicalProperties.new(self.material) 
		local pB = PhysicalProperties.new(result.Material)
		local normal = result.Normal
		
		local elast = (pA.Elasticity*pA.ElasticityWeight + pB.Elasticity*pB.ElasticityWeight)/(pA.ElasticityWeight+pB.ElasticityWeight)
		local frict = (pA.Friction*pA.FrictionWeight + pB.Friction*pB.FrictionWeight)/(pA.FrictionWeight+pB.FrictionWeight)
		local dot = 1 - math.abs(v0.Unit:Dot(normal))
		
		local nextVelocity = reflect(v1, normal) * elast + v0 * frict * dot
		
		local angularAcceleration = self:getAccelerationA(a, normal)
		local nextAngularVelocity = w0 + angularAcceleration * self.timeStep
		print(angularAcceleration, nextAngularVelocity)
		local nextPosition = self:nextPositionA(result.Position + result.Normal * self.radius, nextAngularVelocity, angularAcceleration)

		return nextPosition, nextVelocity, nextAngularVelocity
	end
	return p1, v1, w0
end

-- Simulate
function BallSimulation:Simulate(p0, v0, w0)
	local pA = PhysicalProperties.new(self.material) 
	local path = {}
	
	for i = 1, 1000 do
		local p1, v1, w1 = self:nextStep(p0, v0, w0)
		table.insert(path, {p0, v0})
		p0, v0, w0 = p1, v1, w1
	end
	
	for i = 1, 1000, 10 do
		local ball = Instance.new("Part")
		ball.Shape = Enum.PartType.Ball
		ball.Size = Vector3.one * 4
		ball.Transparency = 0.9
		ball.Color = Color3.fromRGB(255, 0, 0)
		ball.Material = Enum.Material.Neon
		ball.CanCollide = false
		ball.Anchored = true
		ball.Position = path[i][1]
		ball.Parent = workspace.Balls
	end

	return path
end

return BallSimulation

image

Oh wait I forgot to also update the position if the ball is grounded but it’s not a bounce I’ll do that as well

Somehow I fixed it with chatgpt since I realized it doesn’t make sense for angularvelocity to be calculated with a timestep since the bounce happens at one instant. Conservation of angular momentum had to be used instead.

Here is the final code. If anyone can explain why this works though that would be greatly appreciated as I’m still kind of confused.

local BallSimulation = {}
BallSimulation.__index = BallSimulation

local function reflect(velocity, normal)
	return -2 * velocity:Dot(normal) * normal + velocity
end

local function angularToLinear(w0, r)
	return w0:Cross(Vector3.new(0, r, 0))
end

local function linearToAngular(v0, r)
	return Vector3.new(0, r, 0):Cross(v0) / (r * r)
end

function BallSimulation.new(radius, gravity, raycastParams)
	local self = setmetatable({}, BallSimulation)

	self.gravity = gravity
	self.radius = radius
	self.timeStep = 1/240
	self.maxBounceInterval = 10
	self.raycastParams = raycastParams
	self.material = Enum.Material.Plastic
	
	return self
end

-- Linear
function BallSimulation:getAccelerationL(p0)
	local result = workspace:Raycast(p0, p0 - Vector3.new(0, self.radius, 0), self.raycastParams)
	if result then
		local normal = result.Normal
		local gravityComponent = self.gravity:Dot(normal)
		return self.gravity - normal * gravityComponent, true
	end
	return self.gravity, false
end

function BallSimulation:nextVelocityL(v0, a)
	return a * self.timeStep + v0
end

function BallSimulation:nextPositionL(p0, v0, a)
	return 0.5 * a * self.timeStep * self.timeStep + v0 * self.timeStep + p0
end

-- Angular
function BallSimulation:getAccelerationA(a, n)
	return (self.radius * n):Cross(a) / (self.radius * self.radius) * 5/2
end

function BallSimulation:nextPositionA(p0, w0)
	return self:nextPositionL(p0, angularToLinear(w0, self.radius), Vector3.zero)
end

function BallSimulation:nextStep(p0, v0, w0)
	local a, isGrounded = self:getAccelerationL(p0)
	local p1 = self:nextPositionL(p0, v0, a)
	local v1 = self:nextVelocityL(v0, a)
	local direction = p1 - p0
	local result = workspace:Raycast(p0, direction + direction.Unit * self.radius, self.raycastParams)
	if result then		
		local pA = PhysicalProperties.new(self.material) 
		local pB = PhysicalProperties.new(result.Material)
		local normal = result.Normal
		
		local elast = (pA.Elasticity*pA.ElasticityWeight + pB.Elasticity*pB.ElasticityWeight)/(pA.ElasticityWeight+pB.ElasticityWeight)
		local frict = (pA.Friction*pA.FrictionWeight + pB.Friction*pB.FrictionWeight)/(pA.FrictionWeight+pB.FrictionWeight)
		local dot = 1 - math.abs(v0.Unit:Dot(normal))
		
		local nextVelocity = reflect(v1, normal) * elast + v0 * frict * dot
		
		local deltaAngularVelocity = -v0:Cross(normal) * self.radius * elast
		local nextAngularVelocity = w0 + deltaAngularVelocity
		local nextPosition = self:nextPositionA(result.Position + result.Normal * self.radius, nextAngularVelocity)
		
		return nextPosition, nextVelocity, nextAngularVelocity
	end
	if isGrounded then
		local nextPosition = self:nextPositionA(p1, w0)
		return nextPosition, v1, w0
	end
	return p1, v1, w0
end

-- Simulate
function BallSimulation:Simulate(p0, v0, w0)
	local pA = PhysicalProperties.new(self.material) 
	local path = {}
	
	for i = 1, 1000 do
		local p1, v1, w1 = self:nextStep(p0, v0, w0)
		table.insert(path, {p0, v0})
		p0, v0, w0 = p1, v1, w1
	end
	
	for i = 1, 1000, 10 do
		local ball = Instance.new("Part")
		ball.Shape = Enum.PartType.Ball
		ball.Size = Vector3.one * 4
		ball.Transparency = 0.9
		ball.Color = Color3.fromRGB(255, 0, 0)
		ball.Material = Enum.Material.Neon
		ball.CanCollide = false
		ball.Anchored = true
		ball.Position = path[i][1]
		ball.Parent = workspace.Balls
	end

	return path
end

return BallSimulation

Nevermind its way faster than what it should be

Pretty sure this is the correct formula but it is way too slow still

local BallSimulation = {}
BallSimulation.__index = BallSimulation

local function reflect(velocity, normal)
	return -2 * velocity:Dot(normal) * normal + velocity
end

local function angularToLinear(w0, r)
	return w0:Cross(Vector3.new(0, r, 0))
end

local function linearToAngular(v0, r)
	return Vector3.new(0, r, 0):Cross(v0) / (r * r)
end

function BallSimulation.new(radius, gravity, raycastParams)
	local self = setmetatable({}, BallSimulation)

	self.gravity = gravity
	self.radius = radius
	self.timeStep = 1/240
	self.maxBounceInterval = 10
	self.raycastParams = raycastParams
	self.material = Enum.Material.Plastic
	
	return self
end

-- Linear
function BallSimulation:getAccelerationL(p0)
	local result = workspace:Raycast(p0, p0 - Vector3.new(0, self.radius, 0), self.raycastParams)
	if result then
		local normal = result.Normal
		local gravityComponent = self.gravity:Dot(normal)
		return self.gravity - normal * gravityComponent, true
	end
	return self.gravity, false
end

function BallSimulation:nextVelocityL(v0, a)
	return a * self.timeStep + v0
end

function BallSimulation:nextPositionL(p0, v0, a)
	return 0.5 * a * self.timeStep * self.timeStep + v0 * self.timeStep + p0
end

-- Angular
function BallSimulation:getAccelerationA(a, n)
	return (self.radius * n):Cross(a) / (self.radius * self.radius) * 5/2
end

function BallSimulation:nextPositionA(p0, w0)
	return self:nextPositionL(p0, angularToLinear(w0, self.radius), Vector3.zero)
end

function BallSimulation:nextStep(p0, v0, w0)
	local a, isGrounded = self:getAccelerationL(p0)
	local p1 = self:nextPositionL(p0, v0, a)
	local v1 = self:nextVelocityL(v0, a)
	local direction = p1 - p0
	local result = workspace:Raycast(p0, direction + direction.Unit * self.radius, self.raycastParams)
	if result then		
		local pA = PhysicalProperties.new(self.material) 
		local pB = PhysicalProperties.new(result.Material)
		local normal = result.Normal
		
		local elast = (pA.Elasticity*pA.ElasticityWeight + pB.Elasticity*pB.ElasticityWeight)/(pA.ElasticityWeight+pB.ElasticityWeight)
		local frict = (pA.Friction*pA.FrictionWeight + pB.Friction*pB.FrictionWeight)/(pA.FrictionWeight+pB.FrictionWeight)
		local dot = 1 - math.abs(v0.Unit:Dot(normal))
		
		local nextVelocity = reflect(v1, normal) * elast + v0 * frict * dot
		
		local deltaAngularVelocity = linearToAngular((v0 - nextVelocity), self.radius) / (2/5 * self.radius * self.radius)
		local nextAngularVelocity = w0 + deltaAngularVelocity
		print("delta", deltaAngularVelocity)
		print(nextAngularVelocity)
		local nextPosition = self:nextPositionA(result.Position + result.Normal * self.radius, nextAngularVelocity)
		
		return nextPosition, nextVelocity, nextAngularVelocity
	end
	if isGrounded then
		local nextPosition = self:nextPositionA(p1, w0)
		return nextPosition, v1, w0
	end
	return p1, v1, w0
end

-- Simulate
function BallSimulation:Simulate(p0, v0, w0)
	local pA = PhysicalProperties.new(self.material) 
	local path = {}
	
	for i = 1, 1000 do
		local p1, v1, w1 = self:nextStep(p0, v0, w0)
		table.insert(path, {p0, v0})
		p0, v0, w0 = p1, v1, w1
	end
	
	return path
end

function BallSimulation:Visualize(path, elapsed)
	local index = math.floor(elapsed / self.timeStep)
	if index > 1000 then
		return
	end
	local ball = Instance.new("Part")
	ball.Shape = Enum.PartType.Ball
	ball.Size = Vector3.one * 4
	ball.Transparency = 0.9
	ball.Color = Color3.fromRGB(255, 0, 0)
	ball.Material = Enum.Material.Neon
	ball.CanCollide = false
	ball.Anchored = true
	ball.Position = path[index][1]
	ball.Parent = workspace.Balls
	game.Debris:AddItem(ball, 0.3)
end

return BallSimulation