ポケポケのホログラム表現に挑戦
先日、CEDEC+KYUSHU2025で、このような講演を聴いてきました
「Pokemon TCG Pocket デジタルカード表現とその制作手法」
https://cedec-kyushu.jp/2025/session/06.html
Cedilでの資料公開がないそうなので、記憶に残っている間に再現したいと思いました
じつはあんまりカードゲームは得意ではないため、ゲームそのものに惹かれたというよりも、表現が面白そうだからネタにしました
当然の権利のようにポケカももってないし、ポケポケもインストールしてないしなので、分かってない部分もあるかなと思いますが、その辺はあんまり突っ込まないでください
動いてる所は以下を確認してください
https://x.com/CTsuchinoko/status/1994961644284514548
まずはこの試作品の解説から行っていきます。とはいえ全然それっぽくないため、それっぽくしていくための努力をチマチマ書いていきます
試作品の解説
まず、試作品の解説をします。最初にどういう考えで作ろうと思ったかというと
「テキトーな法線マップと現在の視線ベクトルとの反射ベクトルで、スペキュラとグラデーションをつければそれっぽくなるんじゃね?」
と、思いました。
そこでこういう2つの画像を用意
円グラデーション画像

また、CEDEC+KYUSHU2025の講演では「真正面を向いたときにはカードの表示を邪魔しない」と言う事なので、色を付けないように…と考えました
カード本体の法線マップと視線ベクトルのなす角度が0の時にはエフェクトの影響を与えたくないのだから
影響度をfactorとして、こういう計算をしました
float3 eyepos = normalize(float3(0, 0, -10));
float3 eyeDir = normalize(input.pos.xyz-eyepos);
float factor = saturate(dot(input.norm.xyz, eyeDir);//エフェクト強さ
factor = 1.0 - factor;//真正面の時に0になる
次にカードのハイライトを入れようと、スペキュラを載せようと考えました。
そこで露わ火事目取得しておいた法線マップを法線に適用し…
float4 normTex = norm.Sample(smp, input.uv);
float3 normal = normalize(input.norm.xyz + normalize(normTex.xyz * 2.0 - 1.0));
この法線を用いてスペキュラを載せてみました(ここではハーフベクトルではなく反射ベクトルを用いたスペキュラにしています)
float3 eyepos = normalize(float3(0, 0, -10));
float3 eyeDir = normalize(input.pos.xyz-eyepos);
float3 light = normalize(float3(1.0, -1.0, 1.0));
float3 reflectDir = reflect(-light, normal);
float3 rayReflectDir = normalize(reflect(eyeDir, normal));
float spec = pow(saturate(dot(reflectDir, eyeDir)), 50.0);
最後に事前計算しておいた影響度と、グラデーションテクスチャのカラーをもとに構成します
影響度が大きければ大きいほど、グラデーションの色が強く出ます
return float4(lerp(cardCol.rgb, gradCol.rgb, factor) + spec, cardCol.a);
結果

まぁ、なんか色がついとりますけれどもなんか違うわけです。そもそも法線マップを入れてる影響か表面が凸凹して見える
そこで実際のカードを見たり、ポケポケのアプリを見ながらしばらく考えてみました
観察
まず、次の3つの部分に分かれています
- 輪郭線部分
- カード内枠の部分と
- モンスターイラスト部分
に分かれていますね(テキスト部分については今回は取り扱わない事にします)
ところで面白い記事を見つけました
https://qiita.com/degudegu2510/items/81526a0c48a76d7ec793
これもポケカを再現しようとしていますが、これはCSSでやってるというツワモノですね
あとこの記事かな
https://gamemakers.jp/article/2025_08_04_113058/
ほぼこれ答えですね。中の人のお話ですしね
ということでグラデーションテクスチャを新しく作りました。GIMPでグラデーション:放射状+カスタムで作れます

…あ、いや、違いますねこれ。ちょっと記事を拝借しますが

このテクスチャを作るために右下の2枚のテクスチャがありますね。これ、完全に対応していますね。
もともと下のグラデーションテクスチャ1枚を使って、グラデーションテクスチャのUV値を右下の濃淡値で決めてる感じですね。
なるほど、こうすればグラデーションテクスチャ1枚で様々なグラデーションができるというわけですね
黒っぽい所を0とし、白っぽい所を1として、それをU値として扱っていますね。また、グラデーションテクスチャ自体の解像度もかなり低く抑えられるので良いですね。
実際の計算に関してはこの後のプログラムの話の時にやりましょう。
ひとまずこういう画像を作っておきます
次に困ったのが「位相テクスチャ」ですね。なんか波みたいなテクスチャです。さて、どうしましょうかね
「位相テクスチャ」で検索しても適切なものは出てきません。
これも右下の図に注目してみます

法線マップであることは明らかです。明らかですがどうやってこのテクスチャを作りましょ。プロシージャルに作ってもいいのですが、何かそれっぽいものをAIに作ってもらいました

ちょっと違うけど、まあヨシ!
試行錯誤
ひとまず枠の部分にだけ適用したいため
if (frameCol.r < 0.5 && frameCol.g<0.5 && frameCol.a>0.0)
{
return lerp(cardCol, gcol + u * 0.75, 1 - saturate(dot(normalize(normTex.xyz * 2.0 - 1.0), input.norm.xyz)));
}
else
{
return cardCol;
}
としておきます。
次に、現在の傾きによってグラデの方向を変えたいためこのような計算式を作っておきます
float u = 1 -distance((input.uv * float2(2, -2)) + float2(-1, 1), input.norm.xy * 2);
これがグラデーションテクスチャに対するU値になります。ちょっと解説するとカードのUV値を中心からのXY座標に置き換え、カードの向いてる方向の法線ベクトルのXYも座標に置き換えて距離を測っています。これにより

画像のように目に近い側が大きくなるような値を求めます
さて、このuをもとに、グラデーションを描画すると
float4 gcol = grad.Sample(smp, float2(fmod(u, 1), 0));

こうなりますので、これをキラテクスチャとして利用しましょう
最後に合成ですが、これはキラの部分はちょっと派手めに明るくしたいためu値を足した値をキラのカラー値とします。
あとは元のカード画像と線形補間すればいいのですから
float kiraFactor = 1 - saturate(dot(normalize(normTex.xyz * 2.0 - 1.0), input.norm.xyz));
return lerp(cardCol, gcol + u ,kiraFactor );
こうします。ここでのキラファクターは、カードの法線と波のノーマルマップとの内積をとり、位相テクスチャ状態にしています。ちょうど合致した時に0.0、そこから離れるほど1.0に近い値になるようにしています。これは斜めから見た時によりキラが強く出るようにしたいため、リムライトと同じような感じにしています
ちょっと時間が間に合わなかったので、今日はここまで
プロシージャル的なのはその②でやります
その②に続く↓



