Lipsyncの環境を作る

ripsyncに必要な rhubarb というファイルをgithubからダウンロードする
OSに合わせる必要があるため、rhubarb の mac osをダウンロードする
https://github.com/DanielSWolf/rhubarb-lip-sync/releases

$ mkdir vrm-project

この vrm-project に rhubarb を配置する

$ chmod +x rhubarb

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Simple VRM Lipsync (Final)</title>
    <style>
        body { margin: 0; overflow: hidden; }
        canvas { display: block; }
        #playButton { 
            position: absolute; 
            top: 20px; 
            left: 20px; 
            padding: 10px 20px;
            font-size: 16px;
            z-index: 10;
        }
    </style>
</head>
<body>
    <button id="playButton" disabled>VRMとデータをロード中...</button>

    <script src="https://unpkg.com/three@0.158.0/build/three.min.js"></script>

    <script src="https://unpkg.com/three@0.158.0/examples/js/controls/OrbitControls.js"></script>

    <script src="https://unpkg.com/three@0.158.0/examples/js/loaders/GLTFLoader.js"></script>
    
    <script>
        // 1. OrbitControls の定義をコピー
        if (typeof window.OrbitControls !== 'undefined' && typeof THREE.OrbitControls === 'undefined') {
            THREE.OrbitControls = window.OrbitControls;
        }
        
        // 2. GLTFLoader の定義をコピー
        if (typeof window.GLTFLoader !== 'undefined' && typeof THREE.GLTFLoader === 'undefined') {
            THREE.GLTFLoader = window.GLTFLoader;
        }
    </script>
    
    <script src="https://unpkg.com/@pixiv/three-vrm@2.0.0/lib/three-vrm.min.js"></script>
    
    <script>
        // --- 設定値 ---
        const VRM_MODEL_PATH = './28538335112454854.vrm'; 
        const AUDIO_PATH = './output.wav'; 
        const LIPSYNC_JSON_PATH = './output_lipsync.json';
        
        let renderer, scene, camera, clock, vrm, audio, lipsyncData;
        
        let vrmLoaded = false;
        let jsonLoaded = false;
        let currentVisemeIndex = 0;

        function checkReady() {
            if (vrmLoaded && jsonLoaded) {
                 const button = document.getElementById('playButton');
                 button.innerHTML = '音声再生&リップシンク開始';
                 button.disabled = false;
            }
        }
        
        // --- 初期化処理 ---
        function init() {
            // 1. レンダラーのセットアップ
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setClearColor(0xeeeeee);
            document.body.appendChild(renderer.domElement);

            // 2. シーンとカメラのセットアップ
            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
            camera.position.set(0, 1.3, 1.5); 
            
            // 3. ライト
            scene.add(new THREE.AmbientLight(0xffffff, 1.0));

            // 4. コントロール (OrbitControlsが見つからない場合はスキップ)
            let OrbitControlsClass = THREE.OrbitControls;
            
            if (!OrbitControlsClass) {
                console.error("致命的エラー: OrbitControlsが見つかりません。カメラ操作はできませんが、続行します。");
            } else {
                const controls = new OrbitControlsClass(camera, renderer.domElement);
                controls.target.set(0, 1.3, 0); 
            }

            // 5. アニメーションクロック
            clock = new THREE.Clock();
            
            // 6. VRMとJSONデータのロード
            loadVRM();
            loadLipsyncData();
            
            // 7. リップシンク開始ボタン
            document.getElementById('playButton').addEventListener('click', startLipsync);

            // 8. ウィンドウリサイズ対応
            window.addEventListener('resize', onWindowResize);
            
            // 9. アニメーションループ開始
            animate();
        }

        // --- VRMモデルのロード ---
        function loadVRM() {
            const loader = new THREE.GLTFLoader(); // ★修正されたGLTFLoaderを参照
            loader.crossOrigin = 'anonymous';

            loader.load(
                VRM_MODEL_PATH,
                (gltf) => {
                    THREE.VRM.from(gltf).then((vrmInstance) => {
                        vrm = vrmInstance;
                        scene.add(vrm.scene);
                        
                        VRMUtils.rotateVRM0(vrm); 
                        vrm.scene.rotation.y = Math.PI;
                        
                        resetMouth(); 
                        
                        vrmLoaded = true;
                        checkReady();
                    })
                    .catch(e => {
                         console.error("VRMインスタンス生成中にエラーが発生しました(互換性問題の可能性):", e);
                    });
                },
                undefined,
                (error) => {
                    console.error("GLTFファイルのロード中にエラーが発生しました。", error);
                }
            );
        }

        // --- リップシンクデータのロード (省略) ---
        function loadLipsyncData() {
            fetch(LIPSYNC_JSON_PATH)
                .then(response => {
                    if (!response.ok) {
                        throw new Error(`HTTP error! status: ${response.status}`);
                    }
                    return response.json();
                })
                .then(data => {
                    lipsyncData = data;
                    jsonLoaded = true;
                    checkReady();
                })
                .catch(error => {
                    console.error('Error loading lipsync JSON:', error);
                });
        }

        // --- 口のブレンドシェイプをリセットする関数 (省略) ---
        function resetMouth() {
            if (!vrm) return;
            const blendShapeProxy = vrm.humanoid.getBlendShapeProxy();
            blendShapeProxy.setValue('vrc.v_a', 0.0);
            blendShapeProxy.setValue('vrc.v_i', 0.0);
            blendShapeProxy.setValue('vrc.v_u', 0.0);
            blendShapeProxy.setValue('vrc.v_e', 0.0);
            blendShapeProxy.setValue('vrc.v_o', 0.0);
        }

        // --- リップシンク開始 (省略) ---
        function startLipsync() {
            if (!vrm || !lipsyncData || document.getElementById('playButton').disabled) {
                return;
            }
            
            document.getElementById('playButton').disabled = true;
            document.getElementById('playButton').innerHTML = '再生中...';

            audio = new Audio(AUDIO_PATH);
            audio.play();
            
            audio.onended = () => {
                resetMouth();
                currentVisemeIndex = 0;
                document.getElementById('playButton').innerHTML = '音声再生&リップシンク開始';
                document.getElementById('playButton').disabled = false;
            };
        }
        
        // --- リップシンク実行関数 (省略) ---
        function performLipsync() {
            if (!audio || audio.paused || audio.ended || !vrm) return;

            const currentTime = audio.currentTime;
            const visemes = lipsyncData.mouthCues;
            const blendShapeProxy = vrm.humanoid.getBlendShapeProxy();
            
            while (currentVisemeIndex < visemes.length) {
                const currentCue = visemes[currentVisemeIndex];
                
                if (currentTime < currentCue.start) {
                    break; 
                }
                
                if (currentTime >= currentCue.end) {
                    currentVisemeIndex++;
                    continue;
                }

                resetMouth(); 

                const rhubardValue = currentCue.value;

                // RhubarbのVisemeとVRMキーのマッピング
                const visemeKey = {
                    'A': 'vrc.v_a', 'B': 'vrc.v_o', 'C': 'vrc.v_i', 
                    'D': 'vrc.v_e', 'E': 'vrc.v_e', 'F': 'vrc.v_o', 
                    'G': 'vrc.v_e', 'H': 'vrc.v_a', 'X': null
                }[rhubardValue];

                if (visemeKey) {
                    blendShapeProxy.setValue(visemeKey, 1.0);
                }
                
                break; 
            }
        }

        // --- アニメーションループ (省略) ---
        function animate() {
            requestAnimationFrame(animate);

            const deltaTime = clock.getDelta();
            
            if (vrm) {
                vrm.update(deltaTime);
                performLipsync();
            }

            renderer.render(scene, camera);
        }

        // --- リサイズ処理 (省略) ---
        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        // アプリケーション開始
        init();
    </script>
</body>
</html>