私たちの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の動画:3GP、MOV、MP4、MPEG、MPG、OGG、OGV、WebM等
- 2Dの画像:APNG、AVIF、GIF、JPG、PNG、SVG、WebP等
- 2Dの動画:3GP、MOV、MP4、MPEG、MPG、OGG、OGV、WebM等
Laravelのブレード
Laravelコントローラーからのブレードの引数として渡されるアセットのデータは下記の通りです:
[
...
{
"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の動画
AFrameのSceneコンポネントで直接表示できます:
<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のアセット管理システムを使用してファイルからデータをキャッシュします:
<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コントローラーからのブレードの引数として渡されるマーカーのデータは下記の通りです:
{
"◯◯◯◯": {
"id": ◯◯◯◯,
"image_path": "default\/marker\/image-□□.png",
"pattern_path": "default\/markers\/pattern-□□.patt",
...
},
...
}
aframe-arのマーカーコンポネントを使用してマーカーを定義します(3Dおよび2Dのすべてのアセットの種類のためにマーカーを定義しますが、マーカーに子として追加する必要があるのは3Dアセットのみです。):
<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モデルを表示するために使用されます:
@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で表示するために使用されます:
@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で表示するために使用されます:
@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アセットのエンティティを定義しません!
最後に、カメラをセットアップします:
<a-entity camera></a-entity>
</a-scene>
2Dコンテンツ
2Dコンテンツは下記の2つのアセットの種類です:
- 2Dの画像(静止画とアニメーション画像)
- 2Dの動画
2Dアセットを2Dで表示するためには、<img>
と<video>
のHTMLタグを使用します。デフォルトでアッセトを見えないようにします:
<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
.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のマーカー関連イベント(markerfound
とmarkerlost
)をキャッチするために、JavaScriptでAFrameのコンポーネントを登録し、初期化の時にはイベントのリスナーを追加します。
※ マーカーの前に読み込まないと動きません
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;
マーカーを検出したイベントのリスナーを追加します:
marker.addEventListener('markerFound', async function () {
markerStatus = arStatus.markerFound;
誤認識を防ぐため、sleep
ユーティリティ関数を使用して一定時間処理を待ちます:
await sleep(arStatus.msecUntilArRecognition);
sleep
完了するまでにマーカーを見失った場合は、処理を止めます:
if (markerStatus === arStatus.markerLost) {
return;
}
マーカーを見失ってない場合は2Dアセットを表示します:
show2DAsset(marker.id)
});
マーカーを見失ったイベントのリスナーを追加します。マーカーを見失ったときには2Dアセットを非表示にします:
marker.addEventListener('markerLost', async function () {
markerStatus = arStatus.markerLost;
hide2DAsset(marker.id)
});
}
});
2Dアセットを非表示にする関数はアセットのHTML要素を取得してinvisible
のクラスを追加することで要素を非表示にします:
function hide2DAsset(assetId) {
const asset = document.getElementById(`${assetId}-2D`);
if(asset) {
asset.classList.add('invisible');
}
}
2Dアセットを表示する関数はアセットのHTML要素を取得してinvisible
のクラスを削除することで要素を表示します:
function show2DAsset(assetId) {
const asset = document.getElementById(`${assetId}-2D`);
if(asset) {
asset.classList.remove('invisible');
2D画像がループしないアニメーション画像の場合、img
タグのsrc
プロパティをリセットしてアニメーションを再起動する必要があります:
if (asset.tagName === 'IMG') {
asset.src = asset.getAttribute('src');
}
}
}
※ アニメーション画像を3D画像として使用することはできません。ビデオを使用する必要がありますのでご注意ください
結論
AFrameを使用すると、ARアプリケーションで3Dモデル以外のさまざまなアセットタイプ(3Dおよび2D)を使用することが、当初の予想よりも簡単でした。