ARKit 2 no Xamarin.iOS
O ARKit amadureceu consideravelmente desde sua introdução no ano passado no iOS 11. Em primeiro lugar, agora você pode detectar planos verticais e horizontais, o que melhora muito a praticidade das experiências internas de realidade aumentada. Além disso, há novos recursos:
- Reconhecendo imagens e objetos de referência como a junção entre o mundo real e as imagens digitais
- Um novo modo de iluminação que simula a iluminação do mundo real
- A capacidade de compartilhar e persistir ambientes de AR
- Um novo formato de arquivo preferido para armazenar conteúdo AR
Reconhecendo objetos de referência
Um recurso de demonstração no ARKit 2 é a capacidade de reconhecer imagens e objetos de referência. As imagens de referência podem ser carregadas a partir de arquivos de imagem normais (discutidos posteriormente), mas os objetos de referência devem ser verificados, usando o .ARObjectScanningConfiguration
Aplicativo de exemplo: Digitalizando e detectando objetos 3D
O exemplo é uma porta de um projeto da Apple que demonstra:
- Gerenciamento de estado do aplicativo usando
NSNotification
objetos - Visualização personalizada
- Gestos complexos
- Verificação de objetos
- Armazenando um
ARReferenceObject
A varredura de um objeto de referência consome muita bateria e processador, e dispositivos mais antigos geralmente terão problemas para obter um rastreamento estável.
Gerenciamento de estado usando objetos NSNotification
Esse aplicativo usa uma máquina de estado que faz a transição entre os seguintes estados:
AppState.StartARSession
AppState.NotReady
AppState.Scanning
AppState.Testing
E, além disso, usa um conjunto incorporado de estados e transições quando em AppState.Scanning
:
Scan.ScanState.Ready
Scan.ScanState.DefineBoundingBox
Scan.ScanState.Scanning
Scan.ScanState.AdjustingOrigin
O aplicativo usa uma arquitetura reativa que posta NSNotificationCenter
notificações de transição de estado e assina essas notificações. A configuração se parece com este trecho de 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);
Um manipulador de notificação típico atualizará a interface do usuário e possivelmente modificará o estado do aplicativo, como este manipulador que é atualizado à medida que o objeto é verificado:
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})"));
}
}
Por fim, Enter{State}
os métodos modificam o modelo e a experiência do usuário conforme apropriado para o novo estado:
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();
}
Visualização personalizada
O aplicativo mostra a "nuvem de pontos" de baixo nível do objeto contido em uma caixa delimitadora projetada em um plano horizontal detectado.
Esta nuvem de pontos está disponível para desenvolvedores na ARFrame.RawFeaturePoints
propriedade. Visualizar a nuvem de pontos com eficiência pode ser um problema complicado. Iterar sobre os pontos e, em seguida, criar e colocar um novo nó SceneKit para cada ponto eliminaria a taxa de quadros. Como alternativa, se feito de forma assíncrona, haveria um atraso. A amostra mantém o desempenho com uma estratégia de três partes:
- Usando código não seguro para fixar os dados no local e interpretá-los como um buffer bruto de bytes.
- Convertendo esse buffer bruto em um
SCNGeometrySource
e criando um objeto "modeloSCNGeometryElement
". - "Costurando" rapidamente os dados brutos e o modelo usando
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;
}
}
}
O resultado será semelhante a este:
Gestos complexos
O usuário pode dimensionar, girar e arrastar a caixa delimitadora que circunda o objeto de destino. Há duas coisas interessantes nos reconhecedores de gestos associados.
Primeiro, todos os reconhecedores de gestos são ativados somente depois que um limite é ultrapassado; Por exemplo, um dedo arrastou tantos pixels ou a rotação excede algum ângulo. A técnica é acumular o movimento até que o limite seja excedido e, em seguida, aplicá-lo de forma incremental:
// 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;
}
}
}
A segunda coisa interessante que está sendo feita em relação aos gestos é a maneira como a caixa delimitadora é movida em relação aos planos detectados do mundo real. Esse aspecto é discutido nesta postagem no blog do Xamarin.
Outros novos recursos do ARKit 2
Mais configurações de rastreamento
Agora, você pode usar qualquer um dos itens a seguir como base para uma experiência de realidade misturada:
- Apenas o acelerômetro do dispositivo (
AROrientationTrackingConfiguration
, iOS 11) - Rostos (
ARFaceTrackingConfiguration
, iOS 11) - Imagens de referência (
ARImageTrackingConfiguration
, iOS 12) - Digitalizando objetos 3D (
ARObjectScanningConfiguration
, iOS 12) - Odometria inercial visual (
ARWorldTrackingConfiguration
, aprimorada no iOS 12)
AROrientationTrackingConfiguration
, discutido nesta postagem de blog e no exemplo de F#, é o mais limitado e fornece uma experiência de realidade misturada ruim, pois coloca apenas objetos digitais em relação ao movimento do dispositivo, sem tentar vincular o dispositivo e a tela ao mundo real.
O ARImageTrackingConfiguration
permite que você reconheça imagens 2D do mundo real (pinturas, logotipos, etc.) e use-as para ancorar imagens digitais:
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;
Há dois aspectos interessantes nessa configuração:
- É eficiente e pode ser usado com um número potencialmente grande de imagens de referência
- As imagens digitais são ancoradas na imagem, mesmo que essa imagem se mova no mundo real (por exemplo, se a capa de um livro for reconhecida, ela rastreará o livro à medida que ele for retirado da prateleira, deitado, etc.).
O foi discutido anteriormente e é uma configuração centrada ARObjectScanningConfiguration
no desenvolvedor para digitalizar objetos 3D. É altamente intensivo em processador e bateria e não deve ser usado em aplicativos de usuário final.
A configuração final de rastreamento, ARWorldTrackingConfiguration
, é o carro-chefe da maioria das experiências de realidade misturada. Essa configuração usa "odometria inercial visual" para relacionar "pontos de característica" do mundo real a imagens digitais. A geometria digital ou os sprites são ancorados em relação aos planos horizontais e verticais do mundo real ou em relação às instâncias detectadas ARReferenceObject
. Nessa configuração, a origem do mundo é a posição original da câmera no espaço com o eixo Z alinhado à gravidade, e os objetos digitais "permanecem no lugar" em relação aos objetos no mundo real.
Texturização ambiental
O ARKit 2 suporta "texturização ambiental" que usa imagens capturadas para estimar a iluminação e até mesmo aplicar realces especulares a objetos brilhantes. O cubemap ambiental é construído dinamicamente e, uma vez que a câmera tenha olhado em todas as direções, pode produzir uma experiência impressionantemente realista:
Para usar a texturização ambiental:
- Seus
SCNMaterial
objetos devem usarSCNLightingModel.PhysicallyBased
e atribuir um valor no intervalo de 0 a 1 paraMetalness.Contents
eRoughness.Contents
e - Sua configuração de acompanhamento deve definir
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
};
Embora a textura perfeitamente reflexiva mostrada no trecho de código anterior seja divertida em um exemplo, a texturização ambiental provavelmente é melhor usada com contenção para não desencadear uma resposta de "vale estranho" (a textura é apenas uma estimativa baseada no que a câmera gravou).
Experiências de RA compartilhadas e persistentes
Outra grande adição ao ARKit 2 é a ARWorldMap
classe, que permite compartilhar ou armazenar dados de rastreamento mundial. Você obtém o mapa-múndi atual com ARSession.GetCurrentWorldMapAsync
ou 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());
}
Para compartilhar ou restaurar o mapa-múndi:
- Carregue os dados do arquivo,
- Desarquive-o em um
ARWorldMap
objeto, - Use isso como o valor da
ARWorldTrackingConfiguration.InitialWorldMap
propriedade:
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
};
O ARWorldMap
único contém dados de rastreamento de mundo não visíveis e os ARAnchor
objetos, não contém ativos digitais. Para compartilhar geometria ou imagens, você terá que desenvolver sua própria estratégia apropriada ao seu caso de uso (talvez armazenando/transmitindo apenas a localização e orientação da geometria e aplicando-a à estática SCNGeometry
ou talvez armazenando/transmitindo objetos serializados). O benefício do é que os ARWorldMap
ativos, uma vez colocados em relação a um compartilhado ARAnchor
, aparecerão consistentemente entre dispositivos ou sessões.
Formato de arquivo de descrição de cena universal
O recurso principal final do ARKit 2 é a adoção pela Apple do formato de arquivo Universal Scene Description da Pixar. Esse formato substitui o formato DAE do Collada como o formato preferencial para compartilhar e armazenar ativos do ARKit. O suporte para visualização de ativos está integrado ao iOS 12 e ao Mojave. A extensão de arquivo USDZ é um arquivo zip descompactado e não criptografado contendo arquivos USD. A Pixar fornece ferramentas para trabalhar com arquivos USD, mas ainda não há muito suporte de terceiros.
Dicas de programação do ARKit
Gerenciamento manual de recursos
No ARKit, é crucial gerenciar manualmente os recursos. Isso não apenas permite altas taxas de quadros, mas também é necessário evitar um confuso "congelamento de tela". A estrutura ARKit é preguiçosa em fornecer um novo quadro de câmera (ARSession.CurrentFrame
. Até que a corrente ARFrame
o Dispose()
chame, o ARKit não fornecerá um novo quadro! Isso faz com que o vídeo "congele", mesmo que o restante do aplicativo seja responsivo. A solução é sempre acessar ARSession.CurrentFrame
com um using
bloco ou chamá-lo Dispose()
manualmente.
Todos os objetos derivados de são e implementam o padrão Dispose, portanto, você normalmente deve seguir esse padrão para implementar Dispose
em uma classe derivada.NSObject
IDisposable
NSObject
Manipulando matrizes de transformação
Em qualquer aplicativo 3D, você estará lidando com matrizes de transformação 4x4 que descrevem de forma compacta como mover, girar e cisalhar um objeto através do espaço 3D. No SceneKit, esses são SCNMatrix4
objetos.
A SCNNode.Transform
propriedade retorna a SCNMatrix4
matriz de transformação para o SCNNode
conforme apoiado pelo tipo row-main simdfloat4x4
. Então, por exemplo:
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)"
Como você pode ver, a posição é codificada nos três primeiros elementos da linha inferior.
No Xamarin, o tipo comum para manipular matrizes de transformação é NVector4
, que, por convenção, é interpretado de uma maneira principal de coluna. Ou seja, o componente de translação/posição é esperado em M14, M24, M34, não em M41, M42, M43:
Ser consistente com a escolha da interpretação da matriz é vital para o comportamento adequado. Como as matrizes de transformação 3D são 4x4, os erros de consistência não produzirão nenhum tipo de exceção em tempo de compilação ou mesmo em tempo de execução — é apenas que as operações agirão inesperadamente. Se os objetos do SceneKit / ARKit parecerem travados, voarem ou tremerem, uma matriz de transformação incorreta é uma boa possibilidade. A solução é simples: NMatrix4.Transpose
realizará uma transposição de elementos no local.