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…
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.
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.
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.
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.
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)
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