Is Common UI supported for VR?

I started looking at Common UI in Unreal Engine 5.

It’s not immediately clear how well this will work in VR. I have working UI now using UMG but there’s this concept of game viewport. In my game, in VR I create widgets attached to UMG components, but in non-VR I attach widgets to the game viewport. Does everything still work in VR when using Common UI?

3 Likes

Have you been able to get some clarity on this @illYay ? I’m just now starting down this path - coming from using 3d widgets so far.

2 Likes

I think so, in fact now I know more about Common UI. It plays nicely with regular UI.

1 Like

Sorry to necro-post, but I was wondeirng if you have any recommendations for learning Common UI for use in VR.

There shouldn’t be too much different for using Common UI in VR as opposed to regular UMG. Common UI has things that sit on top of wrap regular UMG classes.

1 Like

I recommend to load the LayerSample from Oculus fork and study it.

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();
        });
    }
}

Thanks so much for responding.

I have CommonUI working pretty well now in the raw project, but in my actual game I keep losing control of my Index Controllers. Once I open a CommonUI widget my controller no longer accepts commands. Where is a good place to start looking to troubleshoot?

Do you have a widget pointer component in use somewhere?

It might be that CommonUI is intercepting the inputs if there’s some stuff bound in UI. I forget exactly where that happens but it’s part of that Common UI input system.

It might be things like direction controls and “OK” / “Cancel” inputs which would normally be Enter and Escape. It’s been a while since I touched the common UI stuff in my game but I plan on touching that again soon.