SFゲームのホログラム通信やサイバーパンク風のノイズ演出を作る際、モデルの胴体を水平に輪切り(スライス)にして横方向へガクッとずらす頂点スライスグリッチは非常に効果的です[cite: 1]。
しかし、いざシェーダー(HLSL)で実装してみると、以下の3つのトラブルに遭遇しがちです。
- カメラがモデルの横に回り込むと、横ズレではなく「奥行き方向(手前・奥)」に頂点が飛び出して見える
-
sin()を使った乱数式に大きなSeed値を入れると、規則的な縞模様が出たり値が0に張り付いたりする[cite: 1] - URPで透過シェーダーにしたはずなのに、Alphaを
0にすると透明にならず「真っ黒なシルエット」になる
本記事では、これら3つの問題を数学・仕様の両面から解決する実践テクニックを解説します。
全体の処理フロー
頂点シェーダーでカメラの向きに合わせたスライス変形を行い、フラグメントシェーダーで安全に透過合成するまでの流れは以下の通りです。
1. sin を使わない!大きな数値でも破綻しない疑似乱数(Hash2D)
シェーダーでよく使われる frac(sin(dot(co, float2(12.9898, 78.233))) * 43758.5453) という乱数式は、入力値やシード値(_Seed)が大きくなると32bit浮動小数点数の精度限界を超え、規則性が出たり 0 しか返さなくなったりする弱点があります[cite: 1]。
環境や入力値の大きさに左右されず安定したノイズを得るには、四則演算と frac / dot のみで構成されたハッシュ式を使用します[cite: 1]。さらに _Seed に巨大な数値(例:314159265.312 など)が設定されてもオーバーフローしないよう、冒頭でシード値を正規化しておくのが安全です[cite: 1]。
// 基本的な四則演算のみを使用した、環境非依存の2Dハッシュ乱数
float Hash2D(float2 co, float seed)
{
float safeSeed = frac(seed * 0.00137) * 100.0 + 1.0;
float3 p3 = frac(float3(co.xyx * safeSeed) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return frac((p3.x + p3.y) * p3.z);
}
2. どの角度から見ても綺麗に横ズレする「カメラ右ベクトル」の抽出
頂点をワールド空間のX軸(float3(1, 0, 0))に沿ってずらしてしまうと、カメラを横(X軸方向)から見たときに頂点がカメラに向かって前後移動してしまい、スライスずれに見えなくなります。
常に現在のカメラ画面に対して水平方向へスライスをずらすには、ビュー行列 UNITY_MATRIX_V の0行目(UNITY_MATRIX_V[0].xyz)からカメラの右方向ベクトルを取得し、Y成分を 0 にして水平面で正規化します[cite: 1]。
Varyings vert(Attributes input)
{
Varyings output;
// 時間ステップの離散化(カクカクしたデジタルな切り替わりを作る)
float steppedTime = floor(_Time.y * _GlitchSpeed);
// ワールド座標への変換
float3 posWS = TransformObjectToWorld(input.positionOS.xyz);
// ビュー行列からカメラの水平右ベクトルを抽出(ゼロ除算防止の微小値を加算)
float3 cameraRightWS = UNITY_MATRIX_V[0].xyz;
cameraRightWS.y = 0.0;
cameraRightWS = normalize(cameraRightWS + float3(1e-5, 0.0, 0.0));
// Y座標をスライス分割し、確率(_GlitchProbability)に応じて発火判定(0 or 1)
float sliceY = floor(posWS.y * _GlitchFrequency);
float isGlitch = step(1.0 - _GlitchProbability, Hash2D(float2(sliceY, steppedTime), _Seed));
// -1.0 〜 +1.0 のランダムな横シフト量を算出して加算
float shiftX = (Hash2D(float2(sliceY, steppedTime + 1.0), _Seed) - 0.5) * 2.0 * _GlitchAmount;
posWS += cameraRightWS * (shiftX * isGlitch);
output.positionWS = posWS;
output.positionCS = TransformWorldToHClip(posWS);
output.uv = input.uv;
return output;
}
3. URPで「Alphaを0にすると真っ黒になる」バグの原因と恒久対策
カスタムシェーダーでアルファブレンドと加算ブレンドを切り替えられるようにしようとして、以下のように _DstBlend というプロパティ名を定義すると、思わぬ罠にハマります。
// 【危険】URP標準シェーダーと同じ名前を使うと値が汚染される
Properties {
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 10
}
Pass {
Blend SrcAlpha [_DstBlend]
}
なぜ真っ黒になるのか?
Unity(URP)で新規マテリアルを作成すると、最初は Universal Render Pipeline/Lit(Opaque:不透明)が割り当てられており、マテリアル内部に _DstBlend = 0(BlendMode.Zero) が保存されています。
その後シェーダーを自作シェーダーに切り替えた際、同じ _DstBlend という変数名を使っていると、シェーダー側の初期値(10 = OneMinusSrcAlpha)が無視されて 0(背景色を0倍して黒く塗りつぶす設定)がそのまま引き継がれてしまいます。
解決策:固有のプロパティ名に変更する
URP標準シェーダーの内部プロパティ名(_DstBlend, _SrcBlend, _Cull など)と絶対に名前が被らないよう、以下のようにプレフィックス付きの固有名称(_HoloDstBlend など)に変更するだけで、新規マテリアル作成時から100%確実に透過されるようになります。
Properties {
[Enum(AlphaBlend_OneMinusSrcAlpha, 10, Additive_One, 1)]
_HoloDstBlend ("Blend Mode (透過ブレンド方式)", Float) = 10
}
Pass {
Blend SrcAlpha [_HoloDstBlend]
ZWrite Off
}
さらに本格的な「走査線×グリッチ統合ホログラム」を作るには?
本記事で紹介した「カメラ追従の頂点スライスグリッチ」[cite: 1] に加え、以下の要素を1パスに統合すると、SF映画や商用ゲームレベルのホログラム表現が完成します。
- 速度と太さの異なる「デュアル走査線(Scanline 0 / 1)」の干渉スクロール[cite: 2]
- UVの水平ジャンプ、ブロック欠損(信号ロスト)、デジタル砂嵐ノイズの合成[cite: 1]
- 輪郭を際立たせるフレネル発光(Rim Lighting)と2階層アルファ制御(Master Alpha / Body Alpha)
MaterialPropertyBlockによるGCゼロ制御と、TriggerGlitchBurst()1行で被ダメージ時の激しいノイズを再生するC#コントローラー
📦 すぐ使える完成品パッケージ(BOOTH)
「自分のゲームやVRChatアバターに今すぐ30秒で導入したい!」という方向けに、統合シェーダー・C#バースト制御スクリプト・専用インスペクター拡張(3種のワンクリックプリセット付き)をまとめた完成品アセットをBOOTHで公開しています。
👉 【Unity URP】HoloFX - 走査線×グリッチ統合ホログラムシェーダー(BOOTHリンク)