2
Help us understand the problem. What are the problem?

AFrameで2D・3Dの画像とビデオを表示する

私たちのARアプリケーションの1つでは、AFrameにARマーカー付きの3Dモデルに加えて、さまざまな2Dおよび3Dアセットを表示して、アプリケーションの機能を拡張したいと考えていました。 最初は、作業に時間がかかるように見えましたが...

どうなったでしょう?

AFrameで下記の5つのアセットの種類が表示できるようになりました:

  • 3Dのモデル
  • 3Dで2Dの画像(静止画のみ)
  • 3Dで2Dの動画
  • 2Dの画像(静止画とアニメーション画像)
  • 2Dの動画

適切なファイル形式

以下は、ブラウザのサポートに応じて、各アセットタイプに使用できるファイル形式の非網羅的なリストです:

  • 3Dのモデル:GLB、GLTF、OBJ、COLLADA (DAE)、PLY、JSON、FBX、three.js等
  • 3Dで2Dの画像:AVIF 、GIF、JPG、PNG、SVG、WebP等
  • 3Dで2Dの動画:3GPMOV、MP4、MPEG、MPG、OGGOGV、WebM等
  • 2Dの画像:APNG、AVIF、GIF、JPG、PNG、SVG、WebP等
  • 2Dの動画:3GPMOV、MP4、MPEG、MPG、OGGOGV、WebM等

Laravelのブレード

Laravelコントローラーからのブレードの引数として渡されるアセットのデータは下記の通りです:

$assets
[
  ...
  {
    "id": ◯◯◯◯,
    "key": "asset01",
    "marker_id": □□□□,
    "model_animation_mixer": "loop: repeat;"
    "model_path": "assets\/model-◯◯◯◯.glb",
    "rotation_x": 270,
    "rotation_y": 0,
    "rotation_z": 0,
    "scale": 7,
    "translation_x": 0,
    "translation_y": -0.8,
    "translation_z": 0,
    "type": "3D model",
  },
  {
    "id": ◯◯◯◯,
    "height": 540,
    "key": "asset02",
    "marker_id": □□□□,
    "rotation_x": 270,
    "rotation_y": 0,
    "rotation_z": 0,
    "scale": 0.003,
    "translation_x": 0,
    "translation_y": 0,
    "translation_z": 0,
    "type": "3D video",
    "video_path": "assets\/video-◯◯◯◯.mp4",
    "width": 960,
  },
  {
    "id": ◯◯◯◯,
    "height": 370,
    "image_path": "assets\/image-◯◯◯◯.png",
    "key": "asset03",
    "marker_id": □□□□,
    "scale": 2,
    "translation_x": 0.5,
    "translation_y": 0.5,
    "type": "2D image",
    "width": 278,
  },
  ...
]

3Dコンテンツ

3Dコンテンツは下記の3つのアセットの種類です:

  • 3Dのモデル
  • 3Dで2Dの画像(静止画のみ)
  • 3Dで2Dの動画

AFrameSceneコンポネントで直接表示できます:

AR.blade.php
    <a-scene
        vr-mode-ui="enabled: false;"
        renderer="logarithmicDepthBuffer: true;"
        arjs="trackingMethod: best; sourceType: webcam; debugUIEnabled: false;"
        device-orientation-permission-ui="enabled: false;"
        embedded
    >
        @foreach ($assets as $assetData)

アセットごとに、最初にAFrameアセット管理システムを使用してファイルからデータをキャッシュします:

AR.blade.php
            <a-assets>
                @if ($assetData['type']=='3D model')
                    <a-asset-item id="model-{{ $assetData['id'] }}" src="{{ $assetData['model_path'] }}" response-type="arraybuffer"></a-asset-item>
                @elseif ($assetData['type']=='3D image')
                    <img id="image-{{ $assetData['id'] }}" src="{{ $assetData['image_path'] }}">
                @elseif ($assetData['type']=='3D video')
                    <video id="video-{{ $assetData['id'] }}" autoplay loop="true" src="{{ $assetData['video_path'] }}">
                @endif
            </a-assets>

使用してる拡張現実(AR)技術はマーカーベースであり、各アセットは、読み取ったときにアセットを表示するマーカーにリンクされています。

Laravelコントローラーからのブレードの引数として渡されるマーカーのデータは下記の通りです:

$markers
{
  "◯◯◯◯": {
    "id": ◯◯◯◯,
    "image_path": "default\/marker\/image-□□.png",
    "pattern_path": "default\/markers\/pattern-□□.patt",
    ...
  },
  ...
}

aframe-arマーカーコンポネントを使用してマーカーを定義します(3Dおよび2Dのすべてのアセットの種類のためにマーカーを定義しますが、マーカーに子として追加する必要があるのは3Dアセットのみです。):

AR.blade.php

            <a-marker
                id="asset-{{ $assetData['id'] }}"
                data-stampid="{{ $assetData['id'] }}"
                type="pattern"
                preset="custom"
                url="{{ $markers[$assetData['marker_id']]->pattern_path }}"
                registerevents
            >

次に、アセットを子として<a-marker>タグに追加します:

  • <a-entity>は3Dモデルを表示するために使用されます:
AR.blade.php

                @if ($assetData['type']=='3D model')
                    <a-entity
                        gltf-model="#model-{{ $assetData['id'] }}"
                        scale="{{ $assetData['scale'] }} {{ $assetData['scale'] }} {{ $assetData['scale'] }}"
                        rotation="{{ $assetData['rotation_x'] }} {{ $assetData['rotation_y'] }} {{ $assetData['rotation_z'] }}"
                        animation-mixer="{{ $assetData['model_animation_mixer'] }}"
                        position="{{ $assetData['translation_x'] }} {{ $assetData['translation_y'] }} {{ $assetData['translation_z'] + 0.65 }}"
                    ></a-entity>
  • <a-image>は2D画像を3Dで表示するために使用されます:
AR.blade.php

                @elseif ($assetData['type']=='3D image')
                    <a-image
                        src="#image-{{ $assetData['id'] }}"
                        height="{{$assetData['height']}}"
                        width="{{$assetData['width']}}"
                        position="{{ $assetData['translation_x'] }} {{ $assetData['translation_y'] }} {{ $assetData['translation_z'] + 0.65 }}"
                        rotation="{{ $assetData['rotation_x'] }} {{ $assetData['rotation_y'] }} {{ $assetData['rotation_z'] }}"
                        scale="{{ $assetData['scale'] }} {{ $assetData['scale'] }} {{ $assetData['scale'] }}"
                    ></a-image>
  • <a-video>は2D動画を3Dで表示するために使用されます:
AR.blade.php

                @elseif ($assetData['type']=='3D video')
                    <a-video
                        src="#video-{{ $assetData['id'] }}"
                        height="{{$assetData['height'] * $assetData['scale']}}"
                        width="{{$assetData['width'] * $assetData['scale']}}"
                        position="{{ $assetData['translation_x'] }} {{ $assetData['translation_y'] }} {{ $assetData['translation_z'] + 0.65 }}"
                        rotation="{{ $assetData['rotation_x'] }} {{ $assetData['rotation_y'] }} {{ $assetData['rotation_z'] }}"
                    ></a-video>
                @endif
            </a-marker>
        @endforeach

※ 注意: マーカーに子として2Dアセットのエンティティを定義しません!

最後に、カメラをセットアップします:

AR.blade.php

        <a-entity camera></a-entity>
    </a-scene>

2Dコンテンツ

2Dコンテンツは下記の2つのアセットの種類です:

  • 2Dの画像(静止画とアニメーション画像)
  • 2Dの動画

2Dアセットを2Dで表示するためには、<img><video>のHTMLタグを使用します。デフォルトでアッセトを見えないようにします:

AR.blade.php

    <div class="2D-ar">
        @foreach ($stamps as $assetData)
            <div class="2D-layer">
                @if ($assetData['type']=='2D image')
                    <img
                        class="2D-layer-asset invisible"
                        id="asset-{{ $assetData['id'] }}-2D"
                        src="{{ $assetData['image_path'] }}"
                        style="left: {{ $assetData['translation_x'] * 100 }}%; top: {{ $assetData['translation_y'] * 100 }}%; "
                        height="{{ $assetData['height'] * $assetData['scale'] }}"
                        width="{{ $assetData['width'] * $assetData['scale'] }}"
                    />
               @elseif ($assetData['type']=='2D video')
                    <video
                        class="2D-layer-asset invisible"
                        id="asset-{{ $assetData['id'] }}-2D"
                        src="{{ $assetData['video_path'] }}"
                        style="left: {{ $assetData['translation_x'] * 100 }}%; top: {{ $assetData['translation_y'] * 100 }}%; "
                        height="{{ $assetData['height'] * $assetData['scale'] }}"
                        width="{{ $assetData['width'] * $assetData['scale'] }}"
                        autoplay
                        loop="true"
                    />
                @endif
            </div>
        @endforeach
    </div>

2DコンテンツのCSS

AR.scss
  .2D-ar {
  }
  .2D-ar .2D-layer {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    min-width: 100%;
    height: 100%;
    min-height: 100%;
    visibility: show;
  }
  .2D-ar .2D-layer .2D-layer-asset {
    position: relative;
    top: 50%;
    left: 50%;
    max-width: 320px;
    max-height: auto;
    visibility: show;
    transition: 1s;
    transform: translate(-50%, -50%);
    @media (min-width: 429px) {
      max-width: 450px;
    }
  }
  .2D-ar .2D-layer .2D-layer-asset.invisible {
    visibility: hidden;
    opacity: 0;
  }

JavaScript

3DアセットはHTMLタグを使用して簡単に表示できますが、ARマーカーがカメラで検出された場合は、2Dアセットを手動で表示する必要があります。AFrameマーカー関連イベントmarkerfoundmarkerlost)をキャッチするために、JavaScriptでAFrameのコンポーネントを登録し、初期化の時にはイベントのリスナーを追加します。

※ マーカーの前に読み込まないと動きません

AR.js
const sleep = function (msec) {
  return new Promise(function(resolve) {
    setTimeout(function() { resolve() }, msec);
  });
}

AFRAME.registerComponent('registerevents', {
    init: function () {
        var marker = this.el;
        const arStatus = window.arStatus;
        let markerStatus = arStatus.markerLost;

マーカーを検出したイベントのリスナーを追加します:

AR.js
        marker.addEventListener('markerFound', async function () {
            markerStatus = arStatus.markerFound;

誤認識を防ぐため、sleepユーティリティ関数を使用して一定時間処理を待ちます:

AR.js
            await sleep(arStatus.msecUntilArRecognition);

sleep完了するまでにマーカーを見失った場合は、処理を止めます:

AR.js
            if (markerStatus === arStatus.markerLost) {
                return;
            }

マーカーを見失ってない場合は2Dアセットを表示します:

AR.js
            show2DAsset(marker.id)
        });

マーカーを見失ったイベントのリスナーを追加します。マーカーを見失ったときには2Dアセットを非表示にします:

AR.js
        marker.addEventListener('markerLost', async function () {
            markerStatus = arStatus.markerLost;
            hide2DAsset(marker.id)
        });
    }
});

2Dアセットを非表示にする関数はアセットのHTML要素を取得してinvisibleのクラスを追加することで要素を非表示にします:

AR.js
function hide2DAsset(assetId) {
    const asset = document.getElementById(`${assetId}-2D`);
    if(asset) {
        asset.classList.add('invisible');
        
    }
}

2Dアセットを表示する関数はアセットのHTML要素を取得してinvisibleのクラスを削除することで要素を表示します:

AR.js
function show2DAsset(assetId) {
    const asset = document.getElementById(`${assetId}-2D`);
    if(asset) {
        asset.classList.remove('invisible');

2D画像がループしないアニメーション画像の場合、imgタグのsrcプロパティをリセットしてアニメーションを再起動する必要があります:

AR.js
        if (asset.tagName === 'IMG') {
            asset.src = asset.getAttribute('src');
        }
    }
}

※ アニメーション画像を3D画像として使用することはできません。ビデオを使用する必要がありますのでご注意ください

結論

AFrameを使用すると、ARアプリケーションで3Dモデル以外のさまざまなアセットタイプ(3Dおよび2D)を使用することが、当初の予想よりも簡単でした。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
2
Help us understand the problem. What are the problem?