ARKit 2 v Xamarin.iOS

ARKit se od svého zavedení v iOSu 11 výrazně zralý. Především teď můžete detekovat svislé i vodorovné roviny, což výrazně zlepšuje praktičnost prostředí vnitřní rozšířené reality. Kromě toho existují nové funkce:

  • Rozpoznávání referenčních obrázků a objektů jako spojení mezi reálným světem a digitálními obrazci
  • Nový režim osvětlení, který simuluje skutečné osvětlení
  • Schopnost sdílet a uchovávat prostředí rozšířené reality
  • Preferovaný nový formát souboru pro ukládání obsahu rozšířené reality

Rozpoznávání referenčních objektů

Jednou z předváděných funkcí arKitu 2 je schopnost rozpoznávat referenční obrázky a objekty. Referenční obrázky lze načíst z normálních souborů obrázků (popsáno později), ale referenční objekty musí být zkontrolovány pomocí vývojář-prioritní ARObjectScanningConfiguration.

Ukázková aplikace: Skenování a zjišťování 3D objektů

Ukázka je port projektu Apple, který ukazuje:

Skenování referenčního objektu je náročné na baterie a procesor a starší zařízení často mají problémy se stabilním sledováním.

Správa stavu pomocí objektů NSNotification

Tato aplikace používá stavový počítač, který přechází mezi následujícími stavy:

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

A navíc používá vloženou sadu stavů a přechodů v těchto případech AppState.Scanning:

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

Aplikace používá reaktivní architekturu, která publikuje oznámení o přechodu stavu a NSNotificationCenter přihlásí se k odběru těchto oznámení. Nastavení vypadá jako tento fragment kódu z 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);

Typická obslužná rutina oznámení aktualizuje uživatelské rozhraní a případně upraví stav aplikace, například tuto obslužnou rutinu, která se aktualizuje při kontrole objektu:

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} Nakonec metody upraví model a uživatelské prostředí podle potřeby pro nový stav:

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

Vlastní vizualizace

Aplikace zobrazuje "mračna bodu" objektu obsaženého v ohraničujícím rámečku promítaného na rozpoznanou vodorovnou rovinu.

Tento bodový cloud je k dispozici vývojářům ARFrame.RawFeaturePoints ve vlastnosti. Efektivní vizualizací bodového cloudu může být složitý problém. Iterace přes body a následné vytvoření a umístění nového uzlu SceneKit pro každý bod by zabila snímkovou frekvenci. Pokud byste to udělali asynchronně, došlo by k prodlevě. Ukázka udržuje výkon se třídílnou strategií:

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

Výsledek vypadá takto:

point_cloud

Složitá gesta

Uživatel může škálovat, otáčet a přetahovat ohraničující rámeček obklopující cílový objekt. Existují dvě zajímavé věci v přidružených rozpoznávání gest.

Za prvé, všechny rozpoznávání gest aktivují až po uplynutí prahové hodnoty; Například prst přetáhl tolik pixelů nebo otočení překračuje určitý úhel. Technika se má nashromáždět, dokud nedojde k překročení prahové hodnoty, a pak ji použít postupně:

// 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;
        }
    }
}

Druhá zajímavá věc, která se provádí ve vztahu k gestům, je způsob, jakým se ohraničující rámeček přesouvá ve vztahu k zjištěným reálným rovinám. Tento aspekt je popsán v tomto blogovém příspěvku o Xamarinu.

Další nové funkce v ARKitu 2

Další konfigurace sledování

Nyní můžete jako základ prostředí hybridní reality použít některou z následujících možností:

AROrientationTrackingConfiguration, který je popsán v tomto blogovém příspěvku a ukázce jazyka F#, je nejvíce omezený a poskytuje špatné prostředí hybridní reality, protože umístí pouze digitální objekty ve vztahu k pohybu zařízení, aniž by se pokusil spojit zařízení a obrazovku s reálným světem.

Umožňuje ARImageTrackingConfiguration rozpoznávat 2D obrázky z reálného světa (obrazy, loga atd.) a používat je k ukotvení digitálních obrázků:

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;

Tato konfigurace má dva zajímavé aspekty:

  • Je efektivní a dá se použít s potenciálně velkým počtem referenčních obrázků.
  • Digitální obrázek je ukotvený k obrázku, i když se tento obrázek přesune ve skutečném světě (například pokud je rozpoznán titulek knihy, bude sledovat knihu, jak je stažena z police, rozložena atd.).

Toto ARObjectScanningConfiguration téma bylo popsáno dříve a jedná se o konfiguraci zaměřenou na vývojáře pro skenování 3D objektů. Je vysoce procesor a baterie náročné a neměl by se používat v aplikacích koncových uživatelů.

Konečná konfigurace sledování je ARWorldTrackingConfiguration pracovní rse většiny prostředí hybridní reality. Tato konfigurace používá "vizuální inerciální odometry" ke vztahu skutečných "bodů funkcí" k digitálnímu obrazci. Digitální geometrie nebo sprity jsou ukotvené vzhledem k vodorovné a svislé rovině reálného světa nebo vzhledem k zjištěným ARReferenceObject instancím. V této konfiguraci je počátek světa původní umístění kamery v prostoru s osou Z zarovnanou k závažnosti a digitální objekty "zůstávají na místě" vzhledem k objektům ve skutečném světě.

Environmentální textury

ARKit 2 podporuje "environmentální texturování", které používá zachycené obrázky k odhadu osvětlení a dokonce použití specular zvýraznění na lesklé objekty. Mapa datové krychle prostředí se vytváří dynamicky a jakmile fotoaparát vypadá ve všech směrech, může vytvořit působivě realistický zážitek:

obrázek ukázky textury prostředí

Aby bylo možné používat textury prostředí:

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

I když dokonale reflexní textura zobrazená v předchozím fragmentu kódu je zábavná v ukázce, environmentální textury se pravděpodobně lépe používají se zádržným způsobem, aby se aktivovala "neokázalá údolí" (textura je pouze odhad založený na tom, co kamera zaznamenala).

Sdílená a trvalá prostředí rozšířené reality

Dalším důležitým doplňkem ARWorldMap arKitu 2 je třída, která umožňuje sdílet nebo ukládat data pro sledování světa. Aktuální mapu světa získáte pomocí ARSession.GetCurrentWorldMapAsyncGetCurrentWorldMap(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());
}

Sdílení nebo obnovení mapy světa:

  1. Načtěte data ze souboru.
  2. Unarchive it into an ARWorldMap object,
  3. Použijte ji jako hodnotu vlastnosti 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
};

Jediné ARWorldMap obsahuje neviditelná data pro sledování světa a ARAnchor objekty, které neobsahují digitální prostředky. Pokud chcete sdílet geometrii nebo snímky, budete muset vytvořit vlastní strategii odpovídající vašemu případu použití (například uložením/přenesením pouze umístění a orientace geometrie a jeho použitím na statické SCNGeometry nebo možná uložením/přenosem serializovaných objektů). Výhodou je ARWorldMap , že prostředky, které se umístí vzhledem ke sdílenému ARAnchor, se budou mezi zařízeními nebo relacemi zobrazovat konzistentně.

Univerzální formát souboru Popis scény

Poslední hlavní funkcí ARKitu 2 je přijetí souboru Universal Scene Description společnosti Apple společnosti Apple. Tento formát nahrazuje formát DAE společnosti Collada jako upřednostňovaný formát pro sdílení a ukládání prostředků ARKitu. Podpora vizualizace prostředků je integrovaná do iOS 12 a Mojave. Přípona souboru USDZ je nekomprimovaný a nešifrovaný archiv zip obsahující soubory USD. Pixar poskytuje nástroje pro práci se soubory USD, ale zatím není k dispozici mnoho podpory třetích stran.

Tipy pro programování ARKitu

Ruční správa prostředků

V ARKitu je zásadní ručně spravovat prostředky. To nejen umožňuje vysoké snímkové frekvence, ve skutečnosti je nutné vyhnout se matoucímu "zablokování obrazovky". Architektura ARKit je opožděná o poskytování nového snímku fotoaparátu (ARSession.CurrentFrame. Až do té doby, než na ni proud ARFrameDispose() volal, ARKit nebude poskytovat nový rám! To způsobí, že se video "zablokuje", i když zbytek aplikace reaguje. Řešením je vždy přistupovat s ARSession.CurrentFrame blokem using nebo ručně volat Dispose() .

Všechny objekty odvozené z jsou a implementují model Dispose, takže byste obvykle měli postupovat podle tohoto vzoru pro implementaci Dispose v odvozené třídě.NSObjectIDisposableNSObject

Manipulace s maticemi transformace

V jakékoli 3D aplikaci budete řešit transformační matice 4x4, které kompaktně popisují, jak se pohybovat, otáčet a střídat objekt přes prostor 3D. Ve SceneKitu se jedná o SCNMatrix4 objekty.

Vlastnost SCNNode.Transform vrátí matici SCNMatrix4 transformace proSCNNodetyp hlavního simdfloat4x4 řádku. Například:

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 vidíte, pozice se zakóduje do prvních tří prvků dolního řádku.

V Xamarinu je běžný typ pro manipulaci s transformačními maticemi NVector4, který je podle konvence interpretován hlavním způsobem sloupce. To znamená, že se v M14, M24, M34, nikoli M41, M42, M43 očekává komponenta překladu a pozice:

row-major vs column-major

Vzhledem k tomu, že je konzistentní s výběrem interpretace matice, je nezbytné pro správné chování. Vzhledem k tomu, že matice 3D transformace jsou 4x4, chyby konzistence nevyvolají žádný druh kompilace nebo dokonce výjimku za běhu – je to jen to, že operace budou neočekávaně fungovat. Pokud se zdá, že objekty SceneKit / ARKit jsou zablokované, odletět nebo jitter, je dobrá možnost nesprávná matice transformace. Řešení je jednoduché: NMatrix4.Transpose provede místní provedení prvků.