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:
- Správa stavu aplikace pomocí
NSNotification
objektů - Vlastní vizualizace
- Složitá gesta
- Prohledávání objektů
- Uložení
ARReferenceObject
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í:
- Použití nebezpečného kódu k připnutí dat a interpretaci dat jako nezpracované vyrovnávací paměti bajtů.
- Převod této nezpracované vyrovnávací paměti na
SCNGeometrySource
objekt a vytvoření objektu "šablona"SCNGeometryElement
. - Rychlé "spojování" nezpracovaných dat a šablony pomocí
SCNGeometry.Create(SCNGeometrySource[], SCNGeometryElement[])
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 = &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:
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í:
- Pouze akcelerometr zařízení (
AROrientationTrackingConfiguration
, iOS 11) - Tváře (
ARFaceTrackingConfiguration
, iOS 11) - Referenční obrázky (
ARImageTrackingConfiguration
, iOS 12) - Skenování 3D objektů (
ARObjectScanningConfiguration
, iOS 12) - Vizuální inerciální odometrie (
ARWorldTrackingConfiguration
vylepšená v iOSu 12)
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:
Aby bylo možné používat textury prostředí:
- Objekty
SCNMaterial
musí používatSCNLightingModel.PhysicallyBased
a přiřazovat hodnotu v rozsahu 0 až 1 proMetalness.Contents
aRoughness.Contents
- Konfigurace sledování musí být nastavená
EnvironmentTexturing
=AREnvironmentTexturing.Automatic
:
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.GetCurrentWorldMapAsync
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());
}
Sdílení nebo obnovení mapy světa:
- Načtěte data ze souboru.
- Unarchive it into an
ARWorldMap
object, - 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 ARFrame
Dispose()
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ě.NSObject
IDisposable
NSObject
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 pro SCNNode
typ 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:
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ů.