The best way to optimize my grass generation script?

Hello scripters.
Today I decided to play around and script something.
I made a pretty simple script that generated grass (All of the grass is mesh parts, collision set to box, uncollideable, etc).
Though, the problem I have is that after some time passed the game starts to lag.
What is the best way to optimalize it but keep the same visual effects?

Here is a screenshot of what it looks like.

Here is the script’s content.
What could be done better?
Note that script activity stays below 1.5%.

local rs = game:GetService("RunService")

local cam = workspace.CurrentCamera

local folder = game:GetService("ReplicatedStorage"):WaitForChild("RubysBeautifulWorld")
local grass = folder:WaitForChild("grass")

local raywhitelist = {workspace:WaitForChild("World")}



local bin = workspace:FindFirstChild("visualbin")
if bin then bin:Destroy() end
--if not bin then
	bin = Instance.new("Folder")
	bin.Name = "visualbin"
	bin.Parent = workspace
--end

local rdvalue = script.renderdistance.Value * 10

local objtable = {}
local objs = 0
local maxobjs = (script.quality.Value * 20) * script.renderdistance.Value

--local multiplier = 50

local time = tick()



function update()
	rdvalue = script.renderdistance.Value
	maxobjs = (script.quality.Value * 20) * script.renderdistance.Value
	print("Settings were updated.")
end



local function round(x)
	local up = math.ceil(x)
	if x + 0.5 >= up then
		return up
	else
		return math.floor(x)
	end
end


--local function roundvector3(x, y, z)
--	return Vector3.new(round(x), round(y), round(z))
--end


local camv3 = function() return cam.CFrame.Position end


local function raydown(pos)
	--local ray = Ray.new(pos, CFrame.new(pos, pos - Vector3.new(0,10,0)).LookVector)
	return workspace:FindPartOnRayWithWhitelist(Ray.new(pos, CFrame.new(pos, pos - Vector3.new(0,70,0)).LookVector*100), raywhitelist, true)
end


function remove(obj)
	for k, v in ipairs(objtable) do
		if v == obj then
			table.remove(objtable, k)
			obj:Destroy()
			objs = objs - 1
			return true --Success
		end
	end
	return false --Failure
end


local function cleanup()
	for k, v in ipairs(objtable) do
		if (v.Position - cam.CFrame.Position).magnitude > script.renderdistance.Value * 10 and objs > (maxobjs / 6) * 3 then
			remove(v)
		end
	end
end



local function addgrass(pos, p)
	if objs >= maxobjs then return end
	local g = grass:Clone()
	g.CFrame = CFrame.new(pos)
	g.Color = p.Color
	--g.Material = p.Material
	g.Parent = bin
	table.insert(objtable, #objtable, g)
	objs = objs + 1
end




function randomgrass()
	local hit, pos = raydown(camv3() + Vector3.new(math.random(-rdvalue, rdvalue), 5, math.random(-rdvalue, rdvalue)))
	
	if hit and hit.Material == Enum.Material.Grass then
		addgrass(pos, hit)
	end
end




script.renderdistance.Changed:Connect(update)
script.quality.Changed:Connect(update)

spawn(function()
	while wait(3) do
		print("Object count:", objs)
		cleanup() 
	end
end)

while rs.Heartbeat:wait() and workspace.CurrentCamera do
	
	
	if objs < maxobjs then
	--
	--for x = 1, round(math.random(script.renderdistance.Value, script.renderdistance.Value * 20)) do
		randomgrass()
		randomgrass()
		randomgrass()
	--end
	--
	end
	
	
--	if tick() > time then
--		cleanup()
--		time = tick() + 2
--	end
end

5 Likes

Isn’t Roblox going to add grass effects to terrain in an upcoming update?

Roblox 2019 Roadmap

Terrain Vegetation

  • Grass and rocks become procedurally placed on terrain surface. Grass will procedurally react to character movement.
1 Like

Even though that’s the case its no time soon. Doesn’t hurt to mess around :slight_smile:

Well for one, ipairs is a sloooooooooow function. It’s better to use a numeric loop.

You’re also using a lot of unnecessary function calls, like these.
image
Completely unnecessary.

You also can substitute the table.remove for a FastRemove function.

local function remove(obj)
	local Size = #objtable
	for Index = 1, Size do
		local v = objtable[Index]
		if v == obj then
			objtable[Index] = objtable[Size]
			obj:Destroy()
			objs = objs - 1
			objtable[Size] = nil
			return true --Success
		end
	end
	return false --Failure
end

You also can localize a ton of these functions.

local random = math.random
local insert = table.insert
local CFrame_new = CFrame.new
local Ray_new = Ray.new
local Vector3_new = Vector3.new

local FindPartOnRayWithWhitelist = workspace.FindPartOnRayWithWhitelist

(yes, I know about namecall, don’t bother me about it)

Finally, creating new Roblox datatypes is slow, so that Vector3.new(0,70,0) should be a constant.

local workspace = workspace -- WOOO MICRO OPTIMIZATIONS OF THE MICRO-EST DEGREE
local rs = game:GetService("RunService")

local cam = workspace.CurrentCamera

local random = math.random
local insert = table.insert
local wait = wait
local CFrame_new = CFrame.new
local Ray_new = Ray.new
local Vector3_new = Vector3.new

local FindPartOnRayWithWhitelist = workspace.FindPartOnRayWithWhitelist

local folder = game:GetService("ReplicatedStorage"):WaitForChild("RubysBeautifulWorld")
local grass = folder:WaitForChild("grass")

local raywhitelist = {workspace:WaitForChild("World")}

local bin = workspace:FindFirstChild("visualbin")
if bin then bin:Destroy() end
bin = Instance.new("Folder")
bin.Name = "visualbin"
bin.Parent = workspace

local rdvalue = script.renderdistance.Value * 10

local objtable = {}
local objs = 0
local maxobjs = (script.quality.Value * 20) * script.renderdistance.Value

--local multiplier = 50

local time = tick()

local function update()
	rdvalue = script.renderdistance.Value
	maxobjs = (script.quality.Value * 20) * script.renderdistance.Value
	print("Settings were updated.")
end

local function remove(obj)
	local Size = #objtable
	for Index = 1, Size do
		local v = objtable[Index]
		if v == obj then
			objtable[Index] = objtable[Size]
			obj:Destroy()
			objs = objs - 1
			objtable[Size] = nil
			return true --Success
		end
	end
	return false --Failure
end

local function cleanup()
	for Index = 1, #objtable do
		local v = objtable[Index]
		if (v.Position - cam.CFrame.Position).Magnitude > script.renderdistance.Value * 10 and objs > (maxobjs / 6) * 3 then
			remove(v)
		end
	end
end

local function addgrass(pos, p)
	if objs >= maxobjs then return end
	local g = grass:Clone()
	g.CFrame = CFrame_new(pos)
	g.Color = p.Color
	g.Parent = bin
	insert(objtable, #objtable, g)
	objs = objs + 1
end

local VECTOR_CONST = Vector3_new(0,70,0)
local Grass = Enum.Material.Grass

local function randomgrass()
	local ca = cam.CFrame.Position + Vector3_new(random(-rdvalue, rdvalue), 5, random(-rdvalue, rdvalue))
	local hit, pos = FindPartOnRayWithWhitelist(workspace, Ray_new(ca, CFrame_new(ca, ca - VECTOR_CONST).LookVector*100), raywhitelist, true)
	
	if hit and hit.Material == Grass then
		addgrass(pos, hit)
	end
end

script.renderdistance.Changed:Connect(update)
script.quality.Changed:Connect(update)

spawn(function()
	while true do
		wait(3)
		print("Object count:", objs)
		cleanup() 
	end
end)

while workspace.CurrentCamera do
	if objs < maxobjs then
		randomgrass()
		randomgrass()
		randomgrass()
	end
	rs.Heartbeat:Wait()
end

Of course, there is probably more you can do. You might be able to keep a length variable going, but that might be dumb (still less stupid than the local workspace = workspace I put).

I think doing this would be completely fine as well, but someone less tired and with better judgement than me would know better.

local function cleanup()
	local Length = #objtable
	for Index = 1, Length do
		local v = objtable[Index]
		if (v.Position - cam.CFrame.Position).Magnitude > script.renderdistance.Value * 10 and objs > (maxobjs / 6) * 3 then
			objtable[Index] = objtable[Length]
			v:Destroy()
			objs = objs - 1
			objtable[Length] = nil
			break
		end
	end
end

But that’s just what I have to say about optimizing. I’ll shut up and go to bed now.

6 Likes

Thank you very much.
I still need to figure out what works best and what the best solution would be but this already helps a lot.
I updated my script to place grass only in front of the camera and remove anything behind the camera and/or further away than x studs.
I kind of feel like it also has to do with the meshes, I use a grass mesh that’s like 10 studs in size, maybe it’s the poly count too?
The high amounts of grass placed in the world consumed 95% of my GPU (It’s a Nvidia geforce gtx 1050 with 4 GB VRAM, my laptop has 16GB RAM, Intel Core i7 with quad core).
I tried upgrading perfomance by no longer placing grass where already exists grass, etc.