Performance Friendly: Custom Render

Aight I’m back with another Performance Friendly question, this one is based around Rendering would a Custom Render that turns the parts that you don’t see invisible and those that you see visible be more performance friendly?

My assumptions: turning the parts visible and invisible would eat up a lot of memory.

7 Likes

I have a system that groups parts together in a table based on their positions, and then spawns them in based on a mixture of the developers specified rendering distance and the players Graphics Level.

(This does not change visibility, but rather unparents and then reparents them on the client-side)

It seems to work just fine, and actually make my game easily playable on lower-end devices.

6 Likes

I was thinking of parenting the parts you don’t see to nil instead of turning them invisible thanks for your help!

1 Like

Roblox already does this for parts not in the frustum of the camera. It’s called “occlusion culling.” Vernando is talking about LOD, which is good for reducing polygons that you don’t need to see from far distances. You shouldn’t do occlusion culling yourself, but LOD is a good idea for high-poly maps.

1 Like

Wait what since when did Roblox add occlusion culling? 100% sure they didn’t have it

1 Like

Here’s the code I used to accomplish this:

do
  local insert, remove = table.insert, table.remove
  local LODSpecs = {}
  local function bfind(t, v)
    local a, b = 1, #t
    while a <= b do
      local u = floor((a + b) * 0.5)
      local w = t[u].Sort
      if v > w then
        a = u + 1
      elseif v < w then
        b = u - 1
      else
        return u
      end
    end
    return a
  end
  local function CalculateCenter(m)
    local Center = v3b
    local Children = m:GetChildren()
    for _, Model in next, Children, nil do
	  if Model.ClassName == "Model" then
        Center = Center + Model.PrimaryPart.Position
	  else
		Center = Center + Model.Position
	  end
    end
    Center = Center / #Children
    return Center
  end
  function LODSplitEach(m)
    local Groups = {}
    for _, Model in next, m:GetChildren() do
      local Group = Instance.new("Model")
      Model.Parent = Group
      table.insert(Groups, Group)
    end
    for _, Model in next, Groups, nil do
      Model.Parent = m
    end
  end
  function LODSplitModel(m, n)
    local w = floor(n ^ 0.5)
    local Size = m:GetExtentsSize()
    local Center = CalculateCenter(m)
    local wx = floor(Size.X / w + 0.5)
    local wz = floor(Size.Z / w + 0.5)
    local Groups = {}
    for _, Model in next, m:GetChildren() do
      local p = Model.ClassName == "Model" and Model.PrimaryPart.Position or Model.Position
      local rx = floor((p.X - Center.X) / wx) * wx
      local rz = floor((p.Z - Center.Z) / wz) * wz
      local v = ("%d/%d"):format(rx, rz)
      local Group = Groups[v]
      if not Group then
        Group = Instance.new("Model")
        Group.Parent = workspace
        Group.Name = v
        Groups[v] = Group
      end
      Model.Parent = Group
    end
    for _, Group in next, Groups, nil do
      Group.Parent = m
    end
    return 0.25 * (wx + wz) * 1.4142135623730951
  end
  function LODAddModel(m, d)
    local Parent = m.Parent
    assert(Parent)
    local Spec = {
      Model = m,
      Parent = Parent,
      Center = CalculateCenter(m),
      MinDist = d,
      Loaded = true,
      Locked = false
    }
    table.insert(LODSpecs, Spec)
    return Spec
  end
  function LODAddModels(m, d)
    local Specs = {}
    for _, v in next, m:GetChildren() do
      local Spec = LODAddModel(v, d)
      table.insert(Specs, Spec)
    end
    return Specs
  end
  function LODRemoveModel(m)
    for i = 1, #LODSpecs do
      local Spec = LODSpecs[i]
      if Spec.Model == m then
        LODForceLoad(Spec)
        Spec.Locked = true
      end
    end
    return false
  end
  function LODForceLoad(Spec)
    Spec.Model.Parent = Spec.Parent
    Spec.Loaded = true
    Spec.Locked = true
  end
  local LoadQueue = {}
  local UnloadQueue = {}
  function LODUpdateInterest(p)
	local GFX = UserSettings():GetService("UserGameSettings").SavedQualityLevel
	local Multiplier = GFX.Value > 0 and GFX.Value/2 or 2.5
    LoadQueue = {}
    UnloadQueue = {}
    for _, Spec in next, LODSpecs, nil do
      local d = (Spec.Center - p).magnitude
      Spec.Sort = d
      local ShouldBeLoaded = d < (Spec.MinDist*Multiplier)
      if Spec.Locked then
        ShouldBeLoaded = true
      end
      local Loaded = Spec.Loaded
      if ShouldBeLoaded and not Loaded then
        local i = bfind(LoadQueue, d)
        insert(LoadQueue, i, Spec)
      elseif not ShouldBeLoaded and Loaded then
        local i = bfind(UnloadQueue, d)
        insert(UnloadQueue, i, Spec)
      end
    end
  end
  local function ProcessQueue()
    if #LoadQueue > 0 then
      local Spec = remove(LoadQueue, 1)
      Spec.Model.Parent = Spec.Parent
      Spec.Loaded = true
    end
    if #UnloadQueue > 0 then
      local Spec = remove(UnloadQueue)
      Spec.Model.Parent = nil
      Spec.Loaded = false
    end
  end
  function LODStart(i)
    ThreadLoopAdd(i, ProcessQueue,"LOD Queue Processing")
  end
end

In order to get this to work, you will need to put this code below it.

local ThreadLoopAdd
do
  local Loops = {}
  function ThreadLoopAdd(Interval, Callback, Desc)
    local Thread = {
      t = 0,
      i = Interval,
      c = Callback
    }
    table.insert(Loops, Thread)
  end
  local function Stepped(t, dt)
    for i = 1, #Loops do
      local Loop = Loops[i]
      if t - Loop.t > Loop.i then
        Loop.t = t
        Loop.c(t, dt)
      end
    end
  end
  RunService.Stepped:connect(Stepped)
end

And here’s an example of how to use it:

  local Trees = workspace:FindFirstChild("Trees")
  if Trees then
	LODSplitEach(Trees)
    LODAddModels(Trees, 400)
  end
LODStart(0.1)

The main reason this is so useful is because you can specify certain things to be rendered from certain distances (So you aren’t rendering small objects from miles away just because the players device can handle it)

12 Likes

Since forever AFAIK. Polygons outside of the camera frustum have no use in being seen.

That’s a thread from 5 years ago. Arseny confirmed on Twitter some years ago that Roblox has occlusion culling because CloneTrooper was trying to implement it himself.

My bad. It might have been frustum culling.

1 Like

It’s fine mate, thanks for your information though like this is really useful to me.