CullingService V2 - Custom Client Sided Culling/Streaming Module

V2 UPDATE SUMMARY

  • Added animation framework for developers to control streaming aesthetics
  • Added five animation presets: Rise, CartoonRise, SpaceShuttle, Transparency, and Blink
  • Fully modular (previously involved some hard coding related to the default ‘Short,’ ‘Medium,’ and ‘Long’ distance folders)
  • Significantly more reliable (now employs back-up checking)

Full V2 update write-up: CullingService V2 - Custom Client Sided Culling/Streaming Module - #36 by https_KingPie


Hi there and happy middle of the summer!

CullingService (CS) is a tool that I developed for my current project. It’s a configurable approach to content streaming that lets developers control, specifically, what is and isn’t streamed in their experience. I realized that there wasn’t anything like it on the DevForum and decided to open-source it for large scale use. Feel free to use it in your games or as a learning resource.

Check out the GitHub page to view the source code (in Rojo format): GitHub Repository
Download the model here: Roblox Library Model
Download the plugin here: CullingService Plugin

Example place (open sourced) with CullingService in action: Example Place

Read below for set-up information


What is Culling and Why Should I Care?

Culling, by definition, is a way of controlling rendering and typically handled by the engine. This is a necessity in games that have large maps, high detail, or a combination of both. If you look closely while playing many open-world games (Skyrim, Assassin’s Creed, etc.), you might notice that things at the edge of your screen load in or out depending on your distance). This is to reduce the performance costs of large and detailed maps. Conversely, you can flip the definition and reason that using culling also allows you to increase the detail of your map, and this is true too.

Not making sense? Think of it as a custom StreamingEnabled.

How Does it Work?

Using CS, you (the developer) assign anchor points to models that you want to be culled in and out. CS subdivides the map into cube regions (the size of these are controllable). As the player enters a region, CS checks to see if anchor parts in the regions adjacent to the one the player is currently in are within a distance to be culled. If they are, the model is culled in. Doing this increases performance by reducing the amount of distance calculations performed.

Here are some pictures to visualize what’s going

Map is divided into regions

External Media

Adjacent regions are checked (they do go away after you leave them, I just forgot to add that to the visualization)

External Media

Based on your region and distance, instances are culled in and out accordingly (modify your settings to make it look less choppy - the current settings are in place to clearly demonstrate the long, medium, and short ranges which can be culled)

External Media

CullingService now supports moving parts - read more about this in Advantages + Limitations

More Information on…

Advantages + Why Not Use StreamingEnabled?

Great question - why not use a tool built into the engine? CS offers more control than StreamingEnabled and allows you to dodge problems that have plagued StreamingEnabled. Here are a couple advantages/issues solved:

  • Create specific render distances for various types of instances. For example: in a forest, cull in trees at a further distance (because they are big) and only cull in small details, like flowers, when the player comes to a much shorter distance.
  • Customize everything. Customize culling distances (as mentioned above) and other settings. Because these are customizable, this allows you to provide players options, such as a render setting of “low, medium, high, ultra high, etc.” if you wish. StreamingEnabled does not allow it or related properties to be modified via code, whereas CS allows you to make immediate changes.
  • If you use duplicate models (such as trees), you’re in luck! Once you set up the distance folders for one model, all of them will be set up. Only one model is stored, which enhances performance and expedites set-up.
  • Only cull what you want to cull. If you have certain parts that absolutely should not be culled, CS allows you to simply, easily, and quickly exclude them from being affected. This tackles the problem of all parts being uniformly streamed in and out by StreamingEnabled, which can be a massive issue for programmers.
  • Control CS via plugin to set things up. This means less work for the individual client and more work for you! Kidding, the plugin is very easy to use and makes set-up as simple as selecting the models in the Explorer menu that you want to cull and hitting a button
  • Cull out parts of the map (or the entire map) as you develop, reducing the overhead costs - especially for lower-end PCs. Cull it back in as desired.
  • Quickly integrate existing games with this. Anchor points can be automatically created through the plugin - just select your models and click a button.
  • Simple. Call CullingService.Initialize(). Afterwards, you can always pause or resume CS using CullingService:Pause() or CullingService:Resume()
  • New: CullingService allows support for moving models. Rather than move the entire model, all you need to do is move the associated anchor point (this makes CullingService minimally intrusive). CullingService features an optional built-in welding to support this (however, this only happens for moving parts)

Limitations

  • Only models can be culled
  • Models must have (kinda) unique names. Models with the same name will be presumed to be the same model. Therefore, duplicate models that are the same in every aspect (minus position and orientation which will automatically be handled once they are assigned an anchor point) can (and should) have the same name, whereas models that are different should be assigned a different name. Basically, don’t name all of your models “Model”
  • Models which are being culled will have their PrimaryParts overwritten (to utilize movement controls)
  • Only what you specify to be culled in your game design (i.e. Studio) will be culled. This can be problematic for things created mid-game. CS can be thought of as ‘semi-automatic’. At this stage, it can’t do it all.
  • This has all purpose-use; however, it does not have an extreme control (used loosely, there are still lots of settings to be modified) set that allows it to be useful in every game. It may be more useful, in certain situations, to use StreamingEnabled or to fork this.
  • Moving parts must have the same render distance (i.e. put them all in either Long, Medium, or Short) and only one distance folder in the model. Having multiple may break it.

How To Use It

  1. Install the plugin (linked above) and open Studio
  2. Create the following folders as children of workspace: AnchorPoints, CulledObjects, CullingRegions
  3. Create the following folders as children of ReplicatedStorage: ModelStorage, NonCulledObjects
  4. Install the CullingService model via Roblox (or import directly from GitHub)
  5. If installing from Roblox, put UtilityModules and CullingService in ReplicatedStorage
  6. Create a LocalScript on the client, require CullingService, and call CullingService.Initialize()
  7. Open the plugin and hit the initialize button (this will check that all folders in step 2 are created)
  8. Create folders in your model with the names: Short, Medium, Long (these will decide which parts are culled in and out at which ranges). Place the parts within these relevant folders to control which culling range they fall into.
  9. Select (i.e. click on/highlight in your explorer menu) the model (or all models which have been set up) and generate anchor points for your selection via the plugin
  10. With the same selection, click add selection model storage [Note: duplicate models (with the same name) are automatically handled, so feel free to mass select many models]
  11. You are now free to delete those models from your workspace - they will be automatically culled in once the player enters their culling radius
  12. If you wish to adjust the model, simply delete the anchor point associated with that specific model (or adjust the position of the anchor point)
  13. Adjust settings in CullingService.Settings to create a smooth experience for your players. In it’s most ideal state, CS will run and players will not even realize it. If you’re able to tweak the settings to give the illusion of a fully loaded map while things are actually being efficiently culled in and out, you’ve hit adjusted your Settings perfectly. Don’t stress if you aren’t able to do that (since you’ll also want to factor in performance costs of culling more instances in), but I would advise this to be the goal.

I highly recommend saving a copy of the open-sourced test game (linked above) and playing around with the plugin there - it should make a lot more sense

Settings Overview

  • Distances: Customize the various culling distances. These are measure in studs, so these are the distances that a player must enter to have instances in the relevant distance folder of the model culled in. If they exceed this distance, those instances will be culled out. Search radius is the distance
  • Paused: boolean value that is set via CullingService:Resume() and CullingService:Pause()
  • Region Length: the length (in studs) of the invisible regions that will divide up your map. Make sure that the length of this is at least 1/3rd of the Long Distance (although I would recommend making sure it’s around half of that distance)
  • Wait Time: this is the time that CullingService checks for adjustments (updating ranges, culling whole models in, culling models out)
  • Welded Anchor Points: table of strings for anchor points that should be welded to their respective models. Use this to make models moveable and make changes to the anchor point’s CFrame value

API

module:CreateSignalForModelCullIn(ModelName: string)
Returns a Signal which is fired every time a model with the name provided is culled in. Signal provides the model as a first argument

module:CreateSignalForModelCullOut(ModelName: string)
Returns a Signal which is fired every time a model with the name provided is culled out. Signal provides the model (in this case, nil) as a first argument

module:CreateSignalForModelCullInAtRange(ModelName: string, RangeName: string)
Returns a Signal which is fired every time a model with the name provided is culled in. When the Signal is fired, it provides the model (in this case nil) as the first parameter

module:CreateSignalForModelCullOutAtRange(ModelName: string, RangeName: string)
Returns a Signal which is fired every time a model with the name provided is culled out. When the Signal is fired, it provides the model as the first parameter

module:Refresh
Refreshes CullingService, essentially manually calling the Cull function around the player

module:Resume
Resumes CullingService

module:Pause
Pauses CullingService

module:ManualCull(Position: Vector3) Culls around a designated position versus just the player. Normal behavior can be resumed using module:Resume`

Example API usage (direct from my project, which makes a boat sway with the waves when culled in)

local function ListenForShips()
    for _, ShipName in pairs (AllShipNames) do
        local ShipAdded = CullingService:CreateSignalForModelCullIn(ShipName)

        ShipAdded:Connect(function(Ship: Model)
            HandleShip(Ship)
        end)
    end
end

Closing Thoughts

This was a headache to work on and a very niche subject in programming (since engines typically handle this), so I wanted to open source it to give others a stepping stone in their creations. Feel free to use it as a development tool, learning resource, or whatever else you think of. If you think of improvements, feel free to submit a pull request to the GitHub repo. Unfortunately, I don’t have a ton of time right now to make a tutorial video, but it is relatively self explanatory. If anyone is able to do so, let me know and I will link it in the thread.

Special thanks to (in no particular order besides alphabetical) @Ahlvie, @boatbomber, and @DevBuckette who were immensely useful in helping me understand the concept and how to approach the topic. Huge thanks to @XAXA for her BoundingBox formula, which allowed me to create a more accurate BoundingBox of the workspace and avoid problems caused by the Terrain instance. Regions are handled using @VerdommeManDevAcc 's Object Tracker & Area Manager module (which I always find extremely useful).

139 Likes

I find this useful. One question: Does the bounding boxes affect performance? The first image actually scares me.

1 Like

In all my tests it has not. Any (potential) performance hits will only happen at the beginning of the player’s experience, and this is also offset by not having to load the entire map in.

If you use reasonable region sizes as well (ex: not chopping the entire map into 10x10 stud boxes) the amount of regions created will be fewer which results in less initial computations. If there’s an exception to this (such as a huge map that will require a large amount of regions by nature), adding a small wait to extend the calculations performed by the RegionHandling.GenerateInternalRegions() will help reduce the strain on the client.

2 Likes

I have a suggestion, instead of working with different folders you can use tags from CollectionService instead. This would be more flexible and easier to work with imo.

2 Likes

Thanks for the suggestion! The feedback is really greatly appreciated.

I originally opted for folders just for visual organization since it cleans up the Explorer display when lots of regions or anchor points are being created. Since part of this relies on being able to get a live picture of what this looks like in action while in Studio and manage the system via plugin, a clear and neat Explorer is handy.

You’re right that this would make it more flexible though, and I may make the switch over in a later update if I can find a way to manage to keep the Explorer tidy as well.

4 Likes

Will this work with both Streaming Enabled + This system at the same time without any complications?

1 Like

I haven’t tested it and can’t promise 0 complications, but using both seems redundant.

If StreamingEnabled works fine for you, your project has been set up to work with it, or you have no need of the additional features CullingService offers then you may be better off sticking to that.

CullingService was developed to offer more features/customization than StreamingEnabled and solve certain problems or complications that StreamingEnabled brings along, specifically coming from the development of my current project. If your project is at a point where you find both equally attractive, then it may be either a matter of personal preference or your project’s intended end state which decides which is best for you.

2 Likes

Added an update (ability to cull moving models):

  • Remove the Search Radius setting, since that’s basically obsolete due to how regions are tracked
  • Added support for culling moving models. Read the Advantages + Limitations + Settings Overview sections for more information. TL;DR is that you can make changes to the anchor point’s CFrame, because it will be automatically welded; all instances within the model need to be in the same distance folder + only have one distance folder; and you need to add the name of the anchor point to the table in Settings called “Welded Anchor Points”
  • Speak of the devil! Added a Settings Overview section to this thread
  • Put the main loop in a coroutine so that you don’t have to wrap it in one yourself (or do other methods)
  • Standardized the formatting on settings
2 Likes

Bug Fix

  • Pushed a fix that restricted the size of the system (because I was working with position rather than CFrames + was using a part to visualize the CFrame and working with values associated with the BoundingBox rather than the true values)
  • CullingService should now scale as intended for maps of any size
1 Like

This is really helpful! I’ve always wanted the ability to be able to control more than what StreamingEnabled gave us.

2 Likes

Quick update

Added a new setting called UseParts which allows you to enable or disable the use of parts in CullingService.

Not sure if anyone was using the regions created by CullingService in other parts of their code and relied on the actual regions being created, hence why this is now a setting versus a switch in behavior. However, now this means that CullingService adds 0 parts to your game (something that I realized was a very ironic downside in a module that aims to reduce the amount of total parts loaded in the first place)!

There should be 0 issues updating any projects to this version, just remember to adjust the settings accordingly. As always, feel free to play around with this in the example place. The change should be reflected on the GitHub and Roblox Library as well.

2 Likes

Just a quick suggestion, if you dont have the folder(s) created before enabling/installing the plugin it will give out this error


And looking through the plugin code I noticed as soon as the plugin is enabled it searches for the folders, so just small a suggestion to make it look for the folders either on pressing initialize button or whenever you click one of the required buttons

Its not really a big deal all you have to do is make the missing folders and re-open the file game might be confusing to new users that don’t know

1 Like

POV: Anchor points have collisions enabled
Good module though, definitely will be put to use in my project Feudal Japan.

1 Like

If so, I think it should be possible to choose between the two instead of having the original system completely replaced.

I have a question, the module is really cool tho there isn’t collision right on the server ? So an IA will just go throught object ? I have the idea to create for each part being culled a replica of them with a part with transparency 1 even tho idk, if it’s a good choice

so uh, This is a cool module. But why cant it identify models that have the same name? its kinda… bizzare…

Correct, there is not a collision on the server, models only appear for the player. However, you have the option of controlling which models are affected by CS. If a model needs to have universal collisions or appear on the server, just don’t set up an anchor point for it. This affords you the control to specify exactly which instances are affected by CS and which aren’t, while still retaining the performance benefits of a culling system.

The way CS works, at its most basic level, is that when you get within a certain distance to an anchor point, the player loads in a model with the same name as the anchor point.

Because CS is set up to require everything to be culled out initially, using name associations is the easiest and most performant way to ensure that everything goes in the right place. This means that minimal models need to be stored in ModelStorage (since duplicate models of common items like trees, lightposts, crates, etc. need only be cloned and have their CFrames adjusted, versus storing every single model in the game), instances like ObjectValues or even attributes can be avoided (which, in bulk, will still be less performant than simply referencing the name property) and it reduces the initial strain on players compared to culling everything out upon join, which could lead to crashing or significant performance issues on initial join.

Plus, it’s just a good habit to not have all of your models named “Model”!

I guess when it comes to dummies or normal models that arent that important. I guess the best way to deal with this is to add a random number to their name? I plan on using this module in combination with well ContentStreaming…

Because to my knowledge, isnt occlusion culling what most big games used? they unrender things that cant be seen by the player

CS can be used for important models. All models don’t need unique names, only models which aren’t duplicates. For example, if you have three different variations of a tree design, you can name them “Tree1”, “Tree2”, and “Tree3” and leave it at that. Then when you create anchor points appropriately named “Tree1”, “Tree2”, and “Tree3” (all this can be done via the plugin), and only three actual models will be saved into ModelStorage, even if you end up having hundreds of anchor points.

If you turn on Workspace.StreamingEnabled (what I think you mean when you say using ContentStreaming) you are doing something very similar. It’s not actually occlusion culling, it takes into account where the player is in relation to other instances. If you look, there’s properties for Workspace.StreamingMinRadius and Workspace.StreamingTargetRadius. CS operates with a similar philosophy, but you’re getting more control over what specifically is streamed in and out. The increased control is especially useful for programmers, since you have a specific and exact idea of exactly what is streaming in and out, as well as when. You also have manual control over the distances that specific instances included in Short, Medium, and Long folders will be culled in, allowing for manual control of level of detail and opening the door to player’s customizing it themselves (ex: allowing settings for low, medium, high, or ultra streaming settings).

One of the downsides of manual control (CS) versus automatic control (StreamingEnabled) is that it’s simply going to take a bit longer to set up. I created a plugin which is designed to expedite the process significantly, making it so that your longest and most arduous step is simply give your models unique names if they are distinct models (again, to clarify all your models do not need unique names if they are the same model, but placed in a different position or orientation, like the tree example I gave earlier). However, one of the big upsides of manual control is that you have precise control over exactly what is happening in your game, what models are being affected (and what models aren’t), levels of detail, configuration settings, etc.

If that’s still not making sense, I’d recommend checking out the open-sourced example place and messing around with the configurations there.

Oh ok, thanks now i understand.

I actually plan on making an Open Sourced Module/Script that allows devs to implement Occlusion Culling. Its really simple if you think about it, the module/script also takes into account what instances that shouldnt be affected. This doesnt reparent (As you should know that isnt the most performant way of doing things), instead it manipulates the cframes/vector3 of the models/parts and sends them extremely far away (Similar to partCache) to the extent of it not being rendered any more. Forgot to say, the script constantly checks wether the position of affected models/parts is within view, if it is, then obvi it will bring them back.