I have found a solution. I want to take some time to do a more comprehensive write-up at some point, but here’s the broad strokes:
We are essentially going to use a render target to fake a render layer such as what you may find in another engine like Godot or Unity.
First, a couple caveats: this method will not work with any translucent materials on your meshes that you want to render over everything. Translucent materials will simply be invisible. This is unfortunate, but minor in my case. Also, using a render target of this size in this way may introduce significant overhead. In our case it was okay, but it’s up to you to profile and ensure that it will hit your project’s performance targets.
-
On begin play of your CameraManager, create a render target with a size equal to the platform’s native resolution. I had to write my own c++ function to get the native resolution, as the built-in GetDefaultResolution was returning 0, 0 for some reason. You can probably find a way around this in BP by simply keeping track of your last resolution setting. Default color settings are fine.
- It is important you do this with blueprint nodes rather than creating a render target asset in the editor, because different people will have different native resolutions based on their monitor.
- If you don’t know about the CameraManager class, check it out here (UE4 Docs) and here (Community Wiki, more helpful imo)
-
Add a SceneCapture component to your CameraManager. It is important you add the component in here rather than in your character, as the CameraManager’s transform matches with any camera animations such as screen shake.
-
Using blueprint nodes, set the render target you created as the target texture for the SceneCapture component. This is shown in the screenshot in Step 1.
-
Set the SceneCapture’s color mode to any of the FinalColor modes, and set the SceneCapture’s show mode to ShowOnlyActors. Then populate the array with the actors you wish to render over everything (for example, your player character).
-
To avoid objects being rendered to both the RenderTarget and your player’s normal camera, causing double rendering, make sure that all mesh (both skeletal and static) components you wish to render over everything have “Visible in Scene Capture Only” checked.
-
It’s not apparent yet, but we have a bit of an issue. If you were to attempt to draw your RenderTarget to the screen now, you will find that it has no Alpha Channel. This is because the FinalColor modes on the SceneCapture component do not include opacity. We will have to generate our own mask. We will use CustomDepth for this.
-
Ensure that any object you with to render over everything has Render Custom Depth enabled. If you use custom depth for other effects, you should filter by stencil value.
-
Create a new PostProcess material - RenderOverMask. This material will ensure that the SceneCapture component perfectly renders 0 in all RGB channels. Set it as the Post Process Material in your SceneCatpture component. We are doing this because depending on your post process settings, the background of your RenderTarget (the portion that does not render anything) may not be pure black (mine was a deep maroon). Thus, we use the CustomDepth stencil to ensure that everything not rendered is exactly 0 in all RGB channels.
-
Create a new PostProcess material. This will be the material that renders your RenderTarget to the player’s screen. Add a texture sample to it. You will be setting this equal to your created render target later. Below, I named mine “Objects”. The Alpha portion of the lerp is length of the Render Target’s channels, slightly blurred, multiplied by a large number, and clamped from 0-1. Why are we doing it this way? Why not just use custom depth? Well, this material is on the player’s camera, not your SceneCapture component. Thus, the CustomDepth objects will not appear here since we enabled “Visible in Scene Capture Only” in step 5. So unfortunately, we kind of have to BS our way into a mask. The SpiralBlur is to help fill in any holes from your RenderOver meshes hitting exactly 0 in their RGB channels. The 5000 multiplier is just to tighten the mask and ensure it is always 0 or 1. 0 * 5000 = 0. Thus, our material in step 8 ensures our mask stays intact.
-
Finally, put the PostProcess material on the player’s screen using your preferred method - I am using a CameraModifier I have named RenderOverModifier. Do not forget to set the RenderTarget as the texture parameter in your material using a Dynamic Material Instance:
There are some other details I have yet to work out, such as how it looks at lower resolutions, etc. But for now it seems to scale down fine so long as the player sticks to resolutions that match their monitor’s aspect ratio. It’s possible you could refresh the render target on resolution change to mitigate that. You may even be able to provide a separate resolution scale for this to help people with lower end hardware.
You can add/remove actors to this fake render layer at runtime like so:
- There is probably a more optimized way to do this without doing the GetComponentsByClass call.
Results: The arms and explosive barrel are rendered over everything with no clipping.