Fake Volumetric Lighting w/ Beams (Proximity Transparency)

Also why are people complaining about performance? couldn’t you just hook this up to a CollectionService tag so that you don’t have to loop through the whole workspace every frame. And if you have a lot of beams you can only update the ones near a player.

2 Likes

Yes there are ways further to optimize this, but I can leave that up to the person who is need of further optimization, I’m mainly advocating for this to happen in games.

3 Likes

Okay thank you this is really useful to know and keep in mind!

I was testing it out and see the free flashlight in the toolbox has the same problem as well.


image

1 Like

I have acknowledged it’s less effective with beams pointing horizontally, however you can reposition the ancestor BasePart and adjust the Min and Max distance values to accommodate best for it.

2 Likes

If I come up with a very good fix or something for it, I’ll give it a shot. Also let me know how it works out or if you got any questions.

2 Likes

To solve the performance hit from iterating through so many (unnececary) instances at once, you can force only tagged instances of beams to have this effect (thus narrowing it down to possibly tens of instances instead of thousands).

Although I’m not too experienced in how these things would work, I do have a theoretical way of fixing the issue @batteryday forwarded. The beam could be hidden in replacement of a billboard gui at the source location when the camera intersects with the beam and vice versa.

2 Likes

To be clear, the image of the flashlight is how it comes out of the toolbox by default not after running the script.

1 Like

I could consider trying something like this. Doesn’t sound like a bad idea (the billboards)

1 Like

You should save the instances named “VolumetricBeam” so you don’t need to loop through every descendant every frame.

Optimized code:

--Client sided script
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Player = Players.LocalPlayer

local Character = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart")
local Camera = workspace.CurrentCamera

local MaxDistance = 50 --Maximum distance in studs at which beams gradually begin to disappear.
local MinDistance = 4 --Lowest distance in studs at which beams are completely transparent.
local BasedOnCamera = false --Whether distance from the beams is based on the position of the camera or the player's character.

local volumetricBeams = {}

local function onDescendantAdded(descendant)
	if descendant:IsA("Beam") and descendant.Name == "VolumetricBeam" then
		local sourcePart = descendant:FindFirstAncestorWhichIsA("BasePart")
		
		if sourcePart then
			volumetricBeams[descendant] = sourcePart
		end
	end
end

workspace.DescendantAdded:Connect(onDescendantAdded)

workspace.DescendantRemoving:Connect(function(descendant)
	volumetricBeams[descendant] = nil
end)

for i, descendant in workspace:GetDescendants() do
	onDescendantAdded(descendant)
end

local function UpdateBeamTransparency()
	for Object, SourcePart in volumetricBeams do
		local NormalizedSourcePartPosition = Vector3.new(SourcePart.Position.X, 0, SourcePart.Position.Z)
		
		local NormalizedSubjectPosition = BasedOnCamera and Vector3.new(Camera.CFrame.Position.X, 0, Camera.CFrame.Position.Z) 
			or Vector3.new(HumanoidRootPart.Position.X, 0, HumanoidRootPart.Position.Z)

		local Magnitude = (NormalizedSubjectPosition - NormalizedSourcePartPosition).Magnitude - MinDistance
		local NormalizedValue = 1 - math.min(1, Magnitude / MaxDistance)
		
		Object.Transparency = NumberSequence.new({
			NumberSequenceKeypoint.new(0, NormalizedValue),
			NumberSequenceKeypoint.new(1, 1),
		})
	end
end

RunService.RenderStepped:Connect(UpdateBeamTransparency) --Updated before every frame. However, you can connect it to a different event like only when the camera or player's character is actively moving.

This looks very nice though, good work.

2 Likes

Yes, this is not a bad solution to the previous concern. I encourage anybody else here to use this method you’ve presented. But if it’s not already obvious it doesn’t need to be updated every single frame anyway, it’s just for the example. This is also an extremely great method for saving the initial transparency of the beams if you need to restore it later, or you want the transparency to be at it’s lowest value, x instead of 0.

2 Likes

Here is a more optimized and new version of the original design. It includes @Katrist’s optimizations of adding the beam instances to their own table to avoid looping through potentially thousands of instances in the workspace. Additionally, it now remembers the initial transparency of the beam before the script modifies it.

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Player = Players.LocalPlayer

local Character = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart")
local Camera = workspace.CurrentCamera

local MaxDistance = 50 --Maximum distance in studs at which beams gradually begin to disappear.
local MinDistance = 4 --Lowest distance in studs at which beams are completely transparent.
local BasedOnCamera = false --Whether distance from the beams is based on the position of the camera or the player's character.

local VolumetricBeams = {}

local function OnDescendantAdded(Descendant: Instance)
	if Descendant:IsA("Beam") and Descendant.Name == "VolumetricBeam" then
		local SourcePart = Descendant:FindFirstAncestorWhichIsA("BasePart")
		if SourcePart then
			VolumetricBeams[Descendant] = {SourcePart = SourcePart, InitialTransparency = Descendant.Transparency}
		end
	end
end

local function OnDescendantRemoved(Descendant: Instance)
	VolumetricBeams[Descendant] = nil
end

local function UpdateVolumetricBeamsTable()
	for _, Descendant in workspace:GetDescendants() do
		if not VolumetricBeams[Descendant] then
			OnDescendantAdded(Descendant)
		end
	end
end

local function UpdateVolumetricBeams()
	for Beam, Details in VolumetricBeams do
		local NormalizedSourcePartPosition = Vector3.new(Details.SourcePart.Position.X, 0, Details.SourcePart.Position.Z)
		local NormalizedSubjectPosition
		if BasedOnCamera then
			NormalizedSubjectPosition = Vector3.new(Camera.CFrame.Position.X, 0, Camera.CFrame.Position.Z)
		else
			NormalizedSubjectPosition = Vector3.new(HumanoidRootPart.Position.X, 0, HumanoidRootPart.Position.Z)
		end
		local StartTransparency = Details.InitialTransparency.Keypoints[1]
		local EndTransparency = Details.InitialTransparency.Keypoints[#Details.InitialTransparency.Keypoints]
		local Magnitude = (NormalizedSubjectPosition - NormalizedSourcePartPosition).Magnitude - MinDistance
		local NormalizedValue = math.max(StartTransparency.Value, 1 - math.min(1, Magnitude / MaxDistance))
		Beam.Transparency = NumberSequence.new({
			NumberSequenceKeypoint.new(0, NormalizedValue),
			NumberSequenceKeypoint.new(1, EndTransparency.Value),
		})
	end
end

RunService.RenderStepped:Connect(UpdateVolumetricBeams)
workspace.DescendantAdded:Connect(OnDescendantAdded)
workspace.DescendantRemoving:Connect(OnDescendantRemoved)

UpdateVolumetricBeamsTable()

I may completely rewrite my original post in the future if additional features are added or come along. Considering methods of preventing the beams from looking funny when pointing horizontally. Possibly @VegetationBush’s suggestion.

3 Likes

… Or you could just use collectionservice

3 Likes

I purposefully kept the same use as the original. You could easily change it to CollectionService if you wanted to.

Also, your suggestion was already mentioned earlier:

1 Like

it’s as far as personal preference goes

1 Like

Where do I put the script? and make the fake volumetric lighting to work

1 Like

Literally drops my games fps from 60 to 30 and I can get the exact same look with roblox’s
atmosphere :expressionless:

I personally prefer collectionservice because if you have streaming enabled, this will not affect anything unloaded- which for this script thats fine but for other cases it isnt
And I’m pretty sure if you use collectionservice, it still affects streamed objects
correct me if I’m wrong!

1 Like

I was completely unaware of this, so if that is the case, that’s very nice!

1 Like

Could you share how? As far as I am aware, Atmosphere is just fog with realistically-based parameters like Density, except the fog appears like the skybox. This is quite different to what both Volumika and this resource should be doing, which is to create localized “light shafts” instead of there just being uniformly-distributed fog that just… sits there.

From: “Roblox - Volumetric Lighting advanced implementation

3 Likes

I didnt even know volumika could do this because no matter how I used it, it just looked like plain fog, which is what roblox’s atmosphere is. I just thought it was straight up fog that wasnt affected by the sun in that way
also it puts large circles on the players screen and you cant remove it


without:

And the settings I used for my atmosphere, its blue but you can make it white
It looks very similar to the volumika fog
image

1 Like

elttob mentioned it in their blog that the implementation of it is rather ridiculous and best works indoors.
i recreated the same environment as it was in the blog and the banding was quite obvious and it looked ridiculous as expected outdoors.

We really do need an official implementation of it though. Poor mobile compartibility/performance is no excuse to this. It’s up to a developer how would they handle the multi-platform compartibility (to either propose changes for cross-platform or restrict it to PC users)

2 Likes