Como o GWT desbloqueia a realidade aumentada no seu navegador

Publicados: 2022-03-11

Em nosso post anterior sobre o GWT Web Toolkit, discutimos os pontos fortes e as características do GWT, que, para relembrar a ideia geral, nos permite transpilar o código-fonte Java em JavaScript e misturar bibliotecas Java e JavaScript perfeitamente. Observamos que o JavaScript gerado pelo GWT é drasticamente otimizado.

No post de hoje, gostaríamos de ir um pouco mais fundo e ver o GWT Toolkit em ação. Vamos demonstrar como podemos aproveitar o GWT para construir uma aplicação peculiar: uma aplicação web de realidade aumentada (AR) que roda em tempo real, totalmente em JavaScript, no navegador.

Realidade aumentada no navegador? É mais fácil do que você pensa.

Neste artigo, vamos nos concentrar em como o GWT nos dá a capacidade de interagir facilmente com muitas APIs JavaScript, como WebRTC e WebGL, e nos permite aproveitar uma grande biblioteca Java, NyARToolkit, nunca destinada a ser usada no navegador. Mostraremos como o GWT permitiu que minha equipe e eu no Jooink juntássemos todas essas peças para criar nosso projeto de estimação, Picshare , um aplicativo de AR baseado em marcadores que você pode experimentar no seu navegador agora mesmo.

Este post não será um passo a passo abrangente de como construir o aplicativo, mas mostrará o uso do GWT para superar desafios aparentemente esmagadores com facilidade.

Visão geral do projeto: da realidade à realidade aumentada

Pipeline para realidade aumentada baseada em marcadores no navegador com GWT, com WebRTC, WebGL e ARToolKit.

Picshare usa realidade aumentada baseada em marcadores. Esse tipo de aplicativo de RA procura na cena um marcador : um padrão geométrico específico e facilmente reconhecido, como este. O marcador fornece informações sobre a posição e orientação do objeto marcado, permitindo que o software projete cenários 3D adicionais na imagem de forma realista. As etapas básicas desse processo são:

  • Acesso à câmera: Ao lidar com aplicativos de desktop nativos, o sistema operacional fornece acesso de E/S a grande parte do hardware do dispositivo. Não é a mesma coisa quando lidamos com aplicações web. Os navegadores foram construídos para serem uma espécie de “sandbox” para código JavaScript baixado da rede e, originalmente, não se destinavam a permitir que sites interagissem com a maioria dos dispositivos de hardware. O WebRTC rompe essa barreira usando os recursos de captura de mídia do HTML5s, permitindo que o navegador acesse, entre outras coisas, a câmera do dispositivo e seu fluxo.
  • Analisar o fluxo de vídeo: Temos o fluxo de vídeo... e agora? Temos que analisar cada quadro para detectar marcadores e calcular a posição do marcador no mundo 3D reconstruído. Essa tarefa complexa é o negócio do NyARToolkit.
  • Aumente o vídeo: Finalmente, queremos exibir o vídeo original com objetos 3D sintéticos adicionados. Usamos WebGL para desenhar a cena final aumentada na página da web.

Aproveitando as APIs do HTML5 com o GWT

O uso de APIs JavaScript como WebGL e WebRTC possibilita interações inesperadas e inusitadas entre o navegador e o usuário.

Por exemplo, o WebGL permite gráficos acelerados por hardware e, com a ajuda da especificação do array tipado, permite que o mecanismo JavaScript execute processamento de números com desempenho quase nativo. Da mesma forma, com o WebRTC, o navegador pode acessar fluxos de vídeo (e outros dados) diretamente do hardware do computador.

WebGL e WebRTC são bibliotecas JavaScript que devem ser incorporadas ao navegador da web. A maioria dos navegadores HTML5 modernos vem com pelo menos suporte parcial para ambas as APIs (como você pode ver aqui e aqui). Mas como podemos aproveitar essas ferramentas no GWT, que é escrito em Java? Conforme discutido no post anterior, a camada de interoperabilidade do GWT, JsInterop (lançado oficialmente no GWT 2.8) torna isso fácil.

Usar JsInterop com GWT 2.8 é tão fácil quanto adicionar -generateJsInteropExports como um argumento ao compilador. As anotações disponíveis são definidas no pacote jsinterop.annotations , empacotado em gwt-user.jar .

WebRTC

Como exemplo, com o mínimo de trabalho de codificação, usar o getUserMedia do WebRTC no Chrome com GWT se torna tão simples quanto escrever:

 Navigator.webkitGetUserMedia( configs, stream -> video.setSrc( URL.createObjectURL(stream) ), e -> Window.alert("Error: " + e) );

Onde a classe Navigator pode ser definida da seguinte forma:

 @JsType(namespace = JsPackage.GLOBAL, isNative = true, name="navigator") final static class Navigator { public static native void webkitGetUserMedia( Configs configs, SuccessCallback success, ErrorCallback error); }

De interesse é a definição das interfaces SuccessCallback e ErrorCallback , ambas implementadas pela expressão lambda acima e definidas em Java por meio da anotação @JsFunction :

 @JsFunction public interface SuccessCallback { public void onMediaSuccess(MediaStream stream); } @JsFunction public interface ErrorCallback { public void onError(DomException error); }

Por fim, a definição da URL da classe é quase idêntica à do Navigator e, da mesma forma, a classe Configs pode ser definida por:

 @JsType(namespace = JsPackage.GLOBAL, isNative = true, name="Object") public static class Configs { @JsProperty public native void setVideo(boolean getVideo); }

A implementação real de todas essas funcionalidades ocorre no mecanismo JavaScript do navegador.

Você pode encontrar o código acima no GitHub aqui.

Neste exemplo, para simplificar, a API obsoleta navigator.getUserMedia() é usada porque é a única que funciona sem polyfilling na versão estável atual do Chrome. Em um aplicativo de produção, podemos usar o adapter.js para acessar o stream por meio da API navigator.mediaDevices.getUserMedia() mais recente, uniformemente em todos os navegadores, mas isso está além do escopo da presente discussão.

WebGL

O uso do WebGL do GWT não é muito diferente do uso do WebRTC, mas é um pouco mais tedioso devido à complexidade intrínseca do padrão OpenGL.

Nossa abordagem aqui espelha a seguida na seção anterior. O resultado do wrap pode ser visto na implementação GWT WebGL utilizada no Picshare , que pode ser encontrada aqui, e um exemplo dos resultados produzidos pelo GWT pode ser encontrado aqui.

Habilitar o WebGL por si só não nos dá capacidade de gráficos 3D. Como escreve Gregg Tavares:

O que muitas pessoas não sabem é que WebGL é na verdade uma API 2D, não uma API 3D.

A aritmética 3D deve ser executada por algum outro código e transformada na imagem 2D para WebGL. Existem algumas boas bibliotecas GWT para gráficos 3D WebGL. Meu favorito é o Parallax, mas para a primeira versão do Picshare seguimos um caminho mais “faça você mesmo”, escrevendo uma pequena biblioteca para renderizar malhas 3D simples. A biblioteca nos permite definir uma câmera em perspectiva e gerenciar uma cena de objetos. Fique à vontade para conferir, aqui.

Compilando bibliotecas Java de terceiros com GWT

NyARToolkit é uma porta Java puro do ARToolKit, uma biblioteca de software para construir aplicativos de realidade aumentada. A porta foi escrita pelos desenvolvedores japoneses da Nyatla. Embora o ARToolKit original e a versão Nyatla tenham divergido um pouco desde a porta original, o NyARToolkit ainda é mantido e aprimorado ativamente.

A RA baseada em marcadores é um campo especializado e requer competência em visão computacional, processamento digital de imagens e matemática, como fica evidente aqui:

Análise de imagem de realidade aumentada baseada em marcadores com ARToolKit.

Reproduzido da documentação do ARToolKit.

Pipeline de realidade aumentada baseado em marcadores com ARToolKit.

Reproduzido da documentação do ARToolKit.

Todos os algoritmos usados ​​pelo kit de ferramentas são documentados e bem compreendidos, mas reescrevê-los do zero é um processo longo e propenso a erros, por isso é preferível usar um kit de ferramentas existente e comprovado, como o ARToolKit. Infelizmente, ao segmentar a web, não existe tal coisa disponível. Os kits de ferramentas mais poderosos e avançados não têm implementação em JavaScript, uma linguagem que é usada principalmente para manipular documentos e dados HTML. É aqui que o GWT prova sua força inestimável, permitindo-nos simplesmente transpilar o NyARToolkit em JavaScript e usá-lo em um aplicativo da Web com muito pouco incômodo.

Compilando com GWT

Como um projeto GWT é essencialmente um projeto Java, usar o NyARToolkit é apenas uma questão de importar os arquivos de origem em seu caminho de origem. No entanto, observe que, como a transpilação do código GWT para JavaScript é feita no nível do código-fonte, você precisa das fontes do NyARToolkit, e não apenas de um JAR com as classes compiladas.

A biblioteca usada pelo Picshare pode ser encontrada aqui. Depende apenas dos pacotes encontrados dentro lib/src e lib/src.markersystem da compilação NyARToolkit arquivada aqui. Devemos copiar e importar esses pacotes para nosso projeto GWT.

Devemos manter esses pacotes de terceiros separados de nossa própria implementação, mas para prosseguir com a “GWT-ização” do NyARToolkit devemos fornecer um arquivo de configuração XML que informe ao compilador GWT onde procurar as fontes. No pacote jp.nyatla.nyartoolkit , adicionamos o arquivo NyARToolkit.gwt.xml

 <module> <source path="core" /> <source path="detector" /> <source path="nyidmarker" /> <source path="processor" /> <source path="psarplaycard" /> <source path="markersystem" /> </module>

Agora, em nosso pacote principal, com.jooink.gwt.nyartoolkit , criamos o arquivo de configuração principal, GWT_NyARToolKit.gwt.xml , e instruímos o compilador a incluir a fonte do Nyatla no classpath herdando de seu arquivo XML:

 <inherits name='jp.nyatla.nyartoolkit.NyARToolkit'/>

Bem fácil, na verdade. Na maioria dos casos, isso seria o suficiente, mas infelizmente ainda não terminamos. Se tentarmos compilar ou executar através do modo Super Dev neste estágio, encontraremos um erro informando, surpreendentemente:

 No source code is available for type java.io.InputStream; did you forget to inherit a required module?

A razão para isso é que o NyARToolkit (ou seja, uma biblioteca Java destinada a projetos Java) usa classes do JRE que não são suportadas pelo JRE emulado do GWT. Discutimos isso brevemente no post anterior.

Nesse caso, o problema é com InputStream e classes de E/S relacionadas. Acontece que nem precisamos usar a maioria dessas classes, mas precisamos fornecer alguma implementação ao compilador. Bem, poderíamos investir muito tempo removendo manualmente essas referências da fonte do NyARToolkit, mas isso seria uma loucura. O GWT nos oferece uma solução melhor: fornecer nossas próprias implementações das classes não suportadas por meio da tag XML <super-source> .

<super-source>

Conforme descrito na documentação oficial:

A tag <super-source> instrui o compilador a refazer o root de um caminho de origem. Isso é útil para casos em que você deseja reutilizar uma API Java existente para um projeto GWT, mas a fonte original não está disponível ou não pode ser traduzida. Um motivo comum para isso é emular parte do JRE não implementado pelo GWT.

Então <super-source> é exatamente o que precisamos.

Podemos criar um diretório jre no projeto GWT, onde podemos colocar nossas implementações para as classes que estão nos causando problemas:

 java.io.FileInputStream java.io.InputStream java.io.InputStreamReader java.io.StreamTokenizer java.lang.reflect.Array java.nio.ByteBuffer java.nio.ByteOrder

Todos eles, exceto java.lang.reflect.Array , não são realmente usados, então só precisamos de implementações burras. Por exemplo, nosso FileInputStream é o seguinte:

 package java.io; import java.io.InputStream; import com.google.gwt.user.client.Window; public class FileInputStream extends InputStream { public FileInputStream(String filename) { Window.alert("WARNING, FileInputStream created with filename: " + filename ); } @Override public int read() { return 0; } }

A instrução Window.alert no construtor é útil durante o desenvolvimento. Embora devamos ser capazes de compilar a classe, queremos garantir que nunca a usemos, então isso nos alertará caso a classe seja usada inadvertidamente.

java.lang.reflect.Array é realmente usado pelo código que precisamos, então uma implementação não completamente burra é necessária. Este é o nosso código:

 package java.lang.reflect; import jp.nyatla.nyartoolkit.core.labeling.rlelabeling.NyARRleLabelFragmentInfo; import jp.nyatla.nyartoolkit.markersystem.utils.SquareStack; import com.google.gwt.user.client.Window; public class Array { public static <T> Object newInstance(Class<T> c, int n) { if( NyARRleLabelFragmentInfo.class.equals(c)) return new NyARRleLabelFragmentInfo[n]; else if(SquareStack.Item.class.equals(c)) return new SquareStack.Item[n]; else Window.alert("Creating array of size " + n + " of " + c.toString()); return null; } }

Agora, se colocarmos <super-source path="jre"/> no arquivo do módulo GWT_NyARToolkit.gwt.xml , podemos compilar e usar o NyARToolkit com segurança em nosso projeto!

Colando tudo junto com GWT

Agora estamos na posição de ter:

  • WebRTC, uma tecnologia capaz de pegar um stream da webcam e exibi-lo em uma tag <video> .
  • WebGL, uma tecnologia capaz de manipular gráficos acelerados por hardware em um <canvas> HTML.
  • NyARToolkit, uma biblioteca Java capaz de pegar uma imagem (como um array de pixels), procurar um marcador e, se encontrado, nos fornecer uma matriz de transformação que define totalmente a posição do marcador no espaço 3D.

O desafio agora é integrar todas essas tecnologias juntas.

Projetar um espaço 3D na câmera.

Não entraremos em detalhes sobre como fazer isso, mas a ideia básica é usar as imagens de vídeo como plano de fundo de nossa cena (uma textura aplicada ao plano “distante” na imagem acima) e construir uma estrutura de dados 3D permitindo-nos projetar esta imagem no espaço usando os resultados do NyARToolkit.

Essa construção nos dá a estrutura certa para interagir com a biblioteca do NyARToolkit para reconhecimento de marcadores e desenhar o modelo 3D no topo da cena da câmera.

Tornar o fluxo da câmera utilizável é um pouco complicado. Os dados de vídeo só podem ser desenhados para um elemento <video> . O elemento HTML5 <video> é opaco e não nos permite extrair os dados da imagem diretamente, então somos forçados a copiar o vídeo para um <canvas> intermediário, extrair os dados da imagem, transformá-lo em um array de pixels e finalmente empurre-o para o método Sensor.update() do NyARToolkit. Então o NyARToolkit pode fazer o trabalho de identificar o marcador na imagem e retornar uma matriz de transformação correspondente à sua posição em nosso espaço 3D.

Com esses elementos, podemos colocar um objeto sintético exatamente sobre o marcador, em 3D, na transmissão de vídeo ao vivo! Graças ao alto desempenho do GWT, temos muitos recursos computacionais, então podemos até aplicar alguns efeitos de vídeo na tela, como sépia ou desfoque, antes de usá-lo como plano de fundo para a cena WebGL.

O código abreviado a seguir descreve o núcleo do processo:

 // given a <canvas> drawing context with appropriate width and height // and a <video> where the mediastream is drawn ... // for each video frame // draw the video frame on the canvas ctx.drawImage(video, 0, 0, w, h); // extract image data from the canvas ImageData capt = ctx.getImageData(0, 0, w, h); // convert the image data in a format acceptable by NyARToolkit ImageDataRaster input = new ImageDataRaster(capt); // push the image in to a NyARSensor sensor.update(input); // update the NyARMarkerSystem with the sensor nyar.update(sensor); // the NyARMarkerSystem contains information about the marker patterns and is able to detect them. // After the call to update, all the markers are detected and we can get information for each // marker that was found. if( nyar.isExistMarker( marker_id ) ) { NyARDoubleMatrix44 m = nyar.getMarkerMatrix(marker_id); // m is now the matrix representing the pose (position and orientation) of // the marker in the scene, so we can use it to superimpose an object of // our choice ... } ...

Com esta técnica, podemos gerar resultados como este:

Resultados do aplicativo de realidade aumentada no navegador Picshare.

Resultados do aplicativo de realidade aumentada no navegador Picshare com vários marcadores.

Este é o processo que usamos para criar o Picshare , onde você é convidado a imprimir um marcador ou exibi-lo em seu celular e jogar com AR baseado em marcadores em seu navegador. Aproveitar!

Considerações finais

Picshare é um projeto de estimação de longo prazo para nós no Jooink. A primeira implementação data de alguns anos e, mesmo assim, foi rápida o suficiente para ser impressionante. Neste link você pode ver um de nossos experimentos anteriores, compilado em 2012 e nunca tocado. Observe que no exemplo há apenas um <video> . As outras duas janelas são elementos <canvas> exibindo os resultados do processamento.

O GWT era poderoso o suficiente mesmo em 2012. Com o lançamento do GWT 2.8, ganhamos uma camada de interoperabilidade muito melhorada com JsInterop, aumentando ainda mais o desempenho. Além disso, para a celebração de muitos, também ganhamos um ambiente de desenvolvimento e depuração muito melhor, o Super Dev Mode. Ah sim, e suporte a Java 8.

Estamos ansiosos pelo GWT 3.0!