3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】ミアレシティのホログラムをUnityちゃんで再現してみた【Shader芸】

Posted at

今回は久しぶりにゲームをプレイして感銘を受けた表現を再現してみようシリーズ第2弾として
Pokémon LEGENDS Z-A のストーリーで出てくるホログラム表現をどうやったら再現できるのかを考えてみました。

ちなみに第1弾は下記のリンクよりご参照ください

できたもの

Github

こちらのリポジトリに制作物をアップロードしております。

Subgraph とかを詳しくみたい場合は是非ご利用ください。

考察

まずは実際のゲーム画面を見てみましょう。

screenshot_01.png
screenshot_02.png
screenshot_03.png

(※ ゲーム内ScreenShotより引用)

このホログラム表現、非常に近未来的でXR界隈で開発をしている身としてはとても魅力的に映る表現だと思います。

このホログラム表現、大きく分けると以下のような構成ではないかと推察しました。

  1. 元々のTexture の色 (Albedo) はそのまま出ていない
  2. 元々のTexture の明度は維持されている
  3. ホログラムはキャラごとに色が異なる
  4. ライティングは反映される

上記より

Lit Shaderをベースとして Toonレンダリング + ホログラム表現という大きな道筋をたてました

ホログラム表現

ホログラムに注目すると

  • UVスクロールを上方向にしている
  • ボーダーは水平面と平行
    • Mesh のUVに沿っているわけではない(1敗)
  • ホログラムのメインと影部分のColor は異なる

このあたりを再現できればいけそうです

実装

まずは 実験体 被写体を用意しなければならないので、今回もUnityちゃんにお願いします。

ダウンロードページやリリースノートを覗くと

Unity-chan-SD.png

ん?

ver3_0.png

スクリーンショット 2026-01-08 22.15.20.png

(;゚Д゚)

(;゚Д゚)

(;゚Д゚)

ついにURP対応してる!!!!!!!

これでBuilt-in RPのMaterial のStandardシェーダーがアタッチされてピンクなUnity-chanを直す作業をしなくて済む!!

Thank you UTJ forever!!

ということでUnity-chanの規約を確認してダウンロードして取り込みます。

開発環境

今回は以下の環境で製作しています

項目 Version
Unity unity6000.3.2f1
Platform Android
SD Unity-chan v3.0a
MacOS Tahoe 26.2

Unity-Chan 同梱物の確認

unity-chan-pkg.png
中身としてはこんな形です

ポイントとしては

  • SD Unity-chan 3D Model Data ディレクトリの中にいろいろあります

  • すぐにPrefabをポン置きしたい場合はPrefabs のSD_unitychan_humanoid.prefab かSD_unitychan_generic.prefab を利用

    • ただし一部アタッチずみのScriptがNullRef になってしまう
    • この場合はScenes/SD_unitychan_Generic/SD_unitychan_Generic.unity というシーンファイルがあるのでそこでMissing前と後を比較して対応するのが良き
  • Material はAssets/UnityChan/SD Unity-chan 3D Model Data/Materials/UnityChan の中にあります
    mesh.png

    • Material ですが基本は Mesh_SD_unitychan 以下のRendererに設定されているのですが、顔周りは専用のMeshRendererで描画しているので階層が異なるので注意しましょう
      faceMesh.png

ShaderGraph でShaderを用意する

機能をカスタマイズするにあたり、Subgraph で簡単に機能を取り込みやすくするためにまずは簡易Toon Shader を作ります

その後各機能単位でSubgraph を作って最終的に本体に結合していきます。

簡易ToonShader の作成

toon.png

今回作る最終系はこんな感じ。
う〜ん、パラメータが多い!!

このToonColor Node ですが内部はこのような感じです。

toon02.png

そもそもトゥーンレンダリングとは、陰影のつけかたをセル画のようにパキッとN段階(Nは小さめの自然数) で表現する技法です。

今回は

  1. オリジナル
  2. 影1段階目
  3. 影2段階目

と影を2段階設定できるようにしたのでパラメータが多いです。
ただ、結局は

  1. 法線ベクトルとMainLight からそのピクセルのColorのWeight を計算
  2. FinalColor = BaseColor * w0 + ShadeColor01 * w1 + ShadeColor02 * w2 と加算合成

というシンプルな構造で作りました。
この w0~w2 の部分がトゥーンレンダリングの肝になってきます。

さて、続いては重みパラメータの計算部分ですが、全体像は下記の通りです。

toon03.png

付箋で貼り付けている通り影s1,s2を計算して

w1 = (s2 - s1) * Apply1st
w2 =( 1-s2)* Apply2nd
w0 = saturate(1-w1-w2)

という感じで影の有効On/Offを設定して計算しています。
計算自体は 加算(減算)と乗算と saturate, smoothstep なのでかなり軽量です。

で、影のs1, s2 ですが

s = smoothstep( T - f, T - f, dot(N, L) )

T → 閾値(Threshold)
f → ぼかし度合い(Feather)
N → 法線ベクトル(ワールド座標系)
L → ワールド座標系のライトの方向

の計算式で出しています。

なぜこれでうまくいくのかはLLMに聞いてください。(とりあえずこれでできる)

一旦ここまでを適応すると下記の画像のように普通のLit から髪の毛などが顕著ですがtoonシェーディングができます。

apply_toon.png

もちろん綺麗さでいえばUnity 公式のToon Sheder が良いですが、ShaderGraph で機能拡張を簡単にSubGraph追加対応でできるようにするために簡易Toon Shader ということでここはご愛嬌。

Rimライトの機能

とりあえずUnity ToonShader にある機能はなるべく乗っけたいので次はRimLightです。
いわゆるアウトライン的に使える機能でもあります。

rimlight.png

RimLight の方はNodeをまとめきれてないのですが端的に言うと

  1. RimLight で光らせるべき部分のMaskを作成
  2. Maskに対してLightの強さと色を乗算
  3. 上記の結果を他の結果に対して 加算 合成

という3ステップに分かれます。
肝は Maskの作り方 ですので、そこを詳しく見ていきます。

RimMask.png

RimMask の作り方は以下のステップです。

1, ライトの反射度合いを計算するために saturate( dot(Normal, ViewDirection)) を計算します。
ViewDirectionはカメラの視線ベクトルです。

  1. 1の結果を 1から引くことで視線ベクトルと直角方向に近ければ近いほど1, そうでなければ0に近くなるように変換します
  2. RimPower乗して強度を反映します。
    ここまでを数式にすると (1-saturate(dot(N,V)))^RimPower となります
  3. 続いて上側ですが、こちらはRimLightの閾値と滲み度合いを計算しています。
  4. 最終的にSmoothStep(T-f, T+f, 3の結果)を計算します
  5. 基本的にはこれで出力でも良いのですが、計算式によっては結果が反転する場合があるので結果反転フラグと値の切り替えとして lerp(output, 1-output, InverseFlag) として後段にNodeを設定しています

以上のステップでRimLight用Mask作成ができます。

Specular

Specular(鏡面反射) も一応用意します。
spec.png

中身はこちら
spec_node.png

Specular は視線方向に向かって鏡面反射する光の強さを計算するNodeです。
ちょっと多いですが数式的には

L = normalize(-MainLightDirWS)
V = normalize(ViewDirWS)
N = normalize(NormalWS)
H = normalize(L + V)

nh = saturate(dot(N, H))
raw = pow(nh, SpecPower)
f = max(SpecFeather, 1e-4)(Feather=0対策)
mask = smoothstep(SpecThreshold - f, SpecThreshold + f, raw)

out = SpecColor * mask

という感じです。
smoothstep(SpecThreshold - f, SpecThreshold + f, raw) はRimLight とかでも見た数式です。
結局は色の滲み度合いをどう制御するのか?という話になり、そのどう制御するのか=SmoothStepの第3引数 の設計が肝になります。

今回は Blinn–Phong反射モデル を利用しました。
モデルについての解説は下記ブログが参考になると思います。

最終的に得られた結果はEmmision に加算合成でいれます。

グレースケール機能

ホログラムのキャラクターを見ると、元々の明度は保ちつつ、彩度については保持されていません。
つまり、元々の色ではなくグレースケールに変換してその上にホログラムの色を載せています。

ということでグレースケール機能を作ります。

luma.png

使い回しできるようにColor を受け取ってそれをグレースケールするSubGraphを作ります。

中身はこれ!!
luma02.png

昨今のLLMとの壁打ちでとりあえず Rec.709(Rec.709(ITU-R BT.709) の規格に従ってグレースケール化させました。

詳しい仕組みは人間の視覚特性とか色々知る必要があるので今回は割愛します。

ホログラム機能

さて、いよいよ本題のホログラム部分です。
holo.png

一番右側のLerp の部分では以下のように実装しています

Lerp(グレースケールにHologramのMainColorを乗算したもの, グレースケールにHologramのMainColorを乗算したもの, ボーダーになるようにするMask画像)

第1,2引数はシンプルで luma Nodeでグレースケール化したToonRenderingの結果に対して
ホログラムで表示する色をそれぞれ乗算しているだけです。
最終的な塗り分けはMask画像の方で用意しています。

さて、肝心のMask画像ですが、以下のような要件になります

  1. Scrollはする
  2. UVScrollではなくWorld.up 方向にする
  3. ボーダーはUVに対してではなく、WorldのX-Z平面に並行 (World.up に対して直角 )

その結果作った数式=Nodeは以下の通りです。

yLocal = world.y - BaseY
u = (yLocal * TilingPerMeter) + TimeY * ScrollSpeed + PhaseOffset
phase = frac(u)
d = | phase-0.5 |
halfW = BorderWidth * 0.5
Mask = saturate(1 - SmoothStep( halfW, halfW + Softness, d))
変数 説明
BaseY エフェクト原点の基準位置。特になければ0でOK
world.y World座標系におけるy
TimeY 時間を返す関数
ScrollSpeed スクロール速度。これで上にも下にもスクロールが可能
|PhaseOffset 位相Offset。Scrollの周期をずらしたい時などに使用

holo_mask.png

数式解説

yLocal = world.y - BaseY
これは素直にエフェクト適応の原点設定です。BaseYをずらせば模様全体が上下にずれます

u = (yLocal * TilingPerMeter) + TimeY * ScrollSpeed + PhaseOffset
帯の位相を計算しています。

  1. (yLocal * TilingPerMeter)
    • ここは1mで何周期かを計算している。TilingPerMeterはいわば周波数パラメータ
  2. TimeY * ScrollSpeed
    • UVScroll でお馴染みの計算。Scroll速度を調整
  3. PhaseOffset
    • これは初期位相の位置をズラすため
    • 複数体表示する時にみんな一致してると気持ち悪いときなどに使う

phase = frac(u)
ここは結構Shader的には大事。1で割った時の余り、つまり小数点以下を切り出している。
数式で言えば x-[x] , x-floor(x)
Time関数はアプリ起動時間につれてどんどん大きくなるので [0 1] に正規化しないとだんだんTimeの値が大きくなるとホログラムのボーダーがだんだん消滅するバグに見舞われます。

d = | phase - 0.5 |
周期の中心からの距離を返す。
ここが0.5なので中心がピークな山(三角波) ができあがります。

halfW = BorderWidth * 0.5
帯幅を中心から片側半分にするという意味

Mask = saturate(1 - SmoothStep( halfW, halfW + Softness, d))
最終的なMask部分

  1. SmoothStep( halfW, halfW + Softness, d)
    • d<= halfW → 0
    • d >= half+Softness → 1
    • その間は0~1 を滑らかに補完
  2. 1 - (...)
    • SmoothStepの結果を反転
  3. saturate()
    • 数値誤差のための安全対策

つまり、ボーダーを作る部分は d の計算が肝になりますね。

最終的に統合

result.png

最終的なToonHolo Shaderはこんな構成になりました。

最後をよく見ると以下のようになっています。
merge.png

左側のAdd は Toon + RimLight + Specular の加算結果を HoloEnable のOffへ
Onは下を辿ると先ほどのホログラムの出力につながっています。

こちらのHoloEnable ですが、ShaderFeatureのフラグとしてまずは定義があります。
ShaderFeature.png

ShaderFeatureはいわゆる設定のOn/Offを切り替えてOnバージョンとOffバージョンのShaderを生成してくれる機能になります。

keyword.png
このShaderFeature としてのBooleanをパラメータに定義するとCreateNode でKeywordノードが見つかるので、ここに入れています。

つまりここのフラグがOnの時は ホログラムの結果 を、Offの時は Toon + Rim + Spec の結果を出力するShader をそれぞれ作ってる形になります。

そしてここが大事なのですが、ライティングはすでに計算済みなのでBaseColor ではなくEmissionに接続しています。
BaseColor だと色を適応したあとにBaseColor に対してライティング処理がゲームエンジン側で走るのでToonレンダリングする場合はBaseColor に入れてしまうとToonレンダリングした結果に物理ベースのライティングが重なる という事象が発生します。

今回はToonレンダリングをしたいので物理ベースのライティングを避けるためにEmissionというわけです。

ハマったポイント

Transparent にしたせいで腕が服を貫通して表示される

Hologram だからTransparent かぁと思って作ってたらUnity-chanの袖を貫通して手が表示されてしまいました。
シンプルに今回のモデルの設定的にOpaque で作られているのでOpaque に設定しましょう
スクリーンショット 2026-01-09 20.12.55.png

グレースケールにしないからホログラムの後ろにAlbedo の色が見えてしまう

最初ToonColor をLitShaderのBaseColor に渡してEmissionでHologram の色を設定していたのですが
画像右側のようにホログラムの奥にオリジナルの衣服の色がしっかり視認できてしまって、ホログラム感が薄まってしまいます。(水色が悪目立ちしている)

そこで、グレースケールにしたのをBaseColor にいれることで画像左側のように色味はホログラムの色を乗せることができて、実際の衣服の色を抜くことでホログラム感が増加しています。

BaseColor.png

ここまでの成果物

もうちょっと改良

オリジナルをよくよく見ると細いボーダーとは別に一定周期で幅の大きめなボーダーのMaskが別にScrollしていますadd_border.jpg

ということでボーダーのMask部分を 細いパターンでゆっくりScrollする ものと 一定周期で太い影が早めにScrollする 2種類の結果をマージしてみました。

2nd_border_node.png

先ほどのBorderMask用のNodeを再利用します。

  • 1回の周期を長くしたいので TilePerMeter を0.166 (=6m を1周期)
  • Scroll 速度は早めにしたいので -0.5
  • そのままだと白黒が反転してるので 1-x を出力結果に入れる
  • そのままだと影色が濃く出過ぎてしまうので Addで影となる部分の濃さを調整(今回は0.2)

改良後

なかなかよさそう

ついでにARにもしてみました

実装するにあたって

以前は周囲の方に色々アドバイスもらって進めていましたが今回はLLMと壁打ちしながら0から始めて3h弱でできました。

ShaderGraphの作成手順もだいぶ初回から精度高めに回答してくれるので、今後はAIでベースを作ってアーティストさんにたたきを持っていってブラッシュアップしていくというワークフローも全然有り得るなと思いました。

ライセンス

imageLicenseLogo.png

本件はユニティちゃんライセンス条項の元に提供されています。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?