Water droplet physics simulation

Hello guys! After a long time, I finally was able to finish (I believe) water droplet system, which will try to behave like real-one droplets! Droplets will try to react to physics, slide off surfaces, and merge together!

(On my PC, I was able to smoothly run 200 droplets)

File: WaterDropletSimulation.rbxl (105.3 KB)

And, code, so you can see it:

task.wait(5)

local RunService = game:GetService("RunService")

local DropletFolder = workspace.WaterDroplets

local Droplets = DropletFolder:GetChildren()

DropletFolder.ChildAdded:Connect(function(Droplet)
	table.insert(Droplets, Droplet)
end)

DropletFolder.ChildRemoved:Connect(function(Droplet)
	local Id = table.find(Droplets, Droplet)
	if Id then
		table.remove(Droplets, Id)
	end
end)

local SurfaceParams = RaycastParams.new()
SurfaceParams.FilterType = Enum.RaycastFilterType.Exclude

local DropMergeParams = OverlapParams.new()
DropMergeParams.FilterType = Enum.RaycastFilterType.Include

local Debounce = false
local CurDelay = 0
local IncDelay = 0

local Gravity = 9.8
local Friction = 0.99

RunService.Heartbeat:Connect(function(Delta)
	IncDelay += Delta
	if Debounce == false then
		Debounce = true
		CurDelay = IncDelay
		IncDelay = 0
		local i = 1
		local EndValue = #Droplets
		while i <= EndValue do
			local Droplet = Droplets[i]
			if not Droplet then continue end
			local StartVelocity = Droplet.AssemblyLinearVelocity
			SurfaceParams.FilterDescendantsInstances = Droplets
			local Normals = {}
			local NormalPos = {}
			local function TestVelocity(Start, Velocity)
				local VelocityGrav = Velocity - Vector3.new(0, Gravity * CurDelay, 0) 
				local Offset = VelocityGrav.Unit / 20
				local RayStart = -Offset + Start
				local RayDirection = VelocityGrav + Offset
				local Result = workspace:Raycast(RayStart, RayDirection, SurfaceParams)
				local RaycastDistanceTarget = (VelocityGrav.Magnitude + Offset.Magnitude) * CurDelay + 0.05
				local ResultSucceed = Result and Result.Distance <= RaycastDistanceTarget and 2 or Result and 1 or 0
				local Distance = math.huge
				if ResultSucceed == 2 then
					local found = false
					for i = 1, #Normals, 1 do
						if Normals[i]:FuzzyEq(Result.Normal, 0.001) then
							found = true
							break
						end
					end
					if not found then
						table.insert(Normals, Result.Normal)
						table.insert(NormalPos, Result.Position)
						local PreDistance
						if #Normals < 3 then
							local Normal = #Normals == 1 and Normals[1] or Normals[1]:Cross(Normals[2])
							local NextVelocity = #Normals == 1 and (Velocity - Velocity:Dot(Normal) * Normal) or (Velocity:Dot(Normal) * Normal)
							PreDistance = TestVelocity(Start, NextVelocity)
						elseif #Normals > 2 then
							return 0
						end
						Distance = math.min(Result.Distance - Offset.Magnitude, RaycastDistanceTarget - 0.05)
						Distance = PreDistance ~= math.huge and PreDistance or Distance
					end
				elseif ResultSucceed == 1 then
					Distance = Result.Distance - Offset.Magnitude
				end
				return Distance
			end

			local Distance = TestVelocity(Droplet.Position, StartVelocity)
			local Velocity = StartVelocity - Vector3.new(0, Gravity * CurDelay, 0)
			local PossibleMovement = Velocity * CurDelay
			local Normal
			if #Normals > 0 and #Normals < 3 then
				Normal = #Normals == 1 and Normals[1] or Normals[1]:Cross(Normals[2]).Unit
				local NewVelocity = #Normals == 1 and (Velocity - Velocity:Dot(Normal) * Normal) or (Velocity:Dot(Normal) * Normal)
				local DistanceProportion = math.clamp(math.max(Distance, 0) / (Velocity.Magnitude * CurDelay), 0, 1)
				Velocity = (Velocity * DistanceProportion + NewVelocity * (1 - DistanceProportion)) * Friction
				PossibleMovement = Velocity * CurDelay
				local PositionShift = Droplet.Position + PossibleMovement
				local NormalsOffset = Normals[1] * math.clamp(-Normals[1]:Dot(PositionShift - NormalPos[1]), 0, 0.01) + (Normals[2] and (Normals[2] * math.clamp(-Normals[2]:Dot(PositionShift - NormalPos[2]), 0, 0.01)) or Vector3.zero)
				Velocity = NewVelocity * Friction^(CurDelay * 60)
				PossibleMovement = Velocity * CurDelay + NormalsOffset
			elseif #Normals > 2 then
				Velocity = Vector3.zero
				PossibleMovement = Vector3.zero
			end
			local DropletTargetPosition = Droplet.Position + PossibleMovement
			
			if Velocity.Magnitude >= 0.25 * CurDelay then
				DropMergeParams.FilterDescendantsInstances = Droplets

				local DropletVolume = Droplet.Size.X * Droplet.Size.Y * Droplet.Size.Z
				
				local NearbyDroplets = workspace:GetPartsInPart(Droplet, DropMergeParams)
				if NearbyDroplets then
					for a = 1, #NearbyDroplets, 1 do
						local NewDroplet = NearbyDroplets[a]
						DropletTargetPosition += NewDroplet.Position
						DropletVolume += NewDroplet.Size.X * NewDroplet.Size.Y * NewDroplet.Size.Z
						Velocity += NewDroplet.AssemblyLinearVelocity
						local DropIndex = table.find(Droplets, NewDroplet)
						table.remove(Droplets, DropIndex)
						NewDroplet:Destroy()
						EndValue -= 1
						i = i - DropIndex > i and 0 or 1
					end
					DropletTargetPosition /= #NearbyDroplets + 1
					Velocity /= #NearbyDroplets + 1
				end
				local DropletLength = (Droplet.Size.Z + DropletVolume^(1/3) * (math.min(Velocity.Magnitude, Distance or math.huge) + 1)^0.5) / 2
				local DropletWidth = (DropletVolume / DropletLength)^0.5
				
				local UpVector = Distance and Normal or Vector3.yAxis
				local LookVector = not (Velocity:FuzzyEq(Vector3.zero, 0.00001) or UpVector:FuzzyEq(Velocity.Unit, 0.00001)) and Velocity.Unit or not (UpVector:FuzzyEq(Vector3.zAxis, 0.00001) or UpVector:FuzzyEq(-Vector3.zAxis, 0.00001)) and Vector3.zAxis or Vector3.xAxis
				local RightVector = not (UpVector:FuzzyEq(LookVector, 0.00001) or UpVector:FuzzyEq(-LookVector, 0.00001)) and (-LookVector):Cross(UpVector).Unit or Vector3.xAxis
				UpVector = RightVector:Cross(LookVector).Unit

				Droplet.AssemblyLinearVelocity = Velocity
				Droplet.CFrame = CFrame.fromMatrix(DropletTargetPosition, UpVector, RightVector, LookVector)
				Droplet.Size = Vector3.new(DropletWidth, DropletWidth, DropletLength)
			elseif StartVelocity.Magnitude >= 0.25 * CurDelay then
				local DropletVolume = Droplet.Size.X * Droplet.Size.Y * Droplet.Size.Z
				local DropletLength = (Droplet.Size.Z + DropletVolume^(1/3) * (math.min(Velocity.Magnitude, Distance or math.huge) + 1)^0.5) / 2
				local DropletWidth = (DropletVolume / DropletLength)^0.5
				Droplet.Size = Vector3.new(DropletWidth, DropletWidth, DropletLength)
				Droplet.AssemblyLinearVelocity = Vector3.zero
				Droplet.Position = DropletTargetPosition
			end
			i += 1
		end
		Debounce = false
	end
end)

Inside studio file, you can also see droplet generator script, under ServerScriptService.

I tried to optimize it as much as I knew, so, feel free to suggest any improvements. And, if you find any bugs or glitches, send them to me, and I’ll try to fix them!

(@msix29, take a look if you want!)

NO VIDEO, BC MY POTATO PC WON'T HANDLE IT, SORRY

29 Likes

This is really cool! could these be used for a gore system? (with lots of zombies)

2 Likes

Probably yes. But if you need droplets to squish on ground, then this script won’t do this for now.

1 Like

How can I destroy the droplets?

2 Likes

Tbh I forgot about thing that you may need to destroy them, I’ll update code now :sweat_smile:
EDIT: now you can simply call :Destroy() function on Droplet part.

1 Like


For anyone who wants a video:

To OP: It looks decent, I won’t say amazing but just decent as its quite slow, and not realistic.

14 Likes

Hm… strange thing is that it slided on my pc normally trough all this surfaces. I’ll try to fix that now.
Edit 1: this’s caused by “sleep mode”, which tries to use less calculations if droplet speed is < 0.1. Trying to change some logic.

1 Like

Finally, a long journey came to an end… Well, actually, not quite, a lot of improvements can be made till it reaches the point of actual droplets, but for now, very well done :clap:.

3 Likes

I think I fixed droplet sleep problem.
About why they slow: I use constant gravity 9.8, and you can change that value in script. So, multiplication by 3 (29.4) will result in x3 simulation speed.

1 Like

Hi,
How about adding to it when the water ball is about to dissappear , it becomes flat like a water drop… then dissappears …
Thankd

1 Like

This looks amazing, Holy crap, I am impressed with this resource, honestly I might use this for something in the future.

2 Likes

Wow, amazing! Everything works great. It has some issues with unions though.

Can you tell me them, so I’ll be able to fix them?

So I tried to have a block with a sphere negated in it, to make a bowl, and the water sometimes clipped through the bowl section. And other times, it just jiggled quickly.

Can I ask you, is dropled clipped trough bowl, and then fell trough it? or it was visual clipping only, and not physical?

I saw it fall through, so physical.

1 Like

I think it would be more optimized if you would make your own system for collisions between Droplets. Here are two options for implementing it:

  1. For every particle, you could iterate over all particles and check distance between them. But this option is very bad for performance.
  2. Slightly harder option would be to implement Spatial Partitioning technique. Instead of iterating over all particles for every particle, you could store particles in a 3D Grid, and for every particle you could check for collisions between particles in nearby Grid Cells. You will see a big performance difference than in first option. I use this technique in my Module so i advice you to do the same.

hey, you should try doing this but using meshdeformation
like imagine a ball, throw bones into that, port it into roblox, make code, when a ball collides with eachother the bones position themselves to the other droplet and increase size, (if its a droplet), if its a big body of water like a small waterfall for example, itll slide down the river, and stay as a entity, but be connected to the water.

Wouldn’t that be bad for performance?

Hello.
(First one important message before reply)
Guys, I had read everything above, and should tell you that I will be unable to update it until new year due to the amount of work I need to do for highschool. Sorry.

(Reply below)
Won’t it be more time consuming to move droplets from 1 grid ceil to another one, check where droplet relatively to grid? I think that magnitude scheck is best here, BUT If you can tell me why and how grid can be better, I’ll probably can try to make it. (After bug fixes + new year)