Fluid Animation

Fluid Animation!

Video:

Functions;

SetAmplitude(Number)

SetAmplitude, determines the “WaveHeight”. Updates real-time.

MaximumColor3fromRGB(Table)

MaximumColor3fromRGB, determines the maximum Color3.fromRGB colour the water can shift to. Updates real-time.

SetMinimumColor3fromRGB(Table)

SetMinimumColor3fromRGB, determines the minimum Color3.fromRGB colour the water can shift to. Updates real-time.

SetParent(Instance)

SetParent, sets the parent of the parts. Does not Update real-time.

SetPartSize(Vector3)

SetPartSize, sets the size of the parts (PartSize.Y works best when it is set the the Amplitude value). Does not Update real-time.

SetSize(Vector3)

SetSize, sets the size of the “Plane” (expect lag with number values larger than 50). Does not Update real-time.

SetSpeed(Number)

SetSpeed, determines the “WaveSpeed” (works best with number values lower than .1). Updates real-time.

CreatePlane()

CreatePlane, this creates a “Plane” where the Fluid Animation will take place. Only needs to be called once, does not update real-time.

Start()

Start, starts the Fluid Animation. Updates real-time.

Pause()

Pause, pauses the Fluid Animation. Updates real-time.

Resume()

Resume, resumes the Fluid Animation. Updates real-time.

Stop()

Stop, stops the Fluid Animation. Updates real-time.

Reset()

Reset, reverses Fluid Animation Table back to its original state. Updates real-time.

Code;

Script:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ModuleScript = ReplicatedStorage.ModuleScript
local FluidAnimation = require(ModuleScript.FluidAnimation)

local Terrain = workspace.Terrain

local Folder = Instance.new("Folder")
Folder.Name = "Fluid Animation"
Folder.Parent = Terrain

local FluidAnimationnew = FluidAnimation.new()
FluidAnimationnew:SetAmplitude(6.25)
FluidAnimation:SetMaximumColor3fromRGB({
	Red = 0,
	Green = 0,
	Blue = 255
})
FluidAnimation:SetMinimumColor3fromRGB({
	Red = 0,
	Green = 204,
	Blue = 153
})
FluidAnimationnew:SetParent(Folder)
FluidAnimationnew:SetPartSize(Vector3.new(6.25, 6.25, 6.25))
FluidAnimationnew:SetSize(Vector3.new(50, 0, 50))
FluidAnimationnew:SetSpeed(.005)
FluidAnimationnew:CreatePlane()
FluidAnimationnew:Start()

ModuleScript:

local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")

local ModuleTable = {}
ModuleTable.__index = ModuleTable

ModuleTable.new = function()
	local FluidAnimation = {}
	FluidAnimation.Amplitude = 0
	FluidAnimation.MaximumColor3fromRGB = nil
	FluidAnimation.MinimumColor3fromRGB = nil
	FluidAnimation.Motion = 0
	FluidAnimation.OffsetXTable = nil
	FluidAnimation.OffsetZTable = nil
	FluidAnimation.Parent = nil
	FluidAnimation.PartOffsetX = 0
	FluidAnimation.PartOffsetZ = 0
	FluidAnimation.PartSize = nil
	FluidAnimation.PartTable = nil
	FluidAnimation.Paused = false
	FluidAnimation.Size = nil
	FluidAnimation.Speed = 0
	FluidAnimation.Started = false
	FluidAnimation.Stopped = false
	setmetatable(FluidAnimation, ModuleTable)
	return FluidAnimation
end

ModuleTable.SetAmplitude = function(FluidAnimation, Amplitude)
	FluidAnimation.Amplitude = Amplitude
	return
end

ModuleTable.SetMaximumColor3fromRGB = function(FluidAnimation, MaximumColor3fromRGB)
	FluidAnimation.MaximumColor3fromRGB = MaximumColor3fromRGB
	return
end

ModuleTable.SetMinimumColor3fromRGB = function(FluidAnimation, MinimumColor3fromRGB)
	FluidAnimation.MinimumColor3fromRGB = MinimumColor3fromRGB
	return
end

ModuleTable.SetMotion = function(FluidAnimation, Motion)
	FluidAnimation.Motion += Motion
	return
end

ModuleTable.SetOffsetXTable = function(FluidAnimation, OffsetXTable)
	FluidAnimation.OffsetXTable = OffsetXTable
	return
end

ModuleTable.SetOffsetZTable = function(FluidAnimation, OffsetZTable)
	FluidAnimation.OffsetZTable = OffsetZTable
	return
end

ModuleTable.SetParent = function(FluidAnimation, Parent)
	FluidAnimation.Parent = Parent
	return
end

ModuleTable.SetPartOffsetX = function(FluidAnimation, PartOffsetX)
	FluidAnimation.PartOffsetX = PartOffsetX
	return
end

ModuleTable.SetPartOffsetZ = function(FluidAnimation, PartOffsetZ)
	FluidAnimation.PartOffsetZ = PartOffsetZ
	return
end

ModuleTable.SetPartSize = function(FluidAnimation, PartSize)
	FluidAnimation.PartSize = PartSize
	return
end

ModuleTable.SetPartTable = function(FluidAnimation, PartTable)
	FluidAnimation.PartTable = PartTable
	return
end

ModuleTable.SetPaused = function(FluidAnimation, Paused)
	FluidAnimation.Paused = Paused
	return
end

ModuleTable.SetSize = function(FluidAnimation, Size)
	FluidAnimation.Size = Size
	return
end

ModuleTable.SetSpeed = function(FluidAnimation, Speed)
	FluidAnimation.Speed = Speed
	return
end

ModuleTable.SetStarted = function(FluidAnimation, Started)
	FluidAnimation.Started = Started
	return
end

ModuleTable.SetStopped = function(FluidAnimation, Stopped)
	FluidAnimation.Stopped = Stopped
	return
end

ModuleTable.CreatePlane = function(FluidAnimation)
	local Parent = FluidAnimation.Parent
	local PartSize = FluidAnimation.PartSize
	local PartTable = {}
	local Size = FluidAnimation.Size
	for Number = -Size.X / 2, Size.X / 2 do
		for Number2 = -Size.Z / 2, Size.Z / 2 do
			local Part = Instance.new("Part")
			Part.Name = HttpService:GenerateGUID(true)
			Part.CFrame = CFrame.new(Number * PartSize.X, 0, Number2 * PartSize.Z)
			Part.Anchored = true
			Part.Size = PartSize
			Part.Parent = Parent
			PartTable[Number .. "." .. Number2] = Part
		end
	end
	FluidAnimation:SetPartTable(PartTable)
	return
end

local Color3fromRGBLerp = function(Number, Minimum, Maximum)
	local Color3fromRGBTable = {}
	table.insert(Color3fromRGBTable, Maximum.Red + (Minimum.Red - Maximum.Red) * Number)
	table.insert(Color3fromRGBTable, Maximum.Green + (Minimum.Green - Maximum.Green) * Number)
	table.insert(Color3fromRGBTable, Maximum.Blue + (Minimum.Blue - Maximum.Blue) * Number)
	return Color3.fromRGB(Color3fromRGBTable[1], Color3fromRGBTable[2], Color3fromRGBTable[3])
end

local BoundNumber = function(Number, Minimum, Maximum)
	return (-Maximum + Number) / (-Maximum / Minimum)
end

ModuleTable.Start = function(FluidAnimation)
	local PartSize = FluidAnimation.PartSize
	local PartTable = FluidAnimation.PartTable
	local Size = FluidAnimation.Size
	local Started = FluidAnimation.Started
	local Stopped = FluidAnimation.Stopped
	if Started then
		return
	else
		FluidAnimation:SetStarted(true)
	end
	if Stopped then
		FluidAnimation:SetStopped(false)
	end
	RunService:BindToRenderStep("FluidAnimation", Enum.RenderPriority.First.Value, function()
		local Amplitude = FluidAnimation.Amplitude
		local MaximumColor3fromRGB = FluidAnimation.MaximumColor3fromRGB
		local MinimumColor3fromRGB = FluidAnimation.MinimumColor3fromRGB
		local Paused = FluidAnimation.Paused
		local Speed = FluidAnimation.Speed
		if Paused then
			return
		else
			FluidAnimation:SetMotion(Speed)
			FluidAnimation:SetOffsetXTable({
				OffsetX = FluidAnimation.Motion,
				OffsetX2 = -FluidAnimation.Motion,
				OffsetX3 = 0,
				OffsetX4 = 0,
				OffsetX5 = 0
			})
			for Number = -Size.X / 2, Size.X / 2 do
				FluidAnimation:SetOffsetZTable({
					OffsetZ = 0,
					OffsetZ2 = 0,
					OffsetZ3 = 0,
					OffsetZ4 = FluidAnimation.Motion,
					OffsetZ5 = -FluidAnimation.Motion
				})
				for Number2 = -Size.Z / 2, Size.Z / 2 do
					local Part = PartTable[Number .. "." .. Number2]
					if Part ~= nil then
						local MathNoise = Amplitude * math.noise(FluidAnimation.OffsetXTable.OffsetX, FluidAnimation.OffsetZTable.OffsetZ)
						local MathNoise2 = Amplitude * math.noise(FluidAnimation.OffsetXTable.OffsetX2, FluidAnimation.OffsetZTable.OffsetZ2)
						local MathNoise3 = Amplitude * math.noise(FluidAnimation.OffsetXTable.OffsetX3, FluidAnimation.OffsetZTable.OffsetZ3)
						local MathNoise4 = Amplitude * math.noise(FluidAnimation.OffsetXTable.OffsetX4, FluidAnimation.OffsetZTable.OffsetZ4)
						local MathNoise5 = Amplitude * math.noise(FluidAnimation.OffsetXTable.OffsetX5, FluidAnimation.OffsetZTable.OffsetZ5)
						local Height = MathNoise + MathNoise2 + MathNoise3 + MathNoise4 + MathNoise5
						Part.Color = Color3fromRGBLerp(BoundNumber(Part.CFrame.Position.Y / PartSize.Y / Amplitude, -5, 5), MinimumColor3fromRGB, MaximumColor3fromRGB)
						Part.CFrame = CFrame.new((FluidAnimation.PartOffsetX + Number) * PartSize.X, Amplitude + Height + PartSize.Y, (FluidAnimation.PartOffsetX + Number2) * PartSize.Z)
					end
					FluidAnimation:SetOffsetZTable({
						OffsetZ = FluidAnimation.OffsetZTable.OffsetZ + .01,
						OffsetZ2 = FluidAnimation.OffsetZTable.OffsetZ2 + .025,
						OffsetZ3 = FluidAnimation.OffsetZTable.OffsetZ3 + .050,
						OffsetZ4 = FluidAnimation.OffsetZTable.OffsetZ4 + .075,
						OffsetZ5 = FluidAnimation.OffsetZTable.OffsetZ5 + .1
					})
				end
				FluidAnimation:SetOffsetXTable({
					OffsetX = FluidAnimation.OffsetXTable.OffsetX + .01,
					OffsetX2 = FluidAnimation.OffsetXTable.OffsetX2 + .025,
					OffsetX3 = FluidAnimation.OffsetXTable.OffsetX3 + .050,
					OffsetX4 = FluidAnimation.OffsetXTable.OffsetX4 + .075,
					OffsetX5 = FluidAnimation.OffsetXTable.OffsetX5 + .1
				})
			end
		end
	end)
end

ModuleTable.Pause = function(FluidAnimation)
	local Paused = FluidAnimation.Paused
	if Paused then
		return
	else
		FluidAnimation:SetPaused(true)
	end
end

ModuleTable.Resume = function(FluidAnimation)
	local Paused = FluidAnimation.Paused
	if Paused then
		FluidAnimation:SetPaused(false)
	else
		return
	end
end

ModuleTable.Stop = function(FluidAnimation)
	local Started = FluidAnimation.Started
	local Stopped = FluidAnimation.Stopped
	if Started then
		FluidAnimation:SetStarted(false)
	else
		return
	end
	if Stopped then
		return
	else
		FluidAnimation:SetStopped(true)
	end
	RunService:UnbindFromRenderStep("FluidAnimation")
	FluidAnimation:SetMaximumColor3fromRGB(nil)
	FluidAnimation:SetMinimumColor3fromRGB(nil)
	FluidAnimation:SetMotion(0)
	FluidAnimation:SetOffsetXTable(nil)
	FluidAnimation:SetOffsetZTable(nil)
	return
end

ModuleTable.Reset = function(FluidAnimation)
	FluidAnimation:SetAmplitude(0)
	FluidAnimation:SetMaximumColor3fromRGB(nil)
	FluidAnimation:SetMinimumColor3fromRGB(nil)
	FluidAnimation:SetMotion(0)
	FluidAnimation:SetOffsetXTable(nil)
	FluidAnimation:SetOffsetZTable(nil)
	FluidAnimation:SetParent(nil)
	FluidAnimation:SetPartOffsetX(0)
	FluidAnimation:SetPartOffsetZ(0)
	FluidAnimation:SetPartSize(nil)
	FluidAnimation:SetPartTable(nil)
	FluidAnimation:SetPaused(false)
	FluidAnimation:SetSize(nil)
	FluidAnimation:SetSpeed(0)
	FluidAnimation:SetStarted(false)
	FluidAnimation:SetStopped(false)
	return
end

return ModuleTable

Place File

Fluid Animation.rbxl (25.0 KB)

Updates;

Color3.fromRGB Shifting

With some help from @CoderHusk I have been able to interpret and Color3.fromRGB instead of a Color3.new. Interpolation is based on the part height. Very Smooth.

Improved Lag:

By creating a whole new script and transforming it into a “ModuleScript”, I have exponentially decreased the lag. Although Size.X and Size.Z values over 50 still, regrettably, cause lag.

Remade and Re-modeled:

I have entirely re-modeled “Water Simulation” into “Fluid Animation”, making it easier-to-use and more customizable.

45 Likes

Looks awesome! :+1: :grin: Is it alright if I use this in my game?

Sure. But it can get very laggy with larger values.

This is honestly pretty cool! Though I think this is abit unoptimized and could potentially cause a lot of lag. You should try using skinned meshes for better & smoother results ^

1 Like

I’m not familiar with meshes, are there any resources that you could show me about how to use meshes and skinned meshes?

You would add more comments and explain your code next time

I don’t know what you mean, I have explained it?

3 Likes

Yea sure! Here is an example of ocean waves simulation using skinned meshes 2020 10 09 19 37 09 - YouTube
I can’t really find more of an informative stuff sorry! You could possibly do some research on skinned meshes if you would like to achieve these certain effects ^ ^

Overall, good job! :+1:

However, the system makes quite a dent in performance. There are some improvements I think you should take into consideration (there might be more, but this might help):

  • Try to avoid using while true do if you can because of this.
  • If this will be used in games, move your code to the client: visuals like these (especially constantly updating CFrame and position for animations) should be on the client so the server doesn’t have to bother computing and replicating any of that.
  • If you use RunService to replace the loop and you’ve moved it to the client, make use of deltaTime when determining how much to move everything.
  • Keep it small: if this will be in a game, updating this many parts so often can create a large performance impact so I don’t recommend keeping the grid too big (this is more of a suggestion for those utilizing the system).
  • Try to cut down on the amount of calculations you are doing each iteration of any loop and take into consideration how you have loops inside loops - you might even need to restructure the system to limit how many total iterations that every loop goes through (with enough work, I’m sure you can figure something out).

Lastly, some quick possible code improvements:

  • Comment your code for newer developers! Even if you understand what certain parts do, that doesn’t mean somebody else would.
  • Lines like these var = var + number can be replaced with var += number. Read about that here.
  • Add some spacing in your code: separate a constant stream of lines with the occasional double new line to visually separate different parts of your code (this is the way I prefer to do it, but it can really be done any way - see below for example).

Code spacing example

Side note: code spacing can also help make room for commenting chunks of your code!

local function GetBlock(Name) 
	local Block = LevelGrp:FindFirstChild(Name)
	
	if(Block == nil) then
		Block = Instance.new("Part",LevelGrp)
		Block.Size = Vector3.new(BlockSize,BlockSize,BlockSize)
		Block.Material = Enum.Material.SmoothPlastic
		Block.BrickColor = BrickColor.Blue()
		Block.Anchored = true
		Block.CanCollide = true
		Block.Name = Name
		Block.Transparency = BaseTransparency
		
		local Faces = {"Left","Right","Top","Bottom","Back","Front"}
		for i,v in pairs(Faces) do
			Block[v.."Surface"] = Enum.SurfaceType.SmoothNoOutlines
		end
	end
	
	return Block
end

I hope this feedback helps!

4 Likes

Thank you for the feedback. I will be sure to try and add these things.

Thanks, I have seen that article but I will keep looking.

Did you use some math formula equation?

Or did you just do that with your experience?
If “yes” could you tell me how I can learn to get into that result?

Great job. Moving water is tough!

Thanks! I know it definitely is.

1 Like

I had made something similar to this in the past and basically I just varied the perlin “density” (the inputs you give the math.noise function) to give it the “illusion” of it moving.

Wow! That looks very smooth. I am currently trying to port my one to a Plane Mesh.

I’ve heard that the trick is to create “bones” for each point we generate

Yes, but the problem is when I do a For Loop to create the bones, the Plane Mesh does not deform. I made a thread about it. But I have still not found a Solution.

I have made something similar to that in the program itself but not in roblox. Basically you need to create its end state in the program then play whats called literally a “Mesh Deform Modifier”. You then set the keyframe state to 1 then move the meshes verticies save the keyframe then u can animate its state in the program. Perhaps what you could do is animate it from 0 to 1 and expand it to the perlin range.

Would you mind explaining that a bit more simple. I have been using Blender for 1 day, so I am very new.