Oh yeah, I definitely recommend stereo layers for this. In fact I wrote a component to make that easier. My component extends a widget component to keep the UMG UI hidden in game but render to a stereo layer component. The UI looks super sharp but no longer clips as part of the world geometry since it’s not rendered by Unreal, but by the VR compositor system. It’s great for game UI though.
//Copyright 2017 Rival Dust, Corp. All Rights Reserved.
#pragma once
#include "Components/WidgetComponent.h"
#include "RDVRStereoWidgetComponent.generated.h"
class UStereoLayerComponent;
// TODO: Make a non VR mode that disables the use of the stereo layer so you can just reuse the same 3D UI outside of VR without creating a version of the 3D UI actor that is using a UWidgetComponent
/**
* This helps set up the Widget component so it's rendered into a Stereo component.
* This helps with VR HUD.
* The widget itself will be invisible in the world but still exist so it can be interacted with.
* The widget won't be clipped by 3D geometry in the world because it's rendered in a separate pass, but the UI
* ends up looking crisp and clean instead of all blurry and dithered the way a normal WidgetComponent would appear.
* Pretty sure this doesn't work unless you're rendering it in a VR headset.
*/
UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))
class RDVIRTUALREALITY_API URDVRStereoWidgetComponent : public UWidgetComponent
{
GENERATED_BODY()
public:
URDVRStereoWidgetComponent();
virtual void UpdateRenderTarget(FIntPoint DesiredRenderTargetSize) override;
protected:
virtual void InitializeComponent() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport) override;
public:
/**
* If set, this component will automatically create a stereo layer component.
* If false, you should manually attach an existing one.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "StereoLayer")
bool bAutoCreateStereoLayer = true;
/**
* If set, this component will automatically destroy the attached stereo layer component.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "StereoLayer")
bool bAutoDestroyStereoLayer = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "StereoLayer")
int32 AutoCreatedStereoLayerPriority;
/**
* Call this to manually attach a stereo layer component
*/
UFUNCTION(BlueprintCallable, Category = "StereoLayer")
void AttachStereoLayer(UStereoLayerComponent* StereoLayerComponent);
/**
* Call this if you change dimensions or other properties about the Widget so it tries to make the attached stereo layer match up exactly
*/
UFUNCTION(BlueprintCallable, Category = "StereoLayer")
void SyncStereoLayerProperties();
/**
* This is hopefully a temporary workaround for a bug: https://forums.unrealengine.com/t/world-locked-stereo-layer-lags-forward-opposite-of-lags-behind-from-its-location-as-you-move/526491/2
* World locked stereo layers have a bug where if the camera moves in the world, like when the player moves, the layers are off by a frame
* This will instead use tracker locked stereo layers that position themselves relative to a VR origin object to simulate world locked
* Usually you would want to pass a VR Origin scene component that is part of your character
* I also tried Face Locked, and using the camera component, but that didn't update cleanly as you look around
*/
UFUNCTION(BlueprintCallable, Category = "StereoLayer")
void SetRelativeComponent(USceneComponent* InRelativeComponent);
FORCEINLINE USceneComponent* GetRelativeComponent() const
{
return RelativeComponent;
}
protected:
void CreateStereoLayerIfNeeded();
UPROPERTY(BlueprintReadOnly, meta = (AllowPrivateAccess = true), Category = "StereoLayer")
UStereoLayerComponent* AttachedStereoLayerComponent;
// Remove relative component when the world locked bug is fixed
UPROPERTY(BlueprintReadOnly, meta = (AllowPrivateAccess = true), Category = "StereoLayer")
USceneComponent* RelativeComponent;
FDelegateHandle RelativeComponentTransformUpdateDelegateHandle;
};
//Copyright 2017 Rival Dust, Corp. All Rights Reserved.
#include "RDVRStereoWidgetComponent.h"
#include "Engine/TextureRenderTarget2D.h"
#include "RDVRStereoLayerComponent.h"
URDVRStereoWidgetComponent::URDVRStereoWidgetComponent()
: Super()
{
bWantsInitializeComponent = true;
bWantsOnUpdateTransform = true;
BlendMode = EWidgetBlendMode::Transparent;
}
void URDVRStereoWidgetComponent::UpdateRenderTarget(FIntPoint DesiredRenderTargetSize)
{
Super::UpdateRenderTarget(DesiredRenderTargetSize);
if (AttachedStereoLayerComponent)
{
AttachedStereoLayerComponent->SetTexture(RenderTarget);
// This fixes the stereo layer sometimes not updating if it's created super early and never moves due to being head space UI
// It doesn't seem like that big a performance hit to always mark dirty since hand space UI updates constantly with no issues
AttachedStereoLayerComponent->MarkStereoLayerDirty();
AttachedStereoLayerComponent->MarkTextureForUpdate();
}
}
void URDVRStereoWidgetComponent::InitializeComponent()
{
Super::InitializeComponent();
CreateStereoLayerIfNeeded();
// Force material to invisible, but here instead of the constructor so we still see the widget in the blueprint editor
if (UMaterialInterface* InvisibleMaterial = LoadObject<UMaterialInterface>(nullptr, TEXT("/RDVirtualReality/Materials/M_UI_RDVRWidgetInvisible")))
{
TranslucentMaterial = InvisibleMaterial;
TranslucentMaterial_OneSided = InvisibleMaterial;
OpaqueMaterial = InvisibleMaterial;
OpaqueMaterial_OneSided = InvisibleMaterial;
MaskedMaterial = InvisibleMaterial;
MaskedMaterial_OneSided = InvisibleMaterial;
}
}
void URDVRStereoWidgetComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (EndPlayReason == EEndPlayReason::Destroyed)
{
if (AttachedStereoLayerComponent && bAutoDestroyStereoLayer)
{
AttachedStereoLayerComponent->DestroyComponent();
}
}
Super::EndPlay(EndPlayReason);
}
void URDVRStereoWidgetComponent::OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport)
{
Super::OnUpdateTransform(UpdateTransformFlags, Teleport);
SyncStereoLayerProperties();
}
void URDVRStereoWidgetComponent::CreateStereoLayerIfNeeded()
{
if (bAutoCreateStereoLayer && !AttachedStereoLayerComponent)
{
URDVRStereoLayerComponent* NewStereoLayerComponent = NewObject<URDVRStereoLayerComponent>(GetOwner(), URDVRStereoLayerComponent::StaticClass(), NAME_None, RF_NoFlags, nullptr);
NewStereoLayerComponent->SetupAttachment(this);
// The stereo layer needs to face the other direction
// Uncomment if the world locked bug is fixed
NewStereoLayerComponent->SetRelativeRotation(FRotator(0.0, 180.0, 0.0));
NewStereoLayerComponent->SetStereoLayerType(EStereoLayerType::SLT_WorldLocked);
// Currently using TrackerLocked, and depending on a VR Origin style relative component
//NewStereoLayerComponent->SetStereoLayerType(EStereoLayerType::SLT_TrackerLocked);
NewStereoLayerComponent->bLiveTexture = true;
NewStereoLayerComponent->SetPriority(AutoCreatedStereoLayerPriority);
NewStereoLayerComponent->RegisterComponent();
AttachStereoLayer(NewStereoLayerComponent);
}
}
void URDVRStereoWidgetComponent::AttachStereoLayer(UStereoLayerComponent* StereoLayerComponent)
{
if (StereoLayerComponent == AttachedStereoLayerComponent)
{
return;
}
if (AttachedStereoLayerComponent && bAutoDestroyStereoLayer)
{
AttachedStereoLayerComponent->DestroyComponent();
}
AttachedStereoLayerComponent = StereoLayerComponent;
SyncStereoLayerProperties();
}
void URDVRStereoWidgetComponent::SyncStereoLayerProperties()
{
if (AttachedStereoLayerComponent)
{
const FVector WidgetWorldScale = GetComponentScale();
AttachedStereoLayerComponent->SetQuadSize(FVector2D(DrawSize.X * WidgetWorldScale.Y, DrawSize.Y * WidgetWorldScale.Z));
// Remove relative component when the world locked bug is fixed
if (RelativeComponent)
{
FTransform StereoFaceTransform = GetComponentTransform().GetRelativeTransform(RelativeComponent->GetComponentTransform());
// The stereo layer needs to face the other direction
StereoFaceTransform.ConcatenateRotation(FQuat(FRotator(0.0, 180.0, 0.0)));
AttachedStereoLayerComponent->SetRelativeTransform(StereoFaceTransform);
}
}
}
// Remove relative component when the world locked bug is fixed
void URDVRStereoWidgetComponent::SetRelativeComponent(USceneComponent* InRelativeComponent)
{
if (InRelativeComponent == RelativeComponent)
{
return;
}
if (RelativeComponent)
{
RelativeComponent->TransformUpdated.Remove(RelativeComponentTransformUpdateDelegateHandle);
RelativeComponentTransformUpdateDelegateHandle.Reset();
}
RelativeComponent = InRelativeComponent;
if (RelativeComponent)
{
RelativeComponentTransformUpdateDelegateHandle = RelativeComponent->TransformUpdated.AddWeakLambda(this, [this](USceneComponent* UpdatedComponent, EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport)
{
SyncStereoLayerProperties();
});
}
}