前回の記事では、VRMモデルデータの変換を行い、glb形式のデータの作成を行いました。
今回は、そのデータを「A-Frame」を使用して、WEB上に表示させてみましょう。

サンプルコード

まずは全体のコードです。
とりあえず動かしてみたい場合は、ソースをコピーしてモデルファイルのパスだけ差し替えれば動くと思います。

HTML

<html>

<head>
    <!-- aframe -->
    <script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
    <!-- アニメーション用拡張 -->
    <script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.2.0/dist/aframe-extras.min.js"></script>
</head>

<body>


    <a-scene class="sceneFrame" embedded vr-mode-ui="enabled:false">
        <!-- Sky, Emvironment -->
        <a-sky color="#cccccc"></a-sky>

        <a-entity light="type: ambient; color: #ffffff; intensity: 0.9" position="-5 3 3"></a-entity>
        <a-entity light="type: point; color: #ddddff; intensity: 0.5" position="1 2 1"></a-entity>

        <!-- GLTF, Obj -->
        <a-entity
            position="0 0.7 -1.5"
            rotation="0 0 0"
            gltf-model="src: url(モデルファイル名.glb);"
            animation-mixer="clip: *; timeScale:1"  id="vrmChara">
        </a-entity>
    </a-scene>


    <!-- UI -->
    <div class="uiArea">
        <!-- moveArea -->
        <div class="moveArea">
            <div class="arrowUp">
                <img src="./images/arrow_up.svg" width="50" height="50" class="moveBtn" data-move="up">
            </div>
            <div class="arrowLeft">
                <img src="./images/arrow_left.svg" width="50" height="50" class="moveBtn" data-move="left">
            </div>
            <div class="arrowRight">
                <img src="./images/arrow_right.svg" width="50" height="50" class="moveBtn" data-move="right">
            </div>
            <div class="arrowDown">
                <img src="./images/arrow_down.svg" width="50" height="50" class="moveBtn" data-move="down">
            </div>
        </div>
        <!-- /moveArea -->

        <!-- animationButton -->
        <div class="animationButton ro on">
            <img src="./images/button_play.png" width="60" height="60" class="play">
            <img src="./images/button_stop.png" width="60" height="60" class="stop">
        </div>
        <!-- /animationButton -->

        <!-- rotateArea -->
        <div class="rotateArea">
            <div class="rotateBtn right ro">
                <img src="./images/button_rotate_right.png" width="60" height="60">
            </div>
            <div class="rotateBtn left ro">
                <img src="./images/button_rotate_left.png" width="60" height="60">
            </div>
        </div>
        <!-- /rotateArea -->

    </div>
    <!-- /UI -->


    <!-- ui処理用 -->
    <script type="text/javascript" src="./js/index.js"></script>
</body>

</html>

JavaScript(index.js)

document.addEventListener("DOMContentLoaded", function () {

    // キャラクターの要素を取得
    const charaObj = document.getElementById("vrmChara");

    // 移動方向の定数
    const MoveType = {
        NONE: "none",
        UP: "up",
        DOWN: "down",
        LEFT: "left",
        RIGHT: "right"
    };

    // 回転方向の定数
    const RotateType = {
        NONE: "none",
        LEFT: "left",
        RIGHT: "right"
    };

    // キャラクターの移動方向と回転方向を保持する変数
    let charaMoveDir = MoveType.NONE;
    let charaRotateDir = RotateType.NONE;

    // 移動ボタンの要素を取得
    const moveButtons = document.querySelectorAll(".moveBtn");

    // 移動ボタンにイベントリスナーを設定
    moveButtons.forEach(function (btn) {
        btn.addEventListener("mousedown", function () {
            charaMoveDir = this.dataset.move;
        });
        btn.addEventListener("mouseup", function () {
            charaMoveDir = MoveType.NONE;
        });
    });

    // アニメーション切り替えボタンの要素を取得
    const animationButton = document.querySelector(".animationButton");

    // アニメーション切り替えボタンにイベントリスナーを設定
    animationButton.addEventListener("click", function () {
        this.classList.toggle("on");
        charaObj.setAttribute("animation-mixer", { timeScale: this.classList.contains("on") ? 1 : 0 });
    });

    // 回転ボタンの要素を取得
    const rotateButtons = document.querySelectorAll(".rotateBtn");

    // 回転ボタンにイベントリスナーを設定
    rotateButtons.forEach(function (btn) {
        btn.addEventListener("mousedown", function () {
            charaRotateDir = this.classList.contains("left") ? RotateType.LEFT : RotateType.RIGHT;
        });
        btn.addEventListener("mouseup", function () {
            charaRotateDir = RotateType.NONE;
        });
    });

    // 定期実行処理
    setInterval(updateCharacter, 50);

    // キャラクターの移動と回転を更新する関数
    function updateCharacter() {
        moveCharacter();
        rotateCharacter();
    }

    // キャラクターの移動を更新する関数
    function moveCharacter() {
        const moveSpeed = 0.1;

        switch (charaMoveDir) {
            case MoveType.UP:
                charaObj.object3D.position.y += moveSpeed;
                break;
            case MoveType.DOWN:
                charaObj.object3D.position.y -= moveSpeed;
                break;
            case MoveType.LEFT:
                charaObj.object3D.position.x -= moveSpeed;
                break;
            case MoveType.RIGHT:
                charaObj.object3D.position.x += moveSpeed;
                break;
        }

        // 移動範囲の制限
        charaObj.object3D.position.x = Math.min(Math.max(charaObj.object3D.position.x, -1.0), 1.0);
        charaObj.object3D.position.y = Math.min(Math.max(charaObj.object3D.position.y, -0.4), 1.3);
    }

    // キャラクターの回転を更新する関数
    function rotateCharacter() {
        const rotateSpeed = 0.1;

        switch (charaRotateDir) {
            case RotateType.LEFT:
                charaObj.object3D.rotation.y -= rotateSpeed;
                break;
            case RotateType.RIGHT:
                charaObj.object3D.rotation.y += rotateSpeed;
                break;
        }
    }
});

解説

次に、各処理の中身を確認していきましょう。

A-frameの読み込み

まずは、headタグ内に「aframe.js」とアニメーションを行うための拡張「aframe-extras.min.js」を呼び出しましょう。
※この2つの読み込みはbodyタグ終了前に配置すると正常に動作しないため、注意しましょう。

<head>
    <!-- aframe -->
    <script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
    <!-- アニメーション用拡張 -->
    <script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.2.0/dist/aframe-extras.min.js"></script>
</head>

[a-scene] 3Dシーンの設置

次に、<a-scene>タグで3D表示エリアを作成します。

<a-scene class="sceneFrame" embedded vr-mode-ui="enabled:false">
</a-scene>

embeddedを指定することで、HTML上の他の要素と共存できるようになり、ページ内にテキストやimgタグなどを配置できるようになります。
また、標準ではVRモードが有効になっているので、vr-mode-ui=”enabled:false”で無効化します。

[a-sky] [a-entity] 環境の設定

<a-sky>で背景、<a-entity light>で光源を追加しましょう。

<!-- Sky, Emvironment -->
<a-sky color="#cccccc"></a-sky>

<a-entity light="type: ambient; color: #ffffff; intensity: 0.9" position="-5 3 3"></a-entity>
<a-entity light="type: point; color: #ddddff; intensity: 0.5" position="1 2 1"></a-entity>

ライトの種類には「ambient」「point」など、色々な種類があるので、リファレンスを見てカスタマイズしましょう。
intensityはライトの強さを指定することができます。
positionはライトの発生位置です。

[a-entity] モデルファイルの読み込み

<a-entity>のgltf-modelで、読み込むモデルファイルを指定します。

<a-entity
    position="0 0.7 -1.5"
    rotation="0 0 0"
    gltf-model="src: url(モデルファイル名.glb);"
    animation-mixer="clip: *; timeScale:1"  id="vrmChara">
</a-entity>

animation-mixerでアニメーションを有効化させましょう。
これでHTML側の設定は完了です。

JS – 3Dモデル要素の取得

次にJavaScriptの処理を見ていきましょう。
要素の取得は、一般的な要素の取得と同じくgetElementで行うことができます。

let charaObj = document.getElementById("vrmChara");     // キャラクター

JS – 3Dモデル要素の操作

要素を操作する場合は、object3D要素のプロパティを設定します。

// 移動
charaObj.object3D.position.y += 0.1;
// 回転
charaObj.object3D.rotation.y += 0.1;

ボタンクリック時に移動や回転を行うなど、好きなようにカスタマイズしましょう。

JS – アニメーションの停止、再生

アニメーションの再生、停止を切り替える場合は、animation-mixerの値を変更させます

// 再生
charaObj.setAttribute("animation-mixer", { timeScale: 1 });
// 停止
charaObj.setAttribute("animation-mixer", { timeScale: 0 });

ざっくり調べた感じでは、Play, Stopのようなものが無さそう(?)なので、
アニメーションを停止する場合は、アニメーションの再生速度を0にして停止させましょう。

以上、A-FrameによるVRMモデル表示の手順になります。
自分で作ったキャラクターを、Webで表示させて色々なコンテンツを作ってみましょう!

ARなどでも活用ができそうです

追記

記事を書くのが遅れている間に、「VRMA」が正式リリースされていました・・・

three-vrmでVRMAを動かすこともできるようなので、気になった方は是非そちらも試してみてください!