I am recreating Minecraft world generation, so I need to render many parts. The max and ideal is 268,435,456 parts. The max I managed to generate was ~300,000 parts before the script timed out. I have implemented Greedymeshing, Client Rendering, LOD, and Data compression for optimizations.
Play with it here: PartsOptimizationV2.rbxl (57.4 KB)
Server Script
local http=game:GetService("HttpService")
--Inputs
local partBasesize=4096
local subdividecount=12
local RenderRadius=5
local BigChunkSize=256
local SmallChunkSize=32
--Derives
----local MegaChunks
local PartSize=partBasesize/math.pow(2,subdividecount)
local Factor=partBasesize/PartSize
local BigChunks=Factor/BigChunkSize
local SmallChunks=BigChunkSize/SmallChunkSize
local bottompos=-((Factor-1)*PartSize)/2
local count=0
print(PartSize,Factor,Factor*Factor,BigChunks,SmallChunks)
local world={}
for i=1,BigChunks do
table.insert(world,{})
for o=1,BigChunks do
table.insert(world[i],{})
for p=1,SmallChunks do
table.insert(world[i][o],{})
for a=1,SmallChunks do
table.insert(world[i][o][p],{})
for s=1,SmallChunkSize do
table.insert(world[i][o][p][a],{})
for d=1,SmallChunkSize do
count+=1
table.insert(world[i][o][p][a][s], if math.random(1,10)==1 then "l" else "o")
if count%100000==0 then task.wait() print("working") end
end
end
end
end
world[i][o]=http:JSONEncode(world[i][o])
end
end
--local function makeport(size)
-- local Part=Instance.new("Part",game.Workspace)
-- count+=1
-- if count%10000==0 then task.wait() print("working") end
-- Part.Anchored=true
-- Part.Size=Vector3.new(size,1,size)
-- Part.TopSurface=Enum.SurfaceType.Smooth
-- Part.BottomSurface=Enum.SurfaceType.Smooth
-- return Part
--end
--for i,ServerColumn in ipairs(world) do
-- for o,ServerChunk in ipairs(ServerColumn) do
-- local UnzippedChunk=http:JSONDecode(ServerChunk)
-- for p,ChunkColumn in ipairs(UnzippedChunk) do
-- for a,Chunk in ipairs(ChunkColumn) do
-- for s,column in ipairs(Chunk) do
-- for d,part in ipairs(column) do
-- local Part=makeport(PartSize)
-- Part.Position=Vector3.new(
-- bottompos+(i-1)*BigChunkSize*PartSize+(p-1)*SmallChunkSize*PartSize+(s-1)*PartSize
-- ,0.5,
-- bottompos+(o-1)*BigChunkSize*PartSize+(a-1)*SmallChunkSize*PartSize+(d-1)*PartSize
-- )
-- if part=="l" then
-- Part.BrickColor=BrickColor.new("Bright green")
-- else
-- Part.BrickColor=BrickColor.new("Bright blue")
-- end
-- end
-- end
-- end
-- end
-- end
--end
game.ReplicatedStorage.StartingValues.OnServerInvoke=function()
return PartSize,Factor,bottompos,RenderRadius,SmallChunkSize,BigChunkSize
end
local unzippedChunkIndex={0,0}
local unzippedChunk=nil
game.ReplicatedStorage.GetChunks.OnServerInvoke=function(Player,ChunksWanted)
local ToRender={}
for i,column in pairs(ChunksWanted) do
for o,Chunk in pairs(column) do
local ServerChunkX=math.ceil(i/SmallChunks)
local InsideChunkX=i-(ServerChunkX-1)*SmallChunks
local ServerChunkZ=math.ceil(o/SmallChunks)
local InsideChunkZ=o-(ServerChunkZ-1)*SmallChunks
if unzippedChunkIndex[1]~=ServerChunkX or unzippedChunkIndex[2]~=ServerChunkZ then
unzippedChunk=http:JSONDecode(world[ServerChunkX][ServerChunkZ])
unzippedChunkIndex={ServerChunkX,ServerChunkZ}
end
if not ToRender[i] then
ToRender[i]={}
end
ToRender[i][o]=unzippedChunk[InsideChunkX][InsideChunkZ]
end
end
return ToRender
end
Client Script
local player=game.Players.LocalPlayer
local http=game:GetService("HttpService")
local Chunks={}
local PlayerArraypos={0,0}
player.CharacterAdded:Wait()
local PartSize,Factor,bottompos,RenderRadius,SmallChunkSize,BigChunkSize=game.ReplicatedStorage.StartingValues:InvokeServer()
local acceptabledistance=1
local function GetHeight(chunk,startposX,startposZ,PartType)
local column=chunk[startposX]
local height=0
for i=startposZ,#column do
local nextpart=column[i]
if nextpart==PartType then
height+=1
chunk[startposX][i]="V"
else
break
end
end
return chunk,height
end
local function GetWidth(chunk,startposX,startposZ,PartType,height)
local width=1
for i=startposX+1,#chunk do
local column=chunk[i]
local Freecolumn=true
for o=startposZ,startposZ+height-1 do
local newpart=column[o]
if newpart~=PartType then
Freecolumn=false
break
end
end
if Freecolumn then
width+=1
for o=startposZ,startposZ+height-1 do
chunk[i][o]="V"
end
else
break
end
end
return chunk,width
end
local function ResetChunks()
for i, chunkcolumn in pairs(Chunks) do
for o,chunk in pairs(chunkcolumn) do
Chunks[i][o]="C"
end
end
end
local function FindOverlap(bottomarrayposX,bottomarrayposZ)
for i=bottomarrayposX,bottomarrayposX+RenderRadius*2 do
if not Chunks[i] then
Chunks[i]={}
end
for o=bottomarrayposZ,bottomarrayposZ+RenderRadius*2 do
if Chunks[i][o] then
Chunks[i][o]="O"
else
Chunks[i][o]="M"
end
end
end
end
local function RemoveOverlap()
local RenderChunks={}
for i, chunkcolumn in pairs(Chunks) do
for o,chunk in pairs(chunkcolumn) do
if chunk=="M" then
if not RenderChunks[i] then
RenderChunks[i]={}
end
RenderChunks[i][o]="M"
end
end
end
return RenderChunks
end
local function GetSacraficialChunks()
local TakeChunks={}
for i, chunkcolumn in pairs(Chunks) do
for o,chunk in pairs(chunkcolumn) do
if chunk=="C" then
table.insert(TakeChunks,tostring(i..","..o))
Chunks[i][o]=nil
local length=0; for _ in pairs(Chunks[i]) do length+=1 end if length==0 then
Chunks[i]=nil
end
end
end
end
return TakeChunks
end
local function GetPartBank(TakeChunks)
local SacraficialChunk
local PossibleParts={}
if #TakeChunks>0 then
SacraficialChunk=game.Workspace:FindFirstChild(TakeChunks[1])
table.remove(TakeChunks,1)
PossibleParts=SacraficialChunk:GetChildren()
end
return SacraficialChunk,PossibleParts
end
local function AdjustPart(Part,partwidth,partheight,folder,chunkposX,chunkposZ,posX,posZ,Type)
Part.Size=Vector3.new(partwidth*PartSize,1,partheight*PartSize)
Part.Parent=folder
Part.Position=Vector3.new(
bottompos+((chunkposX-1)*SmallChunkSize*PartSize)+((posX-1)*PartSize)+((PartSize*partwidth)/2)-(PartSize/2)
,0.5,
bottompos+((chunkposZ-1)*SmallChunkSize*PartSize)+((posZ-1)*PartSize)+((PartSize*partheight)/2)-(PartSize/2)
)
if Type=="l" then
Part.BrickColor=BrickColor.new("Bright green")
else
Part.BrickColor=BrickColor.new("Bright blue")
end
end
local function MakeorFindPart(PossibleParts)
local Part
if #PossibleParts>0 then
Part=PossibleParts[1]
table.remove(PossibleParts,1)
else
Part=Instance.new("Part")
Part.Anchored=true
Part.TopSurface=Enum.SurfaceType.Smooth
Part.BottomSurface=Enum.SurfaceType.Smooth
end
return Part
end
local function Render(ArrayposX,ArrayposZ)
local bottomarrayposX=ArrayposX-RenderRadius
local bottomarrayposZ=ArrayposZ-RenderRadius
ResetChunks()
FindOverlap(bottomarrayposX,bottomarrayposZ)
local RenderChunks=RemoveOverlap()
print(RenderChunks)
local TakeChunks=GetSacraficialChunks()
local ChunkValues=game.ReplicatedStorage.GetChunks:InvokeServer(RenderChunks)
local count=0
for i,chunkcolumn in pairs(ChunkValues) do
for o,chunk in pairs(chunkcolumn) do
local folder=Instance.new("Folder",game.Workspace)
folder.Name=tostring(i..","..o)
local SacraficialChunk,PossibleParts=GetPartBank(TakeChunks)
for p=1,#chunk do
local column=chunk[p]
for a=1,#column do
local partype=column[a]
if partype=="V" then continue end
count+=1
local partheight
chunk,partheight=GetHeight(chunk,p,a,partype)
column=chunk[p]
local partwidth
chunk,partwidth=GetWidth(chunk,p,a,partype,partheight)
local Part=MakeorFindPart(PossibleParts)
AdjustPart(Part,partwidth,partheight,folder,i,o,p,a,partype)
end
end
if SacraficialChunk then
SacraficialChunk:Destroy()
end
end
end
print(count)
end
while wait(1) do
local PlayerPos=player.Character:GetPivot().Position
local ArrayposX=math.ceil(math.ceil((PlayerPos.X)-bottompos)/PartSize/SmallChunkSize)
local ArrayposZ=math.ceil(math.ceil((PlayerPos.Z)-bottompos)/PartSize/SmallChunkSize)
if ArrayposX>=PlayerArraypos[1]+acceptabledistance or ArrayposX<=PlayerArraypos[1]-acceptabledistance or
ArrayposZ>=PlayerArraypos[2]+acceptabledistance or ArrayposZ<=PlayerArraypos[2]-acceptabledistance then
PlayerArraypos={ArrayposX,ArrayposZ}
Render(ArrayposX,ArrayposZ)
end
end
I want to optimize my code further for larger rendering sizes and faster rendering speed. Also, implement any best coding practices to make my code more readable.
Explanations
The Large Comment section in the server script is the total world generator, which helps show errors between the rendered world and the data.
The purpose of the Big Chunks is to act as individual packets of data that will be compressed. Instead of compressing the entire array, the big chunks allow less data to be decompressed when the script tries to get data.
Small Chunks must fit inside a big chunk and serve as the individual data that will be sent to the client to render. They allow for many optimizations. Each part in the chunk is also inside a folder of the same name, which is the resolution the greedymesher runs on.
The Client Script handles an array of the chunks currently visible. It works as such: Every second, it gets the chunk the player stands on; if that chunk has moved, it renders new chunks. Discarding chunks that don’t need to be changed, it takes parts from chunks that will no longer be visible to use in new chunks. After rendering the new chunk, it destroys the old chunk.
I can’t have the greedymesher run on the whole rendered area, as then that would require parts to extend outside of their chunk. If I can’t rely on parts only being inside their chunk, I can’t throw away chunks that don’t need to change, as they might contain a part that extends to another chunk. Having the greedymesher run on a single chunk limits the greedymesher’s efficiency as it can’t combine parts across chunks.
A problem with this is that in my actual game, there may be large swaths of blue cut into small chunk sizes as the greedymesher can’t combine two identical chunks; this may be a problem.
The greedymesher works by looping over the chunk to find the width and height of a group of parts; finding the width requires a lot of looping. I don’t know how badly this affects performance.
To reiterate, I want to make my code better and faster to render. The primary sources of lag I see are the :JSONDecode()s that unzip the data for each render, and the simple moving around of so many parts.
I talked to BrokenBone (the creator of mining tech, a game that also needs to render large amounts of individual parts) on this matter. He said it was possible to get “up to billions of blocks.” I don’t know if these are virtual parts or rendered parts.
RenderRadius=19 world (largest possible)
When walking to the side, the renderer rendered 7788 new parts, some taken from chunks moved out of render range and others made.
I have thought of implementing a system where the renderer will render each chunk as one part for chunks far away, potentially increasing render size for little cost. I have also thought of making a spiral renderer that will render chunks closer to the player faster and then gradually go outwards, perhaps better than the current system of generating a whole pass in one go. I don’t know how this will work with the player moving around.
Sorry if this is very long and complicated.