今回は久しぶりにゲームをプレイして感銘を受けた表現を再現してみようシリーズ第2弾として
Pokémon LEGENDS Z-A のストーリーで出てくるホログラム表現をどうやったら再現できるのかを考えてみました。
ちなみに第1弾は下記のリンクよりご参照ください
できたもの
Github
こちらのリポジトリに制作物をアップロードしております。
Subgraph とかを詳しくみたい場合は是非ご利用ください。
考察
まずは実際のゲーム画面を見てみましょう。
(※ ゲーム内ScreenShotより引用)
このホログラム表現、非常に近未来的でXR界隈で開発をしている身としてはとても魅力的に映る表現だと思います。
このホログラム表現、大きく分けると以下のような構成ではないかと推察しました。
- 元々のTexture の色 (Albedo) はそのまま出ていない
- 元々のTexture の明度は維持されている
- ホログラムはキャラごとに色が異なる
- ライティングは反映される
上記より
Lit Shaderをベースとして Toonレンダリング + ホログラム表現という大きな道筋をたてました
ホログラム表現
ホログラムに注目すると
- UVスクロールを上方向にしている
- ボーダーは水平面と平行
- Mesh のUVに沿っているわけではない(1敗)
- ホログラムのメインと影部分のColor は異なる
このあたりを再現できればいけそうです
実装
まずは 実験体 被写体を用意しなければならないので、今回もUnityちゃんにお願いします。
ダウンロードページやリリースノートを覗くと
ん?
(;゚Д゚)
(;゚Д゚)
(;゚Д゚)
ついに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 同梱物の確認
ポイントとしては
-
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 の中にあります

ShaderGraph でShaderを用意する
機能をカスタマイズするにあたり、Subgraph で簡単に機能を取り込みやすくするためにまずは簡易Toon Shader を作ります
その後各機能単位でSubgraph を作って最終的に本体に結合していきます。
簡易ToonShader の作成
今回作る最終系はこんな感じ。
う〜ん、パラメータが多い!!
このToonColor Node ですが内部はこのような感じです。
そもそもトゥーンレンダリングとは、陰影のつけかたをセル画のようにパキッとN段階(Nは小さめの自然数) で表現する技法です。
今回は
- オリジナル
- 影1段階目
- 影2段階目
と影を2段階設定できるようにしたのでパラメータが多いです。
ただ、結局は
- 法線ベクトルとMainLight からそのピクセルのColorのWeight を計算
- FinalColor = BaseColor * w0 + ShadeColor01 * w1 + ShadeColor02 * w2 と加算合成
というシンプルな構造で作りました。
この w0~w2 の部分がトゥーンレンダリングの肝になってきます。
さて、続いては重みパラメータの計算部分ですが、全体像は下記の通りです。
付箋で貼り付けている通り影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シェーディングができます。
もちろん綺麗さでいえばUnity 公式のToon Sheder が良いですが、ShaderGraph で機能拡張を簡単にSubGraph追加対応でできるようにするために簡易Toon Shader ということでここはご愛嬌。
Rimライトの機能
とりあえずUnity ToonShader にある機能はなるべく乗っけたいので次はRimLightです。
いわゆるアウトライン的に使える機能でもあります。
RimLight の方はNodeをまとめきれてないのですが端的に言うと
- RimLight で光らせるべき部分のMaskを作成
- Maskに対してLightの強さと色を乗算
- 上記の結果を他の結果に対して
加算合成
という3ステップに分かれます。
肝は Maskの作り方 ですので、そこを詳しく見ていきます。
RimMask の作り方は以下のステップです。
1, ライトの反射度合いを計算するために saturate( dot(Normal, ViewDirection)) を計算します。
ViewDirectionはカメラの視線ベクトルです。
- 1の結果を 1から引くことで視線ベクトルと直角方向に近ければ近いほど1, そうでなければ0に近くなるように変換します
- RimPower乗して強度を反映します。
ここまでを数式にすると(1-saturate(dot(N,V)))^RimPowerとなります - 続いて上側ですが、こちらはRimLightの閾値と滲み度合いを計算しています。
- 最終的にSmoothStep(T-f, T+f, 3の結果)を計算します
- 基本的にはこれで出力でも良いのですが、計算式によっては結果が反転する場合があるので結果反転フラグと値の切り替えとして
lerp(output, 1-output, InverseFlag)として後段にNodeを設定しています
以上のステップでRimLight用Mask作成ができます。
Specular
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 に加算合成でいれます。
グレースケール機能
ホログラムのキャラクターを見ると、元々の明度は保ちつつ、彩度については保持されていません。
つまり、元々の色ではなくグレースケールに変換してその上にホログラムの色を載せています。
ということでグレースケール機能を作ります。
使い回しできるようにColor を受け取ってそれをグレースケールするSubGraphを作ります。
昨今のLLMとの壁打ちでとりあえず Rec.709(Rec.709(ITU-R BT.709) の規格に従ってグレースケール化させました。
詳しい仕組みは人間の視覚特性とか色々知る必要があるので今回は割愛します。
ホログラム機能
一番右側のLerp の部分では以下のように実装しています
Lerp(グレースケールにHologramのMainColorを乗算したもの, グレースケールにHologramのMainColorを乗算したもの, ボーダーになるようにするMask画像)
第1,2引数はシンプルで luma Nodeでグレースケール化したToonRenderingの結果に対して
ホログラムで表示する色をそれぞれ乗算しているだけです。
最終的な塗り分けはMask画像の方で用意しています。
さて、肝心のMask画像ですが、以下のような要件になります
- Scrollはする
- UVScrollではなくWorld.up 方向にする
- ボーダーは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の周期をずらしたい時などに使用 |
数式解説
yLocal = world.y - BaseY
これは素直にエフェクト適応の原点設定です。BaseYをずらせば模様全体が上下にずれます
u = (yLocal * TilingPerMeter) + TimeY * ScrollSpeed + PhaseOffset
帯の位相を計算しています。
- (yLocal * TilingPerMeter)
- ここは1mで何周期かを計算している。TilingPerMeterはいわば周波数パラメータ
- TimeY * ScrollSpeed
- UVScroll でお馴染みの計算。Scroll速度を調整
- 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部分
- SmoothStep( halfW, halfW + Softness, d)
- d<= halfW → 0
- d >= half+Softness → 1
- その間は0~1 を滑らかに補完
- 1 - (...)
- SmoothStepの結果を反転
- saturate()
- 数値誤差のための安全対策
つまり、ボーダーを作る部分は d の計算が肝になりますね。
最終的に統合
最終的なToonHolo Shaderはこんな構成になりました。
左側のAdd は Toon + RimLight + Specular の加算結果を HoloEnable のOffへ
Onは下を辿ると先ほどのホログラムの出力につながっています。
こちらのHoloEnable ですが、ShaderFeatureのフラグとしてまずは定義があります。

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

この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 に設定しましょう

グレースケールにしないからホログラムの後ろにAlbedo の色が見えてしまう
最初ToonColor をLitShaderのBaseColor に渡してEmissionでHologram の色を設定していたのですが
画像右側のようにホログラムの奥にオリジナルの衣服の色がしっかり視認できてしまって、ホログラム感が薄まってしまいます。(水色が悪目立ちしている)
そこで、グレースケールにしたのをBaseColor にいれることで画像左側のように色味はホログラムの色を乗せることができて、実際の衣服の色を抜くことでホログラム感が増加しています。
ここまでの成果物
もうちょっと改良
オリジナルをよくよく見ると細いボーダーとは別に一定周期で幅の大きめなボーダーのMaskが別にScrollしています
ということでボーダーのMask部分を 細いパターンでゆっくりScrollする ものと 一定周期で太い影が早めにScrollする 2種類の結果をマージしてみました。
先ほどのBorderMask用のNodeを再利用します。
- 1回の周期を長くしたいので TilePerMeter を0.166 (=6m を1周期)
- Scroll 速度は早めにしたいので -0.5
- そのままだと白黒が反転してるので 1-x を出力結果に入れる
- そのままだと影色が濃く出過ぎてしまうので Addで影となる部分の濃さを調整(今回は0.2)
改良後
なかなかよさそう
ついでにARにもしてみました
実装するにあたって
以前は周囲の方に色々アドバイスもらって進めていましたが今回はLLMと壁打ちしながら0から始めて3h弱でできました。
ShaderGraphの作成手順もだいぶ初回から精度高めに回答してくれるので、今後はAIでベースを作ってアーティストさんにたたきを持っていってブラッシュアップしていくというワークフローも全然有り得るなと思いました。
ライセンス
本件はユニティちゃんライセンス条項の元に提供されています。
























