Screen Distortion Rain

Yup. Super basic and naive.
Doesn’t count invisible parts though, since the player would be confused what blocked the rain. Plus, usage of this is generally paired with an invisible block above the player that has the rain particles and we don’t want that to interfere.

--Returns whether the given position is under cover
local function UnderObject(pos,l)
	local ray = Ray.new(pos, v3(0,1,0) * (l or 120))
	-- raycast
	local hit, position = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)
	if hit then
		return hit.Transparency ~= 1 and true or UnderObject(position+v3(0,0.5,0), (l or 120) - (pos-position).Magnitude)
	else
		return false
	end
end

Interesting, I was under the impression that longer rays were pretty performance heavy.

It’s not terribly long, and it’s generally only one or two per check.

It’s been a long time since I last updated this. The Luau VM came out since then, so there are a lot of optimizations to be made.

I’ll update this today, if I have a chance to.
EDIT: Updated with some minor optimizations.

2 Likes

@boatbomber, have you to remodify the whole script?

I don’t quite understand what you’re asking.

I haven’t updated this in over a year. I won’t be redoing it from scratch, but it’ll be a full refactoring. I’ll make sure to keep it backwards compatible for anyone who wants to easily swap the new version into their game.

1 Like

Ok, i will be wait :smile: . I have tried to say if you need to edit the whole script in LUAU ?

It currently runs well in Luau, but it could be better.

1 Like

Looks like this is the part here @howmanysmaII comes in

Done.

image
image

Here’s the updated code on GitHub and here’s the place file (141.5 KB).

12 Likes

God tier optimization, as usual. Thanks Lily!

I’ll be pushing your changes to master shortly.
Your changes are live! Thanks for the helping hand! <3

2 Likes

I like this! This would be prefect for a horror a game who thinks that would be awesome?!

1 Like

Hello. I found a glitch. If the player is using the automatic graphics mode, the rain will not show up at all. I know it is above 8 graphics because the terrain water is still clear.

1 Like

Not a bug, but rather an annoying feature of the way Automatic works. It doesn’t tell you what it’s being set to, so you have to just assume the worst or you’ll run into problems if it assumes high settings and is really low.

This is very annoying API behavior and developers have been asking for it to change for nearly half a decade at this point. Support a feature request!

3 Likes

I took the script and made it into a module so that you can Enable / Disable the Rain effect during the game, You can also change the settings of the rain during the game.

Module:Enable(Optional Settings) will enable the rain and if the Settings are given it will update the settings of the rain, If settings are not given it will continue to use the previously given settings or the default if non were given.

Module:Disable() Disables the rain

Module:SetSetting(Setting) will change the rain settings to the given settings, Not all settings have to be given.

Code :

--[=[
	
	--Copyright boatbomber 2019--
	
	--Given under a BSD 3-Clause License--
		Explanation of license:		https://tldrlegal.com/license/bsd-3-clause-license-(revised)
		
	--FEATURES--
	
	Creates droplets of "water" on the screen, with a distortion effect, giving great immersion
	for games that have rainy environments.
	
	Droplets will not spawn if the player is indoors or under cover of some sort.
	Droplets will not spawn if the camera is pointed down, as that is avoiding "getting rain in the eyes".
	
	
	--WARNING-- --WARNING-- --WARNING-- --WARNING-- --WARNING-- --WARNING-- --WARNING-- --WARNING--
	
	THIS PRODUCT RELIES ON GLASS MATERIAL, THUS SHARING ALL THE LIMITATIONS OF GLASS.
	
	Non-opaque objects are currently not visible through glass.
	This includes, but is not limited to, transparent parts, decals on transparent
	parts, particles, and world-space gui objects.
	Additionally, it only looks right for users with graphic settings of at least 8.
	Hence, I've set it to only spawn droplets if the user has the graphics set high enough.
	
	--WARNING-- --WARNING-- --WARNING-- --WARNING-- --WARNING-- --WARNING-- --WARNING-- --WARNING--
--]=]

local Module = {}

-- Constants
local Settings = {
	--	Rate: How many droplets spawn per second
	Rate = 8;

	--	Size: How large the droplets roughly are (in studs)
	Size = 0.1;

	--	Tint: What color the droplets are tinted (leave as nil for a default realistic light blue)
	Tint = Color3.fromRGB(226, 244, 255);

	--	Fade: How long it takes for a droplet to fade
	Fade = 1.5;
}

local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")

local UpVec = Vector3.new(0, 1, 0)
local DROPLET_SIZE = Vector3.new(1, 1, 1)
local EMPTY_CFRAME = CFrame.new()

----------------------------------------------------------------------------
---  Variables  ------------------------------------------------------------
----------------------------------------------------------------------------

--Player related
local Camera = Workspace.CurrentCamera
local Player = Players.LocalPlayer
local GameSettings = UserSettings().GameSettings
local CanShow = GameSettings.SavedQualityLevel.Value >= 8

--Raycasting
local ignoreList = {Player.Character or Player.CharacterAdded:Wait()}
local IgnoreLength = 1

--Localizing
local ipairs = ipairs

--Settings localized
local Rate = Settings.Rate
local Size = Settings.Size
local Tint = Settings.Tint or Color3.fromRGB(226, 244, 255)
local Fade = Settings.Fade
--Fade tween
local fadeInfo = TweenInfo.new(Fade, Enum.EasingStyle.Sine, Enum.EasingDirection.In)
local strechInfo = TweenInfo.new(Fade / 1.05, Enum.EasingStyle.Quint, Enum.EasingDirection.In)
local fadeGoal = {Transparency = 1}

local accumulatedChance = 0

----------------------------------------------------------------------------
---  Prefab Basic Objects  -------------------------------------------------
----------------------------------------------------------------------------
--Droplet holder
local ScreenBlock = Instance.new("Part")
ScreenBlock.Size = Vector3.new(2, 2, 2)
ScreenBlock.Transparency = 1
ScreenBlock.Anchored = true
ScreenBlock.CanCollide = false
ScreenBlock.Parent = Camera

local ScreenBlockCFrame = EMPTY_CFRAME

RunService:BindToRenderStep("ScreenRainUpdate", Enum.RenderPriority.Camera.Value + 1, function()
	ScreenBlockCFrame = Camera.CFrame
	ScreenBlock.CFrame = ScreenBlockCFrame
end)

----------------------------------------------------------------------------
---  Functions  ------------------------------------------------------------
----------------------------------------------------------------------------

local function DestroyDroplet(d)
	wait(Fade)

	-- Proper GC
	for _, Child in ipairs(d:GetChildren()) do
		local Index = table.find(ignoreList, Child)
		if Index then
			ignoreList[Index] = ignoreList[IgnoreLength]
			ignoreList[IgnoreLength] = nil
			IgnoreLength = IgnoreLength - 1
		end
	end

	local Index = table.find(ignoreList, d)
	if Index then
		ignoreList[Index] = ignoreList[IgnoreLength]
		ignoreList[IgnoreLength] = nil
		IgnoreLength = IgnoreLength - 1
	end

	d:Destroy()
end

--Returns whether the given position is under cover
local function UnderObject(pos, l)
	l = l or 120

	local hit, position = Workspace:FindPartOnRayWithIgnoreList(Ray.new(pos, UpVec * l), ignoreList)
	if hit then
		return hit.Transparency ~= 1 and true or UnderObject(position + UpVec, l - (pos - position).Magnitude)
	else
		return false
	end
end

--Creates a random droplet on screen
local function CreateDroplet()
	--Setup
	local stretch = 1 + math.random(15) / 10

	local RunAmount = math.random(4)
	local Tweens = table.create(RunAmount * 2 + 2)
	local TweensLength = 0

	local SizeOffset = math.random((Size / 3) * -10, (Size / 3) * 10) / 10
	local Scale = Size + SizeOffset
	local MeshScale = Vector3.new(Scale, Scale, Scale)

	--Main droplet object
	local DropletMain = Instance.new("Part")
	DropletMain.Material = Enum.Material.Glass
	DropletMain.CFrame = EMPTY_CFRAME
	DropletMain.CanCollide = false
	DropletMain.Transparency = 0.5
	DropletMain.Name = "Droplet_Main"
	DropletMain.Color = Tint
	DropletMain.Size = DROPLET_SIZE

	local Mesh = Instance.new("SpecialMesh")
	Mesh.MeshType = Enum.MeshType.Sphere
	Mesh.Scale = MeshScale
	Mesh.Parent = DropletMain

	--Create droplet extrusions
	for i = 1, RunAmount do
		local eSizeOffset = math.random(
			(Size / 3) * -100,
			(Size / 3) * 100
		) / 100

		local ExtrusionCFrame = CFrame.new(Vector3.new(
			math.random(-(Size * 40), Size * 40) / 100,
			math.random(-(Size * 40), Size * 40) / 100,
			0
			))

		local ExtrusionScale = Size / 1.5 + eSizeOffset
		local ExtrusionMeshScale = Vector3.new(ExtrusionScale, ExtrusionScale, ExtrusionScale)

		local Extrusion = Instance.new("Part")
		Extrusion.Material = Enum.Material.Glass
		Extrusion.CFrame = ExtrusionCFrame
		Extrusion.CanCollide = false
		Extrusion.Transparency = 0.5
		Extrusion.Name = "Extrusion_" .. i
		Extrusion.Color = Tint
		Extrusion.Size = DROPLET_SIZE

		local ExtrusionMesh = Instance.new("SpecialMesh")
		ExtrusionMesh.MeshType = Enum.MeshType.Sphere
		ExtrusionMesh.Scale = ExtrusionMeshScale
		ExtrusionMesh.Parent = Extrusion
		Extrusion.Parent = DropletMain

		local weld = Instance.new("Weld")
		weld.C0 = ExtrusionCFrame:Inverse() * EMPTY_CFRAME
		weld.Part0 = Extrusion
		weld.Part1 = DropletMain
		weld.Parent = Extrusion

		IgnoreLength = IgnoreLength + 1
		TweensLength = TweensLength + 1
		ignoreList[IgnoreLength] = Extrusion
		Tweens[TweensLength] = TweenService:Create(Extrusion, fadeInfo, fadeGoal)

		TweensLength = TweensLength + 1
		Tweens[TweensLength] = TweenService:Create(ExtrusionMesh, strechInfo, {
			Scale = Vector3.new(ExtrusionScale, ExtrusionScale * stretch, ExtrusionScale);
			Offset = Vector3.new(0, -(ExtrusionScale * stretch) / 2.05, 0);
		})
	end

	IgnoreLength = IgnoreLength + 1
	TweensLength = TweensLength + 1
	ignoreList[IgnoreLength] = DropletMain
	Tweens[TweensLength] = TweenService:Create(DropletMain, fadeInfo, fadeGoal)

	TweensLength = TweensLength + 1
	Tweens[TweensLength] = TweenService:Create(Mesh, strechInfo, {
		Scale = Vector3.new(Scale, Scale * stretch, Scale);
		Offset = Vector3.new(0, -(Scale * stretch) / 2.05, 0);
	})

	local NewCFrame = ScreenBlockCFrame:ToWorldSpace(CFrame.new(
		math.random(-100, 100) / 100,
		math.random(-100, 100) / 100,
		-1
		))

	DropletMain.CFrame = NewCFrame
	local weld = Instance.new("Weld")
	weld.C0 = NewCFrame:Inverse() * ScreenBlockCFrame
	weld.Part0 = DropletMain
	weld.Part1 = ScreenBlock
	weld.Parent = DropletMain

	for _, t in ipairs(Tweens) do
		t:Play()
	end

	local DestroyRoutine = coroutine.create(DestroyDroplet)
	coroutine.resume(DestroyRoutine, DropletMain)
	DropletMain.Parent = ScreenBlock
end

local function OnGraphicsChanged()
	CanShow = GameSettings.SavedQualityLevel.Value >= 8
end

GameSettings:GetPropertyChangedSignal("SavedQualityLevel"):Connect(OnGraphicsChanged)

----------------------------------------------------------------------------
---  Functionality Loop  ---------------------------------------------------
----------------------------------------------------------------------------

local Connection
local Enabled = false

function Module:SetSetting(Setting)
	if typeof(Setting) then
		if typeof(Setting.Rate) == "number" then
			Settings.Rate = Setting.Rate
		end
		if typeof(Setting.Size) == "number" then
			Settings.Size = Setting.Size
		end
		if typeof(Setting.Tint) == "Color3" then
			Settings.Tint = Setting.Tint
		end
		if typeof(Setting.Fade) == "number" then
			Settings.Fade = Setting.Fade
		end
		Rate = Settings.Rate
		Size = Settings.Size
		Tint = Settings.Tint
		Fade = Settings.Fade
	end
end

function Module:Enable(Setting)
	if Enabled == false then
		Enabled = true
		Connection = RunService.Heartbeat:Connect(function(deltaTime)
			accumulatedChance += deltaTime * Settings.Rate

			if CanShow and ScreenBlockCFrame.LookVector.Y > -0.4 and not UnderObject(ScreenBlockCFrame.Position) then
				for i = 1, math.floor(accumulatedChance) do
					CreateDroplet()
				end

				accumulatedChance %= 1
			else
				accumulatedChance %= 1
			end
		end)
		
		if Setting then
			Module:SetSetting(Setting)
		end
		
	end
end

function Module:Disable()
	if Enabled == true then
		Enabled = false
		Connection:Disconnect()
	end
end

return Module

3 Likes

Update!

  • Uses object pooling to avoid creating and destroying so many parts constantly
  • No longer uses Welds, uses WordRoot:BulkMoveTo instead
  • No more TweenService usage
  • Adjustable update frequency
  • Better performance (getting 300FPS in the demo place on my laptop!)
  • Neater code

Also put together a proper Rojo compliant repository instead of that one random .lua file.

8 Likes

Update!

  • Now a module instead of LocalScript
  • Added ScreenRain:Enable(settings?)
  • Added ScreenRain:Disable()
  • Added ScreenRain:Configure(settings)
  • Tweaked the transparency values for nicer effect

This update makes it easy to have weather systems and zone areas since you can now easily turn rain on and off and configure the settings on the fly!

If you want the behavior from before this update, your LocalScript should just look like this:

local ScreenRain = require(script:WaitForChild("ScreenRain"))
ScreenRain:Enable()
11 Likes

What do i put in the ScreenRain:Enable(setting) what do I put in the setting place?

You can put in a dictionary containing any of the settings that you wish to change from the defaults, although it is not necessary if you don’t wish to change them. Note that you do not need to include all the settings, only ones that you wish to change. Below is an example (using the default settings) of an input.

local Settings: {[string]: number | Color3} = {
	Rate = 5, -- Spawn rate (droplets/second)
	Size = 0.08, -- Average droplet size
	Tint = Color3.fromRGB(226, 244, 255), -- Droplet colour
	Fade = 1.5, -- Time to fade out
	UpdateFreq = 1 / 45, -- Update frequency
}

ScreenRain:Enable(Settings)

oh okay now i understand thank you!