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!)