You’re looking at an unfortunate combination of several features. I’m not sure we can fix this in short term, but I can tell you what’s going on.
ROBLOX uses a clustering system which is somewhere between Unity’s static and dynamic batching, but unique in its own way (I think we developed it before Unity got dynamic batching). It solves a problem of draw calls - to be efficient on CPU, you can’t dispatch too many draws to the GPU. However, you may want to render a lot of parts - which is where the system comes in (known as featherweight parts).
We group parts into big clusters spatially - parts in the same spatial region are put in the same cluster. You can sort-of see the cluster distribution if you enable “Show bounding boxes” in render settings. We used to have a debug visualization mode that would paint clusters in different colors but it does not work anymore.
Every part belongs to a cluster; within a cluster every part belongs to a batch - parts from a single batch are drawn using one draw call. The more parts there is in a single batch, the more efficient it is to draw.
A cluster can be static (= contains parts that don’t change) or dynamic (= contains moving parts). If a cframe of a part changes, part becomes dynamic; if it stops changing for a second, it becomes static again.
If the part is in a dynamic cluster, updating it’s CFrame is almost free - but it’s slightly more expensive on GPU to render (we use a slightly more complex vertex shader), and also we’re more limited in grouping parts into draw calls - we can pack a lot of small parts in one batch if they’re static (up to 2.7k small blocks, I think), but we can pack at most 72 parts into one batch on desktop (32 on mobile) due to hardware restrictions.
If any property of a part that’s not a cframe changes, we have to “rebake” the entire cluster, redistributing parts between draw calls and recreating GPU data for them. This can happen semi-frequently and is thus heavily optimized. In addition, we limit the number of regenerations per frame - so we can delay visual updates in part properties but keep render latency down.
Finally, since rebaking the cluster can be expensive for huge clusters, we artificially limit the number of parts in a cluster to 1000 - so a lot of small parts will still get split between two or more clusters to optimize for regeneration time.
Now, here’s what happens in various cases:
-
When you start the scene, all parts are initially anchored so we assume that they won’t move and put them in static clusters. You have a lot of parts so they are split into 3 clusters. Since you add parts gradually and not instantly, this results in a nice spatial split between clusters - our logic for defining a cluster for a part that belongs to an already full region is very simply “create a new cluster in this region if necessary”).
-
In case you only update transparency, whenever you update transparency we have to rebake the affected clusters - since we are limiting the updates per frame, sometimes some clusters won’t get enough time to update - but the entire cluster is always in sync. You can actually see that the wave in this case is occasionally delayed at the boundary between clusters.
-
In case you only update the cframe, when you update it for the first time all parts move to dynamic clusters. Since all parts move to other clusters in the same frame, and we use an unordered container for keeping track of updates, the split between two clusters within the same region (as I mentioned, you have more than 1000 parts in the same spatial region so we have to split) is arbitrary - you get pseudo-random split into one big (1000 part) cluster and one smaller one. However, clusters are dynamic so cframe updates are instantaneous and you don’t observe any issues.
-
Now, when you update both cframe and transparency we go to the case of 3 - with the difference that while cframe updates are instantaneous, transparency updates require rebaking the cluster. Since the split of parts between clusters is pseudo-random, if we don’t update both clusters in the same frame - which we don’t always have time for (see 2.), you see weird visual hitching.
An additional problem that you’re hitting is that we don’t really have a good prioritization scheme for property updates for clusters, so you can sometimes miss updates for one cluster for many consecutive frames. All of this also depends on how powerful your computer is.
Phew.
We don’t make decisions that depend on what’s visible and what’s not because that changes too frequently. You don’t want to turn around only to see nothing or to see stale state. We could make decision based on distance to the character but currently don’t.