Share via


Toque e ouça

Orientação com a bússola do Windows Phone

Charles Petzold

Baixar o código de exemplo

Charles PetzoldNós humanos raramente usamos nossos sentidos por inteiro quando isolados um do outro. Uma combinação de visão e audição nos permite construir uma imagem mental de nossos arredores; uma mistura de paladar, olfato e tato afeta nosso prazer ao comer; e usamos uma combinação de tato, visão e olfato ao tocar um instrumento musical.

É o mesmo com um smartphone: Um smartphone pode “ver” através de suas lentes da câmera, “ouvir” através de seu microfone, “sentir” através de sua tela sensível ao toque, e sabe sua localização no mundo usando GPS e o sensor de orientação. Mas comece a combinar a entrada desses sensores e não há palavra para descrever o resultado senão sinergia.

O problema do acelerômetro

O acelerômetro no Windows Phone é um bom exemplo de sensor que fornece algumas informações essenciais, mas torna-se muito mais valioso quando combinado com outro sensor, especialmente a bússola.

Na verdade, o hardware do acelerômetro mede força, mas como sabemos da física, força é igual a massa vezes aceleração, portanto, o acelerômetro responde a qualquer espécie de aceleração. Quando o telefone é mantido parado, o acelerômetro mede a gravidade e fornece um vetor 3D que aponta em direção ao centro da Terra. Esse vetor é relativo a um sistema de coordenadas 3D, como mostrado na Figura 1. Esse sistema de coordenadas é o mesmo esteja você codificando um programa do Silverlight ou XNA, ou executando em modo retrato ou paisagem.

The Phone’s Sensor Coordinate System
Figura 1 O sistema de coordenadas do sensor do telefone

A classe Accelerometer fornece o vetor de aceleração na forma de um valor Vector3. Esse é um tipo XNA, portanto, se precisar usá-lo em um programa do Silverlight, precisará de uma referência ao assembly Microsoft.Xna.Framework.

Embora o vetor de aceleração permita um programa em execução no telefone para determinar a orientação do telefone relativa à Terra, estão faltando algumas informações cruciais. Vou demonstrar do que estou falando.

No código que pode ser baixado dessa coluna (disponível em archive.msdn.microsoft.com/mag201206TouchAndGo) encontra-se uma solução do Visual Studio chamada 3DPointers que contém quatro projetos 3D do XNA, todos os quais um tanto similares. O programa Accelerometer 3D desenha um “pino” 3D que flutua no espaço na direção do vetor do acelerômetro, como mostrado na Figura 2.

The Accelerometer 3D Display
Figura 2 A tela do Accelerometer 3D

Todo programa que usa um sensor do Windows Phone precisa de uma referência ao assembly Microsoft.Devices.Sensors. Normalmente programas do XNA no telefone são executados em modo paisagem. Isso pode ser um problema, pois não corresponde ao sistema de coordenadas usado pelos sensores. Para facilitar as coisas para mim mesmo, reorientei o sistema de coordenadas do XNA para modo retrato no construtor do derivativo Game do programa.

graphics.IsFullScreen = true;
graphics.PreferredBackBufferWidth = 480;
graphics.PreferredBackBufferHeight = 800;

No Windows Phone 7.1 a API do sensor foi alterada um pouco para proporcionar consistência entre os vários sensores. O construtor do programa Accelerometer 3D usa essa nova API para criar uma instância Accelerometer que é salva como um campo:

if (Accelerometer.IsSupported)
{
  accelerometer = new Accelerometer
  {
    TimeBetweenUpdates = this.TargetElapsedTime
  };
}

A propriedade TimeBetweenUpdates padrão é 25 milissegundos; aqui, está definida como a taxa de quadros do programa, que é 33 ms.

O programa usa uma substituição do método OnActivated para iniciar o Accelerometer:

if (accelerometer != null)
{
  try { accelerometer.Start(); }
  catch { }
}

Embora seja difícil imaginar um cenário em que o método Start irá falhar nesse ponto, recomendamos que ele seja colocado em um bloco try. A bússola é parada em OnDeactivated:

if (accelerometer != null)
    accelerometer.Stop();

O programa usa o método LoadContent para criar vértices 3D para o “pino” e definir um BasicEffect para armazenar as informações da câmera e de iluminação. O pino é definido de modo que a base está na origem e se estende uma unidade acima no eixo positivo Y. A câmera aponta diretamente para a origem a partir do eixo positivo Z.

O método Update do programa então usa o vetor do acelerômetro para definir uma transformação World. Essa transformação World move efetivamente o pino relativo à origem. A Figura 3 mostra o código.

Figura 3 O método Update no Accelerometer 3D

protected override void Update(GameTime gameTime)
{
  if (GamePad.GetState(PlayerIndex.One).Buttons.Back 
    == ButtonState.Pressed)
      this.Exit();
  if (accelerometer != null && accelerometer.IsDataValid)
  {
    Vector3 acceleration = accelerometer.CurrentValue.Acceleration;
    text = String.Format("X = {0:F3}\nY = {1:F3}\nZ = {2:F3}",
                            acceleration.X,
                            acceleration.Y,
                            acceleration.Z);
    textPosition = new Vector2(0, this.GraphicsDevice.Viewport.Height -
                                    segoe24Font.MeasureString(text).Y);
    acceleration.Normalize();
    Vector3 axis = Vector3.Cross(acceleration, Vector3.UnitY);
    // Special case for magnetometer equal to (0, 1, 0) or (0, -1, 0)
    if (axis.LengthSquared() == 0)
        axis = Vector3.UnitX;
    else
        axis.Normalize();
    float angle = -(float)Math.Acos(Vector3.Dot(Vector3.UnitY, 
      acceleration));
    basicEffect.World = Matrix.CreateFromAxisAngle(axis, angle);
  }
  else
  {
    basicEffect.World = Matrix.Identity;
    text = "";
  }
  base.Update(gameTime);
}

O método obtém o valor Acceleration diretamente do objeto Accelerometer se a propriedade IsDataValid for true. O pino deve ser girado com base no ângulo entre o vetor Acceleration e o eixo Y positivo. O produto escalar desses dois vetores fornece esse ângulo, enquanto o eixo de rotação é fornecido pelo produto vetorial.

Experimente isto: Levante-se e segure o telefone em sua mão em um determinado ângulo. O pino aponta para baixo. Agora, ainda em pé, gire 360 graus. Enquanto você está girando, o pino se mantém na mesma posição (ou quase isso). O acelerômetro não pode discernir quando o telefone está se movendo em torno de um eixo paralelo ao vetor de aceleração. Se estiver escrevendo um aplicativo que precisa saber a orientação 3D completa do telefone, o acelerômetro fornece apenas parte do que você precisa.

A bússola em ação

Quando inicialmente o Windows Phone foi lançado, não estava muito claro se os telefones tinham mesmo uma bússola. Ninguém parecia ter uma resposta definitiva. Para programadores de aplicativos, no entanto, a situação era muito simples: Não havia uma interface de programação para uma bússola, portanto, mesmo que existisse não poderíamos usá-la.

O lançamento do Windows Phone 7.1 esclareceu o problema: Embora um dispositivo Windows Phone não seja necessário para ter uma bússola, alguns dispositivos Windows Phone sempre tiveram uma, incluindo um smartphone com o Windows Phone que comprei em dezembro de 2010. Melhor de tudo, os programadores podem realmente usar essa bússola. O Windows Phone 7.1 introduziu uma nova classe Compass que permite ao programador de aplicativos saber se a bússola existe (por meio da propriedade estática Compass.IsSupported) e ter acesso às suas leituras. Compass.IsSupported retorna false no emulador do Windows Phone.

A base da bússola do telefone é uma peça de hardware conhecida como magnetômetro. Esse magnetômetro é afetado por qualquer força magnética próxima ao telefone, incluindo as que talvez estejam em alto-falantes na sua mesa de computador. Se esses ímãs forem mantidos afastados do telefone, o magnetômetro medirá a força e a direção do campo magnético da Terra.

A classe Compass fornece dados na forma de uma estrutura CompassReading. Os dados brutos do magnetômetro estão disponíveis em uma propriedade chamada MagnetometerReading, que é outro Vector3 relativo ao sistema de coordenadas mostrado na Figura 1. Na ausência de ímãs próximos, esse vetor fica alinhado com o campo magnético da Terra. O vetor aponta para uma direção norte, com certeza, mas na maioria das localidades do hemisfério norte, ele também aponta para a Terra. Se você segurar o telefone com a tela virada para cima, esse vetor terá um componente –Z significativo.

A solução 3DPointers contém um projeto chamado Magnetometer 3D que é similar ao projeto Accelerometer 3D, exceto que ele usa a classe Compass em vez da classe Accelerometer. A Figura 4mostra a tela desse programa quando seguro o telefone em meu apartamento em Manhattan com a tela virada para cima e a parte superior do telefone apontando para a “cidade alta,” isto é, com as laterais esquerda e direita do telefone alinhadas (o mais próximo que pude) com as avenidas de Nova York. (Um mapa revela esses avenidas correndo cerca de 30 graus à leste do norte.)


Figura 4 A tela do Magnetometer 3D

A documentação declara que o vetor MagnetometerReading está em unidades de microteslas. Em dois dos dispositivos Windows Phone comerciais que possuo, esse vetor geralmente tem uma magnitude na casa dos 30, que é aproximadamente correto. (Os valores do vetor mostrado na Figure 4 têm uma magnitude composta de 43.) No entanto, para um terceiro telefone que possuo, o vetor MagnetometerReading está normalizado e sempre tem uma magnitude de 1.

Agora experimente isto: Segure o telefone de modo que o vetor Magnetometer 3D esteja quase alinhado com o eixo Y positivo da Figura 1. Agora gire o telefone em torno de um eixo paralelo a esse vetor. O vetor permanece o mesmo (ou quase), indicando que o magnetômetro do telefone também não tem conhecimento completo da orientação do telefone.

Rumos da bússola

Normalmente, quando estamos usando uma bússola, não queremos um vetor 3D alinhado com o campo magnético da Terra. Muito mais útil seria um vetor 2D tangente à superfície da Terra.

Como você provavelmente sabe, o campo magnético da Terra não coincide com o eixo no qual a Terra gira. A direção do eixo da Terra é chamado norte geográfico ou norte verdadeiro, e esse é o norte usado em mapas e virtualmente todo os outros propósitos. Os ângulos em uma superfície bidimensional são frequentemente usados para representar a direção do norte. Essa direção é com frequência chamada rumo.

A diferença entre o norte magnético e o norte verdadeiro varia em todo o globo. Na cidade de Nova York, cerca de 13 graus devem ser subtraídos do rumo magnético para obter o rumo verdadeiro, mas em Seattle, 21 graus devem ser adicionados ao rumo magnético. A classe Compass executa esses cálculos para você com base na localização do telefone. Além do vetor MagnetometerReading, CompassReading também fornece duas propriedades do tipo double chamadas MagneticHeading e TrueHeading. Ambas são ângulos em graus no intervalo de 0 a 360 medidos no sentido anti-horário a partir do eixo Y positivo mostrado na Figura 1.

TrueHeading deve sempre ser interpretado com um valor aproximado, e mesmo assim não é confiável inteiramente. Em dois dos telefones que possuo, TrueHeading está normalmente quase certo, mas em outro telefone, está fora bem uns 70 graus.

Não pude me aprofundar no valor MagneticHeading. Para um local específico, a diferença entre os valores de TrueHeading e MagneticHeading deve ser uma constante. Por exemplo, onde moro, o valor de TrueHeading menos Magnetic­Heading deve ser aproximadamente –13 graus. Em todos os meus três telefones, a diferença entre TrueHeading e MagneticHeading pula esporadicamente entre dois valores dependendo da orientação do telefone. A diferença é algumas vezes –12 (que está quase certo), mas na maioria das vezes a diferença é 92. Esses são os dois únicos valores da diferença que vi. Em nenhum dos meus telefones MagneticHeading é consistente com o ângulo derivado dos valores X e Y do vetor MagnetometerReading.

Em um programa do XNA, como visto, você pode simplesmente obter um valor atual de um sensor durante o método Update. Ao usar uma classe de sensor em um programa do Silverlight, você deve definir um manipulador para o evento CurrentValueChanged. Você pode então obter um objeto de leitura de sensor dos argumentos do sensor.

O código que pode ser baixado para esse artigo contém dois programas do Silverlight—Arrow Compass e Dial Compass—que usam a propriedade TrueHeading para mostrar a direção do norte. Todos os elementos gráficos são definidos em XAML. Como com os programas do XNA, esses programas do Silverlight criam o objeto Compass em seus construtores, mas também definem um manipulador para a propriedade CurrentValueChanged:

if (Compass.IsSupported)
{
  compass = new Compass();
  compass.TimeBetweenUpdates = TimeSpan.FromMilliseconds(33);
  compass.CurrentValueChanged += OnCompassCurrentValueChanged;
}

No Arrow Compass esse manipulador define o ângulo em um objeto RotateTransform anexado a um elemento gráfico de seta:

this.Dispatcher.BeginInvoke(() =>
{
  arrowRotate.Angle = -args.SensorReading.TrueHeading;
  accuracyText.Text = String.Format("±{0}°",
                      args.SensorReading.HeadingAccuracy);
});

O manipulador CurrentValueChanged é chamado em um thread separado, portanto, você precisará usar um Dispatcher para atualizar qualquer objeto da interface do usuário. Como o ângulo TrueHeading indica um deslocamento em sentido anti-horário e as rotações do Silverlight são no sentido horário, o código usa o negativo do ângulo de rumo para a rotação.

O resultado é mostrado na Figura 5, novamente com o telefone apontando para a parte alta da cidade de Nova York.

The Arrow Compass Display
Figura 5 A tela do Arrow Compass

No Dial Compass, a seta permanece fixa enquanto um dial gira para indicar a direção, como mostrado na Figura 6. Use essa variação se quiser saber a direção que a parte superior do telefone está apontando, em vez da direção do norte relativa ao telefone.

The Dial Compass Display
Figura 6 A tela do Dial Compass

Se você executar qualquer um desses dois programas e segurar o telefone de modo que a tela fique virada para baixo, a bússola não mais funcionará corretamente. A rotação é oposta ao que deveria ser. Se precisar corrigir isso, use o valor positivo do valor TrueHeading quando o valor Z do vetor de aceleração for positivo.

Calibrando a bússola

No canto inferior direito, o programa Arrow Compass exibe a propriedade HeadingAccuracy do valor CompassReading. Em teoria, isso fornece a precisão dos valores de rumo. Na prática, vi valores de HeadingAccuracy no intervalo de 5% a 30%. (Mas só vi 5% no telefone que está bem fora!)

A classe Compass também define um evento chamado Calibrate que é disparado quando o valor HeadingAccuracy excede 20%.

Você pode executar uma manobra de calibração para reduzir esse HeadingAccuracy: Segure o telefone afastado de seu corpo com a tela apontando para a esquerda ou direita, e então mova seu braço em um padrão de infinito várias vezes. Um exemplo de código fornecido com os tutoriais do MSDN (em bit.ly/yYrHrL) tem até mesmo um elemento gráfico que pode ser exibido para notificar seus usuários quando a bússola precisar de calibração.

Combinando bússola e acelerômetro

A bússola do telefone é um exemplo perfeito de sensor que, obviamente, tem alguma utilidade por si só, particularmente se estiver perdido na floresta, mas que se torna muito mais valiosa quando combinada com outros sensores, especialmente o acelerômetro. Juntos, esses dois sensores podem fornecer uma orientação completa do telefone no espaço 3D.

De fato, o Windows Phone 7.1 define uma nova classe que faz exatamente isso. Além de fornecer a orientação do telefone de três maneiras diferentes, a classe Motion também incorpora informações da nova classe Gyroscope, se houver uma presente no telefone.

Acima de tudo, a classe Motion faz um trabalho adicional desvendando os dados do acelerômetro e da bússola. Se esteve executando os programas apresentados até o momento, deve ter observado instabilidade significativa nos dados dessas classes. Essa instabilidade desapareceu da classe Motion.

No entanto, como sou uma pessoa que gosta de um bom desafio, pensei em arriscar na combinação dos dados do acelerômetro e da bússola “manualmente”, e devo admitir que a experiência me fez gostar ainda mais da classe Motion.

O programa Compass 3D exibe quatro pinos coloridos diferentemente arranjados em um círculo para apontar o norte (prata), o leste (vermelho), o sul (verde) e o oeste (azul). O programa tenta exibir esse plano de pinos paralelo à superfície da Terra e orientado corretamente em direção aos quatro pontos da bússola.

A estratégia que tomei foi derivar ângulos de Euler. Eles são três ângulos que representam a rotação em torno dos eixos X, Y e Z, e juntos descrevem uma orientação no espaço 3D. Na dinâmica de voo, os três ângulos são denominados pitch, roll e yaw. Da perspectiva de uma aeronave, pitch indica se o nariz está para cima ou para baixo e de quanto, enquanto roll indica qualquer inclinação do avião para a direita ou esquerda. Esses ângulos de rotação podem ser visualizados como relativos a dois eixos: pitch é a rotação em torno de um eixo que se estende pelas asas, enquanto roll é baseado em um eixo da frente do avião até o fundo. Yaw é a rotação em torno de um eixo perpendicular à superfície da Terra e indica o rumo da bússola para o avião.

Para visualizar esses ângulos com respeito ao sistema de coordenadas do telefone da Figura 1, imagine você montando no telefone como uma tapete mágico, sentando na tela com a parte superior do telefone à sua frente e os três botões atrás. Pitch é a rotação em torno do eixo X, roll é a rotação em torno do eixo Y e yaw é a rotação em torno do eixo Z.

Calcular roll e pitch a partir do vetor de aceleração acabou sendo muito fácil e envolve fórmulas padrão:

float roll = (float)Math.Asin(-acceleration.X);
float pitch = (float)Math.Atan2(acceleration.Y, -acceleration.Z);

Com o telefone plano sobre uma mesa com sua tela virada para cima, tanto pitch quanto roll são zero. Roll vai de –π/2 a π/2, e os valores voltam a zero quando o telefone é virado para baixo. Pitch vai de –π a π com os valores máximos quando o telefone está virado para baixo. Com a tela do telefone virada para cima, yaw deve ter mais ou menos o mesmo valor que a propriedade TrueHeading de Compass, mas convertido em radianos para os objetivos do XNA:

float yaw = MathHelper.ToRadians((float)compass.CurrentValue.TrueHeading);

No entanto, como visto com os dois programas de bússola do Silverlight, TrueHeading para de funcionar corretamente quando o telefone é virado com sua tela na direção da Terra, portanto, yaw precisa ser corrigido por isso. Depois de algumas divagações teóricas e empíricas no assunto, deixei como estava, e construí uma transformação a partir dos três ângulos:

basicEffect.World = Matrix.CreateRotationZ(yaw) *
                    Matrix.CreateRotationY(roll) *
                    Matrix.CreateRotationX(pitch);

Os resultados são mostrados na Figura 7.

The Compass 3D Program
Figure 7 O programa Compass 3D

Também incluí um programa Orientation 3D que obtém esses três ângulos da classe Motion. Você mesmo pode ver o quão mais suaves os resultados são (além de funcionar melhor quando o telefone está virado para baixo).

A classe Motion é uma adição muito importante à API do sensor apenas por esse único programa. Como você verá na próxima edição dessa coluna, a classe pode realmente servir como um portal para o mundo 3D.

Charles Petzold é colaborador da MSDN Magazine há muito tempo. O endereço do seu site é charlespetzold.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Drew Batchelor