Through the past week or so, Iâve been trying to restore Robloxâs bevel feature for Super Nostalgia Zone using Robloxâs in-game CSG API. Iâm happy to announce that I have succeeded!
For those who donât know, bevels are essentially just rounded part edges. Youâve likely seen them on the blocky avatars. Back in the day, these rounded edges used to be visible on all parts, rather than just blocky avatars. They were removed prior to the introduction of dynamic lighting for performance reasons, as Robloxâs graphics system wasnât well optimized at the time.
This bevel system is fully dynamic. Parts are solved and switched out at run-time, and they have a level-of-detail mode which switches them back to normal parts when they arenât close to the camera.
Technical Details
Getting something like this to work nicely ended up being a larger challenge than I had anticipated. There were several hurdles I had to overcome in order to get everything working smoothly.
Buckle up, this is about to get insane.
Surface Texture Nightmares
Since unions do a special texture projection for decals, the surface textures I was using in the game did not align correctly to the bevel unions.
I had a few options for how I could tackle this problem
- Use SurfaceGuis to get the specific texture alignment I wanted
- Switch out textures that align to the grid depending on whether the width/height of the face was even or odd.
- Weld parts onto the union to represent the surfaces.
I wanted to avoid option 1 unless it was an absolute last resort, because I figured it would be very inefficient to have thousands of SurfaceGuis rendering in a scene.
I got some interesting results from option #2 (shown above), but ultimately I decided against it because it would be extremely annoying to get it working right in all conditions, and plus some of the texture ended up leaking into the bevel edges and it wasnât really something I wanted to deal with.
Thus I decided on option 3. Every block in the game now gets 3 additional massless parts welded and parented inside of them, which represent the surfaces for those bevels. The axis parts protrude out slightly in their retrospective axes, while the edges are not occupied by any of their volumes.
Since the parts are always invisible, they donât add any cost to rendering. The trade-off is that they take up more memory, but I was willing to live with it. Iâm hoping Roblox provides better options for me to tackle issues like this in the future.
Dealing with Welds
Because I was now representing bevel surfaces with additional parts, I had to make sure the welds holding the surfaces to the part were never broken by any external forces. This introduced two new constraints:
- I cannot use BreakJoints() directly, I have to iterate through Part:GetJoints() instead.
- Explosions must not be allowed to break joints through their standard behavior.
Thankfully these two problems werenât too difficult for me to deal with. Early on in the development of Super Nostalgia Zone, I decided to handle explosion physics using Lua, and I borrowed a Lua port that Roblox made for Roblox Battle. I figured they ported it from an earlier version of the C++ code behind it, and having more precise control over the explosion physics would be a nice thing to have.
To make sure these joints were never broken, I assigned a special CollectionService tag to them and updated the explosion code to ignore any joints that use this tag, rather than using BreakJoints directly.
Trouble with Build Tools
For the building tools in Super Nostalgia Zone, I use Robloxâs Dragger object, which works nicely as an interface to the default drag tool in Roblox Studio. However, the surface parts I added for bevels proved to cause some issues. Since the drag tool does collision checks, the surface parts were interfering with this collision checking.
To work around this, I offset the surface parts outside of the bevel part, and gave them a near-zero size so they would hopefully not influence anything else in the dragger. I then used SpecialMesh objects with a MeshType of Brick
to project them back into their original size and origin relative to the bevel part.
Level of Detail
As I was testing bevels in-game, I had noticed that my framerate was stuttering every few seconds when a lot of bevels were on screen. I discovered that the solver was generating 92 polygons for each block, rather than the 44 that would actually be needed to properly represent the bevels.
I didnât really want to throw any of the work away, so at the last minute, I threw together a client-side LOD system, where the LOD part uses the same system as the surface parts.
Switching between LODs worked by just flipping the LocalTransparencyModifier of the bevel part and the LOD part, which is a lot faster than having to reparent objects. I utilized a lazy update system similar to Robloxâs CPU light-grid so that detecting and switching LODs would eat up less performance than having to render all of the bevels at once.
I wasnât sure if this would help performance, as Iâve heard previously that SpecialMesh objects arenât good for performance. To my surprise though, it worked really damn well. The effect was pretty much seamless and it completely stopped all of the stuttering I was getting.
Caching and Swapping Blocks
Since bevels are expensive to solve, I wanted to only solve each part size once. I decided to use a specific format string to check against a cache of part sizes. I limited the size of the bevels to a maximum of 30x30x30, as Roblox had done the same with their bevels in 2008.
The server handles the generation of the surface parts as well as the LOD part. I utilize CollectionService tags a lot to help with querying and signaling all of this data around to other scripts in the place (shoutouts to @Tiffblocks for this awesome feature )
Switching parts out at runtime is complicated, because a lot of the games donât anticipate such happening. Iâve had to adjust code in a few places to compensate for this. I decided to phase this in gradually rather than all at once, so that I could diagnose place-specific issues that would arise.
The biggest challenge was getting the model regeneration code to mutually cooperate with the bevel system. This is the system that shows a message on screen, (i.e. âRegenerating TowerâŚâ) and then replaces a model with its original form. They are used frequently in brick-battle places.
To make sure no extra work was done when a model gets regenerated, I setup some coordination between the two systems:
- Regeneration system checks in a copy of the models that it plans to regenerate at some point in the future, then signals the bevel system to start working.
- Bevel system starts generating and swapping out parts in the world, and signals the regeneration system when it is completed.
- Regeneration system waits for that signal, then asks the bevel system to update its backup model with beveled parts.
Without this synchronization, the bevel system would have to solve bevels every time the model was regenerated, which wouldnât be super healthy for the server performance.
Wrapping Up
Alrighty! That was a general overview of whatâs going on behind the scenes for the bevel system in Super Nostalgia Zone. Its⌠hard to say whether anyone has had to use crazy workarounds like this in their games before, but Iâm super happy that none of the work went to waste and it all played out pretty smoothly.
Although I havenât switched bevels on for all places in the game yet, Iâm gradually working through the list. You can check the game out here for yourself: