THIS IS NOT SUITABLE FOR A PRODUCTION GAME ENVIRONMENT. It is an interesting tech demo prototype, but is absolutely an abuse of ViewportFrames. Note that it slows exponentially- the more objects, the slower it gets. I’ve optimized plenty, but it is not enough.
Before we begin:
What is a Dual Render scope system?
It is a system that allows the game to make the scope a separate render, so the player doesn’t lose peripheral vision. It’s a bit hard to describe, so I’d rather just show you examples:
Arma 3 and Escape From Tarkov
The Roblox engine had no way to do this… until ViewportFrames! I created a ViewportFrame security camera system and open sourced it, and @SammySpicer suggested I try a dual render scope system! So I built the system up from scratch again, and created this new thread dedicated to it!
With that said, I present: my system!
As you can see in the top left corner, I’m getting a perfect 60FPS!
In addition, the script is running very efficiently:
(Special thank you to @UghLily for helping with the optimization process!)
Edit: There were some issues that have been resolved.
Rather than go redo all the files to support this change, go here for a step by step of how to patch your files.
Issue A: Scaling
My scaling assumed that the part wasn’t rotated, so it broke and distorted when tipped. I’ve redone it using proper
A = bh by getting 3 corners and getting the magnitude without depth.
Issue B: Rotation
When the ScreenPart tips, the GUI wouldn’t. This is also fixed in the patch step-by-step link.
Issue C: Mapping
Another mistake is that the GUI mapped to the center of the ScreenPart, rather than to the face. This has been solved and is updated in the patch step-by-step.
How was this done?
Well, what does it need to do?
We need it to replicate the entire workspace into the ViewportFrame (and keep it refreshed), and keep the ViewportFrame’s CurrentCamera aligned with the scope.
A naive approach:
Every frame, clear the ViewportFrame, then reclone the workspace and update the CurrentCamera CFrame.
Now, obviously this is a terrible idea. So what will we do?
- The Camera:
The CurrentCamera is an easy solve! We can use a :GetPropertyChangedSignal(“CFrame”) to only update it when needed!
Now for the hard part:
- The Objects.
First, we need to prioritize. Most important is definitely Humanoid objects- NPCs and Players. We want to render these with the maximum FPS we can get, using Heartbeat, updating them every frame.
Next up is your average part. We can use a simple
wait() to make them update at a viable FPS; it will slow down its own refresh rate in order to keep the game running as fast as possible. (This can make it jittery, but moving parts aren’t that common so we can get away with it.) We don’t simply update the part at this FPS; instead, we check if it needs an update. If the CFrame has changed, then we update our ViewportFrame clone part. This way, we only do work if needed! But that’s not all we can do. We can choose to only update the part if it’s in front of us! This way, we don’t bother moving parts that are behind us! Now, I know what you’re thinking. In a previous post, I said:
But I’ve been taught a new trick for this, thanks to @denissini on Discord! We can do some angle checks to find out if the part is in front of us! (Note: This doesn’t check if it is visible, as it will update parts behind walls. It only check if we might be able to see it.) So now we only update parts when truly needed!
Now, we (as the creators of the game place) happen to know that the Map never moves. Ever. So why bother doing refresh checks? We render it once, and then never check it again. This means that the map (which is most of the workspace) is barely a performance hit at all!
This allows us to increase map size without a severe performance drop!
Using this implementation, I got it working at 60FPS!
We want to blur the peripheral vision, for this awesome “focus” effect.
My original version was a SurfaceGUI, and a BlurEffect would blur out the scope as well!
Instead, we’ll have to use a ScreenGui! We can use :WorldToScreenPoint() to find out where the face of the scope is, and put the GUI there!
It was suggested and prototyped by @grilme99 to create a night vision edition! Because ViewportFrames don’t render full lighting, it will always be well lit even when the world is dark. That means, all we need to do it put a green overlay and darken the world!
I will include this version in the final product section.
The amazing @Ugh_Lily (Twitter) helped me optimize this even more, eliminating many function calls and table indexes! Thanks to her, it runs much more smoothly than before! (Remember how we made the refresh rate slow down to maintain 60 FPS? Well, thanks to her, it doesn’t need to slow down as much. It’s super smooth )
Here is the place file! Feel free to mess around and learn! If you find any bugs, please report them!
Dual Render Scope.rbxl (406.9 KB)
Here is the night vision edition! It’s pretty sweet!
Dual Render Scope NV.rbxl (406.9 KB)
Bugs (Report more if you find them!)
- [FIXED] Render is slightly vertically stretched (This was due to the SurfaceGui CanvasSize not being a square)
- [FIXED] Objects created during runtime don’t render (I’d forgotten to add a .ChildAdded)
- [FIXED] Objects inside Objects get created twice (I cloned the whole object, and then clone each descendant itself, making two of each descendant)
The lack of bug reports is scaring me… there’s no way something I made actually works perfectly
A lot of people have asked how this holds up in various environments, so I tested it.
My PC Specs:
Processor: Intel Core i7 CPU 920 @ 2.67GHz
Graphics: NVIDIA GeForce GTX 750 Ti
OS: Windows 10 Home 64-bit
My PC is mid-range, perfect for average testing. Not a gaming PC, but not a toaster/laptop.
20 Humanoids, Empty baseplate, No other moving parts
Result- 60FPS, ~10.8% CPU Usage
20 Humanoids, Small map, 50 moving parts
Result- 60FPS, ~15.2% CPU Usage (Note: Moving parts are no longer being refreshed at 60FPS. They slowed themselves down as per our prioritized optimizing, but are still smooth.)
50 Humanoids, Full map, 300 moving parts
For this one, recording mattered, so here’s both results:
When recording: 14.2FPS, ~9.2% CPU Usage
When not recording: 18.9FPS, ~12.7% CPU Usage
The map, as we know, is not a performance hit. (We ran the full map at 60FPS in a previous video) Therefore, we need to determine which factor caused this drastic drop.
50 Humanoids, Full map, No other moving parts
Result- 48FPS, ~26.0% CPU Usage
For this one, recording didn’t change it.
No Humanoids, Full map, 300 moving parts
Result- 60FPS, ~5.7% CPU Usage
For this one, recording mattered. When not recording, the balls were refreshed a bit smoother.
I stand by my original statement. This is not conducive to smooth gameplay.
An average match in Operation Scorpion will have about 12 humanoids, a full map, and probably about 100 moving parts at a time. This would leave the game at about 55FPS and the scope with a 40FPS refresh rate. This is not optimal, especially since other player might have slower PCs than I do.
The ViewportFrames themselves cannot render Terrain, particles, or lighting. This means that dark rooms will still appear well lit, any particle effects (muzzle flashes, breaking glass, blood) will not appear, and maps with terrain won’t work. This is not a viable method. We will simply have to hope Roblox adds an actual Dual Render system!
I hope you enjoy!
When I saw this, I literally screamed so loud my family came running over.