水ノ茉の宣伝
準備中...
ゲームを作る予定なの水ノ茉
始まり
セルルックで採用されることがあるSDFな陰。
このような陰を描くためには、閾値を詰め込んだテクスチャ(以降「陰閾値テクスチャ」と表記します。」が必要です。

作り方は簡単でライト角度ごとに明暗を書き込んだテクスチャを用意して、専用のツールでベイクするだけです。こちらのsdf_shadow_threshold_mapが最推し。やっぱりPythonちゃんは可愛い。

シェーダー実装はバカ簡単なんですが、適切なUV展開と明暗テクスチャの用意がマジで面倒なの。そんな理由からUV展開以外のフローをUEで完結してサボれるようにしてみました。
発端は先々週ぐらいに気分で始めたルックデヴ。

ヤエカさんの雰囲気を召喚するためだけに長いこと漬けていたツール開発を再開。
そして爆速誕生したのがこの子です。
爆速の代償として、機能が足りなかったり、不具合があったり、色々と患っているのですが、ブログネタの為に開放です。
https://github.com/kafues511/ToonShadePaint
※点検したいので気が向くまで非公開です。
そして未来の私は使い方を忘れていることでしょう。
いつものことです。
過去の私は優しいから使い方を残しておいてあげる。
動作環境
- Windows 10 / 11
- DRAM 64 GB
- VRAM 24 GB
- Unreal Engine 5.4
- DirectX12
シェイプと点の内外判定

球、ボックス、円柱、円錐、カプセル、これらの形状でお顔に落書きをしてライト角度ごとの明暗を用意します。
陰閾値テクスチャに都合の良いUVマップを準備
眉毛やまつげ、瞳など、見た目が左右対称なものは、UVを重ねて展開することがあります。よくある手法なのですが、閾値をテクスチャに格納する本手法とは相性が最悪です。
画像を例に挙げると眉毛とまつげのUVが重なっており、同じ座標を指しています。

このUVを利用して陰閾値テクスチャを作成すると、参照される閾値が不適切な状態になるため、結果として対照的な挙動になります。この挙動を避けたい場合は、BlenderなどのDCCツールで適当なUVチャンネルに陰閾値専用のUVを展開する必要があります。
Blenderで適当に展開する
キャラクターと戯れる時にしかBlenderを触らないので毎回記憶が曖昧になる。

UVマップの追加は簡単。
画面の分割方法すら忘れてた。

片方の画面にUV表示しておくと捗る。

すれ違い矢印なアイコンをクリックして有効化した後に、UVエディタで頂点の選択状態を解除すると、編集モードの方の選択も連動して解除される。

常時陰になる箇所はシームを付けて分離しちゃうと空間を効率的に使える。展開方法はアングルベースか最小ストレッチを適用に選択しても意外と耐えられる。UVチャンネルが余っているなら、顔と眉毛は別々に展開した方が良い。眉毛はパーツが小さいから一緒に展開しちゃうと情報の欠損が目立つ。

SkeletalMeshアセットをReimportすれば気軽にUV更新できるので適当で大丈夫。
撮影専用のレベルを作成


キャプチャ専用のレベルを作成します。

PostProcessVolumeを配置して、LumenやBloom、DOFやVignetteなど、作業に不必要な画面効果を全て無効化します。アンチエイリアスも無効化(r.AntiAliasingMethod 0)して生の品質を見た方がいいです。
撮影対象の作成と配置

BP_ToonShadeCaptureTargetActorを継承したBPアクターを作成します。

継承したBPアクターを開いてパラメータを設定をします。
| パラメータ名 | 説明 |
|---|---|
| Enabled | 撮影の有効性 チェックを入れておいてレベルに配置した際に、不要なやつだけチェックを外す運用が推奨 |
| Debug Name | デバッグ表示名 ツールで有効な撮影対象をリストアップされる際に表示される表示名 ![]() |
| Layer | 陰閾値テクスチャを作成する際に投下する順番 |
| Resolution | 解像度 特定の角度だけ解像度上げるとか出来ないから全部同じ解像度を適用すること |
| Resolution Type | Seed を選択 |
| Skeletal Mesh Asset | 撮影対象のスケルタルメッシュアセットを指定 |
| Capture Materials | 指定されたスケルタルメッシュアセットのマテリアルスロットごとに撮影設定をする |
| Material Slot Name | マテリアルのスロット名 |
| Enabled | 撮影の有効性 |
| Coordinate Index | 陰閾値テクスチャを作成する際に参照するUVチャンネルを指定 |
| Base Color Texture | 作業用にベースカラーテクスチャを1枚だけ設定可能 |
配置(Seed編)

こんな感じで先のCaptureTargetをレベルに配置します。
LocationはX座標をキリの良い数値でズラして配置します。RotationはZeroVector、ScaleはOneVectorを推奨します。SRが初期値以外じゃないと成り立たないのは根本的に良くないです。
操作に慣れるまでは最小限の5体ぐらいの配置を推奨します。
配置(Position編)

少し離れた位置に先のCaptureTargetを1つだけ配置します。Layerを適当な大きい値に設定して、Resolution TypeをPositionに変更します。
本ツールは距離マップを作成する際、テクスチャ座標ではなくモデル座標による計算を採用しています。そのモデル座標が必要なので、CaptureTargetを1体だけPositionにしてモデル座標を撮っているのです。
ShadeShapeActorの配置

これは継承させる理由が無いので、ドラッグ&ドロップでレベルに直配置でおっけいです。

レベルに配置したShadeShapeActorのパラメータ設定をします。軽く一読して、後は実際に使いながら悩んだ方が挙動の理解が早いと思います。
| パラメータ名 | 説明 |
|---|---|
| Location | 形状の位置 |
| Rotation | 形状の回転 |
| Scale | 形状の大きさ ShapeTypeがCapsuleの時はOneVectorにすること。 |
| Enabled | 形状の有効性 |
| Debug Name | デバッグ表示名 |
| Paint Type | None: 陰を描かない Fill: 形状の内側に陰を描く Mask: 形状の内側の陰をマスクする(要は明になる) |
| Invalid Type | None: 距離計算が有効 Fill: 形状の内側の距離計算を無効にする Mask: 形状の内側の距離計算を有効にする ※ちょっと難しいから後で実例に触れる |
| Shape Type | 形状を指定する |
| Height | カプセルの高さ ※ShapeTypeがCapsuleの時のみ編集可能 |
| Radius | カプセルの半径 ※ShapeTypeがCapsuleの時のみ編集可能 |
| Mask | 扇形のマスクの有効性 |
| Mask Angle | マスクの角度(単位はDegree) |
| Mask Intensity | マスクの強度 MaskAngle!=180の時に各ベクトルの強度を調整するとマスク形状を歪められる |
| Mask Axis | マスクの向き |
| Flip | フリップの有効性 |
| Flip Center | フリップする際のピボット座標 ※これがあるからCaptureTargetのLocationはキリの良い数字がいいの |
| Flip Axis | フリップの向き |
お絵描きのTips
ShadeShapeActorの扱いは慣れろの一言だけど、基本的には以下の操作で事足りると思います。添付しているパラメータは一例に過ぎないので参考程度に留めてください。結局はモデルの形状に依存する話ですので。
対称的な曲線の作り方

まずはベースの形状を作成します。

| パラメータ名 | パラメータ値 |
|---|---|
| Rotation | 0.0, 0.0, 0.0 |
| Shape Type | Cylinder |
| Mask | True |
| Mask Angle | 90.0 |
| Mask Intensity | 1.3, 1.0, 0.65 |
| Mask Axis | 0.0, 1.0, 0.0 |
曲線を作ります。作り終わったら無効化しておいてください。

| パラメータ名 | パラメータ値 |
|---|---|
| Rotation | 0.0, 0.0, 90.0 |
| Shape Type | Cylinder |
| Mask | True |
| Mask Angle | 180.0 |
| Mask Intensity | 1.0, 1.0, 0.0 |
| Mask Axis | 0.0, 1.0, 0.0 |
真っ二つな陰を作ります。

曲線と真っ二つを有効にすると、それっぽい陰が出来ました。
次にこれの反対側の陰を作ります。

| パラメータ名 | パラメータ値 |
|---|---|
| Rotation | 0.0, 0.0, 0.0 |
| Layer | 58 ※下2つの形状より小さいレイヤー値にすること |
| Shape Type | Cylinder |
| Mask | False |
全陰にします。

| パラメータ名 | パラメータ値 |
|---|---|
| Rotation | 0.0, 0.0, 0.0 |
| Layer | 59 |
| Paint Type | Mask |
| Shape Type | Cylinder |
| Mask | True |
| Mask Angle | 90.0 |
| Mask Intensity | 1.3, 1.0, 0.65 |
| Mask Axis | 0.0, 1.0, 0.0 |
先ほどの曲線の形状をMaskにします。同様に作り終わったら無効化します。

| パラメータ名 | パラメータ値 |
|---|---|
| Rotation | 0.0, 0.0, 90.0 |
| Layer | 60 |
| Paint Type | Mask |
| Shape Type | Cylinder |
| Mask | True |
| Mask Angle | 180.0 |
| Mask Intensity | 1.0, 1.0, 0.0 |
| Mask Axis | 0.0, -1.0, 0.0 |
真っ二つな陰を真逆にします。

曲線と真っ二つを有効にすると、反対側の陰も出来ました。
大体はこんな感じで片方を作ったら片方をマスクする流れで作れます。
InvalidTypeの使い所

画像のように縁に常時陰を入れたい場合に使います。
PaintTypeを用いて常時発生する陰を描きこむと動画のように距離計算に不都合が発生します。
InvalidTypeで塗る潰した箇所は距離計算から除外されるため、常時発生する陰を作る場合には必須なのです。
鼻や目元の明暗の作り方
飽きた。気が向いたら書く。
撮影手順


Toolsの下の方にToon Shade Paintがあるのでクリックして開きます。

開くとこんな感じです。本当ならモトヤLマルベリを使いたかったんだけど、隠蔽されたデータの読込(.pak)が分からんかったから諦めました。フォントは選べるようにしたかった。エロゲっぽく。

Update Capture Targetsをクリックすると、レベルに配置された有効な撮影対象がリストアップされます。

Capture Allをクリックすると撮影対象の明暗がキャプチャされます。見た目が怪奇なのはRチャンネルに明暗、Gチャンネルにユーザー指定の除外領域が格納されているためです。

Shadow Threshold Map Textureに陰閾値テクスチャの出力先をセットします。現時点で対応しているRenderTargetのフォーマットは RGBA8, RGBA16f, RGBA32f の3つです。

Create Shadow Threshold Mapをクリックで陰閾値テクスチャの作成開始されます。同期的な処理の為、暫くエディタの動作が固まります。ガチャガチャクリックしたり、キーボード入力したり、余計なことはしないで下さい。

作成が無事に完了するとRTに結果が書き込まれて、作成に要した時間がログで出力されます。撮影に流れてくるデータは真である前提で色々と組んでいるので、筆者が想定していないデータが流し込まれると基本的にはクラッシュすると思います。
組み込み
作成した陰閾値テクスチャを使えるようにノードを組みます。基本的には以前作成したものを流用です。

カスタムノードの中身だけ少し変更です。Fは顔の前方ベクトル、Rは顔の右ベクトル、Lはライトベクトル、各ベクトルはXY方向だけで良いです。以前はTransformVectorでざっくりなベクトル変換したんですが、アニメーションに追随させたい場合は、お顔のSocketのTransformからRotatorを取ってきて、SetVectorParameterValueで流した方が適切です。
float FoL = dot(F, L);
float RoL = dot(R, L);
// NOTE: 明<->暗の順番次第では逆転しないとおかしくなるかも。環境ごとの規格に合わせていいと思うよ。
// float ShadowThreshold = lerp(ShadowThresholdMap.r, ShadowThresholdMap.g, step(0.0, RoL));
float ShadowThreshold = lerp(ShadowThresholdMap.g, ShadowThresholdMap.r, step(0.0, RoL));
float ShadowDir = FoL * 0.5 + 0.5;
float FaceShadow = step(ShadowThreshold, ShadowDir);
return FaceShadow;

TextureノードはRenderTargetも指定可能なので、こんな感じで良いです。UV編集が特殊な場合はWrapにすると壊れると思うので安牌なClampを選択、MipLevelは当然変わっちゃうと閾値が崩れるのでゼロで固定です。

諸々の動作確認が済んだらRenderTargetからTextureを作成します。

作成後のテクスチャも同様にClampとMip0で且つポイントサンプラを初期値にしておきます。下手にサンプリングされると同様に閾値が崩れます。
おわり!!!

オトメきをきっかけに頑張ったの。
参考
- How to Smooth Anime Face Shadow in Unity URP Without Editing Face Normal – YouTube
- SDFな顔陰と初顔合わせした動画
- https://github.com/akasaki1211/sdf_shadow_threshold_map
- OpenCVベースな処理を参考
- https://github.com/nagakagachi/NagaSdfTextureToolForUE
- OpenCVベースに改修する以前に参考にしていた
SNSを始めたのよさ!
始めたの。
―――ですけど、気が向くまではブログの広報に限定すると思います。
基本的にはROM専で通したいので。
そんなわけで、効果的な使い方をあんまり分かってないけど、よろしくです。
そのうちインディーゲーム用の公式アカウントも作る予定ですが、運用担当者が異なるので詳細は知らんです。
職場の先輩に広報は大事と御教授頂いたので苦手でも頑張るしかないのです。
先人のアドバイスは本当に偉大だから素直に受け止めはするんですが、SNSはマジでよく分からん。
ソフマップの特典開けるの忘れてた
オトメきは間違えてFANZA GAMESとソフマップでそれぞれ買ってたんです。
手軽なFANZA GAMESからダウンロードしたのでソフマップの段ボールを放置していました。
本体は保存用に取っておくとして。
ついでに軽く原画集に目を通します。
やっぱりディフューズが入ったキャラ絵だとリアル調な背景と親和性が高いですね。
画像を表示

アアアアアアアアアアアアアアアア
はい。幸せ。
待って。FANZAの方の特典が見当たりない。
夜中にお部屋を探索中…
画像を表示

見つかりました。
あーかわいいよー。うへへ。
さてと、抱き枕本体を買ってないので保管場所に戻そう…
かわいい画像を餌にして記事を書く私なのです。
我ながら天才的な発想ですね。
フリップ機能
陰は左右対称なことが多いと思うからフリップ機能を追加っと。
色々と機能を追加してたらシェイプあたりfloat4 x8だと今後詰みそうだな。
形状を128個も配置して陰を描くことなんてないだろうしレイアウト変えるか。
ということでfloat4 x16で形状は64個までサポートに変更っと。
範囲外は距離算出をスキップ
範囲外のテクスチャ座標も計算しちゃったので抑制っと。


距離計算をテクスチャ座標からモデル座標に変更
テクスチャ座標だと距離関係が不正になることがあるだろうからモデル座標に変更っと。
これで眉毛やまつげの不自然な挙動が抑制されるはずだったんだが、変わらんね。


試しに全範囲探索
JFAだと計算量が足りないのかと思い、思い切って1ピクセルずつ探索掛けてみることに。
当然だけど最高品質になったね。
なんだけどなー。眉毛とまつげの左右対称が直らないな。


JCBしか勝たん
「まゆ」って打つと「茉優」が出てくる優秀な筆者の辞書。

ユノスさんも大変ですね。
マジでウザイですわ。
DLsite系列やFantiaやらFANZAやら、色々なところが被害受けましたね。
筆者さんは元からJCB民なので切り替えの手間はないので、正直実感はないんですけど、ただただうぜぇですわ。
くそが。
あー、ムカついた。
左右対称な見た目ならそりゃUV座標もそうだよね
例の眉毛とまつげが左右対称になっちゃう件、ステップ数の問題かと思い全探索に変えても解消しませんでした。
ふーむ。
……ん?
………嫌な予感がします。
…………この子のベースカラーのテクスチャを覗いてみますか。

……
…………
………………
はぁ、そりゃ、左右対称になりませんわ。
汎用手法だというのに全然忘れていたわ。
オッドアイ的な左右非対称な見た目ならそれぞれに異なるUV座標を割り当てるものですが、対称ならその必要はないですからねぇ。UVを可視化すると分かりやすい。

久々にしょうもないことで時間を潰してしまった。
BlenderでUV展開してきます。
とりあえず自動展開して再撮影っと。
解消しましたね。よかった。

モデル形状による依存性を減らすためにライトマップUVな展開をサポートしたいところですが、それをするにはセットアップパスも全探索しないとなので、ちょっと改修しないとですね。
タイミングも良いし全部コンピュートシェーダーにお引越しする。
またポイント失効しちゃう
ここまで頑張ったし、ご褒美にエロゲを買おう。
という適当な理由付けで、シンプルにポイント失効しちゃう。

ポイントのお陰で実質無料みたいなものですね。

ふふん♪
嬉しさとは裏腹に、1/10ぐらいしか全攻略が進んでいない悲しい現実。

ゲーム開発とは別に、こういうツール開発をするからエロゲの時間が消えるんだろうな。
とはいえエロゲとは別ベクトルの好きだから捨てがたいのです。
強欲な筆者さんは全部手に入れたいのです。
そのためにも力をもっともっともーっと付けて、暴力とも言える火力で解決できるようにならなければです。
寝る。おやすみ。
PixelShaderからComputeShaderにお引越し
お引越しついでにOpenCVベースな実装に変更しよっと。
本家は探索範囲がcv.DIST_MASK_5に対して、うちの子は任意のイテレーション回数にしているから多少の品質差分は発生する。
諸々のお引越し完了。
ライトマップなUV展開だと解像度が不足しているか除外処理に不備があるかで上手く動作しないわ。
大雑把なUV調整だけで動作するし、ライトマップなUV対応は保留しよ。
あとは未来の私が読める程度にリファクタとエディタ部分の整備をしたら終わりにしよう。
ツール制作は本線ではないので、適当なところで区切りを付けて強制的に終了しましょ。
開発イテレーションが改善されることによって得られる時間以上を充てちゃうと本末転倒なのです。
