Reduce lag caused by lots of trees?

Oh my God, please don’t do that in RenderStepped. This is going to severely impact your performance, as this is run before each render, and that’s 2500+ iterations in two arrays, where you have 2 math operations per tree, so about 5000 math operations just to render one frame - obviously not good. You can spread that out over multiple frames like this:

--// Services
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

--// Globals
local AllTrees = workspace.Trees:GetChildren()
local ShownTreesFolder = workspace.Trees
local HiddenTreesFolder = ReplicatedStorage.UnloadedTrees
local Debounce = false

--// Events
RunService.Heartbeat:Connect(function()
    if Debounce then
        return
    end

    Debounce = true

    for i, tree in ipairs(AllTrees) do -- NB: ipairs for arrays, it's faster, use it!
        if i % 100 == 0 then
            task.wait() -- you can change the 100, higher value = more trees per frame = worse performance
        end

        local treeBase = tree.PrimaryPart
        if not treeBase then
            continue
        end

        local shouldShow = (treeBase.Position - char.PrimaryPart.Position).Magnitude <= renderDist
        tree.Parent = if shouldShow then ShownTreesFolder else HiddenTreesFolder 
    end

    Debounce = false
end)

This should increase your performance a lot :slight_smile:

Edit: added debounce to eleminate clashes

1 Like

I think what they did is use single-mesh models for trees, maybe 2 meshes.
The trunk of the tree and all the leafs in it are 2 separate models (as this also allows for dead / leaf-less trees).

If you combine all leafs together in just one single model and reduce the amount of required polygons, using 1 decal should be enough in that case.

You can still achieve good-looking trees, in fact I’m pretty sure that is how most games do it.
More meshes and decals = worse performance.

Some games use a single texture for all the leafs.
All tree branches are turned into a sprite sheet basically which is just 1 big image with lots of tree branch variants.

And every branch on the tree is just a single (possibly subdivided) plane that has it’s UV coordinates set to one of the variants on that image.

Do this for every plane and the tree looks like as if it’s full of leafs and branches while using only a single image/decal.

1 Like

Yes, this is basically what I was referring to, its basically a “loop” but not actually. Should definitely help with performance.

1 Like

doesnt seem to work. it doesnt give me any errors though so i have no idea why

Is it too late to turn on streaming? xD

i dont know, streaming is really annoying because it just breaks a lot of scripts so i would have to remake all of my scripts to support streaming

or maybe i just dont know how to use it properly, not sure

have you tried using mesh trees and setting the renderfidelity on the mesh to automatic?

This pack has some trees you could test it with
https://create.roblox.com/marketplace/asset/6933438443/Synty-Nature-Pack

2 Likes

i think ive tried using these in the past and it was quite laggy

You didn’t. You even told him that he could attach it to renderstepped with no strings attached, and this guy went on detail to tell him why it’s wrong, which is basically what I was talking about, but in detail. And as for the picture Teef said, this is definitely going to have negative impact on performance for players, especially on low-end devices.

This would work well, but depending on how many trees you have, reparenting objects is a lot worse for performance vs changing CFrame.

  • I suggest CFraming all trees which shouldn’t be shown to a far away cframe. If you wanted to make it even more performant, you could use workspace:BulkMoveTo and move all the trees which shouldn’t be shown to a far away CFrame

I have 3 solutions

  1. Turn off the property “CastShadow” for some of the trees
  2. Turn on StreamingEnabled in workspace
  3. Resort to using Meshes for your trees if you already haven’t

EDIT: don’t use meshes.

1 Like

okay i fixed it and it does in fact look much better on the performance tab
image

This is commonly an issue I’ve encountered when trying to work with big maps; I’ve personally found it notoriously hard to get running performantly without having obscene delays between handling chunks of trees. Though, with some thinking there’s a few things you could try, such as using threads to split work, having waits between parenting some amount of trees, and etc.

I decided to try my hand at making something to handle this, since I feel like this is a very difficult issue to solve. Here’s what I got:

-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
-- Constants
local MIN_RENDER_DISTANCE: number = 512 -- Just loading trees near the camera, so shadows don't look weird
local MAX_RENDER_DISTANCE: number = 2048 -- The maximum distance you'll see trees before they never render

local THREAD_CHUNKS: number = 8 -- How many threads should handle the trees

local THREADS_PER_DELAY: number = math.floor(THREAD_CHUNKS / 4) -- How many threads to spawn before waiting
local THREAD_DELAY: number = 1 / 60 -- The delay between spawning threads
-- Variables
local Unloaded_Directory = ReplicatedStorage:WaitForChild("UnloadedTrees")
local Loaded_Directory = workspace:WaitForChild("Trees")

local Trees: { Model } = Loaded_Directory:GetChildren()

local Tree_Count: number = #Trees
local Chunk_Count: number = math.floor(Tree_Count / THREAD_CHUNKS) -- How many trees one thread will handle

local Rendering: boolean = false
-- Main
RunService:BindToRenderStep("TREE_RENDERING", Enum.RenderPriority.Camera.Value + 1, function()
	if not Rendering then
		local Camera: Camera? = workspace.CurrentCamera
		if Camera then
			Rendering = true
			-- Where the player's camera is
			local Viewpoint: Vector3 = Camera.CFrame.Position
			-- Spawn all 8 threads
			for Thread: number = 1, THREAD_CHUNKS, 1 do
				-- The tree index that the thread will be starting at
				local Start: number = ((Thread - 1) * Chunk_Count) + 1
				-- And, where it should end
				-- If this is the last chunk, just iterate through the rest of the tree table
				-- Otherwise, just handle the default chunk amount
				local End: number = if Thread >= THREAD_CHUNKS then Tree_Count else Thread * Chunk_Count
				task.spawn(function()
					-- Forgive me if anything here is off; I'm a bit rusty at parallel Luau
					task.synchronize()
					for Index: number = Start, End, 1 do
						local Tree: Model = Trees[Index]
						local Tree_Origin: Vector3 = Tree:GetPivot().Position
						-- First, compare distances to see if it can be loaded without the WorldToViewportPoint calculation
						local Distance: number = (Tree_Origin - Viewpoint).Magnitude
						if Distance > MAX_RENDER_DISTANCE then
							Tree.Parent = Unloaded_Directory
							continue
						elseif Distance <= MIN_RENDER_DISTANCE then
							Tree.Parent = Loaded_Directory
							continue
						end
						-- Then, check to see if it's in the point
						local Position: Vector2, Visible: boolean = Camera:WorldToViewportPoint(Tree_Origin)
						Tree.Parent = if Visible then Loaded_Directory else Unloaded_Directory
					end
				end)
				-- If this isn't the last thread, yield before spawning more
				if ((Thread % THREADS_PER_DELAY) == 0) and (Thread < THREAD_CHUNKS) then
					task.wait(THREAD_DELAY)
				end
			end
			Rendering = false
		end
	end
end)

It should be customizable enough for your use case (and if needed, you can change some of the logic around the Camera:WorldToViewportPoint(Tree_Origin) line, since it only checks if the center of the tree is visible on the camera). Also, I’ve made it so that it handles trees from the camera position instead of the character’s position, since it helps more accurately figure out what trees should render.

I’d suggest lowering the MIN_RENDER_DISTANCE to something lower than 512, like 128, and maybe the MAX_RENDER_DISTANCE if you have fog close enough to hide a lot of the trees outside of ~1000 studs. Personal testing showed that this script did not have much effect on the performance, at least with the 4032 trees I have in my test project.
If you’re curious what the trees I used contained:
image
And, my Script Performance tab (TreeRenderer being the script):
image
I’d like to note it is running; I do believe the reason it says 0.000% is because the threads handle the work rather than the script itself, though it is infact running because the work spikes to 0.049% at the start of the script, which I believe is it doing Loaded_Directory:GetChildren().

I hope this is of any help, since some of the code above seems a little unoptimized. If none of the solutions seems to provide any help though, I’d advise learning how to use and utilize StreamingEnabled rather than shying away from it, since it can be a big performance help if you can fix the various issues that it may cause with unloading. Also do try setting the RenderFidelity - Automatic should render the meshes at a lower quality if they are far enough away.

3 Likes

Speaking of render distance, I notice a lot of far-away trees also still use transparent leafs.
To be fair, when a tree is so far away that you can barely see the individual leafs, just replace it with a lower detail version that uses absolutely no transparency at all since you’ll barely notice it.

Some games replace trees that are SUPER far away with literal billboards/sprites which is also pretty clever, I think beams can help with that since they have an option to allow them to always turn towards the camera.

Roblox has a built-in LOD system but it’s not always super efficient.
Consider disabling transparency on trees far away and use a different mesh?

Don’t forget, it also depends on the triangle count in each of the trees. Triangle count in a mesh affects how roblox renders it. The more trees, the more the game has to render the trees cause of the increased triangle count which can affect your game’s performance.

i did that and it seems to be the best solution so far


it looks really goofy though so i will try splitting it into 2 render distances so the trees close to the player are gonna look normal, the trees inbetween are gonna have a low quality texture and the trees beyond the second render distance are not gonna have any textures at all

edit: just did that and it seems to be much less effective
image

1 Like

i just ended up making a third render distance and the trees that are beyond that distance simply just get unloaded. i also added cactuses for the deserts so there arent as many trees and it seems to be working fine. the game runs at about 160-200 fps, not sure about lower end devices but i think it should be fine.

1 Like

As far as I can tell, transparency seems to be one of the major culprits.
Transparency is very hard to (and often horribly inefficiently) rendered.

So what I do personally is just use high-detail trees when the player is really close to them and using a low-poly tree with no transparency when it’s really far away.

Transparency can cause overdraw and rack up draw calls.

Solid models can use deferred rendering which is way cheaper and faster but doesn’t work well for things like glass and translucent things.

Deferred rendering makes rendering 1000 objects easy because things like lighting are also calculated differently or all at the same time.
Plus objects that look the same can be batched/clustered to reduce draw calls.

Forward rendering (often used for transparency) treats every single object as if it were unique and has to (re)draw transparent objects on top of each other.
Sometimes as little as 50 - 100 transparent objects on screen can cause significant performance drops.

I hope that info helps.

(If anyone here is an actual expert on low-level graphics and rendering you can add on/correct any potential mistakes that I’ve made though so far I think this is about correct and how rendering works, oversimplified of course.)

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.