Expo(React NativeのWebView)を使ってA-Frame ARアプリ作成してみました


A-FrameでARアプリを作成してみましたので、作成方法をご紹介します。

インターネット上の情報を色々と参考にさせてもらい実施しました。

ARというと、ネイティブ形式が主流かもしれませんが、試作を行うのに簡単に試してみたい等であれば、A-FrameのWeb形式でも要件を満たすかもしれません。
こったことはしておらず、基本的な流れですが、何かの参考になれば幸いです。

前提として、以下がありますので、ご注意ください。

  • 動作確認の構成
    • PC: M1 Mac
    • OS: 11.4
    • SW
      • Expo: 4.7.2
      • Xcode: 12.5.1
      • A-Frame: 1.2.0
      • AR.js: 3.3.3
    • スマートデバイス: iPhone 7
    • OS: 14.6
  • 2021年7月頃に調べたり動作確認した内容です。

Androidはまだ動作しないのでご注意ください(ユーザ許可設定が正しくできていないようか気がしています)

本ブログの要点的な内容を最初に乗せると以下の流れになりました。

1. A-Frame AR.jsのブラウザ版を作成・確認

最初は、A-FrameでARをするため、インターネット上に情報が多いAR.jsによるブラウザ版を作成しました。(まだネイティブのARアプリではないです)
情報が古いものを活用しているかもしれませんが、まずは動くものを用意するということで、ご了承ください。

まずはA-Frameの情報を参照しました。

A-Frameサイト:https://aframe.io/
GitHub:https://github.com/aframevr/aframe/
 ライセンス:MIT License:https://github.com/aframevr/aframe/blob/master/LICENSE

AR.jsサイト:https://ar-js-org.github.io/AR.js-Docs/
GitHub:https://github.com/AR-js-org/AR.js
 ライセンス:MIT License:https://github.com/AR-js-org/AR.js/blob/master/LICENSE

どなたかの簡単に動作させた記事が手っ取り早く試すのには参考になるので、インターネットで検索して、以下等を参照させてもらいました。

https://j-xaas.github.io/ar-js-x-a-frame-WebAR%E5%85%A5%E9%96%80/
https://ar-js-org.github.io/AR.js-Docs/#getting-started
 Marker Based Example

上記の「Marker Based Example」のコードを活用させてもらい、少しだけ恐竜を回転させました。
あとテキストと箱も入れてみました。


<!DOCTYPE html>
<html>
  <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
  <!-- we import arjs version without NFT but with marker + location based support -->
  <script src="https://raw.githack.com/AR-js-org/AR.js/master/aframe/build/aframe-ar.js"></script>
  <body style="margin : 0px; overflow: hidden;">
    <a-scene embedded arjs>
      <a-marker preset="hiro">
        <a-text value="this is text." position="-2 0 0" align="center" rotation="-90 0 0" color="#4CC3D9"></a-text>
        <a-box position="2.0 0.0 0.0" rotation="0 45 0" color="#4CC3D9" shadow></a-box>
        <a-entity
          position="0 0 0"
          scale="0.04 0.04 0.04"
          rotation="-45 0 -45" 
          gltf-model="https://arjs-cors-proxy.herokuapp.com/https://raw.githack.com/AR-js-org/AR.js/master/aframe/examples/image-tracking/nft/trex/scene.gltf"
        ></a-entity>
      </a-marker>
      <a-entity camera></a-entity>
    </a-scene>
  </body>
</html>

実際の表示は以下のような画像になりました。

今回は、以下のような感じで、ペーパーレス(マーカーを印刷せずに)でテストをしました。

  1. Hiroマーカーは上記サイト上( https://raw.githubusercontent.com/AR-js-org/AR.js/master/data/images/hiro.png )から拝借しました。
  2. Hiroマーカーをフォトビューアー等で表示し、OBS Studioの仮想カメラソフトを使って、マーカーをカメラに写るように設定して仮想カメラを開始しておきます。(Windows PCに付属カメラがついている場合はデバイスマネージャから無効化する必要がありました)
  3. PCのChromeブラウザで、上記のhtmlファイルを表示し、マーカー上にARオブジェクトが表示されることを確認します。

あと、スマートデバイスのブラウザでも表示できることを、以下の流れで確認しました。

  1. AWS S3にhtmlファイルを配置
  2. S3 htmlファイルの署名付き一時URLを生成
  3. iPhoneのSafariやChromeから上記の署名付き一時URLにアクセス
  4. PCのHiroマーカーを映して確認

ざっと、A-FrameとAR.jsでAR表示ができました。
AR.jsには、ロケーションベース等、マーカー以外の機能もあるので今後試して見たいと思います。

2. Expo WebViewアプリを作成・確認

次に、ExpoのWebViewでARを表示するアプリを作成します。

上記で試したhtmlを活用していきます。

WebViewには、ソースを指定する形式に以下の種類がありました。
https://docs.expo.dev/versions/latest/sdk/webview/

  • html形式
    
    <WebView
    style={styles.container}
    originWhitelist={['*']}
    source={{ html: '<h1><center>Hello world</center></h1>' }}
    />
    
  • uri形式
    
    <WebView 
    style={styles.container}
    source={{ uri: 'https://expo.dev' }}
    />
    

試したところ、html形式で、htmlファイルの内容をプログラム内に埋め込んでも、エラーとなり、カメラが正常に動作していないようでした。
「httpsスキーマを使わないと正常に動作しない」という情報をインターネットで目にしたので、uri形式で進めました。

ユーザ許可設定周りも確認したかったので、スタンドアロンアプリとして、M1 MacからiPhoneへアプリをインストールして動作を確認しました。
インストール手順については、以下の過去記事を参照してください。
(Expo使っているなら、Expo Goを使ってアプリを起動するのが通常かと思いますが。。。)
https://qiita.com/k-hideo/items/6aff1c187e53addd1550

動作確認を検証していてわかったのですが、WebViewでカメラを使う場合、NSCameraUsageDescriptionとは別に、NSMicrophoneUsageDescriptionのマイクも必要でした。
マイクがないと以下のエラーメッセージが表示されます。



Webcam Error
Name:
Message: WebRTC issue-!
navigator.mediaDevices not present in your browser


ただ、マイク設定も必要なのは、もしかしたら最新や今後改善されているかもしれません。(こちらのスレッドにこの件についてのやり取りがありました)

それで、WebViewのuri形式を使い、S3の署名付き一時URLを指定して確認したところ、カメラ映像は表示されるのですが、AR表示がうまくいきませんでした。
以下のような、静止画にARオブジェクトが表示されるような感じになってしまいました。(映像は表示されるがARオブジェクトが表示されない。AR表示されるが静止画となり映像とはならない、という感じでどっちつかずでした)

Chromeだと正常に表示できるのになぜだろうと、ちょっとWebViewのリファレンスを調べてみると、allowsInlineMediaPlaybackオプションが説明文的や挙動的に怪しい気がしたので、
さらにallowsInlineMediaPlaybackをキーワードに検索してみると、試す価値がありそうでしたので、やってみました。

結果としては、うまく改善されて以下のようにARオブジェクトの表示ができるようになりました。

参考までに以下にコードやイメージ、ビルド等の実行コマンドを載せておきます。

  • ビルド コマンド類(viで作成するファイルの中身は、下のコード類を参照ください。)
    
    expo init ex-webview-ar-1
    Choose a template: › minimalを指定
    
    
    expo install react-native-webview
    expo install react-navigation
    expo install react-navigation-stack react-navigation-tabs react-native-gesture-handler react-native-reanimated
    expo install react-native-screens react-native-safe-area-context
    
    vi app.json
    mkdir screens
    vi screens/PageHtml1.js
    vi screens/PageUri1.js
    vi screens/PageUri2.js
    vi screens/PageUri3.js
    vi screens/PageTop1.js
    vi App.js
    
    npm install
    arch -x86_64 npx pod-install
    arch -x86_64 npx expo prebuild
    
    Xcodeでファイルを開きビルド・デプロイし動作を確認
    

  • コード類 (うまく転記できていない部分等ありましたらすみません。記法の関係上かインデント等が崩れている部分があります)
    app.json  (ここをクリックするとコードが以下に表示されます)
    
    {
    "expo": {
    "name": "ex-webview-ar-1",
    "slug": "ex-webview-ar-1",
    "version": "1.0.0",
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.k-hideo.ex-webview-ar-1",
      "infoPlist": {
        "NSCameraUsageDescription": "ARのためカメラを使用します",
        "NSMicrophoneUsageDescription": "ARのためカメラとセットでマイクを使用します"
      }
    },
    "android": {
      "package": "com.h_hideo.ex_webview_ar_1"
    }
    },
    "name": "ex-webview-ar-1"
    }
    
    screens/PageHtml1.js html形式でA-Frame AR.jsを記述しています。この場合、カメラが正常に動作しませんでした (ここをクリックするとコードが以下に表示されます)
    
    import * as React from 'react';
    import { WebView } from 'react-native-webview';
    
    
    export default class PageHtml1 extends React.Component {
      render() {
        return (
          <WebView
            originWhitelist={['*']}
            source={{ html: `
    <!DOCTYPE html>
    <html>
      <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
      <!-- we import arjs version without NFT but with marker + location based support -->
      <script src="https://raw.githack.com/AR-js-org/AR.js/master/aframe/build/aframe-ar.js"></script>
      <body style="margin : 0px; overflow: hidden;">
        <a-scene embedded arjs>
          <a-marker preset="hiro">
            <a-text value="this is text." position="-2 0 0" align="center" rotation="-90 0 0" color="#4CC3D9"></a-text>
            <a-box position="2.0 0.0 0.0" rotation="0 45 0" color="#4CC3D9" shadow></a-box>
            <a-entity
              position="0 0 0"
              scale="0.04 0.04 0.04"
              rotation="-45 0 -45" 
              gltf-model="https://arjs-cors-proxy.herokuapp.com/https://raw.githack.com/AR-js-org/AR.js/master/aframe/examples/image-tracking/nft/trex/scene.gltf"
            ></a-entity>
          </a-marker>
          <a-entity camera></a-entity>
        </a-scene>
      </body>
    </html>
            ` }}
            style={{ marginTop: 20 }}
            allowsInlineMediaPlayback={true}
          />
        );
      }
    }
    



    screens/PageUri1.js uri形式でA-FrameサイトのトップページのURIを指定しています。A-Frameサイトは正常に開けました (ここをクリックするとコードが以下に表示されます)

    
    import * as React from 'react';
    import { WebView } from 'react-native-webview';
    
    export default class PageUri1 extends React.Component {
      render() {
        return (
          <WebView
            originWhitelist={['*']}
            source={{ uri: `https://aframe.io/` }}
            style={{ marginTop: 20 }}
          />
        );
      }
    }
    



    screens/PageUri2.js uri形式でURIを入力できるようにしています、A-Frame AR.jsのページを入力すると正常に動作しました (ここをクリックするとコードが以下に表示されます)

    
    import * as React from 'react';
    import { View, Text, TextInput} from 'react-native';
    import { WebView } from 'react-native-webview';
    
    export default class PageUri2 extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          text: "https://aframe.io/",
          debug: "",
        };
      }
    
    render() {
        let webViewRef;
        return (
          <View style={{flex:1}}>
            <Text></Text>
            <Text></Text>
            <TextInput
              style={{
                width: "100%",
                borderBottomWidth: 1,
                borderBottomColor: "#ccc"
              }}
              onChangeText={(text) => {
                this.setState({text});
                console.log("before reload.");
                this.state.debug = " before reload. ";
                if (this.webViewRef) {
                  console.log("do reload.");
                  this.state.debug += " do reload. ";
                  //console.log(this.webViewRef);
                  this.webViewRef.reload();
                }
                console.log("after reload.");
                this.state.debug += " after reload. ";
            }}
            value={this.state.text}
            />
            <Text>{this.state.text}</Text>
            <Text>{this.state.debug}</Text>
            <WebView
              ref={this_ref => (this.webViewRef = this_ref)}
              originWhitelist={['*']}
              source={{ uri: this.state.text }}
              //source={{ uri: `https://aframe.io` }}
              style={{ marginTop: 20 }}
              allowsInlineMediaPlayback={true}
            />
          </View>
        );
      }
    }
    



    screens/PageUri3.js uri形式でA-Frame AR.jsのページを指定してます。こちらも正常に動作しました (ここをクリックするとコードが以下に表示されます)

    
    import * as React from 'react';
    import { WebView } from 'react-native-webview';
    
    export default class PageUri3 extends React.Component {
      render() {
        return (
          <WebView
            originWhitelist={['*']}
            source={{ uri: `https://bucket.s3.amazonaws.com/src/ar-1.2.0.html?AWSAccessKeyId=key&Signature=sig&Expires=1630940983` }}
            style={{ marginTop: 20 }}
            allowsInlineMediaPlayback={true}
          />
        );
      }
    }
    



    screens/PageTop1.js 上記のhtml形式やuri形式のページに遷移するトップページです (ここをクリックするとコードが以下に表示されます)

    
    import React, { Component } from 'react';
    import {
      Text,View,Button
    } from 'react-native';
    
    export default class PageTop1 extends Component {
      render() {
        return (
          <View>
            <Button
              title="go to html 1"
              onPress={() => {
                this.props.navigation.navigate('PageHtml1')
              }}
            />
            <Button
              title="go to uri 1: a-frame top"
              onPress={() => {
                this.props.navigation.navigate('PageUri1')
              }}
            />
            <Button
              title="go to uri 2: any uri"
              onPress={() => {
                this.props.navigation.navigate('PageUri2')
              }}
            />
            <Button
              title="go to uri 3: specify uri"
              onPress={() => {
                this.props.navigation.navigate('PageUri3')
              }}
            />
          </View>
        )
      }
    }
    



    App.js  (ここをクリックするとコードが以下に表示されます)

    
    import React, { Component } from 'react';
    import { createAppContainer } from 'react-navigation';
    import { createStackNavigator } from 'react-navigation-stack';
    import PageTop1 from './screens/PageTop1';
    import PageHtml1 from './screens/PageHtml1';
    import PageUri1 from './screens/PageUri1';
    import PageUri2 from './screens/PageUri2';
    import PageUri3 from './screens/PageUri3';
    
    const MainStack = createStackNavigator(
      {
        PageTop1: PageTop1,
        PageHtml1: PageHtml1,
        PageUri1: PageUri1,
        PageUri2: PageUri2,
        PageUri3: PageUri3,
      }
    )
    
    const AppContainer = createAppContainer(MainStack)
    
    export default class App extends Component {
    
    render() {
        return (
          <AppContainer />
        )
      }
    }
    


  • イメージ類

    • screens/PageTop1.jsの表示
    • creens/PageHtml1.jsのエラー表示
    • screens/PageUri1.jsのURIでA-FrameのトップページのURIを指定した場合の表示
    • screens/PageUri2.jsの表示(上のURIを修正すると、修正したURIにアクセスが可能)
    • screens/PageUri3.jsの表示(AWS S3にa-frame AR,jsのhtmlファイルを配置し署名付き一時URLを生成し指定)

3. さいごに

色々(Mac、ReactNative、Expo)と初心者のため、いくつものエラーが発生しまして、グーグル先生に何度もお世話になりながら、基本的なことですが、なんとか表示できるようになりました。これもみなさんが情報を残してくれているおかげですね。感謝感謝です。
もちろん社内の人にも情報を教えてもらったりしました。感謝感謝です。

A-Frame AR.jsをネイティブアプリで稼働させることにニーズが少ないかもしれませんが、選択肢の一つとなればよいのかと思います。

ひとまず基本的な方式の一つが動かせれるようになったので、今後時間と機会があれば、他の方式や、もうちょっと凝ったこともやってみたいと思います。