VRMモデルをA-Frameを使って表示する。(Three-VRM v2.0.7, A-Frame v1.5.0)

A-FrameのシーンにThree-VRMでロードしたモデルをsetObject3D関数を使って入れた所、A-Frameで読み込んでいるThree.jsとThree-VRMで読み込んでいるThree.jsのインスタンスが違う為にA-Frame側で弾かれた。

[Error : Entity.setObject3D was called with an object that was not an instance of THREE.Object3D.]

A-Frameで使用しているシーン(sceneEl)に直接追加してしてみても弾かれます。

[Error : “Trying to add an element that doesn’t have an object3D“]

そこで今回はA-Frameと違うシーンをThree.jsで作成してからA-Frameのシーンに重ねる方法で表示させてみました。

ライブラリのバージョン

A-Frame : 1.5.0

Tree : r158

Three-VRM : 2.0.7

s-module-shims : 1.8.2

実装イメージ

### 実装の概念的イメージ ###

<!DOCTYPE html>
<html lang="jp">

    <head>
        <!-- ライブラリの読み込み -->
    </head>

    <body>
        <!-- a-frameの要素配置 -->
        <a-scene>
            <a-entity vrm=""></a-entity>
        </a-scene>

        <script type="module">
            // VRMモデルのアニメーション関数
            function vrmModelAnimate() {}
            
            // A-FrameのエンティティにVRMモデルを設定
            AFRAME.registerComponent("three-scene", {
                init: function() {
                  // VRMモデルの読み込み
                },
                tick: function (time, timeDelta) {
                  // VRMモデルの表示 update
                  // A-Frameのレンダラーを使ってカメラ映像の上にVRMモデル表示
                }
            });
        </script>
    </body>
</html>

 

グローバル変数の設定

VRMモデルをアニメーションさせる時計とA-Frameコンポーネント関数から参照出来る変数を用意します。

const clock = new THREE.Clock();
clock.start();
let avatar = undefined;

 

シーンの作成とVRMモデルの読み込み

コンポーネントのinit関数内で実施させます

init: function() {
    // sceneの作成
    this.threeScene = new THREE.Scene();
    
    // lightの設定
    const light = new THREE.DirectionalLight( 0xffffff, 1 );
    light.position.set( 1.0, 3.0, 1.0 ).normalize();
    this.threeScene.add( light );

    // VRMモデルの読み込みローダーの設定
    const loader = new GLTFLoader();
    loader.crossOrigin = 'anonymous';
    loader.register((parser) => {
        return new VRMLoaderPlugin(parser);
    });
    
    // VRMモデルのロード
    loader.load(
        'PATH/your_model.vrm', // VRMファイルのパスを指定
        (gltf) => {
            const vrm = gltf.userData.vrm;

            vrm.scene.traverse((obj) => {
                obj.frustumCulled = false;
            });
            
            // シーンへvrmモデルを追加
            this.threeScene.add(vrm.scene);
            
            // 読み込んだvrmを公開用変数に代入
            avatar = vrm;
            
            // vrmモデルの初期位置の調整
            vrm.scene.position.y = 0.5;
            vrm.scene.position.x = 0;
            vrm.scene.position.z = -3;

            console.log( vrm );
        },
        (progress) => console.log('Loading model...', 100.0 * (progress.loaded / progress.total), '%'),
        (error) => console.error(error)
    );

    // 以下モデルが見えない時の検索用ガイド
    
    // Grid Helper
    const size = 10;
    const divisions = 10;
    const gridHelper = new THREE.GridHelper(size, divisions);
    this.threeScene.add(gridHelper);

    // Axes Helper
    const axesHelper = new THREE.AxesHelper(5);
    this.threeScene.add(axesHelper);
    
    // VRMモデルのアニメーション関数
    animate();

},

 

シーンのレンダリング

コンポーネントのtick関数内で実施させます

tick: function (time, timeDelta) {
    // A-Frameのカメラとレンダラーを取得
    var camera = this.el.sceneEl.camera;
    var renderer = this.el.sceneEl.renderer;

    // A-Frameとinitで作成したシーンの合成
    if (camera && renderer) {
        renderer.autoClear = false; // A-Frameのレンダリングをクリアしない
        renderer.render(this.threeScene, camera);                        
    }
}

 

アニメーション関数

テスト用に首を揺らすアニメーションを用意しました。

function animate() {
    requestAnimationFrame( animate );
    const deltaTime = clock.getDelta();

    // update avatar components
    if ( avatar && avatar.humanoid ) {
    
        // 首振り量
        const neckRotation = 0.25 * Math.PI * Math.sin( Math.PI * clock.elapsedTime );
        
        // 首への回転適用
        avatar.humanoid.getNormalizedBoneNode( 'neck' ).rotation.y = neckRotation;
        
        // vrmモデルアップデート
        avatar.update( deltaTime );
    }
}

 

コード全体

<!DOCTYPE html>
<html lang="jp">

    <head>
        <meta charset="utf-8"/>
        <meta content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" name="viewport"/>
        
        <title>A-Frame & VRM view</title>

        <script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
        <script src="https://raw.githack.com/AR-js-org/AR.js/master/aframe/build/aframe-ar.js"></script>
        
        <script src="https://unpkg.com/es-module-shims@1.8.2/dist/es-module-shims.js"></script>

        <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.158.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.158.0/examples/jsm/",
                "@pixiv/three-vrm": "https://unpkg.com/@pixiv/three-vrm@2.0.7/lib/three-vrm.module.js"
            }
        }
        </script>
    </head>

    <body>
        <a-scene    arjs="trackingMethod: best; sourceType: webcam; debugUIEnabled: false;  patternRatio: 0.80;"
                    embedded="" renderer="logarithmicDepthBuffer: true; precision: medium;" vr-mode-ui="enabled: false">
            <a-entity vrm=""></a-entity>
        </a-scene>

        <script type="module">
        
            import * as THREE from 'three';
            import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
            import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';

            const clock = new THREE.Clock();
            clock.start();
            let avatar = undefined;

            function animate() {
                requestAnimationFrame( animate );
                const deltaTime = clock.getDelta();
            
                // update avatar components
                if ( avatar && avatar.humanoid ) {
                
                    // 首振り量
                    const neckRotation = 0.25 * Math.PI * Math.sin( Math.PI * clock.elapsedTime );
                    
                    // 首への回転適用
                    avatar.humanoid.getNormalizedBoneNode( 'neck' ).rotation.y = neckRotation;
                    
                    // vrmモデルアップデート
                    avatar.update( deltaTime );
                }
            }
            
            AFRAME.registerComponent("vrm", {
            
                init: function() {
                    // sceneの作成
                    this.threeScene = new THREE.Scene();
                    
                    // lightの設定
                    const light = new THREE.DirectionalLight( 0xffffff, 1 );
                    light.position.set( 1.0, 3.0, 1.0 ).normalize();
                    this.threeScene.add( light );
                
                    // VRMモデルの読み込みローダーの設定
                    const loader = new GLTFLoader();
                    loader.crossOrigin = 'anonymous';
                    loader.register((parser) => {
                        return new VRMLoaderPlugin(parser);
                    });
                    
                    // VRMモデルのロード
                    loader.load(
                        'PATH/your_model.vrm', // VRMファイルのパスを指定
                        (gltf) => {
                            const vrm = gltf.userData.vrm;
                
                            vrm.scene.traverse((obj) => {
                                obj.frustumCulled = false;
                            });
                            
                            // シーンへvrmモデルを追加
                            this.threeScene.add(vrm.scene);
                            
                            // 読み込んだvrmを公開用変数に代入
                            avatar = vrm;
                            
                            // vrmモデルの初期位置の調整
                            vrm.scene.position.y = 0.5;
                            vrm.scene.position.x = 0;
                            vrm.scene.position.z = -3;
                
                            console.log( vrm );
                        },
                        (progress) => console.log('Loading model...', 100.0 * (progress.loaded / progress.total), '%'),
                        (error) => console.error(error)
                    );
            
                    // 以下モデルが見えない時の検索用ガイド
                    
                    // Grid Helper
                    const size = 10;
                    const divisions = 10;
                    const gridHelper = new THREE.GridHelper(size, divisions);
                    this.threeScene.add(gridHelper);
                
                    // Axes Helper
                    const axesHelper = new THREE.AxesHelper(5);
                    this.threeScene.add(axesHelper);
                    
                    // VRMモデルのアニメーション関数
                    animate();
                },
            
                tick: function (time, timeDelta) {
                    // A-Frameのカメラとレンダラーを取得
                    var camera = this.el.sceneEl.camera;
                    var renderer = this.el.sceneEl.renderer;
                
                    // A-Frameとinitで作成したシーンの合成
                    if (camera && renderer) {
                        renderer.autoClear = false; // A-Frameのレンダリングをクリアしない
                        renderer.render(this.threeScene, camera);                        
                    }
                }
            
            });
        </script>
    </body>
</html>
sam

sam

流山おおたかの森Techブログの管理人です。 お仕事のご依頼などはmail or Twitter[https://twitter.com/sam_sumario]で連絡頂けると反応出来ます。
Previous post A-Frameで検出したマーカーの位置情報を取得する:コンポーネントシステム
Next post Electronのインストールからpreload scriptを使うまで

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です