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:
And, my Script Performance tab (TreeRenderer being the script):
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.