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>