VRCFTのアドオンと格闘した話 ~ mstdn.maud.io Advent Calendar 2025

これは「mstdn.maud.io Advent Calendar 2025」の1日目の記事です。アバター改変の話でもしようかと思いましたが、アドオン改変の話になりました。

作業しながら書いたので、話があっちこっちします。イチから作るのはあまり得意ではないのだけど、既存のものをちょっと弄ってみるのは結構好き好んでやってます。そんなわけで、今回はVRCFTのアドオンを弄ってみます。

口の隙間が開いている

口の隙間が開いています。

デフォルトの表情ミスったんじゃないのかって?いやいや、フェイストラッキングを通すとちょっと開いてしまうんです。

使用するフェイストラッキングの設定(BOOTHに売ってるもの)によって差はありますが、無表情状態でも僅かに開く感じでした。これについて色々と話を聞いていると、口を閉じたまま笑顔を作ったときに、「MouthUpperUp」が入ってきて半開きになるらしいとのこと。(※自分では確認してない)

MouthUpperUpの説明をざっと読んだところ、「上唇を上げる」動作のようです。「Nose Sneer」とは補完関係にあり、組み合わさることにより強調されるようです。どうなんだろう、リアル系のアバター以外でこれ必要かなぁ?と思うものが幾つかあるので、弄くり回してみると思った通りの動作をしてくれるかもしれません。

笑顔については「v2/MouthSmile(Mouth Corner Puller + Mouth Corner Slant)」が機能すれば良いはずなので、VRCFTアドオン側で対応できないか試してみることにしました。

準備

ソースコードを入手する

とりあえずアドオンのソースコードが必要になりますので、適当に入手しましょう。今回は「VRCFT-ALVR」を使用します。

GitHub - alvr-org/VRCFT-ALVR: VRCFaceTracking module for ALVR
VRCFaceTracking module for ALVR. Contribute to alvr-org/VRCFT-ALVR development by creating an account on GitHub.

自分はQuest Proがないので試せませんが、Virtual Desktopの場合は多分これです。

GitHub - guygodin/VirtualDesktop.VRCFaceTracking: VRCFaceTracking module for Virtual Desktop
VRCFaceTracking module for Virtual Desktop. Contribute to guygodin/VirtualDesktop.VRCFaceTracking development by creatin...

書き方は異なりますが、普段使いしている「VRCFTPicoModule」についても同じような手法で対応可能です。

GitHub - lonelyicer/VRCFTPicoModule: VRCFaceTracking module for PICO
VRCFaceTracking module for PICO. Contribute to lonelyicer/VRCFTPicoModule development by creating an account on GitHub.

Visual Studioをインストール

Visual Studioが必要なので、インストールしておきます。

どのような設定をしたか忘れましたが、「C#およびVisual Basic Roslyn コンパイラ」と「.NET 7.0 ランタイム(サポート対象外)」が入っているということは多分それが必要だったという事ですね。

VRCFaceTrackingのファイルも配置しておく

単純にソースコードをダウンロードしただけでは、リンクされているVRCFaceTrackigのフォルダが見当たらないはずです。Gitコマンドで良い感じにやったのを完全に忘れていたのでTLでそれとなく訊いてみたところ、「–recursive使えば良いよ」と教えていただきました。

git clone --recursive https://github.com/alvr-org/VRCFT-ALVR.git

まぁ、データが手元にあればビルドは通るのでなんでもいいです。「VRCFaceTracking @ad06f29」にリンクされてましたので、拾ってくるなり何なりして用意しておきましょう。

GitHub - benaclejames/VRCFaceTracking at ad06f2967a2243d85ad161a397eb911522182c15
OSC App to allow VRChat avatars to interact with eye and facial tracking hardware - GitHub - benaclejames/VRCFaceTrackin...

準備を終えたらVisual Studio Solutionファイル(.sln)を開いて弄ってみましょう。

MouthUpperUpとのバトル

MouthPressが有効の時にMouthUpperUpが効かないようにしたい

ソースコードを読むと以下のようになっていました。

w[MouthUpperUpRight] = Math.Max(0, p[MouthUpperUpR] - p[NoseSneerR]);
w[MouthUpperUpLeft] = Math.Max(0, p[MouthUpperUpL] - p[NoseSneerL]);

付近にはいつぞや自分で書き込んだこんな感じのコードがあります。

float smileScale = 2.0f;
var mouthSmileLeft = (p[MouthSmileL] - (p[JawShapeOpen] > 0.1f ? p[MouthRollLower] : 0f)) * smileScale;
w[MouthCornerPullLeft] = p[MouthRollLower] < 0.25f ? mouthSmileLeft : 0f;
w[MouthCornerSlantLeft] = p[MouthRollLower] < 0.25f ? mouthSmileLeft - (p[MouthRollLower] * smileScale) : 0f;

なにやら三項演算子が使われていますが、前回はAIに丸投げしたからそんなもん知らねぇぜ!!って事で調べました。

A ? B : C

Aが真ならBを実行し、Aが偽ならCを実行するという意味になる……みたいです。なるほどなるほど、じゃあ既存の[MouthUpperUp] – [NoseSneer]って部分を残しつつ条件式を書けば良さそうだね。うんうん。(いや、前回弄ったときに調べなよ)

Math.max()は何してる?

Math.Max(0, p[MouthUpperUpR] – p[NoseSneerR])ってなに?

Math.max(a,b)

この関数を使うと、AとBのうち大きい方を返すようです。ということは、[MouthUpperUp] – [NoseSneer]と0を比較して大きい方が返ってくる式になってるみたいですね。

うーん、なんだろうこれ。何故NoseSneerを引いてるんだろう。VRCFTのNoseSneerの説明を読んでみます。

Unified Expressions | VRCFaceTracking
Overview of Unified Expressions and shape references.

Nose Sneer

An expression that sneers the face.

顔をすくい上げるように嘲る表情。

Anatomy

Nose Sneer is based on the levator labii superioris alaeque nasi muscle’s ability to lift the upper lip, scrunch the nose, and slightly pull the inner eyebrow downward.

Nose Sneerは上唇鼻翼挙筋の働きに基づいており、これにより上唇を持ち上げ、鼻をしわ寄せし、さらに内側の眉をわずかに下げることができる。

Description

Nose Sneer is an expression that scrunches up the face by slightly lifting up the upper lip, pulling up the nose, scrunching the nose bridge, and pulling down the inner eyebrow slightly.

Nose Sneer は、上唇をわずかに持ち上げ、鼻を引き上げ、鼻筋をしわ寄せし、内側の眉を少し下げることで顔全体をすくい上げるように見せる表情である。

Relationship to Mouth Upper Up

Nose Sneer is complementary to Mouth Upper Up by making the upper lip raise more extremely.

Nose Sneer は、上唇をより強調して持ち上げることで Mouth Upper Up を補完する。

Nose Sneer can be contextualized by Mouth Upper Up
Nose Sneer は Mouth Upper Up によって文脈づけられる。

Nose Sneer can also rely on Mouth Upper Up to know when to reveal the teeth (as in, while Mouth Upper Up is not active, keep the lower lips attached to the upper lips when using Nose Sneer).
また、歯を見せるタイミングを判断する際に Mouth Upper Up に依存することもある。例えば Mouth Upper Up が作動していない場合、Nose Sneer を使う際には下唇を上唇に密着させておく。

VRCFT-ALVRでのMouthUpperUpのパラメーターは、0またはMouthUpperUp – NoseSneerのどちらか大きい方を採用するようですが……。意図を紐解くには実際にヘッドセットから送られてくるパラメーターを確認しなくてはいけませんので大変面倒です。理由が分かるまで一旦ここの処理は触らないでおきましょう。

MouthUpperUpの処理の後に書き足せば良い?

MouthPressが一定値を超えたら0を掛けて打ち消し、MouthPressが一定値未満なら1を掛けて既存の処理内容から変化させないという感じなら上手く行きそうな気がします。

Math.Max(0,(p[MouthUpperUpR] - p[NoseSneerR])
* ここに追加処理 );

先程の三項演算子を使えば良さそうです。

A ? B : C

Aが真ならBを実行し、Aが偽ならCを実行する

MouthPressが0.1を超えたら0を、0.1未満なら変化させないので1fという感じなら、

(p[MouthPressR] < 0.1f ? 1f : 0f)

となりそうなので、

w[MouthUpperUpRight] =
Math.Max(0,
(p[MouthUpperUpR] - p[NoseSneerR])
* (p[MouthPressR] < 0.1f ? 1f : 0f)
);

こんなもんでどうでしょうか。MouthUpperUpの値は、MouthPressが0.1以上で0になりますし、0.1未満ならMouthUpperUpからNoseSneerを引いた値になるはずです。

んー?変わったかなぁ。

変わったかも……?

試しにJawOpenも弄ってみる

w[JawOpen] = p[JawShapeOpen];

JawOpenが僅かに入ってる(0.01ぐらい)のが原因という可能性を考え、ここも弄ってみることにします。

0.1fまでは0にします。

w[JawOpen] = (p[JawShapeOpen] > 0.1f ? p[JawShapeOpen] : 0f);

しかし、変化がないのでどうやらこれは違うようです。戻しておきましょう。

もしかして、Nose Sneerじゃ?

VRCFT対応のサンプルアバターを幾つか試しながらデバッグ情報を眺めていると、なにやら0.1~0.3程度パラメーターが入ったままになっていることに気が付きました。

コードの方はヘッドセットからの値をそのまま受け渡すようになってます。「FT/v2/NoseSneer」は左右に分かれてないので、右と左の値がいい感じに合算?されているのだと思います。(分からなさすぎてそこまでは見てない)

左右あるので両方同じように書き換えれば良いでしょう。

w[NoseSneerRight] = p[NoseSneerR];
w[NoseSneerLeft] = p[NoseSneerL];

口を閉じたまま笑ったりしていなくても、微妙に隙間が空くのはこれが原因かもしれません。試しに0.6までは機能しないようにしてみます。

w[NoseSneerRight] = (p[NoseSneerR] > 0.6f ? p[NoseSneerR] : 0f);
w[NoseSneerLeft] = (p[NoseSneerL] > 0.6f ? p[NoseSneerL] : 0f);

これっぽい気がする……!

そういやさっきNoseSneerを引いてたよね?

Nose Sneer is complementary to Mouth Upper Up by making the upper lip raise more extremely.

Nose Sneer は、上唇をより強調して持ち上げることで Mouth Upper Up を補完する。

NoseSneerに0.2~0.3程度値が入りっぱなしになりやすい事を前提として考えると、MouthUpperUp + NoseSneerで過剰になっている状況が考えられそうです。その為、MouthUpperUpからNoseSneerを引いて相殺することでバランスを取っているのかもしれません。

となると、NoseSneerを0.6まで動作しないようにした場合は、MouthUpperUpからNoseSneerを引く必要がない気がします。

NoseSneerを引かない場合を試してみる

書き換え前のコードを確認します。

w[MouthUpperUpRight] = Math.Max(0, p[MouthUpperUpR] - p[NoseSneerR]);

多分MouthUpperUpからNoseSneerを引いたときにマイナスにならないようにMath.maxを使用しているのでしょう。

試しに以下のように変更して試してみることにします。多分マイナスになることもないので、Math.Maxも消します。

w[MouthUpperUpRight] = (p[MouthPressR] < 0.1f ? p[MouthUpperUpR] : 0f);

NoseSneerを引かなくなった分、笑顔が過剰に……なる感じではなさそうです。PICOのフェイストラッキングは控えめな動作なので丁度いいぐらいですね。

しかし、よ~く見ると笑ったときに微妙な隙間はあるなぁ。

変更前と変更後

変更前と変更後は大体こんな感じです。左が笑顔を作ったときで、右が無表情のときです。

後はどっかに入ってるデフォルト状態のアニメーション(MouthDefaultなど)に、MouthClosedシェイプキーを5ぐらい加えておいたら良いんじゃないかなぁとは。

Smile周りの挙動が雑なのを直す

そもそも今実装してあるSmile関連のコードがとても雑です。ついでに弄っておきます。

float smileScale = 2.0f;

var mouthSmileLeft = (p[MouthSmileL] - (p[JawShapeOpen] > 0.1f ? p[MouthRollLower] : 0f)) * smileScale;
w[MouthCornerPullLeft] = p[MouthRollLower] < 0.25f ? mouthSmileLeft : 0f;
w[MouthCornerSlantLeft] = p[MouthRollLower] < 0.25f ? mouthSmileLeft - (p[MouthRollLower] * smileScale) : 0f;

PICOのフェイストラッキングではMouthSmileが頑張っても0.4~0.5しか来ないので増幅してあるのですが、雑に2倍してあるのがあまりよろしくない気がします。

MathF.Powを使ったり使わなかったりする

倍増しといたデータを非線形補正したいならMathF.Powを使うと良さそうです。

Math.Pow(A,B)とすると、AのB乗という計算がやれます。このままだとdoubleで返ってくるので、MathF.Powにしてfloatで計算します。

var mouthSmileLeft = MathF.Pow(p[MouthSmileL] / 0.5f, 0.8f)

グラフにするとこう。

笑顔になりやすくしたいなら0.6とかに指定すると良さそうですね。もうちょっとカーブが入るはずです。

今回は単純にリニアにしておこうと思うので、Math.Powである必要はありません。結局先程と何も変わりませんが単純に0.5fで割っておきます。

var mouthSmileLeft = (p[MouthSmileL] / 0.5f) - (p[JawShapeOpen] > 0.1f ? p[MouthRollLower] / 0.5f : 0f);
w[MouthCornerPullLeft] = p[MouthRollLower] < 0.25f ? mouthSmileLeft : 0f;
w[MouthCornerSlantLeft] = p[MouthRollLower] < 0.25f ? mouthSmileLeft - (p[MouthRollLower] / 0.5f) : 0f;

こんな感じでしょうか。また弄りたくなったときに多少は書き足しやすいかなぁとは。調子に乗って上げすぎると、出力が1.0を超えたときにトラブったりする可能性があるので、Clampして1.0に収まるようにしておくといいかも?Clampは一旦保留。

MouthUpperUpも微修正

口を開けて笑顔を作ったときにMouthUpperUpがしっかり効かないので、こちらも0.8で割って若干補正しておきました。

w[MouthUpperUpRight] = (p[MouthPressR] < 0.1f ? p[MouthUpperUpR] / 0.8f : 0f);

これでMouthPressが動作しているうちはMouthUpperUpが入らないようにしつつ、MouthUpperUpが入るときは少しだけ強く入るようになったはずです。

ちょっとだけ口が大きく開くようになった気がする

雑なまとめ

何故か常時入りっぱなしのNoseSneerくんが原因でした。

多分動くと思うからリリースしようぜ

した。

Release VRCFT ALVR module v1.3.0-modified.2: Mouth Fix · pikepikeid/VRCFT-ALVR
Adjust MouthUpperUp to reduce influence while MouthPress is activeSlightly correct maximum MouthUpperUp valueFix slight ...

ビルドしたDLLファイルは自動的に「%USERPROFILE%\AppData\Roaming\VRCFaceTracking\CustomLibs」に移動する設定になってるみたいなので、既存のDLLと置き換えてしまえばOKです。ビルドして、VRCFTを閉じて、上書きして動作確認して……と反復するので近くに移動してくれるのは凄く楽。その辺りの設定はALVRModule.csprojにあります。

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy /y &quot;$(TargetDir)ALVRModule.dll&quot; &quot;%25APPDATA%25\VRCFaceTracking\CustomLibs&quot;" />
</Target>

例えばALVRモジュールの場合はModuleIDが「699750bf-e217-4bd8-8039-3d5f97ee80ba」なので、既にインストールされていればそこに上書きしてしまえば動きます。

新規にModuleID生成してちゃんとmodule.json書いた方が良いと思うんですが、この辺りのドキュメントが見当たらないのでどうするのが適切なんでしょうね?

VRCFTPicoModuleもビルドした

中身は大体一緒なのでやってしまおうということで、ビルドしてリリースしました。こっちはこっちで目を見開いたり、口を大きく開けるようにしたり、あとは口を左右に動かせる範囲を広くしたりもしました。単純にパラメーター出力を増幅しただけ。

上の動画と改変前・改変後が逆になってるけどまぁ気にしない。

Release VRCFTPicoModule v0.1.10-modified.1 · pikepikeid/VRCFTPicoModule
動作保証はありません。自己責任で使用してください。There is no guarantee of operation. Please try at your own risk.AboutPICO 4 Pro/Enterpriseから来るパ...

末代アドカレ、明日と明後日はGiraffe Beerさんです。まさかの2日連続とは……楽しみですね。

タイトルとURLをコピーしました