Udostępnij za pośrednictwem


ArKit 2 w środowisku Xamarin.iOS

ARKit znacznie dojrzał od czasu wprowadzenia w zeszłym roku w systemie iOS 11. Przede wszystkim można teraz wykrywać pionowe, a także poziome płaszczyzny, co znacznie poprawia praktyczność środowisk rzeczywistości rozszerzonej w pomieszczeniach. Ponadto istnieją nowe możliwości:

  • Rozpoznawanie obrazów referencyjnych i obiektów jako skrzyżowania świata rzeczywistego i cyfrowych obrazów
  • Nowy tryb oświetlenia, który symuluje rzeczywiste oświetlenie
  • Możliwość udostępniania i utrwalania środowisk AR
  • Nowy format pliku preferowany do przechowywania zawartości AR

Rozpoznawanie obiektów referencyjnych

Jedną z funkcji prezentacji w zestawie ARKit 2 jest możliwość rozpoznawania obrazów i obiektów referencyjnych. Obrazy referencyjne można załadować z normalnych plików obrazów (omówionych później), ale obiekty referencyjne muszą być skanowane przy użyciu obiektu skoncentrowanego na ARObjectScanningConfigurationdeweloperze.

Przykładowa aplikacja: skanowanie i wykrywanie obiektów 3D

Przykład to port projektu firmy Apple, który pokazuje:

  • Zarządzanie stanem aplikacji przy użyciu NSNotification obiektów
  • Wizualizacja niestandardowa
  • Złożone gesty
  • Skanowanie obiektów
  • Przechowywanie ARReferenceObject

Skanowanie obiektu odniesienia jest intensywnie korzystające z baterii i procesora, a starsze urządzenia często mają problemy z osiągnięciem stabilnego śledzenia.

Zarządzanie stanem przy użyciu obiektów NSNotification

Ta aplikacja używa maszyny stanu, która przechodzi między następującymi stanami:

  • AppState.StartARSession
  • AppState.NotReady
  • AppState.Scanning
  • AppState.Testing

Ponadto używa osadzonego zestawu stanów i przejść w programie :AppState.Scanning

  • Scan.ScanState.Ready
  • Scan.ScanState.DefineBoundingBox
  • Scan.ScanState.Scanning
  • Scan.ScanState.AdjustingOrigin

Aplikacja używa reaktywnej architektury, która publikuje powiadomienia o przejściu stanu do NSNotificationCenter tych powiadomień i subskrybuje je. Konfiguracja wygląda następująco:ViewController.cs

// Configure notifications for application state changes
var notificationCenter = NSNotificationCenter.DefaultCenter;

notificationCenter.AddObserver(Scan.ScanningStateChangedNotificationName, State.ScanningStateChanged);
notificationCenter.AddObserver(ScannedObject.GhostBoundingBoxCreatedNotificationName, State.GhostBoundingBoxWasCreated);
notificationCenter.AddObserver(ScannedObject.GhostBoundingBoxRemovedNotificationName, State.GhostBoundingBoxWasRemoved);
notificationCenter.AddObserver(ScannedObject.BoundingBoxCreatedNotificationName, State.BoundingBoxWasCreated);
notificationCenter.AddObserver(BoundingBox.ScanPercentageChangedNotificationName, ScanPercentageChanged);
notificationCenter.AddObserver(BoundingBox.ExtentChangedNotificationName, BoundingBoxExtentChanged);
notificationCenter.AddObserver(BoundingBox.PositionChangedNotificationName, BoundingBoxPositionChanged);
notificationCenter.AddObserver(ObjectOrigin.PositionChangedNotificationName, ObjectOriginPositionChanged);
notificationCenter.AddObserver(NSProcessInfo.PowerStateDidChangeNotification, DisplayWarningIfInLowPowerMode);

Typowy program obsługi powiadomień zaktualizuje interfejs użytkownika i ewentualnie zmodyfikuje stan aplikacji, na przykład ten program obsługi, który aktualizuje się podczas skanowania obiektu:

private void ScanPercentageChanged(NSNotification notification)
{
    var pctNum = TryGet<NSNumber>(notification.UserInfo, BoundingBox.ScanPercentageUserKey);
    if (pctNum == null)
    {
        return;
    }
    double percentage = pctNum.DoubleValue;
    // Switch to the next state if scan is complete
    if (percentage >= 100.0)
    {
        State.SwitchToNextState();
    }
    else
    {
        DispatchQueue.MainQueue.DispatchAsync(() => navigationBarController.SetNavigationBarTitle($"Scan ({percentage})"));
    }
}

Enter{State} Na koniec metody modyfikują model i środowisko użytkownika odpowiednio do nowego stanu:

internal void EnterStateTesting()
{
    navigationBarController.SetNavigationBarTitle("Testing");
    navigationBarController.ShowBackButton(false);
    loadModelButton.Hidden = true;
    flashlightButton.Hidden = false;
    nextButton.Enabled = true;
    nextButton.SetTitle("Share", UIControlState.Normal);

    testRun = new TestRun(sessionInfo, sceneView);
    TestObjectDetection();
    CancelMaxScanTimeTimer();
}

Wizualizacja niestandardowa

Aplikacja pokazuje niski poziom "chmury punktowej" obiektu znajdującego się w polu ograniczenia przewidywanym na wykrytą płaszczyznę poziomą.

Ta chmura punktów jest dostępna dla deweloperów we ARFrame.RawFeaturePoints właściwości . Efektywne wizualizowanie chmury punktów może być trudnym problemem. Iterowanie punktów, a następnie utworzenie i umieszczenie nowego węzła SceneKit dla każdego punktu spowoduje zabicie szybkości klatek. Alternatywnie, jeśli zostanie wykonana asynchronicznie, wystąpi opóźnienie. Przykład utrzymuje wydajność przy użyciu trzyczęściowej strategii:

internal static SCNGeometry CreateVisualization(NVector3[] points, UIColor color, float size)
{
  if (points.Length == 0)
  {
    return null;
  }

  unsafe
  {
    var stride = sizeof(float) * 3;

    // Pin the data down so that it doesn't move
    fixed (NVector3* pPoints = &amp;points[0])
    {
      // Important: Don't unpin until after `SCNGeometry.Create`, because geometry creation is lazy

      // Grab a pointer to the data and treat it as a byte buffer of the appropriate length
      var intPtr = new IntPtr(pPoints);
      var pointData = NSData.FromBytes(intPtr, (System.nuint) (stride * points.Length));

      // Create a geometry source (factory) configured properly for the data (3 vertices)
      var source = SCNGeometrySource.FromData(
        pointData,
        SCNGeometrySourceSemantics.Vertex,
        points.Length,
        true,
        3,
        sizeof(float),
        0,
        stride
      );

      // Create geometry element
      // The null and bytesPerElement = 0 look odd, but this is just a template object
      var template = SCNGeometryElement.FromData(null, SCNGeometryPrimitiveType.Point, points.Length, 0);
      template.PointSize = 0.001F;
      template.MinimumPointScreenSpaceRadius = size;
      template.MaximumPointScreenSpaceRadius = size;

      // Stitch the data (source) together with the template to create the new object
      var pointsGeometry = SCNGeometry.Create(new[] { source }, new[] { template });
      pointsGeometry.Materials = new[] { Utilities.Material(color) };
      return pointsGeometry;
    }
  }
}

Wynik wygląda następująco:

point_cloud

Złożone gesty

Użytkownik może skalować, obracać i przeciągać pole ograniczenia otaczające obiekt docelowy. Istnieją dwie interesujące rzeczy w skojarzonych rozpoznawaniu gestów.

Po pierwsze, wszystkie aparaty rozpoznawania gestów aktywują się dopiero po uchwaleniu progu; na przykład palec przeciągnął tak wiele pikseli lub obrót przekracza jakiś kąt. Technika polega na kumulacji ruchu do momentu przekroczenia progu, a następnie zastosowania go przyrostowo:

// A custom rotation gesture recognizer that fires only when a threshold is passed
internal partial class ThresholdRotationGestureRecognizer : UIRotationGestureRecognizer
{
    // The threshold after which this gesture is detected.
    const double threshold = Math.PI / 15; // (12°)

    // Indicates whether the currently active gesture has exceeded the threshold
    private bool thresholdExceeded = false;

    private double previousRotation = 0;
    internal double RotationDelta { get; private set; }

    internal ThresholdRotationGestureRecognizer(IntPtr handle) : base(handle)
    {
    }

    // Observe when the gesture's state changes to reset the threshold
    public override UIGestureRecognizerState State
    {
        get => base.State;
        set
        {
            base.State = value;

            switch(value)
            {
                case UIGestureRecognizerState.Began :
                case UIGestureRecognizerState.Changed :
                    break;
                default :
                    // Reset threshold check
                    thresholdExceeded = false;
                    previousRotation = 0;
                    RotationDelta = 0;
                    break;
            }
        }
    }

    public override void TouchesMoved(NSSet touches, UIEvent evt)
    {
        base.TouchesMoved(touches, evt);

        if (thresholdExceeded)
        {
            RotationDelta = Rotation - previousRotation;
            previousRotation = Rotation;
        }

        if (! thresholdExceeded && Math.Abs(Rotation) > threshold)
        {
            thresholdExceeded = true;
            previousRotation = Rotation;
        }
    }
}

Drugą interesującą rzeczą wykonywaną w odniesieniu do gestów jest sposób, w jaki pole ograniczenia jest przenoszone w odniesieniu do wykrytych samolotów świata rzeczywistego. Ten aspekt został omówiony w tym wpisie w blogu platformy Xamarin.

Inne nowe funkcje w zestawie ARKit 2

Więcej konfiguracji śledzenia

Teraz możesz użyć dowolnego z następujących elementów jako podstawy środowiska rzeczywistości mieszanej:

AROrientationTrackingConfiguration, omówione w tym wpisie w blogu i przykładzie języka F#, jest najbardziej ograniczone i zapewnia słabe środowisko rzeczywistości mieszanej, ponieważ umieszcza tylko obiekty cyfrowe w odniesieniu do ruchu urządzenia, bez próby powiązania urządzenia i ekranu z rzeczywistym światem.

Umożliwia ARImageTrackingConfiguration rozpoznawanie rzeczywistych obrazów 2D (obrazów, logo itp.) i używanie ich do zakotwiczenia cyfrowych obrazów:

var imagesAndWidths = new[] {
    ("cover1.jpg", 0.185F),
    ("cover2.jpg", 0.185F),
     //...etc...
    ("cover100.jpg", 0.185F),
};

var referenceImages = new NSSet<ARReferenceImage>(
    imagesAndWidths.Select( imageAndWidth =>
    {
      // Tuples cannot be destructured in lambda arguments
        var (image, width) = imageAndWidth;
        // Read the image
        var img = UIImage.FromFile(image).CGImage;
        return new ARReferenceImage(img, ImageIO.CGImagePropertyOrientation.Up, width);
    }).ToArray());

configuration.TrackingImages = referenceImages;

Istnieją dwa interesujące aspekty tej konfiguracji:

  • Jest wydajny i może być używany z potencjalnie dużą liczbą obrazów referencyjnych
  • Cyfrowe obrazy są zakotwiczone na obrazie, nawet jeśli obraz porusza się w świecie rzeczywistym (na przykład jeśli okładka książki zostanie rozpoznana, będzie śledzić książkę, gdy jest ściągana z półki, ułożona itp.).

Element ARObjectScanningConfiguration został omówiony wcześniej i jest konfiguracją skoncentrowaną na deweloperach na potrzeby skanowania obiektów 3D. Jest intensywnie obciążany procesorem i baterią i nie powinien być używany w aplikacjach użytkowników końcowych.

Ostateczna konfiguracja śledzenia, ARWorldTrackingConfiguration , jest koniem roboczym większości środowisk rzeczywistości mieszanej. Ta konfiguracja używa "wizualnej odometrii inertyjnej", aby powiązać rzeczywiste "punkty cech" z obrazami cyfrowymi. Geometria cyfrowa lub sprites są zakotwiczone względem rzeczywistych płaszczyzn poziomych i pionowych lub względem wykrytych ARReferenceObject wystąpień. W tej konfiguracji źródło świata jest pierwotną pozycją kamery w przestrzeni z osią Z wyrównaną do grawitacji, a obiekty cyfrowe "pozostają na miejscu" względem obiektów w świecie rzeczywistym.

Tekstowanie środowiska

Zestaw ARKit 2 obsługuje "formatowanie środowiskowe", które używa przechwyconych obrazów do szacowania oświetlenia, a nawet stosowania wyróżniania widmowego do błyszczących obiektów. Mapa modułu środowiskowego jest tworzona dynamicznie i, gdy kamera wyglądała we wszystkich kierunkach, może stworzyć imponująco realistyczne środowisko:

obraz pokazowy teksowania środowiska

Aby można było używać tekstu środowiskowego:

var sphere = SCNSphere.Create(0.33F);
sphere.FirstMaterial.LightingModelName = SCNLightingModel.PhysicallyBased;
// Shiny metallic sphere
sphere.FirstMaterial.Metalness.Contents = new NSNumber(1.0F);
sphere.FirstMaterial.Roughness.Contents = new NSNumber(0.0F);

// Session configuration:
var configuration = new ARWorldTrackingConfiguration
{
    PlaneDetection = ARPlaneDetection.Horizontal | ARPlaneDetection.Vertical,
    LightEstimationEnabled = true,
    EnvironmentTexturing = AREnvironmentTexturing.Automatic
};

Chociaż idealnie refleksywna tekstura pokazana w poprzednim fragmencie kodu jest zabawa w przykładzie, tekstowanie środowiskowe jest prawdopodobnie lepiej używane z powściągliwością, aby nie wyzwolić odpowiedzi "niesamowitej doliny" (tekstura jest tylko oszacowaniem na podstawie tego, co aparat nagrał).

Współużytkowane i trwałe środowiska AR

Innym ważnym dodatkiem do zestawu ARKit 2 jest ARWorldMap klasa, która umożliwia udostępnianie lub przechowywanie danych śledzenia świata. Bieżąca mapa świata zostanie wyświetlona za pomocą ARSession.GetCurrentWorldMapAsync polecenia lub GetCurrentWorldMap(Action<ARWorldMap,NSError>) :

// Local storage
var PersistentWorldPath => Environment.GetFolderPath(Environment.SpecialFolder.Personal) + "/arworldmap";

// Later, after scanning the environment thoroughly...
var worldMap = await Session.GetCurrentWorldMapAsync();
if (worldMap != null)
{
    var data = NSKeyedArchiver.ArchivedDataWithRootObject(worldMap, true, out var err);
    if (err != null)
    {
        Console.WriteLine(err);
    }
    File.WriteAllBytes(PersistentWorldPath, data.ToArray());
}

Aby udostępnić lub przywrócić mapę świata:

  1. Załaduj dane z pliku,
  2. Niearchiwnie go do ARWorldMap obiektu,
  3. Użyj tej wartości jako wartości właściwości ARWorldTrackingConfiguration.InitialWorldMap :
var data = NSData.FromArray(File.ReadAllBytes(PersistentWorldController.PersistenWorldPath));
var worldMap = (ARWorldMap)NSKeyedUnarchiver.GetUnarchivedObject(typeof(ARWorldMap), data, out var err);

var configuration = new ARWorldTrackingConfiguration
{
    PlaneDetection = ARPlaneDetection.Horizontal | ARPlaneDetection.Vertical,
    LightEstimationEnabled = true,
    EnvironmentTexturing = AREnvironmentTexturing.Automatic,
    InitialWorldMap = worldMap
};

Tylko ARWorldMap zawiera nie widoczne dane śledzenia świata i ARAnchor obiekty, które nie zawierają zasobów cyfrowych. Aby udostępnić geometrię lub obrazy, musisz opracować własną strategię odpowiednią dla danego przypadku użycia (na przykład przez przechowywanie/przesyłanie tylko lokalizacji i orientacji geometrii oraz stosowanie jej do statycznych SCNGeometry lub być może przez przechowywanie/przesyłanie serializowanych obiektów). Zaletą ARWorldMap jest to, że zasoby, po umieszczeniu względem udostępnionego ARAnchorelementu , będą wyświetlane spójnie między urządzeniami lub sesjami.

Format pliku opisu uniwersalnej sceny

Ostatnią funkcją nagłówka ARKit 2 jest przyjęcie przez Apple formatu pliku Universal Scene Description Pixar. Ten format zastępuje format DAE firmy Collada jako preferowany format udostępniania i przechowywania zasobów ARKit. Obsługa wizualizacji zasobów jest wbudowana w systemy iOS 12 i Mojave. Rozszerzenie pliku USDZ to nieskompresowane i niezaszyfrowane archiwum zip zawierające pliki USD. Pixar udostępnia narzędzia do pracy z plikami USD, ale nie ma jeszcze dużej pomocy technicznej innej firmy.

Porady dotyczące programowania ARKit

Ręczne zarządzanie zasobami

W zestawie ARKit kluczowe znaczenie ma ręczne zarządzanie zasobami. Nie tylko pozwala to na wysokie współczynniki klatek, w rzeczywistości jest konieczne , aby uniknąć mylącego "zablokowania ekranu". Struktura ARKit jest leniwa na dostarczanie nowej ramki aparatu (ARSession.CurrentFrame. Dopóki obecny ARFrameDispose() nie wezwał, ARKit nie dostarczy nowej ramki! Spowoduje to zablokowanie filmu wideo, mimo że reszta aplikacji reaguje. Rozwiązaniem jest zawsze uzyskiwanie dostępu za ARSession.CurrentFrame pomocą using bloku lub ręczne wywołanie Dispose() go.

Wszystkie obiekty pochodzące z NSObject klasy są IDisposable i NSObject implementuje wzorzec Dispose, dlatego zazwyczaj należy postępować zgodnie z tym wzorcem implementacji Dispose w klasie pochodnej.

Manipulowanie macierzami przekształcania

W dowolnej aplikacji 3D będziesz mieć do czynienia z macierzami transformacji 4x4, które kompaktowo opisują sposób przenoszenia, obracania i ścinania obiektu przez przestrzeń 3D. W zestawie SceneKit są SCNMatrix4 to obiekty.

Właściwość SCNNode.Transform zwraca macierz przekształcania SCNMatrix4 dla SCNNodeelementu wspieranego przez typ główny simdfloat4x4 wiersza. Na przykład:

var node = new SCNNode { Position = new SCNVector3(2, 3, 4) };  
var xform = node.Transform;
Console.WriteLine(xform);
// Output is: "(1, 0, 0, 0)\n(0, 1, 0, 0)\n(0, 0, 1, 0)\n(2, 3, 4, 1)"

Jak widać, pozycja jest zakodowana w pierwszych trzech elementach w dolnym wierszu.

W środowisku Xamarin typowym typem manipulowania macierzami transformacji jest NVector4, który zgodnie z konwencją jest interpretowany w sposób główny kolumny. Oznacza to, że składnik tłumaczenia/położenia jest oczekiwany w M14, M24, M34, a nie M41, M42, M43:

row-major vs column-major

Spójność z wyborem interpretacji macierzy jest niezbędna do prawidłowego zachowania. Ponieważ macierze transformacji 3D są 4x4, błędy spójności nie spowodują żadnego rodzaju wyjątku w czasie kompilacji, a nawet wyjątku czasu wykonywania — to tylko te operacje będą działać nieoczekiwanie. Jeśli obiekty SceneKit/ARKit wydają się być zablokowane, odlecieć lub roztrzaskiwać, nieprawidłowa macierz przekształcania jest dobrą możliwością. Rozwiązanie jest proste: NMatrix4.Transpose wykona transpozycję elementów w miejscu.