Bypassing Particle LOD Rate Throttle

Figured a way to reverse the LOD for ParticleEmitters this allows you to see the intended amount of particle rate from anywhere without setting quality & camera distance interfering your scenes. I see this to be really useful for graphics artists who want to add more detail to big scenes and to have lower end devices see visuals as it’d be on higher end preferences. Felt like letting people have this because my use-cases at the moment aren’t vast but I’m sure there’s many whom have wanted this since the dawn of time on Roblox.

Other resources you may be interested in also looking at which are similar for visuals in-engine…

https://devforum.roblox.com/t/tracking-roblox-particles/2425825 - Can track actual particles from Particle Emitters in-engine.
https://devforum.roblox.com/t/bypassing-forced-beam-lod/2426888/1 - Removes the LOD for beams


Repro Code Make sure to give the tag of “Particle” through the Tag Editor so that the code picks it up!

  • edit note - recently became aware that the engine does have a cut-off distance, as in it’ll force stop emitter’s Rate property regardless of however high it is, but it’s a non-issue as of right now as users will not notice due to how far it cuts off at.
-- Credits to nurokoi
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")

local function ReverseRate(inputRate : number, fromWorldPoint : Vector3)
	local QualityLevel = math.clamp(UserSettings().GameSettings.SavedQualityLevel.Value / 10, 0.1, 1)
	local DistanceCam = (workspace.CurrentCamera.CFrame.Position - fromWorldPoint).Magnitude
	local DistanceQualityScalar = math.clamp(1 - (DistanceCam - 1e2 * 2) / (1e2 * 8), 0, 1)
	local ActualRate = inputRate * (inputRate / (inputRate * QualityLevel * DistanceQualityScalar)) 

	return ActualRate
end

local function ReverseParticleThrottleRate()
	for _, particleEmitter : ParticleEmitter in CollectionService:GetTagged("Particle") do
		local Source, EmissionCenter = particleEmitter.Parent, nil
		
		if Source:IsA("BasePart") then
			EmissionCenter = Source.Position
		elseif Source:IsA("Attachment") then
			EmissionCenter = Source.WorldPosition
		end
		
		if not EmissionCenter then
			continue
		end

		local DesiredRate = particleEmitter:GetAttribute("DesiredRate")

		if not DesiredRate then
			DesiredRate = particleEmitter.Rate
			particleEmitter:SetAttribute("DesiredRate", DesiredRate)
		end
		
		particleEmitter.Rate = ReverseRate(DesiredRate, EmissionCenter)
	end
end

RunService:BindToRenderStep("ParticleLOD", Enum.RenderPriority.Camera.Value + 1, ReverseParticleThrottleRate)

Video Demonstration

Both particles have same rate, red particles do not have the Particle tag whereas the white ones do.

41 Likes

Great for things that need to have a certain detail/look to them constantly, not recommended for all other particles/effects. Aka, to anyone planning to use this, its a great resource but use sparingly on things you really need to maintain a “constant” look, else you’d run into some annoying performance issues.

3 Likes

Particle throttling is there for a reason, so it’s not a good idea to apply this to all the particles. If I had a horror game with something like “how many times have you seen this particle” then I would definitely use this.

2 Likes

this is pretty useful. is there a way to bypass distance LOD as well? Doesn’t seem work well with far camera distance

1 Like

Particle rate is very inaccurate on mobile by the way. It’s way more than the actual rate

Thanks for bringing this up, turns out yes you’re right and this was a mistake, the updated code should now work as intended for all devices.

As for distance when you mentioned, the code actually does account for distance for a specific distance but after that it starts to lose precision with maintaining the original rate due to the engines LOD causing floating point precision issues, so no matter how high it can go after said distance it’ll lower in rate despite setting a higher rate. However I think it’s a non-issue due to how far it only happens at, and players won’t really take such notice, there’s a trick where you could theoretically use another particle emitter to combat it but i sadly don’t have the time to do.

It was actually the “automatic” setting that bugs it. The code itself works for manual graphics. On a decent PC, automatic would generally be 10, but the function would get its graphic level at 1 or 0.1 since automatic is 0, this makes particle emit 10x more than expected in this case. It’s a small issue but noticeable in large games.

Right, I’ve just come to realise that there isn’t any way for the client to know the Automatic quality level as the only way we have right now is via UserSettings().GameSettings.SavedQualityLevel.Value which returns a value between 0 to 10, anything above 0 is manual values whereas 0 is automatic.

Quite unfortunate but I’ll be bumping this topic since this is a problem and can only be solved if Roblox simply just updates SavedQualityLevel with whatever the Automatic value is.

1 Like

Sadly, the best way to handle this would be to not bypass the LOD if set to automatic.

1 Like

Stupid question, but does this work with flipbooks??

If it’s a particle emitter, chances are this still works, even if it’s a flipbook and whatever else it could become in the future :muscle:

(famous last words here :skull:)

2 Likes

I found that there’s a way to do this without it failing cuz of the user having their graphics on auto, it just replaces the normal Roblox auto emitter with a custom one, it doesn’t use more resources than ur method as it has less calculations, it might be faster who knows, but this is just an edit I made in 30 mins that works all the time

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

local lastParticleEmited = {}

local function ReverseParticleThrottleRate()
    for _, particleEmitter : ParticleEmitter in CollectionService:GetTagged("Particle") do
        local DesiredRate = particleEmitter:GetAttribute("DesiredRate")
        
        if not DesiredRate then
            DesiredRate = particleEmitter.Rate
            
            particleEmitter.Rate = 0
            particleEmitter:SetAttribute("DesiredRate", DesiredRate)
        end
        
        local itterationTime = 1 / DesiredRate
        
        local lastTick = lastParticleEmited[particleEmitter] or 0
        local currentTick = tick()
        
        local deltaTick = currentTick - lastTick
        
        if deltaTick < itterationTime or not particleEmitter.Enabled then
            continue
        end

        particleEmitter:Emit(1)
        
        lastParticleEmited[particleEmitter] = tick()
    end
end

RunService:BindToRenderStep("ParticleLOD", Enum.RenderPriority.Camera.Value + 1, ReverseParticleThrottleRate)
2 Likes

I found the iteration time from getting that the rate is how many times it’s emitted a second, so dividing a second by the rate makes u able to emit it the same amount