0. 最初に
この記事はVRChat向けの技術展示用シェーダー「八方美人シェーダー」の解説です。
VRChatのワールド(https://vrchat.com/home/world/wrld_a8fa8798-13f6-42c2-a162-7339506cb1dd )をご覧になることをおすすめします。
このシェーダーは技術展示用で、関連スクリプト含めCC0ライセンスとしますので、ご自由にお使いください。2次元の嘘をVRChatで再現する試作シェーダー、作ってみました!
— 琴うみ製作所公式 (@KotoumiFac) October 1, 2025
展示ワールドとQiitaも公開したのでぜひ見て、試してください~
いつかこういうシステムが前提でモデリングされたアバターが見てみたい…!!
⬇️リンクはリプにあります⬇️ pic.twitter.com/rnz9sErZTr
1.動機
日本のアニメ的表現において、縦横斜め上下から描かれた最も美しいと感じる顔は、3Dモデルにしようとすると矛盾が生じ、そのままでは実現できません。そのため、3Dモデルを製作するときはどうしても妥協が生じてしまいます。これを解決したいと思ったことが動機です。
2.目標
今回は、このような
ことをリアルタイムで、複数人に対して(つまり複数のカメラに対して)同時に行うことを目標にしました。3DCGアニメを作っているスタジオでは、このような変形を自動で行うツールが存在しているらしく(Camera-O-Maticなど)、VRChatでも実装できないかと考えました。また、このような手法の可能性を広く知ってもらうことも目標です。@kohta0130 この動画がウケてるみたいだから、セミナーのとき一番会場が湧いた動画もアップしとこうかな。 pic.twitter.com/3ZKXQozpDM
— 森江康太 Kohta Morie (@kohta0130) April 6, 2016
3.方法
どのように変形させるか
作画の「嘘」について素人なりに少し調べてみたのですが、今回はとりあえず上下+左右45度+左右90度の6種類の変形をさせることにしました。blenderでこの6種類のシェイプキーを作り、どのように動作させるか考えました。
VRCのギミックで変形させる
まず最初に思いついたのが、contactSenderなどの機能を使ってシェイプキーを動作させる方法です。しかし、自分の顔を見る人それぞれに対してギミックを独立して動作させるには、新しい機能をVRChat側に作って貰う必要があり、他にも様々な問題があり断念しています。
シェーダーを使う
上記の理由からシェーダーを使うことになったのですが、シェーダーは直接シェイプキーのデータにアクセスすることは出来ません。そこで、
変形のためのシェイプキーを作る
↓
顔のメッシュをUV2に画像1のようにUV展開する(BlenderPython用のスクリプトをAIに書いてもらいました。)
↓
指定したシェイプキーの頂点のx,y,z移動量をRGBに置き換え、UV2の通りにテクスチャ(RGBA half、普通のテクスチャだと情報量が足りず、変形が見るに耐えないギジギジなものになります。)に焼く(これもスクリプトをAIに書いてもらいました)
↓
6枚のテクスチャを基に、リアルタイムで変形させる(シェーダーもAIに書いてもらいました。最近のAI賢すぎて怖い…!)
という方法をとることにしました。
テクスチャの容量は、顔のメッシュが16384頂点までは128*128画素で対応できるため、この場合は1枚170KB程度です。VRChatはUV0~3まで使用できるので、UV3あたりにこのUVを置けばUVが足りないということはまずないのではないかと思います。
手元で試すための詳しい説明
まず格子状にUV展開するスクリプトですが、顔meshを選択した状態で以下のスクリプトを実行すると展開してくれます。
import bpy
# アクティブオブジェクトを取得
obj = bpy.context.active_object
mesh = obj.data
# UV2がなければ作成
uv_layer_name = "UVMap2"
if uv_layer_name not in mesh.uv_layers:
uv_layer = mesh.uv_layers.new(name=uv_layer_name)
else:
uv_layer = mesh.uv_layers[uv_layer_name]
# UVアクティブ設定
mesh.uv_layers.active = uv_layer
grid_size = 128 # 128x128 グリッド
# 頂点インデックスごとにUVを割り当てる辞書
uv_coords = {}
for vidx, v in enumerate(mesh.vertices):
x = (vidx % grid_size + 0.5) / grid_size
y = (vidx // grid_size + 0.5) / grid_size
uv_coords[vidx] = (x, y)
# ループごとに頂点インデックスを参照してUVを設定
for loop in mesh.loops:
uv_layer.data[loop.index].uv = uv_coords[loop.vertex_index]
# メッシュ更新
mesh.update()
print("UV2展開完了!")
次にRGBA halfをベイクするスクリプトですが、顔meshを選択し、ベイクの際に参照するUVをアクティブにした状態で実行すると.blendファイルと同じディレクトリに書き出してくれます。unityに取り込むときに、formatをRGBA Halfにするのを忘れないでください。
注意点ですが、AIに書いてもらったシェーダーはなぜか正面の変形量のテクスチャも要求してくるので、# 出力したいシェイプキー名 にBasisと入れたテクスチャも書き出しておきましょう。
import bpy
import numpy as np
# ==== 設定 ====
shape_name = "animeD" # 出力したいシェイプキー名
grid_size = 128 # UVを割り当てた格子サイズ(128x128)
image_name = "hao_" + shape_name
save_path = bpy.path.abspath("//" + image_name + ".exr") # blendファイルと同じ場所に保存
# ==== オブジェクト取得 ====
obj = bpy.context.active_object
mesh = obj.data
if not obj.data.shape_keys:
raise RuntimeError("シェイプキーが存在しません")
basis = obj.data.shape_keys.key_blocks["Basis"]
if shape_name not in obj.data.shape_keys.key_blocks:
raise RuntimeError(f"シェイプキー '{shape_name}' が見つかりません")
shape = obj.data.shape_keys.key_blocks[shape_name]
# ==== 画像作成 ====
if image_name in bpy.data.images:
img = bpy.data.images[image_name]
else:
img = bpy.data.images.new(image_name, width=grid_size, height=grid_size, float_buffer=True)
# ==== 差分計算 ====
deltas = []
for i, v in enumerate(mesh.vertices):
co_base = basis.data[i].co
co_shape = shape.data[i].co
delta = co_shape - co_base
deltas.append(delta)
# ==== ピクセルバッファ ====
pixels = np.zeros((grid_size * grid_size, 4), dtype=np.float32)
# ==== UVからピクセル位置へ ====
uv_layer = mesh.uv_layers.active
if not uv_layer:
raise RuntimeError("UVが見つかりません(先にUVを展開しておいてください)")
for loop in mesh.loops:
vidx = loop.vertex_index
uv = uv_layer.data[loop.index].uv
x = int(uv.x * grid_size)
y = int(uv.y * grid_size)
idx = y * grid_size + x
d = deltas[vidx]
pixels[idx, 0] = -d.x*100/2 +0.214 # Unityではグローバルx方向(変形が大きすぎたりする場合は100を変えてください)
pixels[idx, 1] = d.z*100/2 +0.214 # unityではグローバルz方向
pixels[idx, 2] = -d.y*100/2 +0.214 # unityではグローバy方向
pixels[idx, 3] = 1.0 # A固定
# ==== BlenderのImageに書き込み ====
img.pixels = pixels.flatten().tolist()
# ==== EXRで保存 (Half float) ====
img.filepath_raw = save_path
img.file_format = 'OPEN_EXR'
img.save()
print(f"シェイプキー '{shape_name}' の差分を {save_path} に保存しました。")
シェーダーはこちら
https://drive.google.com/file/d/1Oc618Ak3Gef-cX-51PuVgsOFI-tr0ivK/view?usp=sharing
DeformStrengthを0.01くらいにするとちょうどいいです。エラーが出ますがVRCにアップロードはできます。
VRChatアバター「ハオラン」はクレジット表記をすれば自由に再配布ができるため、このギミックを搭載したprefabが入ったunitypackageを置いておきます。
https://drive.google.com/file/d/1Sn3ZjFAteP8ueR8L3DFz_n-AT52vIZcZ/view?usp=sharing
ハオラン-HAOLAN by かなリぁさんち
4.結果
twitterの動画とVRCのワールドを見てもらうのが一番わかりやすいと思います。
AIに書いてもらったシェーダーなのでいろいろと不具合があるのですが、技術展示用としてはそこそこ良いものができたのではないかと思います。
5.将来の展望
この機能を搭載したシェーダーが普及すれば、アバター製作の際の、顔の造形の妥協が必要なくなり、モデルの形が変わっていくのではないかと思っています。そういう未来を見てみたい…!!