How to make a realistic water splash effect

Ok so this is probably not the best tutorial and there are many things which can make it better. I am just using easy to make and acquire. Found out how to do this from a video about unity: Making Better Water Physics than Cyberpunk in 3 minutes - YouTube

Assets



Mist

(The mist can be made using a triangle and blur)

Getting Started

First you need the water (I’m just using the normal terrain water) and a part that fits around it. This is so we can create a hitbox

The hitbox needs to have anchored on and can collide off and transparency 0

Water:

Hitbox:

You also need to add a part into the hitbox

with can collide off and transparency 0 and anchored on

image

Adding Images

So on the part in the hitbox add in the on water image to a decal. This adds in the ripple effect and foam:

image

Add in 2 particle emitters into your foam part one will be mist the other will be the Splash

These are the settings that work for me:

Mist:

Splash:

You can put the particle emitters into an attachment which is what I usually do.

So now your water hitbox should look like this:

image

The coding bit

Ok so now in the hitbox add in a script.

In that script you can add in this code:

local Ts = game:GetService('TweenService')
local T1 = Ts:Create(script.Parent.[PartName],TweenInfo.new(0.5,Enum.EasingStyle.Linear),{Size=Vector3.new(10, 1, 9.167)})
local T2 = Ts:Create(script.Parent.[PartName].Decal,TweenInfo.new(0.5,Enum.EasingStyle.Linear),{Transparency=1})

--Create The tweens

local PartsIN = {} --Add in the table for the parts inside

local function RunWater()
	script.Parent.[PartName].Attachment.Splash.Enabled = true
	script.Parent.[PartName].Attachment.Mist.Enabled = true


	task.wait(0.5)
	script.Parent.[PartName].Attachment.Splash.Enabled = false
	script.Parent.[PartName].Attachment.Mist.Enabled = false
	
end

--Create the Particle Emitter function

local function RunEffect(Pos)
	script.Parent.[PartName].Size = Vector3.new(6, 1, 5.5)
	
	
	script.Parent.[PartName].Position = Vector3.new(Pos.X,script.Parent.[PartName].Position.Y,Pos.Z)
	script.Parent.[PartName].Decal.Transparency = 0
	coroutine.wrap(RunWater)()
	T1:Play()
	T2:Play()
	
end

-- Make the main function for the touched event

script.Parent.Touched:Connect(function(part) --Detect when the hitbox is touched
	
	if table.find(PartsIN,part) then
		return
	end --Check if the part is already in the area
	
	if part:FindFirstAncestorWhichIsA("Model") then
		for i,v in pairs(part:FindFirstAncestorWhichIsA("Model"):GetChildren()) do
			table.insert(PartsIN,v)
		end
	end --Add all the parts into the table
	

	
	if part.Name~= "Baseplate" and part.Name ~= "Splash" then --Check if it is a part and not the baseplate or splash effect
		if part:IsA('BasePart') then
			if part.AssemblyLinearVelocity.Y < -15 then --Check if has a speed
				RunEffect(part.Position) --Plays the effect (bit of a delay when doing it though)
			end
		end
	end
end)

script.Parent.TouchEnded:Connect(function(part)
	local Check = table.find(PartsIN,part)
	if Check then
		table.remove(PartsIN,Check)
	end
end) --Removes the parts from the table after they left the hitbox

You can probably optimize the code but this works best for me

And there you go it should make a cool little splash effect

File.rbxl (40.0 KB)

Thanks for reading.

17 Likes

Cool Tutorial,
But I would Appreciate if you actually explained what the code does for people who dont know, or dont understand, its the least you could do when trying to make a Tutorial about something like this.

4 Likes

Where it should be parented? If i put in the wrong spot, then its messed up.

2 Likes

Ye sorry about that I rushed it a bit. Will add some explanation soon

3 Likes

Where should what be parented?

3 Likes

I notice that you linked the Unity Video, how would I convert your Lua code into C# code

2 Likes

Because it barley uses any c# code which is mostly tweens and the concepts are the same

2 Likes

I found out that the video is in Godot, not Unity

3 Likes

I took your code and reimplemented it with a more versatile system that is completely optimized with parallel luau

This is the model
WaterSplash - Creator Store (roblox.com)

This is the code I created to implement your function

--!native
local splashing=false
local Character=game.Players.LocalPlayer.Character
local Hum=Character.Humanoid

local Root = Character:WaitForChild("HumanoidRootPart")
local rate=.0333
local writetime=os.time()
local refreshrate=20
local params = RaycastParams.new()
task.synchronize()
params.FilterDescendantsInstances = {workspace.Terrain}--{Character, workspace:FindFirstChild("Enemys"), workspace:FindFirstChild("NPCS"), workspace:FindFirstChild("GroundItems"), workspace:FindFirstChild("Houses")}
params.FilterType=Enum.RaycastFilterType.Include

local lastpos=nil

local function performRaycast(origin, direction, params)
	return workspace:Raycast(origin, direction, params)
end

local function setupRaycastParams()
	local timer=os.time()
	if timer>writetime+refreshrate then
	writetime=timer
	params = RaycastParams.new()
	task.synchronize()
	params.FilterDescendantsInstances = {workspace.Terrain}--{Character, workspace:FindFirstChild("Enemys"), workspace:FindFirstChild("NPCS"), workspace:FindFirstChild("GroundItems"), workspace:FindFirstChild("Houses")}
	params.FilterType=Enum.RaycastFilterType.Include
	task.desynchronize()
	end
	return params
end

local function isWaterInVoxel(position)
	-- Define the region around the position
	local region = Region3.new(position, position + Vector3.new(4, 4, 4)):ExpandToGrid(4)

	-- Read the voxels in the defined region
	local materials, _ = workspace.Terrain:ReadVoxels(region, 4)

	-- Get the material at the specific position
	local x, y, z = math.floor(position.X / 4) + 1, math.floor(position.Y / 4) + 1, math.floor(position.Z / 4) + 1
	local material = materials[x][y][z]

	-- Check if the material is water
	return material == Enum.Material.Water
end

local Splash=script:WaitForChild("SplashObject").Value

local Ts = game:GetService('TweenService')
local T1 = Ts:Create(Splash,TweenInfo.new(0.5,Enum.EasingStyle.Linear),{Size=Vector3.new(10, 1, 9.167)})
local T2 = Ts:Create(Splash.Decal,TweenInfo.new(0.5,Enum.EasingStyle.Linear),{Transparency=1})

local PartsIN = {}

local function RunWater()
	Splash.Attachment.Splash.Enabled = true
	Splash.Attachment.Mist.Enabled = true
	task.wait(0.5)
	Splash.Attachment.Splash.Enabled = false
	Splash.Attachment.Mist.Enabled = false

end

local function RunEffect(Cframe)
	Splash.Size = Vector3.new(6, 1, 5.5)

	Splash.CFrame= Cframe--Vector3.new(Pos.X,Splash.Position.Y,Pos.Z)
	Splash.Decal.Transparency = 0
	coroutine.wrap(RunWater)()
	T1:Play()
	T2:Play()

end

local function EffectTracker(once)
	local params = setupRaycastParams()
	local Root=Character.HumanoidRootPart
	local Roothalf= -(Root.Size.Y / 2)
	local lengthvector=-(Character.LeftUpperLeg.Size.Y+Character.LeftLowerLeg.Size.Y+Character.LeftFoot.Size.Y)-3
	repeat	
		print("Detecting Water")

		local Rootpos=Root.Position
		if Rootpos~=lastpos then
			lastpos=Rootpos
			task.synchronize()
			local result = performRaycast(Rootpos+Vector3.new(0,Roothalf,0), Root.CFrame.LookVector * lengthvector, params)
			task.desynchronize()
			--local otherResult = performRaycast(Root.Position, Root.CFrame.LookVector * 2, params)
			local Yoffset=Rootpos.Y + Roothalf
			--print(result)
			if result then--and not otherResult then
				local hitPart = result.Instance
				--print(hitPart)

				if (hitPart.Name=="Terrain") then--or ((Vector3.new(0, Yoffset, 0) - Vector3.new(0, hitPart.Position.Y + hitPart.Size.Y / 2, 0)).magnitude < 2)) then
					local construct=CFrame.new(result.Position-(result.Normal))		
					task.synchronize()
					if isWaterInVoxel(construct.Position) then
						RunEffect(CFrame.new(Character.LeftFoot.Position.X,construct.Position.Y,Character.LeftFoot.Position.Z))
					task.desynchronize()
					task.wait(rate)
					return 	
					elseif once then
					return 
					end
					--return
				end
			end
		else task.wait(rate)	
		end
		task.wait(rate)
	until (splashing==false)-- or Bars.Flying.Value
end

Hum.FreeFalling:ConnectParallel(function()
	splashing=true
	EffectTracker()
end)


Hum.Running:ConnectParallel(function()
	task.wait(.1)
	splashing=false
end)

Thanks again for the resource! This was a huge inspiration and help! I am planning next to create water trails and air bubbles for when you are swimming in addition to a splashing effect.

The idea of this model is that when you are in a freefalling state, you may splash into the water, so what it does is check for water a few studs beneath the character for water and applies the water effect on the surface of the terrain using raycast() result.Position+ the normal.

3 Likes