VSCode上で好きなVRMアバターと一緒に作業する
この記事は、フラー株式会社 Advent Calendar 2024 の 21 日目の記事です。20 日目は、@yoRyuuuuu さんの 「ISUCON14 に参加しました」でした。
動機
静的サイトジェネレータ Astro のチームが提供している、Houston という VSCode の拡張機能があります。この拡張機能はカラーテーマに加えて、エクスプローラ欄の下に Astro マスコットの Houston 君を表示する機能があります。
癒やし要素として、Houston 君は編集中のファイルのエラー数に応じて表情を変えてくれます、kawaii〜
こちらに影響を受け、自分で選んだキャラクターでも同じことができたらと思い拡張機能を作り出しました。
デモ
アバター表示
ファイルのエラー数が少ない順に、happy > neutral > angry > sad の順で表情が切り替わります。アバターの位置調整はマウス操作で行います。現状は位置調整は再起動でリセットされます。
(デモ用 VRM アバターにずんだもん W-Type を使用させていただいております)
設定
VSCode の設定にて、ローカルの .vrm
ファイルパスを指定してアバターを読み込みます。
実装方法
(細かいところは割愛しています、以下で載せるサンプルコードなどを参考にしてもらえると嬉しいです 🙏)
技術構成
おおまかには、Vite + React で作ったアバター表示部分を静的ページにビルドし、それを VSCode の WebView API を使って表示させる形を取っています。
VRM ファイルの読み取りは VSCode で行い、WebView API 内の postMessage という関数を通じて、アバター表示部分に VRM データを送信します。テキストエディタのエラー数も postMessage を使って同期しています。
プロジェクト作成
公式 docs及び公式の VSCode + React サンプルを参考に VSCode プロジェクトを作成します。
npx --package yo --package generator-code -- yo code
その後、packages/webview のディレクトリを切り、Vite で React プロジェクトを作成します。
mkdir packages
cd packages
pnpm create vite@latest webview -- --template react
また Webview API を通じて拡張機能と Webview でデータをやり取りするため、packages/webview 配下にて Webview API 用のパッケージをインストールします。
ni @vscode/webview-ui-toolkit @types/vscode-webview
構成の都合上、開発中の画面更新は、React プロジェクトでビルド → VSCode のデバッグツールを更新という手順を踏む必要があります。
アバター表示部分 (/packages/webview)
VRM 読み込み & 表示
@pixiv/three-vrm というパッケージを使うことで、Three.js 上で VRM アバターを簡単に読み込んだり、操作することができます。加えて VRM の型定義である @pixiv/types-vrmc-vrm-1.0 も利用します。こちらと React Three Fiber を組み合わせてアバター表示を実装しています。
export default function Model() {
const avatar = useRef<VRM>();
// VRMファイルをDataURLに変換したもの
const [dataUrl, setDataUrl] = useState<string | null>(null);
useEffect(
function loadVrm() {
if (gltf || !dataUrl) {
return;
}
const loader = new GLTFLoader();
loader.register((parser) => {
return new VRMLoaderPlugin(parser);
});
loader.load(
dataUrl,
(gltf) => {
setGltf(gltf);
const vrm: VRM = gltf.userData.vrm;
avatar.current = vrm;
if (vrm.lookAt) {
vrm.lookAt.target = camera;
}
},
...
);
},
[dataUrl]
);
}
VRM のモデル部分をgltf
state に、VRM を操作するためのデータをavatar
にそれぞれ格納しています。
そして、<primitive />
要素を配置して GLTF データを渡すことでモデルを表示します。
export default function Model() {
...
return (
<Float
speed={1.5}
rotation={[0, Math.PI, 0]}
position={[0, -4, 2.3]}
rotationIntensity={0.3}
floatIntensity={0.5}
>
<mesh scale={[5, 5, 5]}>{gltf && <primitive object={gltf.scene} />}</mesh>
</Float>
);
}
表情を変更する
VRM オブジェクトの expressionManager を参照することで、アバターの表情を操作できます。
VRM 1.0 の場合 expression の種類は 5 つほどありますが、今回はその中から happy/sad/angry を操作します。
function getExpression(issuesCount: number): {
happy: number;
angry: number;
sad: number;
} {
if (issuesCount < 2) {
return { happy: 1.0, angry: 0, sad: 0 };
} else if (issuesCount < 4) {
return { happy: 0, angry: 0, sad: 0 };
} else if (issuesCount < 8) {
return { happy: 0, angry: 1.0, sad: 0 };
} else {
return { happy: 0, angry: 0, sad: 1.0 };
}
}
export default function Model() {
const [issuesCount, setIssuesCount] = useState<number>(0);
...
useEffect(function updateExpressionByIssues() {
if (avatar.current?.expressionManager) {
const expression = getExpression(issuesCount);
avatar.current.expressionManager.setValue("happy", expression.happy);
avatar.current.expressionManager.setValue("angry", expression.angry);
avatar.current.expressionManager.setValue("sad", expression.sad);
}
});
}
拡張機能からのデータ受信
message
というイベントで拡張機能からのメッセージを受け取ることができます。
command
プロパティからはメッセージの種類、state
からはデータ本文を参照できます。(送信側の処理は後に説明します)
webview 側の初期レンダリングまでメッセージ送信を保留してほしいため、ready_for_receives
というイベントをこちらから送信しています。
export default function Model() {
...
useEffect(() => {
vscode.postMessage({ command: "ready_for_receives" });
window.addEventListener("message", (event) => {
const message = event.data; // The JSON data our extension sent
switch (message.command) {
case "set_vrm":
setDataUrl(message.state.vrmFileDataUrl);
break;
case "issues_count":
setIssuesCount(message.state.issuesCount);
}
});
}, []);
...
}
拡張機能部分 (/)
エクスプローラへの webview 表示と、VRM ファイルパスの設定項目のための contributes 設定を追加します。
{
...
"contributes": {
"views": {
"explorer": [
{
"type": "webview",
"id": "vrm-companion-vscode.summon",
"name": "vrm-companion"
}
]
},
"configuration": {
"title": "vrm-companion-vscode",
"properties": {
"vrm-companion-vscode.vrmFilePath": {
"type": "string",
"default": null,
"description": "your vrm file path"
}
}
}
}
...
}
VRM 読み込み & Webview への送信
Webview 側でローカルファイル読み込みを行う術が見つけられなかったため、VSCode 側で VRM ファイルを読み取って、 その DataURL を Webview に送信する方法を取っています。
// vrmFilePath は VSCode の設定画面での入力値が入る
async function getVrmFileDataUrl(vrmFilePath: string) {
try {
const vrmFile = await readFile(vrmFilePath);
const base64Data = vrmFile.toString("base64");
return `data:application/octet-stream;base64,${base64Data}`;
} catch (error) {
console.error("Error reading VRM file:", error);
return "";
}
}
WebviewViewProvider の作成と、送信処理実装
WebviewViewProvider を継承したクラスを作成し、Webview が作成されたときに VRM データを送信する処理を内部に実装します。
先程の Webview 側で ready_for_receives メッセージが送信されたら、今度は拡張機能の方から postMessage で DataURL を送信します。
また、エラー数の送信も同じく postMessage で実装します。
export class WebViewProvider implements vscode.WebviewViewProvider {
constructor(
private _extensionUri: vscode.Uri,
private _vrmFileDataUrl: string
) {}
private _view?: vscode.WebviewView;
public resolveWebviewView(webviewView: vscode.WebviewView) {
...
webviewView.webview.onDidReceiveMessage((message) => {
switch (message.command) {
case "ready_for_receives":
webviewView.webview.postMessage({
command: "set_vrm",
state: { vrmFileDataUrl: this._vrmFileDataUrl },
});
}
});
}
public postIssuesCount(issuesCount: number) {
...
view.webview.postMessage({
command: "issues_count",
state: { issuesCount },
});
}
public postVrmFileDataUrl(vrmFileDataUrl: string) {
...
view.webview.postMessage({
command: "set_vrm",
state: { vrmFileDataUrl },
});
}
...
}
こちらで作成した update 関数は、VSCode に一度だけ呼ばれるactivate
関数を通じて実行されます。
取得した DataURL を用いて Webview を初期化し、onDidChangeDiagnostics
時にエディタ上のエラー数を送信する処理を登録します。
async function update(vrmFilePath: string, context: vscode.ExtensionContext) {
try {
const dataUrl = await getVrmFileDataUrl(vrmFilePath);
const provider = new WebViewProvider(context.extensionUri, dataUrl);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
"vrm-companion-vscode.summon",
provider
)
);
vscode.languages.onDidChangeDiagnostics(() => postIssuesCount(provider));
...
} catch (error) {
console.error("Error reading VRM file:", error);
return;
}
}
おわりに
拡張機能及びリポジトリはまだ公開できていませんが、未実装の機能含めて一区切りついたら publish したいと思います。
断片的な説明となってしまいましたが、読んでいただきありがとうございました。
続いて、フラー株式会社 Advent Calendar 22 日目は @canacel さんの記事です!
Discussion