// Copyright (c) Meta Platforms, Inc. and affiliates. #include "OculusXRPassthroughLayer.h" #include "Components/MeshComponent.h" #include "Engine/Engine.h" #include "Materials/Material.h" #include "Materials/MaterialInstanceDynamic.h" #include "Materials/MaterialInterface.h" #include "OculusXRHMDPrivate.h" #include "OculusXRPassthroughXRFunctions.h" #include "OculusXRPassthroughXR.h" #include "OculusXRPassthroughModule.h" #include "ProceduralMeshComponent.h" #include "XRThreadUtils.h" namespace XRPassthrough { static UWorld* GetWorld() { UWorld* World = nullptr; for (const FWorldContext& Context : GEngine->GetWorldContexts()) { if (Context.WorldType == EWorldType::Game || Context.WorldType == EWorldType::PIE) { World = Context.World(); } } return World; } FPassthroughLayer::FPassthroughLayer(XrPassthroughFB PassthroughInstance, TWeakPtr Extension) : PassthroughExtension(Extension) , UserDefinedGeometryMap(nullptr) , PassthroughPokeActorMap(nullptr) , XrPassthroughLayer{ XR_NULL_HANDLE } , XrCompositionLayerHeader{} , XrPassthroughInstance(PassthroughInstance) { } FPassthroughLayer::FPassthroughLayer(const FPassthroughLayer& Layer) : PassthroughExtension(Layer.PassthroughExtension) , UserDefinedGeometryMap(Layer.UserDefinedGeometryMap) , PassthroughPokeActorMap(Layer.PassthroughPokeActorMap) , Session(Layer.Session) , LayerDesc(Layer.LayerDesc) , XrPassthroughLayer(Layer.XrPassthroughLayer) , XrCompositionLayerHeader(Layer.XrCompositionLayerHeader) , XrPassthroughInstance(Layer.XrPassthroughInstance) { } TSharedPtr FPassthroughLayer::Clone() const { return MakeShareable(new FPassthroughLayer(*this)); } FPassthroughLayer::~FPassthroughLayer() { } void FPassthroughLayer::SetDesc(const IStereoLayers::FLayerDesc& InLayerDesc) { LayerDesc = InLayerDesc; if (!PassthroughPokeActorMap) { PassthroughPokeActorMap = MakeShared, ESPMode::ThreadSafe>(); } UpdatePassthroughPokeActors_GameThread(); } void FPassthroughLayer::DestroyLayer() { OculusXRHMD::CheckInGameThread(); ClearPassthroughPokeActors(); } void FPassthroughLayer::DestroyLayer_RenderThread() { OculusXRHMD::CheckInRenderThread(); // Clear user defined meshes for (auto& Entry : *UserDefinedGeometryMap) { const XrTriangleMeshFB MeshHandle = Entry.Value.MeshHandle; const XrGeometryInstanceFB InstanceHandle = Entry.Value.InstanceHandle; RemovePassthroughMesh_RenderThread(MeshHandle, InstanceHandle); } UserDefinedGeometryMap->Empty(); // Destroy passthrough layer if (XrPassthroughLayer != XR_NULL_HANDLE) { xrDestroyPassthroughLayerFB(XrPassthroughLayer); } else { UE_LOG(LogOculusXRPassthrough, Warning, TEXT("Failed to destroy layer as handle was null")); } } bool FPassthroughLayer::IsPassthoughLayerDesc(const IStereoLayers::FLayerDesc& LayerDesc) { return LayerDesc.HasShape() || LayerDesc.HasShape(); } bool FPassthroughLayer::CanReuseResources(const FPassthroughLayer* InLayer) const { if (!InLayer) { return false; } if (!IsPassthoughLayerDesc(InLayer->LayerDesc) || InLayer->LayerDesc.HasShape() != LayerDesc.HasShape() || InLayer->LayerDesc.HasShape() != LayerDesc.HasShape()) { return false; } return true; } bool FPassthroughLayer::Initialize_RenderThread(XrSession InSession, const FPassthroughLayer* InLayer) { OculusXRHMD::CheckInRenderThread(); if (!CanReuseResources(InLayer)) { Session = InSession; if (XrPassthroughLayer != XR_NULL_HANDLE) { xrDestroyPassthroughLayerFB(XrPassthroughLayer); XrPassthroughLayer = XR_NULL_HANDLE; } if (LayerDesc.HasShape() || LayerDesc.HasShape()) { XrPassthroughLayerCreateInfoFB PassthroughLayerCreateInfo = { XR_TYPE_PASSTHROUGH_LAYER_CREATE_INFO_FB }; PassthroughLayerCreateInfo.passthrough = XrPassthroughInstance; PassthroughLayerCreateInfo.purpose = LayerDesc.HasShape() ? XR_PASSTHROUGH_LAYER_PURPOSE_RECONSTRUCTION_FB : XR_PASSTHROUGH_LAYER_PURPOSE_PROJECTED_FB; XrResult CreateLayerResult = xrCreatePassthroughLayerFB(Session, &PassthroughLayerCreateInfo, &XrPassthroughLayer); if (!XR_SUCCEEDED(CreateLayerResult)) { UE_LOG(LogOculusXRPassthrough, Warning, TEXT("Failed to create passthrough layer, error : %i"), CreateLayerResult); return false; } XrResult ResumeLayerResult = xrPassthroughLayerResumeFB(XrPassthroughLayer); if (!XR_SUCCEEDED(ResumeLayerResult)) { UE_LOG(LogOculusXRPassthrough, Warning, TEXT("Failed to resume passthrough layer, error : %i"), ResumeLayerResult); return false; } } } else { PassthroughExtension = InLayer->PassthroughExtension; UserDefinedGeometryMap = InLayer->UserDefinedGeometryMap; PassthroughPokeActorMap = InLayer->PassthroughPokeActorMap; Session = InLayer->Session; XrPassthroughLayer = InLayer->XrPassthroughLayer; XrCompositionLayerHeader = InLayer->XrCompositionLayerHeader; XrPassthroughInstance = InLayer->XrPassthroughInstance; } if (!UserDefinedGeometryMap) { UserDefinedGeometryMap = MakeShared, ESPMode::ThreadSafe>(); } check(IsPassthoughLayerDesc(LayerDesc)); return true; } bool FPassthroughLayer::BuildPassthroughPokeActor(OculusXRHMD::FOculusPassthroughMeshRef PassthroughMesh, FPassthroughPokeActor& OutPassthroughPokeActor) { UWorld* World = GetWorld(); if (!World || !PassthroughExtension.IsValid()) { return false; } const FString BaseComponentName = FString::Printf(TEXT("OculusPassthroughPoke_%d"), LayerDesc.Id); const FName ComponentName(*BaseComponentName); AActor* PassthoughPokeActor = World->SpawnActor(); UProceduralMeshComponent* PassthoughPokeComponentPtr = NewObject(PassthoughPokeActor, ComponentName); PassthoughPokeComponentPtr->RegisterComponent(); const TArray& Triangles = PassthroughMesh->GetTriangles(); const TArray& Vertices = PassthroughMesh->GetVertices(); TArray Normals; TArray UV0; TArray VertexColors; TArray Tangents; PassthoughPokeComponentPtr->CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, UV0, VertexColors, Tangents, false); UMaterial* PokeAHoleMaterial = PassthroughExtension.Pin()->GetSettings()->PokeAHoleMaterial; if (PokeAHoleMaterial) { UMaterialInstanceDynamic* DynamicMaterial = UMaterialInstanceDynamic::Create(PokeAHoleMaterial, nullptr); PassthoughPokeComponentPtr->SetMaterial(0, DynamicMaterial); } OutPassthroughPokeActor.PokeAHoleActor = PassthoughPokeActor; OutPassthroughPokeActor.PokeAHoleComponentPtr = PassthoughPokeComponentPtr; return true; } void FPassthroughLayer::UpdatePassthroughPokeActors_GameThread() { if (LayerDesc.HasShape()) { const FUserDefinedLayer& UserDefinedLayerProps = LayerDesc.GetShape(); const TArray& UserGeometryList = UserDefinedLayerProps.UserGeometryList; TSet UsedSet = {}; if (PassthroughSupportsDepth()) { for (const FUserDefinedGeometryDesc& GeometryDesc : UserGeometryList) { const FString MeshName = GeometryDesc.MeshName; UsedSet.Add(MeshName); FPassthroughPokeActor* FoundPassthroughPokeActor = PassthroughPokeActorMap->Find(MeshName); if (!FoundPassthroughPokeActor) { OculusXRHMD::FOculusPassthroughMeshRef GeomPassthroughMesh = GeometryDesc.PassthroughMesh; if (GeomPassthroughMesh) { FPassthroughPokeActor PassthroughPokeActor; if (BuildPassthroughPokeActor(GeomPassthroughMesh, PassthroughPokeActor)) { PassthroughPokeActor.PokeAHoleComponentPtr->SetWorldTransform(GeometryDesc.Transform); PassthroughPokeActorMap->Add(MeshName, PassthroughPokeActor); } } } else if (GeometryDesc.bUpdateTransform && FoundPassthroughPokeActor->PokeAHoleComponentPtr.IsValid()) { FoundPassthroughPokeActor->PokeAHoleComponentPtr->SetWorldTransform(GeometryDesc.Transform); } } } // find actors that no longer exist TArray ItemsToRemove; for (auto& Entry : *PassthroughPokeActorMap) { if (!UsedSet.Contains(Entry.Key)) { ItemsToRemove.Add(Entry.Key); } } for (FString Entry : ItemsToRemove) { FPassthroughPokeActor* PassthroughPokeActor = PassthroughPokeActorMap->Find(Entry); if (PassthroughPokeActor) { UWorld* World = GetWorld(); if (World && PassthroughPokeActor->PokeAHoleActor.IsValid()) { World->DestroyActor(PassthroughPokeActor->PokeAHoleActor.Get()); } } PassthroughPokeActorMap->Remove(Entry); } } } void FPassthroughLayer::UpdatePassthroughStyle_RenderThread(const FEdgeStyleParameters& EdgeStyleParameters) { if (!PassthroughExtension.IsValid()) { return; } XrPassthroughStyleFB Style = { XR_TYPE_PASSTHROUGH_STYLE_FB }; Style.textureOpacityFactor = EdgeStyleParameters.TextureOpacityFactor; Style.edgeColor = { 0, 0, 0, 0 }; if (EdgeStyleParameters.bEnableEdgeColor) { Style.edgeColor = { EdgeStyleParameters.EdgeColor.R, EdgeStyleParameters.EdgeColor.G, EdgeStyleParameters.EdgeColor.B, EdgeStyleParameters.EdgeColor.A }; } /// Color map union AllColorMapDescriptors { XrPassthroughColorMapMonoToRgbaFB rgba; XrPassthroughColorMapMonoToMonoFB mono; XrPassthroughBrightnessContrastSaturationFB bcs; XrPassthroughColorMapLutMETA lut; XrPassthroughColorMapInterpolatedLutMETA interpLut; }; AllColorMapDescriptors colorMap; if (PassthroughExtension.Pin()->GetSettings()->bExtColorLutAvailable && EdgeStyleParameters.bEnableColorMap) { void* colorMapDataDestination = nullptr; unsigned int expectedColorMapDataSize = 0; switch (EdgeStyleParameters.ColorMapType) { case ColorMapType_None: break; case ColorMapType_GrayscaleToColor: colorMap.rgba = { XR_TYPE_PASSTHROUGH_COLOR_MAP_MONO_TO_RGBA_FB }; expectedColorMapDataSize = sizeof(colorMap.rgba.textureColorMap); colorMapDataDestination = colorMap.rgba.textureColorMap; Style.next = &colorMap.rgba; break; case ColorMapType_Grayscale: colorMap.mono = { XR_TYPE_PASSTHROUGH_COLOR_MAP_MONO_TO_MONO_FB }; expectedColorMapDataSize = sizeof(colorMap.mono.textureColorMap); colorMapDataDestination = colorMap.mono.textureColorMap; Style.next = &colorMap.mono; break; case ColorMapType_ColorAdjustment: colorMap.bcs = { XR_TYPE_PASSTHROUGH_BRIGHTNESS_CONTRAST_SATURATION_FB }; expectedColorMapDataSize = 3 * sizeof(float); colorMapDataDestination = &colorMap.bcs.brightness; Style.next = &colorMap.bcs; break; case ColorMapType_ColorLut: colorMap.lut = { XR_TYPE_PASSTHROUGH_COLOR_MAP_LUT_META }; colorMap.lut.colorLut = reinterpret_cast(EdgeStyleParameters.ColorLutDesc.ColorLuts[0]); colorMap.lut.weight = EdgeStyleParameters.ColorLutDesc.Weight; Style.next = &colorMap.lut; break; case ColorMapType_ColorLut_Interpolated: colorMap.interpLut = { XR_TYPE_PASSTHROUGH_COLOR_MAP_INTERPOLATED_LUT_META }; colorMap.interpLut.sourceColorLut = reinterpret_cast(EdgeStyleParameters.ColorLutDesc.ColorLuts[0]); colorMap.interpLut.targetColorLut = reinterpret_cast(EdgeStyleParameters.ColorLutDesc.ColorLuts[1]); colorMap.interpLut.weight = EdgeStyleParameters.ColorLutDesc.Weight; Style.next = &colorMap.lut; break; default: UE_LOG(LogOculusXRPassthrough, Error, TEXT("Passthrough style has unexpected color map type: %i"), EdgeStyleParameters.ColorMapType); return; } // Validate color map data size and copy it over if (colorMapDataDestination != nullptr) { if (EdgeStyleParameters.ColorMapData.Num() != expectedColorMapDataSize) { UE_LOG(LogOculusXRPassthrough, Error, TEXT("Passthrough color map size for type %i is expected to be %i instead of %i"), EdgeStyleParameters.ColorMapType, expectedColorMapDataSize, EdgeStyleParameters.ColorMapData.Num()); return; } uint8* ColorMapData = (uint8*)EdgeStyleParameters.ColorMapData.GetData(); memcpy(colorMapDataDestination, ColorMapData, expectedColorMapDataSize); } } XrResult Result = xrPassthroughLayerSetStyleFB(XrPassthroughLayer, &Style); if (!XR_SUCCEEDED(Result)) { UE_LOG(LogOculusXRPassthrough, Error, TEXT("Failed setting passthrough style, error : %i"), Result); return; } } static FMatrix TransformToPassthroughSpace(FTransform Transform, float WorldToMetersScale, FTransform TrackingToWorld) { const FVector WorldToMetersScaleInv = FVector(WorldToMetersScale).Reciprocal(); FTransform TransformWorld = Transform * TrackingToWorld.Inverse(); TransformWorld.MultiplyScale3D(WorldToMetersScaleInv); TransformWorld.ScaleTranslation(WorldToMetersScaleInv); const FMatrix TransformWorldScaled = TransformWorld.ToMatrixWithScale(); const FMatrix SwapAxisMatrix( FPlane(0.0f, 0.0f, -1.0f, 0.0f), FPlane(1.0f, 0.0f, 0.0f, 0.0f), FPlane(0.0f, 1.0f, 0.0f, 0.0f), FPlane(0.0f, 0.0f, 0.0f, 1.0f)); return TransformWorldScaled * SwapAxisMatrix; } void FPassthroughLayer::UpdatePassthrough_RenderThread(FRHICommandListImmediate& RHICmdList, XrSpace Space, XrTime Time, float WorldToMetersScale, FTransform TrackingToWorld) { check(IsInRenderingThread()); if (LayerDesc.HasShape()) { const FReconstructedLayer& ReconstructedLayerProps = LayerDesc.GetShape(); UpdatePassthroughStyle_RenderThread(ReconstructedLayerProps.EdgeStyleParameters); } else if (LayerDesc.HasShape()) { const FUserDefinedLayer& UserDefinedLayerProps = LayerDesc.GetShape(); UpdatePassthroughStyle_RenderThread(UserDefinedLayerProps.EdgeStyleParameters); } if (LayerDesc.HasShape()) { const FUserDefinedLayer& UserDefinedLayerProps = LayerDesc.GetShape(); const TArray& UserGeometryList = UserDefinedLayerProps.UserGeometryList; TSet UsedSet; for (const FUserDefinedGeometryDesc& GeometryDesc : UserGeometryList) { const FString MeshName = GeometryDesc.MeshName; UsedSet.Add(MeshName); FPassthroughMesh* LayerPassthroughMesh = UserDefinedGeometryMap->Find(MeshName); if (!LayerPassthroughMesh) { OculusXRHMD::FOculusPassthroughMeshRef GeomPassthroughMesh = GeometryDesc.PassthroughMesh; if (GeomPassthroughMesh) { const FMatrix Transform = TransformToPassthroughSpace(GeometryDesc.Transform, WorldToMetersScale, TrackingToWorld); XrTriangleMeshFB MeshHandle = 0; XrGeometryInstanceFB InstanceHandle = 0; AddPassthroughMesh_RenderThread(GeomPassthroughMesh->GetVertices(), GeomPassthroughMesh->GetTriangles(), Transform, Space, MeshHandle, InstanceHandle); UserDefinedGeometryMap->Add(MeshName, FPassthroughMesh(MeshHandle, InstanceHandle, GeometryDesc.Transform)); } } else { const FMatrix Transform = TransformToPassthroughSpace(GeometryDesc.Transform, WorldToMetersScale, TrackingToWorld); UpdatePassthroughMeshTransform_RenderThread(LayerPassthroughMesh->InstanceHandle, Transform, Space, Time); LayerPassthroughMesh->LastTransform = GeometryDesc.Transform; } } // find meshes that no longer exist TArray ItemsToRemove; for (auto& Entry : *UserDefinedGeometryMap) { if (!UsedSet.Contains(Entry.Key)) { ItemsToRemove.Add(Entry.Key); } } for (FString Entry : ItemsToRemove) { FPassthroughMesh* PassthroughMesh = UserDefinedGeometryMap->Find(Entry); if (PassthroughMesh) { const XrTriangleMeshFB MeshHandle = PassthroughMesh->MeshHandle; const XrGeometryInstanceFB InstanceHandle = PassthroughMesh->InstanceHandle; RemovePassthroughMesh_RenderThread(MeshHandle, InstanceHandle); } else { UE_LOG(LogOculusXRPassthrough, Error, TEXT("PassthroughMesh: %s doesn't exist."), *Entry); return; } UserDefinedGeometryMap->Remove(Entry); } } } XrCompositionLayerBaseHeaderType* FPassthroughLayer::GetXrCompositionLayerHeader() { OculusXRHMD::CheckInRHIThread(); if (XrPassthroughLayer != nullptr) { XrCompositionLayerPassthroughFB& CompositionLayer = XrCompositionLayerHeader; memset(&CompositionLayer, 0, sizeof(CompositionLayer)); CompositionLayer.type = XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_FB; CompositionLayer.layerHandle = XrPassthroughLayer; CompositionLayer.flags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; CompositionLayer.space = XR_NULL_HANDLE; return reinterpret_cast(&CompositionLayer); } return nullptr; } bool FPassthroughLayer::IsBackgroundLayer() const { return (LayerDesc.HasShape() && (LayerDesc.GetShape().PassthroughLayerOrder == PassthroughLayerOrder_Underlay)) || (LayerDesc.HasShape() && (LayerDesc.GetShape().PassthroughLayerOrder == PassthroughLayerOrder_Underlay)); } bool FPassthroughLayer::IsOverlayLayer() const { return (LayerDesc.HasShape() && (LayerDesc.GetShape().PassthroughLayerOrder == PassthroughLayerOrder_Overlay)) || (LayerDesc.HasShape() && (LayerDesc.GetShape().PassthroughLayerOrder == PassthroughLayerOrder_Overlay)); } bool FPassthroughLayer::PassthroughSupportsDepth() const { return ((LayerDesc.Flags & IStereoLayers::LAYER_FLAG_SUPPORT_DEPTH) != 0) && LayerDesc.HasShape(); } // Code taken from OVRPlugin (InsightMrManager.cpp) static bool DecomposeTransformMatrix(FMatrix Transform, XrPosef& OutPose, XrVector3f& OutScale) { FTransform outTransform = FTransform(Transform); FVector3f scale = FVector3f(outTransform.GetScale3D()); FQuat4f rotation = FQuat4f(outTransform.GetRotation()); FVector3f position = FVector3f(outTransform.GetLocation()); if (scale.X == 0 || scale.Y == 0 || scale.Z == 0) { return false; } OutScale = XrVector3f{ scale.X, scale.Y, scale.Z }; OutPose = XrPosef{ XrQuaternionf{ rotation.X, rotation.Y, rotation.Z, rotation.W }, XrVector3f{ position.X, position.Y, position.Z } }; return true; } void FPassthroughLayer::AddPassthroughMesh_RenderThread(const TArray& Vertices, const TArray& Triangles, FMatrix Transformation, XrSpace Space, XrTriangleMeshFB& OutMeshHandle, XrGeometryInstanceFB& OutInstanceHandle) { OculusXRHMD::CheckInRenderThread(); XrTriangleMeshFB MeshHandle = 0; XrGeometryInstanceFB InstanceHandle = 0; // Explicit conversion is needed since FVector contains double elements. // Converting Vertices.Data() to float* causes issues when memory is parsed. TArray VertexData; VertexData.SetNumUninitialized(Vertices.Num()); size_t i = 0; for (const FVector& vertex : Vertices) { VertexData[i++] = { (float)vertex.X, (float)vertex.Y, (float)vertex.Z }; } TArray TriangleData; TriangleData.SetNumUninitialized(Triangles.Num()); i = 0; for (const int32& tri : Triangles) { TriangleData[i++] = (uint32_t)tri; } XrTriangleMeshCreateInfoFB TriangleMeshInfo = { XR_TYPE_TRIANGLE_MESH_CREATE_INFO_FB }; TriangleMeshInfo.flags = 0; // not mutable TriangleMeshInfo.triangleCount = Triangles.Num() / 3; TriangleMeshInfo.indexBuffer = TriangleData.GetData(); TriangleMeshInfo.vertexCount = Vertices.Num(); TriangleMeshInfo.vertexBuffer = VertexData.GetData(); TriangleMeshInfo.windingOrder = XR_WINDING_ORDER_UNKNOWN_FB; if (XR_FAILED(xrCreateTriangleMeshFB.GetValue()(Session, &TriangleMeshInfo, &MeshHandle))) { UE_LOG(LogOculusXRPassthrough, Error, TEXT("Failed creating passthrough mesh surface.")); return; } XrGeometryInstanceCreateInfoFB createInfo = { XR_TYPE_GEOMETRY_INSTANCE_CREATE_INFO_FB }; bool result = DecomposeTransformMatrix(Transformation, createInfo.pose, createInfo.scale); if (!result) { UE_LOG(LogOculusXRPassthrough, Error, TEXT("Failed decomposing the transform matrix.")); return; } createInfo.layer = XrPassthroughLayer; createInfo.mesh = MeshHandle; createInfo.baseSpace = Space; if (XR_FAILED(xrCreateGeometryInstanceFB(Session, &createInfo, &InstanceHandle))) { UE_LOG(LogOculusXRPassthrough, Error, TEXT("Failed adding passthrough mesh surface to scene.")); return; } OutMeshHandle = MeshHandle; OutInstanceHandle = InstanceHandle; } void FPassthroughLayer::UpdatePassthroughMeshTransform_RenderThread(XrGeometryInstanceFB InstanceHandle, FMatrix Transformation, XrSpace Space, XrTime Time) { OculusXRHMD::CheckInRenderThread(); XrGeometryInstanceTransformFB UpdateInfo = { XR_TYPE_GEOMETRY_INSTANCE_TRANSFORM_FB }; bool result = DecomposeTransformMatrix(Transformation, UpdateInfo.pose, UpdateInfo.scale); if (!result) { UE_LOG(LogOculusXRPassthrough, Error, TEXT("Failed decomposing the transform matrix.")); return; } UpdateInfo.baseSpace = Space; UpdateInfo.time = Time; if (XR_FAILED(xrGeometryInstanceSetTransformFB(InstanceHandle, &UpdateInfo))) { UE_LOG(LogOculusXRPassthrough, Error, TEXT("Failed updating passthrough mesh surface transform.")); return; } } void FPassthroughLayer::RemovePassthroughMesh_RenderThread(XrTriangleMeshFB MeshHandle, XrGeometryInstanceFB InstanceHandle) { OculusXRHMD::CheckInRenderThread(); if (XR_FAILED(xrDestroyGeometryInstanceFB(InstanceHandle))) { UE_LOG(LogOculusXRPassthrough, Error, TEXT("Failed removing passthrough surface from scene.")); return; } if (XR_FAILED(xrDestroyTriangleMeshFB.GetValue()(MeshHandle))) { UE_LOG(LogOculusXRPassthrough, Error, TEXT("Failed destroying passthrough surface mesh.")); return; } } void FPassthroughLayer::ClearPassthroughPokeActors() { if (PassthroughPokeActorMap) { UWorld* World = GetWorld(); if (!World) { UE_LOG(LogOculusXRPassthrough, Warning, TEXT("Couldn't retrieve World. Passthrough Pokeahole actors will not be destroyed.")); return; } for (auto& Entry : *PassthroughPokeActorMap) { // Check if actor is still valid. In some specific cases the actor might be destroyed before we clean the PokeActorMap // (e.g. when loading to a new level) if (Entry.Value.PokeAHoleActor.IsValid()) { World->DestroyActor(Entry.Value.PokeAHoleActor.Get()); } } PassthroughPokeActorMap.Reset(); } } } // namespace XRPassthrough