Fixing blurry textures in UE capture components

· Read in about 5 min · (896 Words)

Have you ever had problems with textures being blurry when you render to textures in Unreal using a Scene Capture Component 2D? Maybe you’ve searched for the problem, and come across forum threads that give really terrible advice, such as turning off texture streaming, or adding a global boost to all texture LODs?

If so, you’re me earlier today. The difference is, I can save you some time and give you a better solution.

Comparison between results

About texture streaming

The most common cause of blurry textures is texture streaming. Unreal tries to not load the full resolution textures for a mesh if it doesn’t need to, to save VRAM. That’s good! It can sometimes result in some temporary blurriness that goes away in a second or so thanks to modern asset loading times. You can hide that by delaying hiding your loading screen by a couple of seconds after you’ve really loaded.

The problem

The problem is that Unreal’s streaming system mostly only cares about game viewport clients when calculating what to stream in. That’s player-posessed camera positions, in practice.

If you use a Scene Capture Component 2D to render something that isn’t close to the player, the streaming system simply won’t be aware of it. If that render uses textures that aren’t close to any player cameras, they’ll end up getting the lowest possible mipmap only, and consequently look terrible.

In the above screenshot, I’m rendering a character customisation view into a texture so that it can be displayed in my UI. The “stage” for that character customisation render is tucked away in a corner of the level, where it can’t usually be seen. But because this “stage” is quite far away from players, it means the texture streaming system won’t stream in any new textures I use in that render, if they’re not being used elsewhere that’s close to a real player.

A simple but sub-optimal solution

One way to address this would be to move your player camera close to where the Scene Capture Component 2D is. You could hide this behind an opaque UI, and either teleport your character there while it’s hidden, or switch to a spectator in the same location. This would work, but it’s inelegant and can cause other problems.

A better solution

The content streaming system in UE actually provides an API for this exact scenario, it’s just not very well advertised and is only available in C++. It’s called IStreamingManager::AddViewInformation.

At any time with this function you can tell the streaming system to consider other view origins, either for a frame or for a period of time. It will then stream in content as if there was a player located at those points. That’s exactly what we need!

For convenience, let’s expose it as a blueprint function.

This is an excerpt from my StevesUEHelpers plugin, which includes this feature as well as many many other useful UE tidbits which I use in every project. So if you want, just grab that. Or put the below code in your own project if you prefer.

// Header file
UCLASS()
class STEVESUEHELPERS_API UStevesBPL : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()
public:
	/**
	 * Let the content streaming system know that there is a viewpoint other than a possessed camera that should be taken
	 * into account when deciding what to stream in. This can be useful when you're using a scene capture component,
	 * which if it's capturing a scene that isn't close to a player, can result in blurry textures.
	 * @param ViewOrigin The world space view point
	 * @param ScreenWidth The width in pixels of the screen being rendered
	 * @param FOV Horizontal field of view, in degrees
	 * @param BoostFactor How much to boost the LOD by (1 being normal, higher being higher detail)
	 * @param bOverrideLocation	Whether this is an override location, which forces the streaming system to ignore all other regular locations
	 * @param Duration How long the streaming system should keep checking this location (in seconds). 0 means just for the next Tick.
	 * @param ActorToBoost Optional pointer to an actor who's textures should have their streaming priority boosted
	 */
	UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Streaming")
	static void AddViewOriginToStreaming(const FVector& ViewOrigin,
	                                     float ScreenWidth,
	                                     float FOV,
	                                     float BoostFactor = 1.0f,
	                                     bool bOverrideLocation = false,
	                                     float Duration = 0.0f,
	                                     AActor* ActorToBoost = nullptr); 
};
                                         
// Source file
#include "ContentStreaming.h"

void UStevesBPL::AddViewOriginToStreaming(const FVector& ViewOrigin,
	float ScreenWidth,
	float FOV,
	float BoostFactor,
	bool bOverrideLocation,
	float Duration,
	AActor* ActorToBoost)
{
	IStreamingManager::Get().AddViewInformation(ViewOrigin,
	                                            ScreenWidth,
	                                            ScreenWidth / FMath::Tan(FMath::DegreesToRadians(FOV * 0.5f)),
	                                            BoostFactor,
	                                            bOverrideLocation,
	                                            Duration,
	                                            ActorToBoost);
}

Now, all we need to do is use that function in our Tick event, populating the parameters from our Scene Capture Component 2D:

Calling AddViewOriginToStreaming

I’m using the simple method here of calling it every frame that my capture component is actively ticking. Instead if you didn’t want a tick you could set up a timer to call this periodically, and use the “Duration” argument to make the request last the time in between calls. Up to you really.

That’s it. Really.

Yep, that’s all there is to it. Only really one line of code when you get down to it. And yet I’ve never seen this solution suggested in lots of forum threads asking about this specific problem. So here’s my contribution to that discussion; it’s several years late for the other people who asked, but at least it might help people who encounter this in future.