UE4 VR官方教程学习总结-项目设置

这是我在学习官方VR项目教程中的笔记和总结,以及一些思考,如果有不对的地方也谢谢指出
教程链接:

https://docs.unrealengine.com/4.26/zh-CN/SharingAndReleasing/XRDevelopment/VR/DevelopVR/ContentSetup/

Epic Games

官方文档,最佳实践
在创建针对特定VR平台的新项目时,首先在 游戏(Games) 类别下选择 虚拟现实应用(VR Template),然后在 项目设置(Project Settings) 中选择以下选项:
  • 可缩放3D或2D(Scalable 3D or 2D)
  • 已禁用光线追踪(Raytracing Disabled)
  • 移动设备/平板电脑(Mobile / Tablet)
  • 不带初学者内容包(No Starter Content)
  • 进入  编辑->项目设置->描述 ,并启用  以VR启动
  •   编辑->项目设置->渲染->前向渲染器  中,启用 前向着色
  •   编辑->项目设置->渲染->默认设置  中,将  抗锯齿方法  设置为 多重采样抗锯齿
  •   编辑->项目设置->渲染(Rendering)->VR  中,启用 实例化立体 。 * 对于移动设备VR体验,在 **编辑->项目设置->渲染->VR** 中启用 移动多视图
在DefineEngine.ini中添加
[SystemSettings]

vr.PixelDensity=1
r.SeparateTranslucency=0
r.HZBOcclusion=0
r.MotionBlurQuality=0
r.PostProcessAAQuality=3
r.BloomQuality=1
r.EyeAdaptationQuality=0
r.AmbientOcclusionLevels=0
r.SSR.Quality=1
r.DepthOfFieldQuality=0
r.SceneColorFormat=2
r.TranslucencyVolumeBlur=0
r.TranslucencyLightingVolumeDim=4
r.MaxAnisotropy=8
r.LensFlareQuality=0
r.SceneColorFringeQuality=0
r.FastBlurThreshold=0
r.SSR.MaxRoughness=0.1
r.rhicmdbypass=0
sg.EffectsQuality=2
sg.PostProcessQuality=0
注:不知为何mobile multi-view 无法开启 答案:原来是是与mobile HDR冲突  4.27应该无这个问题

Why we chose forward rendering

Deferred rendering is the default type of rendering used in Unreal Engine. Instead of calculating lighting information of a scene while rendering it’s geometry – the software performs the lighting calculations after the scene’s geometry fragments have been culled. While this type of rendering is more efficient for projects with a large number of lights, deferred rendering takes up a lot more memory, since memory is so limited in real-time projects, it’s wasteful to have rendering take up a majority of it.
On the other hand, forward rendering generates faster render passes, which is optimal for VR platforms where quick renders are necessary to keep up with FPS requirements.  Forward rendering also lets you enable MSAA (multisample anti-aliasing), where adjacent pixels are sampled together instead of rendering an entire scene in high resolution. Choosing forward rendering for our VR project helped us achieve realistic graphic quality for Oculus Quest without compromising frame rate. Here’s how we implemented forward rendering into our VR project to improve its performance.
为什么使用前向渲染:
几个原因:
  1. 由于是在移动端上,而延迟渲染使用GBuffer会大量占用内存,越高分辨率影响越大
  2. 前向渲染的渲染管线速度更快,更能满足vr场景的需求
  3. 前向渲染能够使用msaa
注意移动HDR有默认开启
官方教程步骤:
  • 打开oculus VR插件 和 online subsystem oculus插件(无法开启就先关闭别的online插件)
  • 打开前向渲染 forward Rendering(更快更高效)
  • 关闭动态模糊(前向渲染不支持)
  • 开启instance stereo(实例化立体),在VR场景中会有两个摄像头代表左眼和右眼,is可以让两个摄像头共享特定比例的渲染数据,缩短绘制线程花费的时间
  • 开启MSAA,可以提供更好的平滑效果,注意会占用显存,可能会影响性能
  • 支持平台选为安卓(看设备)
  • 取消使用平滑帧率(fixed frame Rate),因为VR需要锁帧
  • 关闭自定义时间步Custom TimeStep,因为VR项目并不需要
  • 如果是在一体机设备上的项目,关闭mobile HDR,开启移动多视图mobile multi-view
  • 在工程项目config中,DefaultEngine.ini加入如下代码【个人使用中未使用,说是不加appid构建会警告,如果是使用quest这种一体机设备,还要将这些代码配置到AndroidEngine.ini,在引擎文件夹Engine-config-android里】
[OnlineSubsystem]
DefaultPlatformService=Oculus

[OnlineSubsystemOculus]
bEnabled=true
OculuesAppId=
RiftAppId=
  • 使用oculus的节点verify Entitlement 节点来验证应用权限,确保已与设备链接,用户有权使用该app
https://learn.unrealengine.com/course/3746907/module/7254773?moduletoken=UHxxnDLPW8Rp-7N-q4JkIjEQEsOt3snTmaKK-rCvMCbzh~y0iaykoFQik9vT6VYT&LPId=117565
渲染和光照优化建议:
  • 增加一个光源,都会对前向渲染产生很多开销,要控制光源数量和半径
  • MSAA只能解决源锯齿的问题
  • 纹理锯齿需要通过mipmap 和 高品质过滤解决【注意 虚幻里会禁用非二次幂纹理产生Mipmap,这不仅会导致锯齿,还是让渲染变慢】
  • MSAA无法解决镜面微光锯齿,使用复合纹理技术解决
  • 在移动端需要关闭这些功能,因为可能造成长时间的编译和性能损耗,前向渲染会受到更大的影响
UE4 VR官方教程学习总结-项目设置_第1张图片
  • 使用较少种类的材质并提高使用他们的频率
  • 减少动态光源,使用时避免动态阴影,性能开销非常大
  • 图像API(DX, opengl)也会影响耗能耗电
  • 对于复杂场景Occlusion Culling可以进行很好的优化,但是对于VR应用建议关闭,作为替代,可以在场景中使用“预计算可视性体积pre computed visibility volume”,它会计算光照和可视性数据,会增加内存消耗和构建时间,体积越小剔除效率越高
PS指像素着色器
开启PSO缓存
由于OpenGL ES着色器流程的特性,推荐使用PSO缓存来避免着色器编译的运行时开销。
OpenGLES只会在绘制调用使用shader使才会进行编译,这导致许多移动端OpenGL ES应用在画面的第一帧有卡顿,从而不满足oculus一帧13.88ms的要求,因此诞生了OpenGL ES扩展项 "OES_get_program_binary"
开启PSO缓存步骤:
  • 在Engine/Source中搜索ShaderPipelineCache.cpp,改成如下
bool FShaderPipelineCache::Open(FString const& Name, EShaderPlatform Platform)
{
   ......
    if(bReady)
              {
                     uint64 PreCompileMask =  (uint64)CVarPSOFileCachePreCompileMask.GetValueOnAnyThread();
                     //FPipelineFileCache::SetGameUsageMaskWithComparison(PreCompileMask,  PreCompileMaskComparison);
                     FShaderPipelineCache::SetGameUsageMaskWithComparison(PreCompileMask,  PreCompileMaskComparison);
    ......
}
  • 打开引擎,在Project Settings -> Packaging 中勾选 【共享着色器代码】Share Material Shader Code 和【共享材质原生库】 Shared Material Native Libraries
  • 打开window->developer Tools->Device Profiles->Android->Rendering 加入 r.ShaderPipelineCache.Enabled  值设为1
UE4 VR官方教程学习总结-项目设置_第2张图片
  • 关闭编辑器,打开引擎文件夹 Engine\Config\Android\AndroidEngine.ini 加入 如下内容后保存重启编辑器
[DevOptions.Shaders]
NeedsShaderStableKeys=true
  • 淦,好复杂,后续需要再说,链接

https://learn.unrealengine.com/course/3746970/module/7254916?moduletoken=UHxxnDLPW8Rp-7N-q4JkIuBOIC4F1jL4A39qvRh5ceQMXjghbjAvwPyS~Pd5rj0G&LPId=117565
材质优化:
  • 使用Opaque rendering,不透明效果渲染实心物体,这是性能最好的材质,进行深度计算避免被剔除像素的ps计算
  • 使用Masked Rendering,遮罩渲染来生成完全透明和完全非透明的像素
  • Translucent Rendering,透明渲染会造成很大的性能开销,不推荐使用
  • Alpha Transparency, 更推荐实现透明效果的方法,additive使材质明亮,modulate使其暗淡
  • Distance Field fonts 距离场字体有着更好的抗锯齿高分辨率表现去渲染3d文本(Text Render)  SDF(Signed Distance Fonts)字体,被编码到纹理表中以使用GPU的纹理采样去使边缘更平滑
VR相关:
  • 传统的3D渲染会假设图像平面是一个2D矩形平面,为了给VR头显创建图像,需要扭曲图像,Oculus Quest支持固定凹陷渲染(Fixed Foveated Rendering FFR),可以节省头显视野外的ps计算和内存。这个节点在蓝图中调用Set Fixed Oculus Level,设置FFR水平,Level越高性能越好,但会牺牲设备的分辨率【注,今天发现好像4.27.1已经有支持了】
  • 开启移动多视图优化mobile Multi-View,可以很好的节约CPU开销,在典型的立体渲染中,必须按顺序渲染每个眼睛缓冲区,从而使开销加倍。启用“多视图”时,对象将渲染一次到左眼缓冲区,然后自动复制到右眼缓冲区,并对顶点位置和视图相关变量进行适当修改。(要先确保关闭mobile HDR) UE4 VR官方教程学习总结-项目设置_第3张图片
  • 避免渲染靠近摄像机或者与摄像机相交的大型贴图,在3d空间中很容易就看出是贴图是平面的
  • 使用更小的粒子
防止晕动症:
  • 尽量不要去主动移动摄像头或重新设置其FOV等参数
  • 是画面和灯光更加暗淡
  • 垂直上下移动的话使用电梯而不是台阶
  • 移动的加速是瞬时的
  • 不要使用景深和后处理
VR中的移动方式:
  • 静止
  • 轨道移动系统(如过山车)
  • 目光传送
  • 传送移动(抛物线)
意见:在VR中主动控制摄像机旋转时可以使用晕映vignette(val = 2.5)效果,减少晕眩感,(疑似打包取消后处理就会没有效果)
使用 UE4 VR官方教程学习总结-项目设置_第4张图片进行摄像机镜头淡化
IsGuardianConfigured 是否设置守护边界
Stereo Layer 立体层
与游戏引擎世界平行渲染,发送一个单独的纹理到VR头显,并让它在一个单独的渲染通道中与项目的其他部分一起重新渲染(雾)
  • 很难管理,并且会与3d内容进行混合(因为是立体融合)
  • 很容易造成眼睛疲劳
引擎每一帧都会生成左眼和右眼的缓冲区,将这些缓冲区送到合成器进行混合和扭曲并显示在头显上,当使用立体层时,这些图层也会直接应用到合成区,而合成区不会漏掉任何一帧,从而保证无论帧率如何,立体层总是能在每一帧里正确的渲染出来
UE4 VR官方教程学习总结-项目设置_第5张图片
widgetinteraction可以使用custom hit Result,从而使用自定义的射线检测结果等
UE4 VR官方教程学习总结-项目设置_第6张图片
镜头和范围居中 * 100 是因为虚幻单位是厘米,oculus节点返回的是厘米
UE4 VR官方教程学习总结-项目设置_第7张图片
传送系统:
UE4 VR官方教程学习总结-项目设置_第8张图片

UE4 VR官方教程学习总结-项目设置_第9张图片
路径检测
将检测点投射到寻路系统上
UE4 VR官方教程学习总结-项目设置_第10张图片

 传送前在传送点位置处进行一条垂直200的射线检测,优化传送点位置

UE4 VR官方教程学习总结-项目设置_第11张图片

切换关卡时,常用的显示方案三种:
  • 淡出到纯色
  • 使用VR splash 画面
  • 显示一个或多个立体图层
确保传送器永远是贴合的,不会因为box的z值变大而上下一起拉伸
UE4 VR官方教程学习总结-项目设置_第12张图片
Spatial Audio
立体化音效帮助准确计算音效如何在各种环境中发出正确的声音,从而提高沉浸感, 特性:
  • 头部相关的传输函数head related transfer function,简称HRTF,描述了人耳如何从空间中的一点听到声音
  • 空间建模,包括遮挡和混响
开启oculus的空间化音效 Spatializer:
  • 在Plugins中开启 Oculus Audio插件
  • 在Project Settings->Platforms 中选择项目对应的平台打开,如果是quest就是Android->Audio-> 将Spatialization Plugin 和 Reverb Plugin 设置为Oculus Audio,保存重启编辑器
使用oculus的空间化音效: 使用Oculus Audio Geometry(音频几何)组件,这个组件将场景几何信息存储起来使得声音引擎能够进行场景几何体和材质数据的分析识别,使得可以更准确的模拟音效
配置音源:
  • 将声音素材sound wave拖入场景,将这个sound wave Attach to 一个自己指定的actor上
  • 在content目录中,选择需要的目录,右键Sound -> Sound Attenuation, 创建一个声音衰减资源,将这个衰减资源指定到上一步拖入场景中的那个sound wave -> Attenuation -> Attenuation Settings 中
UE4 VR官方教程学习总结-项目设置_第13张图片
  • 打开这个衰减资源,找到Spatialization Method 空间化方法,改为Binaural、找到Spatialization Plugin Settings 指定一个OculusAudioSourceSettings(没有就新建一个)
UE4 VR官方教程学习总结-项目设置_第14张图片
  • 打开这个 OculusAudioSourceSettings,确保Early Reflections Enabled 和 Attenuation Enabled 是开启的

配置传播:
  • 为actor添加一个 Oculus Audio Geometry组件
  • 然后再添加一个OculusAudioMaterial声学材料组件,然后在Settings->Material Preset中设置材料类型如玻璃、瓷砖等
UE4 VR官方教程学习总结-项目设置_第15张图片
  • 针对不能有组件的Landshape和BSP对象,可以在任意或者空actor上附加OculusAudioGeometryLandscape组件或者 OculusAudioGeometry BSP组件
在VR环境中使用空间化音效要注意边界和限制,比如重要音效和对白可能会被环境遮挡而听不见,可以考虑将音源放置在玩家镜头的位置等
实践中遇见的问题:
在编辑器中启动,先是触发了一个断言
Assertion failed: Mesh->bAllowCPUAccess [File:D:/Build/++UE4/Sync/Engine/Plugins/Runtime/Oculus/OculusAudio/Source/OculusAudio/Private/OculusAudioGeometryComponent.cpp] [Line: 53]
要将挂载 Oculus Audio Geometry的staticmesh的bAllowCPUAccess开关开启:
UE4 VR官方教程学习总结-项目设置_第16张图片
然后再次在编辑器中启动又触发断言
Assertion failed: ovrGeometry != nullptr [File:D:/Build/++UE4/Sync/Engine/Plugins/Runtime/Oculus/OculusAudio/Source/OculusAudio/Private/OculusAudioGeometryComponent.cpp] [Line: 58]
由于符号和引擎版本对不上,简单看了下插件代码,怀疑是dll在编辑器没有load,没有更新这个audio geometry,试了下在quest2上运行项目是正常的,这个问题可能要优化一下
对空间化音效的测试:
将音源放置在耳朵一侧,放下音源侧一边耳机,另一只耳朵在空间化音效下会听见声音。或者使用抬升测试,改变音源的高度,会听出声音有所改变
Oculus其他:
使用节点Enable Orientation Tracking 和 Enable Position Tracking 去开启关闭位置旋转跟踪
为Oculus应用添加成就:
打开 https://developer.oculus.com/manage,选择项目,然后选择platform services,找到achievements点击add,然后点击Create Achievement填表
UE4 VR官方教程学习总结-项目设置_第17张图片 UE4 VR官方教程学习总结-项目设置_第18张图片

write policy 对于客户端单机游戏选择Client Authoritative,对于服务器客户端模式的游戏选择serverAuthoritative,总体按detial填表即可
UE4 VR官方教程学习总结-项目设置_第19张图片
在Destinations中设置oculus官网显示app的宣传页,然后在Leaderboards中设置成就排行榜,从 Deep Link Destination中设置跳转地址
UE4 VR官方教程学习总结-项目设置_第20张图片 UE4 VR官方教程学习总结-项目设置_第21张图片 UE4 VR官方教程学习总结-项目设置_第22张图片 UE4 VR官方教程学习总结-项目设置_第23张图片

在蓝图是实现简易成就和排行榜的例子:
在playercontroller中,先缓存成就信息
UE4 VR官方教程学习总结-项目设置_第24张图片
然后编写完成成就的部分:
UE4 VR官方教程学习总结-项目设置_第25张图片

排行榜:对于Read Leaderboard Integer接口,在排行榜上不存在的用户进行调用的话可能会失败
UE4 VR官方教程学习总结-项目设置_第26张图片

UE4 VR官方教程学习总结-项目设置_第27张图片
官方OSS示例:
Darts.Build.cs
// Fill out your copyright notice in the Description page of Project Settings.


using UnrealBuildTool;


public class Darts : ModuleRules
{
    public Darts(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
    
        PublicDependencyModuleNames.AddRange(new string[] {
            
            "Core",
            "CoreUObject",
            "Engine",
            "InputCore",
            "LibOVRPlatform"


        });


        PrivateDependencyModuleNames.AddRange(new string[] {


        "OnlineSubsystem",
        "OnlineSubsystemOculus",
        // "LibOVRAvatar", this is useless


        });


        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });


        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem", "OnlineSubsystemOculus","LibOVRAvatar",);


        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
        if ((Target.Platform == UnrealTargetPlatform.Win32) || (Target.Platform == UnrealTargetPlatform.Win64))
        {
            //PrivateDependencyModuleNames.Add("LibOVRPlatform");
            PublicDelayLoadDLLs.Add("LibOVRPlatform64_1.dll");
        }
    }
}

OculusOSS.h

// Fill out your copyright notice in the Description page of Project Settings.


#pragma once


#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Online.h"
#include "OVR_Platform.h"
#include "OnlineSubsystemOculus.h"
#include "GameFramework/Actor.h"
#include "OculusOSS.generated.h"




UCLASS()
class DARTS_API AOculusOSS : public AActor
{
    GENERATED_BODY()


public:
    // Sets default values for this actor's properties
    AOculusOSS(const FObjectInitializer& ObjectInitializer);
    //AOculusOSS(); //previous constructor
    FOnlineSubsystemOculus* OSS;


private:
    FString MyPlayerName;
    FString MyPlayerID;
    FOnSessionUserInviteAcceptedDelegate OnSessionUserInviteAcceptedDelegate;
    FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate;
    FOnCreateSessionCompleteDelegate OnCreateSessionCompleteDelegate;
    FOnStartSessionCompleteDelegate OnStartSessionCompleteDelegate;
    FOnDestroySessionCompleteDelegate OnDestroySessionCompleteDelegate;
    static FString AcceptedInviteFrom;
    static FOnlineSessionSearchResult AcceptedInvite;


    //matchmaking:
    TSharedPtr SearchSettings;
    FOnMatchmakingCompleteDelegate OnMatchmakingCompleteDelegate;
    FOnCancelMatchmakingCompleteDelegate OnCancelMatchmakingCompleteDelegate;


    FDelegateHandle ovrMessage_Notification_ApplicationLifecycle_LaunchIntentChangedHandle;
    void OnApplicationLifecycle_LaunchIntentChanged(ovrMessageHandle Message, bool bIsError);


    ovrRichPresenceOptionsHandle ovr_RichPresenceOptions_Handle = NULL;


protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;
    FDelegateHandle OnLoginCompleteDelegateHandle;
    FDelegateHandle OnJoinSessionCompleteDelegateHandle;
    FDelegateHandle OnDestroySessionCompleteDelegateHandle;
    FDelegateHandle OnCreateSessionCompleteDelegateHandle;
    FDelegateHandle OnStartSessionCompleteDelegateHandle;






public:
    // Called every frame
    virtual void Tick(float DeltaTime) override;


    virtual void OnLoginComplete(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error);
    void OnPrivilegeCheck(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 CheckResult);
    void OnSessionUserInviteAccepted(const bool bWasSuccessful, const int32 ControllerId, TSharedPtr UserId, const FOnlineSessionSearchResult& InviteResult);
    void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type JoinResult);
    void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
    void OnStartOnlineGameComplete(FName SessionName, bool bWasSuccessful);
    void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful);


    UFUNCTION(BlueprintImplementableEvent, Category = Identity)
        void OnPlayerNameUpdate(const FString& PlayerName);


    UFUNCTION(BlueprintImplementableEvent, Category = Identity)
        void OnPlayerIDUpdate(const FString& PlayerID);


    UFUNCTION(BlueprintCallable, Category = OculusSession)
        void CreateSession();


    UFUNCTION(BlueprintImplementableEvent, Category = OculusSession)
        void OnCreateSessionCompleteBP(FName SessionName, bool bWasSuccessful);


    UFUNCTION(BlueprintImplementableEvent, Category = OculusSession)
        void OnJoinSessionCompleteBP(FName SessionName, bool bWasSuccessful);


    UFUNCTION(BlueprintImplementableEvent, Category = OculusSession)
        void OnLoginCompleteBP(bool bWasSuccessful);


    UFUNCTION(BlueprintCallable, Category = OculusSession)
        void LaunchUserInviteFlow();


    UFUNCTION(BlueprintCallable, Category = OculusSession)
        void LeavingMap();


    UFUNCTION(BlueprintCallable, Category = OculusSession)
        void PlayerLoggedOut(const FString& PlayerID);


    UFUNCTION(BlueprintCallable, Category = OculusSession)
        void TestSessionInvite();


    UFUNCTION(BlueprintCallable, Category = OculusSession)
        bool StartMatchmaking(const FString& PoolName);


    UFUNCTION(BlueprintCallable, Category = OculusSession)
        bool CancelMatchmaking(const FName SessionName);


    void OnMatchmakingComplete(FName SessionName, bool bWasSuccessful);


    UFUNCTION(BlueprintImplementableEvent, Category = OculusSession)
        void OnMatchmakingCompleteBP(FName SessionName, bool bWasSuccessful);


    //UFUNCTION(BlueprintImplementableEvent, Category = OculusSession)
    void OnCancelMatchmakingComplete(FName SessionName, bool bWasSuccessful);


    UFUNCTION(BlueprintImplementableEvent, Category = OculusSession)
        void OnCancelMatchmakingCompleteBP(FName SessionName, bool bWasSuccessful);


    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = OculusSession)
        FString MatchmakingStatus;




    //Rich Presence:
    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void RichPresenceOptions_Destroy();


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void SetRichPresenceAPIName(FString RichPresenceAPIName);


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void SetRichPresenceArgsString(FString RichPresenceArgsKey, FString RichPresenceArgsValue);


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void RichPresenceClearArgs();


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void SetRichPresenceCurrentCapacity(uint8 CurrentCapacity);


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void SetRichPresenceDeeplinkMessageOverride(FString DeeplinkMessageOverride);


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void SetRichPresenceSetEndTime(FString EndTime);


    /*
    //$TODO need to create UENUM to map to this structure
    //UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        //bool SetRichPresenceExtraContext(FString ExtraContext);
    */


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void SetRichPresenceIsIdle(bool IsIdle);


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void SetRichPresenceIsJoinable(bool IsJoinable);


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void SetRichPresenceJoinableId(FString JoinableId);


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void SetRichPresenceMaxCapacity(uint8 MaxCapacity);


    UFUNCTION(BlueprintCallable, Category = OculusRichPresence)
        void SetRichPresenceStartTime(FString StartTime);


    //Destinations:
    void OnReceivedDeepLinkMessage(ovrLaunchDetailsHandle LaunchDetails);


    UFUNCTION(BlueprintImplementableEvent, Category = OculusRichPresence)
        void OnReceivedDeepLinkMessageBP(const FString& DLMessage);
};

OculusOSS.cpp

// Fill out your copyright notice in the Description page of Project Settings.




#include "OculusOSS.h"
#include "Darts.h"
#include "Runtime/Engine/Classes/GameFramework/GameModeBase.h"


FString AOculusOSS::AcceptedInviteFrom;
FOnlineSessionSearchResult AOculusOSS::AcceptedInvite;


// Sets default values
AOculusOSS::AOculusOSS(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    UE_LOG(LogTemp, Verbose, TEXT("In the AOculusOSS constructor"));


    if (IsRunningCommandlet())
    {
        FModuleManager::Get().LoadModule(TEXT("OnlineSubsystem"));
    }
    OSS = static_cast(IOnlineSubsystem::Get());




    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;
    AcceptedInviteFrom.Empty(); // empty signified no invites are outstanding


    OnCreateSessionCompleteDelegate = FOnCreateSessionCompleteDelegate::CreateUObject(this, &AOculusOSS::OnCreateSessionComplete);


    OnSessionUserInviteAcceptedDelegate = FOnSessionUserInviteAcceptedDelegate::CreateUObject(this, &AOculusOSS::OnSessionUserInviteAccepted);


    OnJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate::CreateUObject(this, &AOculusOSS::OnJoinSessionComplete);


    OnStartSessionCompleteDelegate = FOnStartSessionCompleteDelegate::CreateUObject(this, &AOculusOSS::OnStartOnlineGameComplete);


    OnDestroySessionCompleteDelegate = FOnDestroySessionCompleteDelegate::CreateUObject(this, &AOculusOSS::OnDestroySessionComplete);


    ovrMessage_Notification_ApplicationLifecycle_LaunchIntentChangedHandle =
        OSS->GetNotifDelegate(ovrMessage_Notification_ApplicationLifecycle_LaunchIntentChanged).AddUObject(this, &AOculusOSS::OnApplicationLifecycle_LaunchIntentChanged);


    if (!ovr_RichPresenceOptions_Handle)
    {
        ovr_RichPresenceOptions_Handle = ovr_RichPresenceOptions_Create();
    }
}




// Called when the game starts or when spawned
void AOculusOSS::BeginPlay()
{
    Super::BeginPlay();


    if (Online::GetSessionInterface().IsValid())
    {
        Online::GetSessionInterface()->AddOnDestroySessionCompleteDelegate_Handle(OnDestroySessionCompleteDelegate);
        Online::GetSessionInterface()->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate);
        Online::GetSessionInterface()->AddOnSessionUserInviteAcceptedDelegate_Handle(OnSessionUserInviteAcceptedDelegate);
    }


    auto OculusIdentityInterface = Online::GetIdentityInterface();
    if (!OculusIdentityInterface.IsValid())
    {
        UE_LOG(LogTemp, Error, TEXT("No OculusIdentityInterface found!"));
        return;
    }


    OnLoginCompleteDelegateHandle = OculusIdentityInterface->AddOnLoginCompleteDelegate_Handle(0, FOnLoginCompleteDelegate::CreateUObject(this, &AOculusOSS::OnLoginComplete));
    if (OculusIdentityInterface->AutoLogin(0))
    {
        UE_LOG(LogTemp, Verbose, TEXT("Waiting for login response from oculus...."));
    }


}


// Called every frame
void AOculusOSS::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);


}


void AOculusOSS::OnApplicationLifecycle_LaunchIntentChanged(ovrMessageHandle Message, bool bIsError)
{
    UE_LOG(LogTemp, Verbose, TEXT("In OnApplicationLifecycle_LaunchIntentChanged"));
    if (bIsError)
    {
        //error
        UE_LOG(LogTemp, Verbose, TEXT("Got an error retrieving the launch intent"));
        return;
    }


    //do something
    //const char*
    FString launchMessage;
    const char* tesmps = ovr_Message_GetString(Message);
    //UE_LOG(LogTemp, Verbose, TEXT("LaunchMessage: %s"), launchMessage);
    if (launchMessage.Find(TEXT("LaunchType"), ESearchCase::IgnoreCase, ESearchDir::FromStart, 0))
    {
        UE_LOG(LogTemp, Verbose, TEXT("Found LaunchType "));
    }
    auto LaunchDetails = ovr_ApplicationLifecycle_GetLaunchDetails();
    auto LaunchType = ovr_LaunchDetails_GetLaunchType(LaunchDetails);
    switch (LaunchType)
    {
    case   ovrLaunchType_Unknown:
        UE_LOG(LogTemp, Verbose, TEXT("Found LaunchType Unknown "));
        break;
    case ovrLaunchType_Normal:
        UE_LOG(LogTemp, Verbose, TEXT("Found LaunchType Normal "));
        break;
    case ovrLaunchType_Invite:
        UE_LOG(LogTemp, Verbose, TEXT("Found LaunchType Invite "));
        break;
    case ovrLaunchType_Coordinated:
        UE_LOG(LogTemp, Verbose, TEXT("Found LaunchType Coordinated "));
        break;
    case ovrLaunchType_Deeplink:
    {
        UE_LOG(LogTemp, Verbose, TEXT("Found LaunchType Deep link "));
        OnReceivedDeepLinkMessage(LaunchDetails);


        break;
    }
    default:
        UE_LOG(LogTemp, Verbose, TEXT("Found LaunchType DEFAULT CASE "));
        break;
    }
}


void AOculusOSS::OnReceivedDeepLinkMessage(ovrLaunchDetailsHandle LaunchDetails)
{
    UE_LOG(LogTemp, Verbose, TEXT("Made it to OnReceivedDeepLinkMessage"));
    //Parse the deep link message here and then call the BP event
    auto DeepLinkMessage = ovr_LaunchDetails_GetDeeplinkMessage(LaunchDetails);
    FString DeepLinkMessageFS(DeepLinkMessage);
    UE_LOG(LogTemp, Verbose, TEXT("OnReceivedDeepLinkMessage:  DeepLink Message: %s"), *DeepLinkMessageFS);
    //TSharedPtr JsonObject = MakeShareable(new FJsonObject());
    //TSharedRef< TJsonReader<> > JsonReader = TJsonReaderFactory<>::Create()
    //const TSharedRef< TJsonReader<> >& Reader = TJsonReaderFactory<>::Create(DeepLinkMessageFS);




    //if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
    //{
        //UE_LOG(LogTemp, Verbose, TEXT("Parsed the JSON"));
    //}
    //else
        //UE_LOG(LogTemp, Verbose, TEXT("Failed to parse JSON.  Error: '%s'"), *Reader->GetErrorMessage());




    int32 index = DeepLinkMessageFS.Find(TEXT("command"), ESearchCase::IgnoreCase, ESearchDir::FromStart);
    UE_LOG(LogTemp, Verbose, TEXT("Index: %i"), index);
    FString CommandString;
    if (index > -1)
    {
        UE_LOG(LogTemp, Verbose, TEXT("Index: %i"), index);
        CommandString = DeepLinkMessageFS.RightChop(index + 8); //skip the 'command:' char and get the next word
        UE_LOG(LogTemp, Verbose, TEXT("Command: %s"), *CommandString);
        UE_LOG(LogTemp, Verbose, TEXT("Calling OnReceivedDeepLinkMessageBP"));
        OnReceivedDeepLinkMessageBP(CommandString);
    }


    //auto DeepLinkAPIName = ovr_LaunchDetails_GetDestinationApiName(LaunchDetails);  //this requires an updated header and .lib that isn't in 4.24 so commenting out for now.






}


void AOculusOSS::OnLoginComplete(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error)
{
    auto OculusIdentityInterface = Online::GetIdentityInterface();
    OculusIdentityInterface->ClearOnLoginCompleteDelegate_Handle(0, OnLoginCompleteDelegateHandle);


    OculusIdentityInterface->GetUserPrivilege(
        UserId,
        EUserPrivileges::CanPlay,
        IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateUObject(this, &AOculusOSS::OnPrivilegeCheck));


    if (!bWasSuccessful)
    {
        UE_LOG(LogTemp, Warning, TEXT("Unable to login with oculus! %s"), *Error);
        return;
    }
    UE_LOG(LogTemp, Verbose, TEXT("Logged in successfully to oculus!"));


    // Get the Oculus ID
    MyPlayerName = OculusIdentityInterface->GetPlayerNickname(UserId);
    MyPlayerID = UserId.ToString();


    UE_LOG(LogTemp, Verbose, TEXT("Welcome %s!"), *MyPlayerName);
    OnPlayerNameUpdate(MyPlayerName);
    OnPlayerIDUpdate(MyPlayerID);


    OnLoginCompleteBP(bWasSuccessful); //notify Blueprint of the call completion
}


void AOculusOSS::OnPrivilegeCheck(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 CheckResult)
{
    if (CheckResult != (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures)
    {
        UE_LOG(LogTemp, Error, TEXT("Arrg, you failed the entitlement check!"));


        // Developers may want to just quit the game here.
        MyPlayerName = TEXT("FAILED ENTITLEMENT CHECK");
        OnPlayerNameUpdate(MyPlayerName);
        return;
    }
    UE_LOG(LogTemp, Verbose, TEXT("You passed the entitlement check!"));
}




void AOculusOSS::OnSessionUserInviteAccepted(const bool bWasSuccessful, const int32 ControllerId, TSharedPtr UserId, const FOnlineSessionSearchResult& InviteResult)
{
    UE_LOG(LogTemp, Verbose, TEXT("User has accepted an invitation with success = %d"), bWasSuccessful);
    if (!bWasSuccessful)
    {
        UE_LOG(LogTemp, Error, TEXT("Did not successfully accept user invitation!"));
        return;
    }


    //Check if I am in a session already and destroy it if so.
    auto OculusSessionInterface = Online::GetSessionInterface();
    auto Session = OculusSessionInterface->GetNamedSession(TEXT("Game"));
    if (Session)
    {
        AcceptedInvite = InviteResult;
        AcceptedInviteFrom = *UserId->ToString();
        UE_LOG(LogTemp, Verbose, TEXT("Destroying existing session before trying to join new one"));
        Online::GetSessionInterface()->DestroySession(TEXT("Game"), OnDestroySessionCompleteDelegate);
        return;  //exit so OnDestroySessionComplete will handle the join session call in this case
    }
    UE_LOG(LogTemp, Verbose, TEXT("Not in an existing session.  Trying to join session from invitation"));
    OculusSessionInterface->JoinSession(ControllerId, TEXT("Game"), InviteResult);
}


//TODO:  legacy
void AOculusOSS::TestSessionInvite()
{
    const bool bWasSuccessful = true;
    TSharedPtr UserId = 0;
    const int32 ControllerId = 0;
    //const FOnlineSessionSearchResult & InviteResult;




    if (!bWasSuccessful)
    {
        UE_LOG(LogTemp, Error, TEXT("Did not successfully invited user to the session!"));
        return;
    }


    UE_LOG(LogTemp, Verbose, TEXT("Accepted invite to session.  Joining session...."));


    //Check if I am in a session already and destroy it if so.
    auto OculusSessionInterface = Online::GetSessionInterface();
    auto Session = OculusSessionInterface->GetNamedSession(TEXT("Game"));
    if (Session)
    {
        OculusSessionInterface->DestroySession(TEXT("Game"), OnDestroySessionCompleteDelegate);
    }
    UE_LOG(LogTemp, Verbose, TEXT("Would call Join Session here"));
}


void AOculusOSS::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type JoinResult)
{
    UE_LOG(LogTemp, Verbose, TEXT("In OnJoinSessionComplete"));
    auto OculusSessionInterface = Online::GetSessionInterface();
    auto Session = OculusSessionInterface->GetNamedSession(SessionName);
    FString TravelURL;
    APlayerController* PlayerController = NULL;
    //AGameModeBase *GameMode = NULL;
    UWorld* const TheWorld = GetWorld();
    if (!TheWorld)
    {
        UE_LOG(LogTemp, Warning, TEXT("The World Does Not Exist."));
        return;
    }
    else
    {
        PlayerController = GetWorld()->GetFirstPlayerController();
        auto gamemode = (AGameModeBase*)GetWorld()->GetAuthGameMode();
        //gamemode->bUseSeamlessTravel = false;
        UE_LOG(LogTemp, Verbose, TEXT("Seamless Travel Set to : %s"), gamemode->bUseSeamlessTravel ? TEXT("True") : TEXT("False"));
    }


    if (Session)
    {
        OculusSessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegateHandle);


        UE_LOG(LogTemp, Verbose, TEXT("Got back %s's session: %s"), *Session->OwningUserName, *SessionName.ToString());


        if (*Session->OwningUserId == *Online::GetIdentityInterface()->GetUniquePlayerId(0)) // I am the owner
        {
            UE_LOG(LogTemp, Verbose, TEXT("I am the session owner and will host"));
            //GetWorld()->ServerTravel(TEXT("/Game/Maps/Minimal_Default3?listen"));
        }
        else
        {
            UE_LOG(LogTemp, Verbose, TEXT("Not the session owner"));
            if (PlayerController && OculusSessionInterface->GetResolvedConnectString(SessionName, TravelURL))
            {
                TravelURL = TravelURL + TEXT("?multiplayer=true"); //so the level blueprint will see that it was loaded in MP mode.
                UE_LOG(LogTemp, Verbose, TEXT("Calling ClientTravel to: %s"), *TravelURL);
                // Finally call the ClienTravel
                PlayerController->ClientTravel(TravelURL, ETravelType::TRAVEL_Absolute);
            }
        }
        auto gamemode = (AGameModeBase*)GetWorld()->GetAuthGameMode();
        //gamemode->bUseSeamlessTravel = true; //after first travel, start using seamless travel.
        UE_LOG(LogTemp, Verbose, TEXT("Seamless Travel Set to : %s"), gamemode->bUseSeamlessTravel ? TEXT("True") : TEXT("False"));


        auto OculusVoiceInterface = Online::GetVoiceInterface();
        auto OculusIdentityInterface = Online::GetIdentityInterface();
        auto UserId = OculusIdentityInterface->GetUniquePlayerId(0);


        OculusSessionInterface->StartSession(SessionName);//


        auto RegisteredPlayers = Session->RegisteredPlayers; //get list of players in the session


        for (auto RegisteredPlayer : RegisteredPlayers)
        {
            //don't register the local player, only the remote
            if (RegisteredPlayer.Get() != *UserId.Get())
            {
                OculusVoiceInterface->RegisterRemoteTalker(RegisteredPlayer.Get());
                OculusVoiceInterface->StartNetworkedVoice(0);
                UE_LOG(LogTemp, Verbose, TEXT("Registered a Talker: %s"), *RegisteredPlayer.Get().ToString());
            }
        }
    }
    //call the BP event:
    OnJoinSessionCompleteBP(SessionName, (JoinResult == EOnJoinSessionCompleteResult::Success));
}


bool AOculusOSS::StartMatchmaking(const FString& PoolName)
{
    auto OculusSessionInterface = Online::GetSessionInterface();




    if (OculusSessionInterface->IsPlayerInSession(TEXT("Game"), *Online::GetIdentityInterface()->GetUniquePlayerId(0).Get()))
    {
        //need to kill the existing session
        OculusSessionInterface->DestroySession(TEXT("Game"));
    }
    UE_LOG(LogTemp, Verbose, TEXT("Starting Matchmaking"));
    MatchmakingStatus = TEXT("Looking for a Match, X to cancel");
    TArray< TSharedRef > LocalPlayers;


    // Create a matchmaking for two people
    auto SessionSettings = new FOnlineSessionSettings();
    SessionSettings->NumPublicConnections = 2;


    SearchSettings = MakeShareable(new FOnlineSessionSearch());


    // Add the delegate
    if (!OnMatchmakingCompleteDelegate.IsBound())
    {
        OnMatchmakingCompleteDelegate = FOnMatchmakingCompleteDelegate::CreateUObject(this, &AOculusOSS::OnMatchmakingComplete);
        OculusSessionInterface->AddOnMatchmakingCompleteDelegate_Handle(OnMatchmakingCompleteDelegate);
    }


    // Search with this poolname
    SearchSettings->QuerySettings.Set(FName(TEXT("OCULUSPOOL")), PoolName, EOnlineComparisonOp::Equals);


    TSharedRef SearchSettingsRef = SearchSettings.ToSharedRef();


    // Do the search
    return OculusSessionInterface->StartMatchmaking(LocalPlayers, TEXT("Game"), *SessionSettings, SearchSettingsRef);
}




bool AOculusOSS::CancelMatchmaking(FName SessionName)
{
    auto OculusSessionInterface = Online::GetSessionInterface();


    // Add the delegate
    if (!OnCancelMatchmakingCompleteDelegate.IsBound())
    {
        OnCancelMatchmakingCompleteDelegate = FOnCancelMatchmakingCompleteDelegate::CreateUObject(this, &AOculusOSS::OnCancelMatchmakingComplete);
        OculusSessionInterface->AddOnCancelMatchmakingCompleteDelegate_Handle(OnCancelMatchmakingCompleteDelegate);
    }


    UE_LOG(LogTemp, Verbose, TEXT("Cancelling Matchmaking"));
    MatchmakingStatus = TEXT("Cancelling Matchmaking");
    return OculusSessionInterface->CancelMatchmaking(0, TEXT("Game"));


}


void AOculusOSS::OnCancelMatchmakingComplete(FName SessionName, bool bWasSuccessful)
{
    UE_LOG(LogTemp, Verbose, TEXT("Matchmaking Cancel returned: %s"), bWasSuccessful ? TEXT("true") : TEXT("false"));
    MatchmakingStatus = TEXT("Matchmaking Cancelled");
    OnCancelMatchmakingCompleteBP(SessionName, bWasSuccessful);


}


void AOculusOSS::OnMatchmakingComplete(FName SessionName, bool bWasSuccessful)
{
    if (!(bWasSuccessful && SearchSettings->SearchResults.Num() > 0))
    {
        UE_LOG(LogTemp, Error, TEXT("Did not successfully find a matchmaking session!"));
        MatchmakingStatus = TEXT("Did not find a matchmaking session!");
        return;
    }


    UE_LOG(LogTemp, Verbose, TEXT("Found a matchmaking session.  Joining session...."));
    MatchmakingStatus = TEXT("Round a matchmaking session!, Trying to join");


    auto OculusSessionInterface = Online::GetSessionInterface();
    OculusSessionInterface->JoinSession(0, SessionName, SearchSettings->SearchResults[0]);
    OnMatchmakingCompleteBP(SessionName, bWasSuccessful);  //tell the blueprint Matchmaking completed
}




void AOculusOSS::LaunchUserInviteFlow()
{
    UE_LOG(LogTemp, Verbose, TEXT("Calling LaunchUserInviteFlow"));
    //TODO:  Confirm user is in a valid session/room before launching invite flow.
    auto OculusIdentityInterface = Online::GetIdentityInterface();
    auto UserId = OculusIdentityInterface->GetUniquePlayerId(0);


    //test  
    ovrSystemVoipStatus v_status = ovr_Voip_GetSystemVoipStatus();
    switch (v_status)
    {
    case ovrSystemVoipStatus::ovrSystemVoipStatus_Unknown:
        UE_LOG(LogTemp, Verbose, TEXT("ovrSystemVoipStatus_Unknown"));
        break;
    case ovrSystemVoipStatus::ovrSystemVoipStatus_Unavailable:
        UE_LOG(LogTemp, Verbose, TEXT(" ovrSystemVoipStatus_Unavailable"));
        break;
    case ovrSystemVoipStatus::ovrSystemVoipStatus_Suppressed:
        UE_LOG(LogTemp, Verbose, TEXT(" ovrSystemVoipStatus_Suppressed"));
        break;
    case ovrSystemVoipStatus::ovrSystemVoipStatus_Active:
        UE_LOG(LogTemp, Verbose, TEXT(" ovrSystemVoipStatus_Active"));
        break;
    };


    ovrRequest RequestId1 = ovr_Room_GetCurrent();


    OSS->AddRequestDelegate(RequestId1, FOculusMessageOnCompleteDelegate::CreateLambda([this](ovrMessageHandle Message, bool bIsError)
        {
            // ovrMessageHandle can be handled just like in the native SDK here to get the room
            if (!ovr_Message_IsError(Message))
            {
                ovrRoomHandle room = ovr_Message_GetRoom(Message);
                UE_LOG(LogTemp, Verbose, TEXT("Called ovr_Room_GetCurrent()"));
                ovrRequest RequestId = ovr_Room_LaunchInvitableUserFlow(ovr_Room_GetID(room));
                UE_LOG(LogTemp, Verbose, TEXT("RoomID: %llu"), ovr_Room_GetID(room));
            }
            else
            {
                const ovrErrorHandle error = ovr_Message_GetError(Message);
                UE_LOG(LogTemp, Verbose, TEXT("Received get room failure: %s"), ovr_Error_GetMessage(error));
                printf("Received get room failure: %s\n", ovr_Error_GetMessage(error));
            }
        }));
}






void AOculusOSS::LeavingMap()
{


    auto OculusVoiceInterface = Online::GetVoiceInterface();
    auto OculusIdentityInterface = Online::GetIdentityInterface();


    auto UserId = OculusIdentityInterface->GetUniquePlayerId(0);


    if (OculusVoiceInterface.IsValid())
    {
        //OculusVoiceInterface->RegisterRemoteTalker(RegisteredPlayer.Get());
        OculusVoiceInterface->StopNetworkedVoice(0);
        OculusVoiceInterface->RemoveAllRemoteTalkers();
        //Online::GetVoiceInterface()->StartNetworkedVoice(0);
        UE_LOG(LogTemp, Verbose, TEXT("Ending VoIP"));
    }
}




void AOculusOSS::PlayerLoggedOut(const FString& PlayerID)
{
    //UE_LOG(LogTemp, Verbose, TEXT("AOculusOSS::PlayerLoggedOut - %i"), UserID);
    auto OculusVoiceInterface = Online::GetVoiceInterface();
    auto OculusIdentityInterface = Online::GetIdentityInterface();


    auto UserId = OculusIdentityInterface->GetUniquePlayerId(0); //not right if called on server when a different player leaves?
    UE_LOG(LogTemp, Verbose, TEXT("AOculusOSS::PlayerLoggedOut - %s"), *PlayerID);


}




void AOculusOSS::CreateSession()
{
    auto OculusSessionInterface = Online::GetSessionInterface();


    if (!OculusSessionInterface.IsValid())
    {
        return;
    }


    UE_LOG(LogTemp, Verbose, TEXT("Trying to create a session"));


    OnCreateSessionCompleteDelegateHandle = OculusSessionInterface->AddOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegate);


    TSharedPtr SessionSettings = MakeShareable(new FOnlineSessionSettings());
    SessionSettings->NumPublicConnections = 4;
    SessionSettings->bShouldAdvertise = true;
    OculusSessionInterface->CreateSession(/* Hosting Player Num*/ 0, TEXT("Game"), *SessionSettings);
}


void AOculusOSS::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
    auto OculusSessionInterface = Online::GetSessionInterface();


    if (!OculusSessionInterface.IsValid())
    {
        return;
    }


    OculusSessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegateHandle);


    UE_LOG(LogTemp, Verbose, TEXT("CreateSession Call complete and was: %d"), bWasSuccessful);


    auto Session = OculusSessionInterface->GetNamedSession(SessionName);
    if (Session)
    {
        UE_LOG(LogTemp, Verbose, TEXT("Session owned by %s"), *Session->OwningUserName);
        UE_LOG(LogTemp, Verbose, TEXT("Session state: %s"), EOnlineSessionState::ToString(Session->SessionState));
    }
    if (bWasSuccessful)
    {
        // Set the StartSession delegate handle
        OnStartSessionCompleteDelegateHandle = OculusSessionInterface->AddOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegate);
        // Our StartSessionComplete delegate should get called after this
        OculusSessionInterface->StartSession(SessionName);
    }
    //Tell the BP we have tried to create a session
    OnCreateSessionCompleteBP(SessionName, bWasSuccessful);
}


void AOculusOSS::OnStartOnlineGameComplete(FName SessionName, bool bWasSuccessful)
{
    GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Red, FString::Printf(TEXT("OnStartSessionComplete %s, %d"), *SessionName.ToString(), bWasSuccessful));


    // Get the Online Subsystem so we can get the Session Interface
    IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
    if (OnlineSub)
    {
        // Get the Session Interface to clear the Delegate
        IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
        if (Sessions.IsValid())
        {
            // Clear the delegate, since we are done with this call
            Sessions->ClearOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegateHandle);
        }
    }


    // If the start was successful, we can open a NewMap if we want. Make sure to use "listen" as a parameter!
    if (bWasSuccessful)
    {
        UE_LOG(LogTemp, Verbose, TEXT("Started the Session"));
    }
}






void AOculusOSS::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful)
{
    GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Red, FString::Printf(TEXT("OnDestroySessionComplete %s, %d"), *SessionName.ToString(), bWasSuccessful));
    UE_LOG(LogTemp, Verbose, TEXT("OnDestroySessionComplete()"));
    // Get the OnlineSubsystem we want to work with
    IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
    if (OnlineSub)
    {
        // Get the SessionInterface from the OnlineSubsystem
        IOnlineSessionPtr OculusSessionInterface = OnlineSub->GetSessionInterface();


        if (OculusSessionInterface.IsValid())
        {
            // Clear the Delegate
            OculusSessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(OnDestroySessionCompleteDelegateHandle);


            if (bWasSuccessful)
            {
                //check if there is a pending invite and attempt to join the invited session if so
                if (!AcceptedInviteFrom.IsEmpty())
                {
                    UE_LOG(LogTemp, Verbose, TEXT("Found a pending invite and trying to join %s's game"), *AcceptedInviteFrom);
                    OnJoinSessionCompleteDelegateHandle = OculusSessionInterface->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate);
                    OculusSessionInterface->JoinSession(0, FName(*AcceptedInviteFrom), AcceptedInvite);
                    AcceptedInviteFrom.Empty();
                }
                UE_LOG(LogTemp, Verbose, TEXT("Destroy Session success"));


                //close the VoIP channel when leaving the session.
                Online::GetVoiceInterface()->StopNetworkedVoice(0);
                Online::GetVoiceInterface()->RemoveAllRemoteTalkers();
            }
            else
            {
                UE_LOG(LogTemp, Verbose, TEXT("Destroy Session fail"));
            }
        }
    }
}




void AOculusOSS::RichPresenceOptions_Destroy()
{
    UE_LOG(LogTemp, Verbose, TEXT("RichPresenceOptions_Destroy"));
    ovr_RichPresenceOptions_Destroy(ovr_RichPresenceOptions_Handle);
}


void AOculusOSS::SetRichPresenceAPIName(FString RichPresenceAPIName)
{
    UE_LOG(LogTemp, Verbose, TEXT("SetRichPresenceAPIName"));
    ovr_RichPresenceOptions_SetApiName(ovr_RichPresenceOptions_Handle, TCHAR_TO_ANSI(*RichPresenceAPIName));
}


void AOculusOSS::SetRichPresenceArgsString(FString RichPresenceArgsKey, FString RichPresenceArgsValue)
{
    UE_LOG(LogTemp, Verbose, TEXT("SetRichPresenceArgsString"));
    ovr_RichPresenceOptions_SetArgsString(ovr_RichPresenceOptions_Handle, TCHAR_TO_ANSI(*RichPresenceArgsKey), TCHAR_TO_ANSI(*RichPresenceArgsValue));
}


void AOculusOSS::RichPresenceClearArgs()
{
    UE_LOG(LogTemp, Verbose, TEXT("RichPresenceClearArgs"));
    ovr_RichPresenceOptions_ClearArgs(ovr_RichPresenceOptions_Handle);
}


void AOculusOSS::SetRichPresenceCurrentCapacity(uint8 CurrentCapacity)
{
    UE_LOG(LogTemp, Verbose, TEXT("SetRichPresenceCurrentCapacity"));
    ovr_RichPresenceOptions_SetCurrentCapacity(ovr_RichPresenceOptions_Handle, CurrentCapacity);
}


void AOculusOSS::SetRichPresenceDeeplinkMessageOverride(FString DeeplinkMessageOverride)
{
    UE_LOG(LogTemp, Verbose, TEXT("SetRichPresenceDeeplinkMessageOverride"));
    ovr_RichPresenceOptions_SetDeeplinkMessageOverride(ovr_RichPresenceOptions_Handle, TCHAR_TO_ANSI(*DeeplinkMessageOverride));
}




void AOculusOSS::SetRichPresenceSetEndTime(FString EndTime)
{
    UE_LOG(LogTemp, Verbose, TEXT("SetRichPresenceSetEndTime"));


    ovr_RichPresenceOptions_SetEndTime(ovr_RichPresenceOptions_Handle, FCString::Atoi64(*EndTime));
}


void AOculusOSS::SetRichPresenceIsIdle(bool IsIdle)
{
    UE_LOG(LogTemp, Verbose, TEXT("SetRichPresenceIsIdle"));
    ovr_RichPresenceOptions_SetIsIdle(ovr_RichPresenceOptions_Handle, IsIdle);
}


void AOculusOSS::SetRichPresenceIsJoinable(bool IsJoinable)
{
    UE_LOG(LogTemp, Verbose, TEXT("SetRichPresenceIsJoinable"));
    ovr_RichPresenceOptions_SetIsJoinable(ovr_RichPresenceOptions_Handle, IsJoinable);
}


void AOculusOSS::SetRichPresenceJoinableId(FString JoinableId)
{
    UE_LOG(LogTemp, Verbose, TEXT("SetRichPresenceJoinableId"));
    ovr_RichPresenceOptions_SetJoinableId(ovr_RichPresenceOptions_Handle, TCHAR_TO_ANSI(*JoinableId));
}


void AOculusOSS::SetRichPresenceMaxCapacity(uint8 MaxCapacity)
{
    UE_LOG(LogTemp, Verbose, TEXT("SetRichPresenceMaxCapacity"));
    ovr_RichPresenceOptions_SetMaxCapacity(ovr_RichPresenceOptions_Handle, MaxCapacity);
}




void AOculusOSS::SetRichPresenceStartTime(FString StartTime)
{
    UE_LOG(LogTemp, Verbose, TEXT("SetRichPresenceStartTime"));
    ovr_RichPresenceOptions_SetStartTime(ovr_RichPresenceOptions_Handle, FCString::Atoi64(*StartTime));
}

混合现实捕捉Mixed Reality Capture(MRC):
从第三人称视角捕捉头显画面,以3d的方式展现vr世界和体验,可以看见vr玩家
不知道怎么支持android端,这是window端的方法,总结:
在打包后的build目录,游戏.exe文件夹中,加入脚本.bat,运行即可
GameName.exe -mixedreality
在UE4中的简单使用示例:
UE4 VR官方教程学习总结-项目设置_第28张图片
UE4 VR官方教程学习总结-项目设置_第29张图片
思路就是检查MRC Setting是否有效,无效就生成,是否投射,没有投射就进行投射,对于投射方法External composition 、Direct Composition,Rift两个都能用,Quest只能用前者。
(看了半天也不知道这MRC到底是用来干嘛的。。。)
项目优化
在Quest中,由于是android平台,可以使用adb在程序运行时输入控制台指令 以 'stat unit'为例,先运行程序,用数据线将quest和电脑连接起来,打开cmd输入即可
adb shell "am broadcast -a android.intent.action.RUN -e cmd 'stat unit'"
  • stat unit 以毫秒为单位提供帧数信息:Frame 总帧数时间 、Game 游戏线程帧数时间、Draw 渲染线程帧数时间、GPU 渲染一帧所需时间【如果GPU时间低于Game+Draw,而接近Frame,可能是GPU存在瓶颈】
  • Stat Unitgraph 以毫秒为单位,显示unit前10s内的动态图表
  • Stat FPS 只显示每秒帧数和一帧数时间
  • Stat Game 显示与游戏线程相关的CPU时间
  • Stat Engine 显示网格体三角形数量,帧数时间,帧同步时间【游戏线程等待渲染线程完成的时间】
  • Stat Scenerendering 显示CPU渲染线程成本
  • Stat GPU 显示GPU工作成本【Quest不支持,架构问题】
  • ProfileGPU 捕获GPU渲染成本
在编辑器中运行测试的时候要设置预览渲染水平,Quest选择ES 3.1,Rift 选择Shader Model5
UE4 VR官方教程学习总结-项目设置_第30张图片 UE4 VR官方教程学习总结-项目设置_第31张图片
不建议使用Shader Model5专有的光照复杂度视图【Light Complexity】来观察Quest的光照消耗,因为平台不同,计算方式也不同
ShaderComplexity在两个模式下都可以使用 
UnrealFrontend工具
打开Engine\Binaries\Win64\UnrealFrontend.exe文件
依次如下点击,点击Data Capture后点开项目运行一段时间,然后再次点击停止捕捉,弹出一个对话框,点击是,然后右下角有个Load File按钮,点击
UE4 VR官方教程学习总结-项目设置_第32张图片 UE4 VR官方教程学习总结-项目设置_第33张图片
UE4 VR官方教程学习总结-项目设置_第34张图片

这个捕获包含了ue4在游戏运行期间收集的所有指标,包含所有的Stat命令指标
Systrace工具
打开monitor.bat工具,这个工具在Android SDK目录下,我的是在C:\Users\xxx\AppData\Local\Android\Sdk\tool下
【要追踪GPU,要先在程序运行前使用adb 输入 adb shell ovrgpuprofiler -e,然后正常运行程序】
打开monitor后,按如下顺序点击,TraceBufferSize设置为10000防止捕捉失败,如果需要GPU捕捉就勾选GPU RenderStage,点击ok开始捕捉
UE4 VR官方教程学习总结-项目设置_第35张图片

将捕捉的文件用chrome打开【好像sdk更新后有bug,systrace生成的trace文件打开报错】
在代码中使用 scoped_named_event宏注释系统某个部分,其计时信息将自动显示在systrace捕获中,eg:
RenderDoc工具
网上搜索下载,Rift可以使用虚幻自带的RenderDoc插件,在plugins中搜索启用,然后在视口右上角点击按钮截帧。Oculus我使用的是RenderDocForOculus【打开RenderDoc,左下角需要链接Quest】
大概参数:
  • Sence组表示整个帧的绘制调用,这个组之外的调用可能与调试系统有关
  • PrePass DDM_AllOpaque表示ue用于准备深度缓冲区的z预通道【是优化和延迟阴影的先决条件】
  • ShadowDepths包含生成阴影贴图的所有绘制调用
  • BasePass渲染不透明和遮盖的材质,向 GBuffer 输出材质属性。光照图贡献和天空光照也会在此计算并加入场景颜色。
  • 如果材质使用光照着色器,那就不需要镜面高光,勾选完全粗糙 选项【而不是将材质粗糙度设为1】,慎用High Quality Reflections【会引入大量计算】
  • 确保关闭移动HDR,启用单通道线性渲染Single-Pass linear rendering
优化意见
  • 先解决GPU再解决CPU问题
  • 可以将多个网格体合并为单个
  • 最大的两个开销:顶点处理和片元处理,可以选择降低数量和优化着色器
  • 纹理会造成很大的开销,推荐在Quest上每个材质不要超过两种纹理
  • 使用好法线贴图,对于平面材质不要使用法线贴图
  • 减少动态光源数量
  • 使用FFR降低周边视觉分辨率从而降低实际片元处理数量【pc还不支持】
  • 确保开启移动多视图
  • Rift可以使用虚幻的一些高级功能
  • Rift和Quest如果要跨平台,需要注意使用的材质质量和光照质量
  • Rift和Quest应该使用不同的材质以及不同的网格体
  • 使用RenderDoc
Oculus DrawCall 两个步骤:
  • 合并  使用简单版本的顶点着色器去计算图元位置,并分配到RenderTarget上相应的部分
  • 渲染  在这个过程中被分配到合并计算调用过程的每个顶点着色器都会重新执行,从而计算片元计算中所需的插值,然后执行片元着色器
  • 因为顶点着色器会计算两次,所以要控制顶点的数量
虚拟现实检查VRC
  • 兼容性(ue已实现)
  • 性能(快速启动,视角正确,保持帧率Quest 72fpx)
  • 安全性(权限验证【在sdk】)
  • 功能性(正常游戏功能【无崩溃,能暂停等】,正常的硬件输入要求【右手手柄菜单键功能不应该被覆盖】,精确匹配手柄控制器)
VRC详细链接: https://developer.oculus.com/resources/vrc-pc-sdk-2/

你可能感兴趣的:(Ue4,VR,vr,ue4)