商品サンプル画像
Minarena 3Dシール 立体シール 手帳DIY装飾 4枚セット 柔らかい 立体ぷっくり触感 子供向け PVC製 貼り付け可能 さまざまなスタイル 手芸用品 DIY用 手帳用 贈り物 ノート・スマホ・ウォーターボトル装飾用 (B) [並行輸入品]
商品ページ
Amazon
収益広告(自動登録)
サクラ度:△(要確認)
商品サンプル画像
タカラトミー(TAKARA TOMY) CX-15 ブースター ラグナレイジFE4-55Y
商品ページ
Amazon
収益広告(自動登録)
サクラ度:△(要確認)
商品サンプル画像
バンダイ(BANDAI) ONE PIECEカードゲーム スタートデッキ 緑 ロロノア・ゾロ【ST-32】(2個セット)
商品ページ
Amazon
収益広告(自動登録)
サクラ度:△(要確認)
商品サンプル画像
BANDAI SPIRITS(バンダイ スピリッツ) 30MS アイドルマスター シンデレラガールズ 島村卯月 (20th Anniv. YOU AND アイ!) ノンスケール 色分け済みプラモデル
商品ページ
Amazon
収益広告(自動登録)
サクラ度:△(要確認)
商品サンプル画像
D.C.Ⅱ.S.S.~ダ・カーポⅡセカンドシーズン~(TV番組)
商品ページ
Amazon
非収益広告(手動登録)
サクラ度:○(問題なし)
記事の概要
three.js・three-vrm.jsを使用して、VRoidをWebページ上に表示する Part-03
作成日:2025-04-20
最終更新日:2025-04-20
記事の文字数:8734
VRoidWebツール情報技術
本記事のトピック
  • 概要
  • VRoidStudioとポーズが異なってしまう問題
  • vroidposeデータから指ポーズを指定する
  • VRoidを撮影して保存する
  • サンプルコードはこちら
three.js・three-vrm.jsを使用して、VRoidをWebページ上に表示する Part-03
概要

three.js・three-vrm.jsを使用して、VRoidをWebページ上に表示する方法を載せています。
こちらはPart-03となり、Part-01Part-02の内容を前提としたものになっています。

今回は以下のコード例を載せています。

今回紹介するコード例

  • VRoidStudioとポーズが異なってしまう問題
  • vroidposeデータから指ポーズを指定する
  • VRoidを撮影して保存する

また、今回も例によって、実際に動作確認できる(であろう)サンプルコードを下部に載せています。

VRoidStudioとポーズが異なってしまう問題

Part-02でも触れましたが、VRoidStudioでvroidposeデータを読み込んだ時とポーズが異なってしまう問題が起こります。特に背骨のカーブが極端になります。

Boothで上げているこちらの中の「異議あり」ポーズが分かりやすいです。

本処理でvroidposeをVRoidに設定する際、vroidposeデータ内のBoneDefinitionを使っていますが、実はVRoidStudioではこの部分は使っていないようです。
すなわちこのBoneDefinitionというものはVRoidStudio以外で使えるように書き出しているのだと思いますが、その中の「SpineControlPointDeltaPosition」というパラメータはボーンに上手く設定できず、それがVRoidStudioと差が出てしまう原因、なのだろうと思います。

なので、この差分を上手く埋めないといけないのですが、「SpineControlPointDeltaPosition」がどのように使われるか分からないので、これを追うのは難しいです。

以下の処理では、各ボーンに個別に補正を加えてなるべくVRoidStudioに近づけています(やはりポーズによってカーブ具合が変わってしまっていそうなので寸分の狂いもなくとなると、ちゃんと原因調査が必要そうです)。

const BONE_CORRECTION_MAP_X = { Spine: 10, Chest: -18, UpperChest: -9, Neck: 15, Head: 0, LeftUpperLeg: 2, RightUpperLeg: 2, LeftShoulder: 16, RightShoulder: 16, } // ポーズデータを読み込んだら、それをvrmに適用する document.getElementById("poseDataInput").addEventListener('change', (event)=>{ const file = event.target.files[0]; if(file && vrm){ // ファイルを読み込む const reader = new FileReader(); reader.onload = () => { const poseData = JSON.parse(reader.result); const bonePose = poseData.BoneDefinition; // ボーンごとに保持する for (const boneName in bonePose) { const lowerBoneName = boneName.charAt(0).toLowerCase() + boneName.slice(1); // ポーズデータとVRMボーンの名前を同じにするために、ローワーキャメルにする // vrmにポーズを適用 const vrmBone = vrm.humanoid.getNormalizedBoneNode(lowerBoneName); // 補正する if(vrmBone){ let quat = bonePose[boneName]; if(BONE_CORRECTION_MAP_X[boneName]){ // X軸回転だけのクォータニオンを作成 const correctionQuat = new THREE.Quaternion().setFromEuler( new THREE.Euler(THREE.MathUtils.degToRad(BONE_CORRECTION_MAP_X[boneName]), 0, 0, 'XYZ') ); // 合成(回転順:original → xOnlyQuat) const originalQuat = new THREE.Quaternion(quat.x, quat.y, -quat.z, -quat.w); quat = correctionQuat.premultiply(originalQuat); // ポーズに反映 vrmBone.quaternion.set(quat.x, quat.y, quat.z, quat.w); } else { // ポーズに反映 vrmBone.quaternion.set(quat.x, quat.y, -quat.z, -quat.w); } } } }; reader.readAsText(file); } });

「BONE_CORRECTION_MAP_X」という定数で、各ボーンのX回転角に補正を掛けています。
それをquaternionにして、premultiplyで元のボーン設定と合成しています。それだけです。

なお、前回パートでは「getRawBoneNode」を使っていましたが、今回は「getNormalizedBoneNode」を使っています。

そのため、前回パートで書いていたコードをベースにする場合、以下の部分を削除する必要があります(理由も前回パートに書いてある通りです)。

// 姿勢はvroidposeに従って制御するので、自動制御はOFFにする vrm.humanoid.autoUpdateHumanBones = false;
vroidposeデータから指ポーズを指定する

前項で触れた通り、基本的なボーン設定はvroidposeデータ内の「BoneDefinition」に入っています。

ただ、指(手)の形だけはVRoidStudioで指定したテンプレートが保存されており、ボーンとしては保存されていません
なのでこのテンプレートをボーンの値に変換する必要があります(他のボーンはVRoidStudioでも使えるように書き出してくれているのに指だけは出してくれてないのは何か意図がありそうだけど……)。

vroidposeデータ内の以下が指の形に関係ある部分です。
・LeftHandAnimationName:左手に適用しているテンプレート
・LeftHandAnimationWeight:左手のテンプレートを適用している度合い
・RightHandAnimationName:右手に適用しているテンプレート
・RightHandAnimationWeight:右手のテンプレートを適用している度合い

これを以下のコードのようにボーン変換します。

/** * VRoidStudioでの手のテンプレートから手のボーンに反映するための値 * https://ringo.ciao.jp/cont/view/20240305/ の値を一部改変して利用 */ const HAND_TEMPLATE = { Natural: { Thumb: { Metacarpal: [0, -0.11, 0.3], Proximal: [0, -0.42, 0], Distal: [-0.11, 0.16, -0.09], }, Index: { Proximal: [0, -0.05, 0.14], Intermediate: [0, 0, 0.18], Distal: [0, 0, 0.18], }, Middle: { Proximal: [0, 0, 0.38], Intermediate: [0, 0, 0.18], Distal: [0, 0, 0.36], }, Ring: { Proximal: [0, 0.05, 0.59], Intermediate: [0, 0, 0.31], Distal: [0, 0, 0.33], }, Little: { Proximal: [0, 0.06, 0.72], Intermediate: [0, 0, 0.41], Distal: [0, 0, 0.76], }, }, Grip: { Thumb: { Metacarpal: [0, -0.3, 0.5], Proximal: [0, -0.8, 0], Distal: [0, -1.5, 0], }, Index: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0.5, 0, 1.5], }, Middle: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, Ring: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, Little: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, }, Open: { Thumb: { Metacarpal: [0, 0.05, -0.05], Proximal: [0, 0.1, 0], Distal: [0, 0.15, 0], }, Index: { Proximal: [0, 0.2, 0], Intermediate: [0, 0, 0], Distal: [0, 0, 0], }, Middle: { Proximal: [0, 0, 0], Intermediate: [0, 0, 0], Distal: [0, 0, 0], }, Ring: { Proximal: [0, -0.2, 0], Intermediate: [0, 0, 0], Distal: [0, 0, 0], }, Little: { Proximal: [0, -0.4, 0], Intermediate: [0, 0, 0], Distal: [0, 0, 0], }, }, Good: { Thumb: { Metacarpal: [0, 0.1, -0.1], Proximal: [0, 0.2, 0], Distal: [0, 0.3, 0], }, Index: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0.5, 0, 1.5], }, Middle: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, Ring: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, Little: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, }, V: { Thumb: { Metacarpal: [0.53, -0.3, 0.5], Proximal: [0, -0.8, 0], Distal: [0, -1.2, 0], }, Index: { Proximal: [0, 0.5, 0], Intermediate: [0, 0, 0], Distal: [0, 0, 0], }, Middle: { Proximal: [0, 0, 0], Intermediate: [0, 0, 0], Distal: [0, 0, 0], }, Ring: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, Little: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, }, Gao: { Thumb: { Metacarpal: [0, 0, 0.5], Proximal: [0, 0, 0], Distal: [0, -1.5, 0], }, Index: { Proximal: [0, 0.2, -0.4], Intermediate: [0, 0, 0.8], Distal: [0, 0, 1], }, Middle: { Proximal: [0, 0, -0.4], Intermediate: [0, 0, 0.8], Distal: [0, 0, 1], }, Ring: { Proximal: [0, -0.2, -0.4], Intermediate: [0, 0, 0.8], Distal: [0, 0, 1], }, Little: { Proximal: [0, -0.4, -0.4], Intermediate: [0, 0, 0.8], Distal: [0, 0, 1], }, }, Index: { Thumb: { Metacarpal: [0.5, -0.3, 0.5], Proximal: [0, -0.8, 0], Distal: [0, -1.2, 0], }, Index: { Proximal: [0, 0, 0], Intermediate: [0, 0, 0], Distal: [0, 0, 0], }, Middle: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, Ring: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, Little: { Proximal: [0, 0, 1.2], Intermediate: [0, 0, 1.5], Distal: [0, 0, 1.5], }, }, }; // ポーズデータを読み込んだら、それをvrmに適用する document.getElementById("poseDataInput").addEventListener('change', (event)=>{ const file = event.target.files[0]; if(file && vrm){ // ファイルを読み込む const reader = new FileReader(); reader.onload = () => { /* ~通常(指以外)のボーン読み込み処理は省略~ */ // 指の形を取り込む setHandPose(poseData.LeftHandAnimationName, poseData.LeftHandAnimationWeight); setHandPose(poseData.RightHandAnimationName, poseData.RightHandAnimationWeight); }; reader.readAsText(file); } }); // 指の形を取り込む function setHandPose(handAnimationName, weight){ if(!handAnimationName) return; const handSide = handAnimationName.includes('L_') ? 'left' : 'right'; const handName = handAnimationName.replace(/^[LR]_/, ""); const handConfig = HAND_TEMPLATE[handName]; for(const finger in handConfig){ for(const joint in handConfig[finger]) { const boneName = `${handSide}${finger}${joint}`; const [rx, ry, rz] = handConfig[finger][joint]; const euler = (handSide === 'left') ? new THREE.Euler(rx * weight, ry * weight, rz * weight) : new THREE.Euler(rx * weight, -ry * weight, -rz * weight); const quat = new THREE.Quaternion().setFromEuler(euler); // vrmにポーズを適用 const vrmBone = vrm.humanoid.getNormalizedBoneNode(boneName); if(vrmBone){ vrmBone.quaternion.set(quat.x, quat.y, -quat.z, -quat.w); } } } }

左手と右手でほぼ同じ処理なので、「setHandPose」というメソッドでまとめています。

まずHAND_TEMPLATEというところで、「手の形のテンプレート」と「指のボーン」の対応表を作成しておきます。この値はコード中にも書いていますが、こちらの方が出した値をベースにしています。

この対応表に従い、「手の形のテンプレート」を「指のボーン」に変換してあとはそのボーンをVRoidに適用すればOKです。

ボーンに変換する前に「LeftHandAnimationWeight / RightHandAnimationWeight」のWeight値を掛けるのを忘れないようにしましょう。上のコードにある通り、Weight値はそのまま掛けてしまって良さそうです。

VRoidを撮影して保存する

Canvasに表示されたVRoidを画像として保存する方法についてです。

単にCanvasに表示された内容を、よくあるやり方で画像として保存すれば良い……と思いきや一工夫必要だったので一応書いておきます。

const canvasElm = renderer.domElement; // renderer.domElementにcanvas要素が入ってます // a タグを作成して、ダウンロードをトリガー const link = document.createElement("a"); renderer.render(scene, camera); // バッファ解放されている可能性が高いため、一度明示的に描画する! link.href = canvasElm.toDataURL("image/png"); link.download = `${Date.now()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link);

上の処理の殆どは、Canvasに表示している内容を画像として保存するコードとしてよくあるモノだと思いますが、「renderer.render(scene, camera);」という部分を追加しています。

どうやらrenderer(THREE.WebGLRenderer)というものは、Canvasに書き込んだ後バッファを自動的に解放するようになっており、解放された状態で「renderer.domElement.toDataURL("image/png");」とやっても何もない空っぽの画像が書き出されてしまいます(タイミングによっては稀に上手く書き出せると思いますが)。

一応バッファの自動解放を防ぐことも簡単にできるようですが、そうするとバッファが圧迫されてパフォーマンスに影響があると思われるため、なるべく取りたくない手段ではあります。

なので、「renderer.domElement.toDataURL("image/png");」する前に、「renderer.render(scene, camera);」でもう一度明示的にレンダリングしておくことで空っぽの画像が出力されるのを防いでいます。

サンプルコードはこちら

サンプルコードはPart-01と同じ場所(GitHub)にあります。

コメントログ
※コメントは最新50件が表示されます
コメント投稿




画面下部の「コンタクト」からも連絡可能です。
管理人ツイート
商品サンプル画像
[BANDAI] [バンダイ 仮面ライダーゼッツ DXデュアルメアカプセム対象年齢 3 才以上
商品ページ
Amazon
収益広告(自動登録)
サクラ度:△(要確認)
商品サンプル画像
タカラトミー(TAKARA TOMY) CX-13 スターター バハムートブリッツBK1-50I
商品ページ
Amazon
収益広告(自動登録)
サクラ度:△(要確認)
商品サンプル画像
[バンダイ(BANDAI)] たまもりしーるリフィル エンジェル&デビルセット 対象年齢 6 才以上 たまごっち
商品ページ
Amazon
収益広告(自動登録)
サクラ度:△(要確認)
商品サンプル画像
タカラトミー(TAKARA TOMY) デュエル・マスターズ TCG DM25-EX4 エピソード4 パンドラ・ウォーズ BOX
商品ページ
Amazon
収益広告(自動登録)
サクラ度:△(要確認)
管理人作品宣伝
ChatGPT-アイコンと名前変更
Chrome 拡張機能 / 最終更新:2025-05-29
ChatGPTのアイコン等を自分の好みに変えられます。好きなキャラと話しているような気…ChatGPTのアイコン等を自分の好みに変えられます。好きなキャラと話しているような気分になれるかもしれません。

ChromeWebストアで閲覧する利用素材等の詳細情報
不思議の幻想郷 TODR:装備の印の組合せシミュレーションツール
Webサイト / 最終更新:2025-11-30
『不思議の幻想郷 TOD RELOADED.』の印考慮用のツールです。 使いたい印の…『不思議の幻想郷 TOD RELOADED.』の印考慮用のツールです。 使いたい印の固有印/追加印ごとの最高性能と入手方法(Wiki調べ)を見ることができます。 また、使いたい印にチェックを入れたあと、画面最下部の集計ボタンを押すことでそれらを一覧で見ることもできます。

HPで閲覧する利用素材等の詳細情報
作品一覧はこちら
関連ページ
OpenAI API:Webサーチモードで参考にしてほしいURLを送っても無視されることがある問題について
最終更新日:2025-12-07
このページのポイントWebサーチでも、まずは「プロンプトだけ」を読んで「解釈」される「前提として見て…
記事を閲覧する
不思議の幻想郷 TODR:装備の印の組合せシミュレーションツール
最終更新日:2025-11-30
概要本ページは『不思議の幻想郷 TOD RELOADED.』の印考慮用のツールページです。基本事項固…
記事を閲覧する
逆転裁判3:あやめさんのナルホド君への呼び方にある3つの伏線
最終更新日:2025-11-26
このページのポイント呼び方ひとつに3つも伏線あるなんてスゲーネタバレ注意本ページには「逆転裁判3」の…
記事を閲覧する
VRoidナビゲーター:質問機能を強化しました
最終更新日:2025-11-25
このページのポイントVナビの質問機能がいま開いているページを参照するようになった必要な情報をネット上…
記事を閲覧する
JavaScriptでアナログ時計とデジタル時計を表示するコード
最終更新日:2025-05-29
概要 最近アナログ時計を使ったので、アナログ時計を表示するJavaScriptコードを載せます。 …
記事を閲覧する
three.js・three-vrm.jsを使用して、VRoidをWebページ上に表示する Part-03
最終更新日:2025-04-20
概要 three.js・three-vrm.jsを使用して、VRoidをWebページ上に表示する方…
記事を閲覧する
three.js・three-vrm.jsを使用して、VRoidをWebページ上に表示する Part-02
最終更新日:2025-04-15
概要 three.js・three-vrm.jsを使用して、VRoidをWebページ上に表示する方…
記事を閲覧する
three.js・three-vrm.jsを使用して、VRoidをWebページ上に表示する Part-01
最終更新日:2025-04-13
概要 three.js・three-vrm.jsを使用して、VRoidをWebページ上に表示する方…
記事を閲覧する
GIF / APNG(アニメーション付きPNG)ファイル解析ページ
最終更新日:2025-03-31
ファイル読込・操作 以下に調べたいファイルを読み込ませてください。 ファイル情報 カラーパレットを…
記事を閲覧する
gifler.js仕様メモ
最終更新日:2025-03-23
本ページの趣旨 「gifler.js」という、gifアニメーションをcanvasに簡単に表示できる…
記事を閲覧する
本サイトのタグ一覧
NovelAIR18VRoidWebサイト作成Webツールととモノ。るろうに剣心アークナイツアークナイツ-ステージ攻略日記アズールレーンアズールレーン-日記ウマ娘ギャラリーゲームデビラビローグネットスラング系プログラミングホラーポケットタウン怪談気ままな日記逆転裁判情報技術情報技術-WebAPI知的財産権統合戦略白夜極光本サイトについて魔王スライム様がんばる!漫画
人気記事
ととモノ。3:各学科の強みを一覧化
最終更新日:2025-10-12
スコア:1491.6994 pt
このページのポイントどの学科が良いかすぐに分かる各学科のどのような点が良いか分かる各役割で欲しいスキ…
記事を閲覧する
ポケットタウン_パズル一覧
最終更新日:2025-05-02
スコア:803.0042 pt
グレーのピースの数 (Number of gray pieces):検索グレーピースの数を入力して、…
記事を閲覧する
TRICK-本物っぽい霊能力者
最終更新日:2025-02-06
スコア:649.8711 pt
概要 TRICKは基本的にインチキ霊能力者のインチキを暴いていく話ですが、中にはトリックでは説明が…
記事を閲覧する
メイド・オブ・ザ・デッド-攻略お助け情報
最終更新日:2024-05-01
スコア:534.1164 pt
ネタバレ注意! 本ページは『メイド・オブ・ザ・デッド』の情報を記録しているものです。 攻略の参考に…
記事を閲覧する
剣と魔法と学園モノ。2G - パーティ編成確認ツール
最終更新日:2024-05-09
スコア:440.0370 pt
ツール概要 ととモノ。2Gのパーティ編成を考える際に使うツールです。 あくまでストーリークリアまで…
記事を閲覧する
剣と魔法と学園モノ。3 前作からの変更点
最終更新日:2025-10-12
スコア:345.1776 pt
概要 もうそろそろ「ととモノ。3」のリマスターが出るので、自分自身のための振り返りと言う意味も込め…
記事を閲覧する
剣と魔法と学園モノ。3 - パーティ編成確認ツール
最終更新日:2025-09-23
スコア:330.1264 pt
ツール概要 ととモノ。3のパーティ編成を考える際に使うツールです。 攻略本や攻略wikiを参考にし…
記事を閲覧する
ヒカルの碁で、なぜ佐為は消えたのか
最終更新日:2025-04-21
スコア:310.1655 pt
概要 ヒカルの碁で佐為が消えた理由について、「ヒカルの才能を目覚めさせるという役割を終えたから」と…
記事を閲覧する
最新記事
鉄鍋のジャン:ジャンのいいところ
最終更新日:2026-02-16
ネタバレ注意※本記事にはネタバレが含まれます。このページのポイントジャンは本当にクズなのかジャンのい…
記事を閲覧する
るろうに剣心-各キャラの持つ正義
最終更新日:2026-01-12
このページのポイントこんな人向けるろ剣を読んだことないけど知ってる人軽く読んだことがある人伝えたいこ…
記事を閲覧する
逆転裁判1:真犯人まとめと悪質度
最終更新日:2025-12-15
概要 逆転裁判1(蘇る込み)に出てくる犯人たちを、やったことや動機なんかを整理して「悪質度」を点数…
記事を閲覧する
OpenAI API:Webサーチモードで参考にしてほしいURLを送っても無視されることがある問題について
最終更新日:2025-12-07
このページのポイントWebサーチでも、まずは「プロンプトだけ」を読んで「解釈」される「前提として見て…
記事を閲覧する
商品サンプル画像
塗るだけでシールになる! シール液 ぺたりん 大容量25g 復活剤 日本製 粘着復活 水性 速効性 簡単 強粘着 こどもも安心 粘着剤 接着剤 手作り シール しーる ぷっくりシール 付箋 復活 粘着 接着 ボンド 刷毛 乾燥 手作りシール ステッカ
商品ページ
Amazon
収益広告(自動登録)
サクラ度:△(要確認)