Unityを触り始めた頃って、ついなんでもかんでも Update() に書きがちですよね。
プレイヤーの移動、入力、カメラ揺れ、エフェクト、UI更新…全部1つのスクリプトに詰め込むと、最初は動いていても、あとから仕様変更が入ったときに「どこを触ればいいのか分からない」「少し直しただけで別の機能が壊れた」という状態になりがちです。

ポストエフェクト系の処理(モーションブラー、色収差、画面フラッシュなど)も、つい「ダメージ判定の中で直接カメラをいじる」ような実装をしてしまうと、どんどん God クラス化していきます。

そこでこの記事では、「衝撃を受けた瞬間だけ、画面全体の色チャンネル(RGB)をずらして歪ませる」という機能を、
1つの責務に絞ったコンポーネント ChromaticAbberation として切り出してみます。

ゲーム側(プレイヤーや敵のスクリプト)は、「ダメージを受けたら PlayEffect() を呼ぶだけ」にして、
色収差の具体的な実装はすべてこのコンポーネントに任せる、という分離を目指しましょう。

【Unity】ダメージ演出を一発追加!「ChromaticAbberation」コンポーネント

ここでは、Unity6標準の URP(Universal Render Pipeline) を前提に、
フルスクリーンのカスタムポストエフェクトとして色収差を実装します。

  • C#コンポーネント(ChromaticAbberation.cs
  • フルスクリーンパス用の C#(ChromaticAbberationRenderFeature.cs
  • シェーダ(ChromaticAbberation.shader

の3つで構成されますが、すべてこの記事内のコードだけで動作するようにしてあります。


1. 色収差コンポーネント本体(ChromaticAbberation.cs)


using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

/// <summary>
/** 衝撃時に画面全体へ色収差エフェクトをかけるコンポーネント。
 *  - 自身は「いつ・どれくらいの強さで」色収差をかけるかだけを管理
 *  - 実際の描画処理は ChromaticAbberationRenderFeature 側のフルスクリーンパスが担当
 */
/// </summary>
[DisallowMultipleComponent]
public class ChromaticAbberation : MonoBehaviour
{
    // エフェクトの最大強度(RGBのずれ量)
    [SerializeField] private float maxIntensity = 0.015f;

    // エフェクトが0になるまでの時間(秒)
    [SerializeField] private float duration = 0.3f;

    // カーブで減衰の仕方を調整できるようにする(0〜1の時間に対して、0〜1の強度)
    [SerializeField] private AnimationCurve fadeCurve =
        AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);

    // 現在の強度(0〜maxIntensity)。RenderFeature 側から参照される
    public float CurrentIntensity { get; private set; } = 0f;

    // 内部タイマー
    private float _timer = 0f;
    private bool _isPlaying = false;

    private void Awake()
    {
        // 初期状態ではエフェクトをオフにしておく
        CurrentIntensity = 0f;
        _timer = 0f;
        _isPlaying = false;
    }

    private void Update()
    {
        if (!_isPlaying)
        {
            return;
        }

        // 経過時間を進める
        _timer += Time.unscaledDeltaTime; // 演出なので TimeScale の影響を受けないようにする

        // 0〜1に正規化した経過率
        float t = Mathf.Clamp01(_timer / duration);

        // カーブに沿って 0〜1 の強度を算出
        float normalized = fadeCurve.Evaluate(t);

        // 実際の強度に変換
        CurrentIntensity = maxIntensity * normalized;

        // 終了判定
        if (t >= 1f)
        {
            _isPlaying = false;
            CurrentIntensity = 0f;
        }
    }

    /// <summary>
    /// 衝撃エフェクトを再生する(外部から呼び出す想定のAPI)。
    /// 例: プレイヤーがダメージを受けた瞬間に呼ぶ。
    /// </summary>
    public void PlayEffect()
    {
        _timer = 0f;
        _isPlaying = true;
    }

    /// <summary>
    /// 強度を直接上書きしたい場合に使えるオプションAPI。
    /// 0以上の値を渡すと、その値を maxIntensity として扱う。
    /// </summary>
    public void PlayEffect(float overrideMaxIntensity)
    {
        maxIntensity = Mathf.Max(0f, overrideMaxIntensity);
        PlayEffect();
    }
}

2. URPのフルスクリーンパス(ChromaticAbberationRenderFeature.cs)

URP の Renderer に差し込む「Render Feature」です。
シーン内に存在する ChromaticAbberation コンポーネントを探し、その CurrentIntensity に応じてポストエフェクトをかけます。


using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

/// <summary>
/// URP の Renderer に追加する Render Feature。
/// 画面全体に色収差シェーダを適用する。
/// </summary>
public class ChromaticAbberationRenderFeature : ScriptableRendererFeature
{
    [System.Serializable]
    public class Settings
    {
        // 使用するシェーダ
        public Shader shader;

        // 描画のタイミング(基本は AfterRendering でOK)
        public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRendering;
    }

    public Settings settings = new Settings();

    private ChromaticAbberationPass _pass;
    private Material _material;

    public override void Create()
    {
        if (settings.shader == null)
        {
            Debug.LogError("ChromaticAbberationRenderFeature: シェーダが設定されていません。");
            return;
        }

        _material = CoreUtils.CreateEngineMaterial(settings.shader);
        _pass = new ChromaticAbberationPass(_material)
        {
            renderPassEvent = settings.renderPassEvent
        };
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (_pass == null || _material == null)
        {
            return;
        }

        // カメラのカラーターゲットを設定
        _pass.Setup(renderer.cameraColorTargetHandle);
        renderer.EnqueuePass(_pass);
    }

    protected override void Dispose(bool disposing)
    {
        CoreUtils.Destroy(_material);
    }

    /// <summary>
    /// 実際にフルスクリーン描画を行う ScriptableRenderPass。
    /// </summary>
    private class ChromaticAbberationPass : ScriptableRenderPass
    {
        private static readonly string ProfilerTag = "ChromaticAbberationPass";
        private static readonly int TempTargetId = Shader.PropertyToID("_ChromaticAbberationTemp");
        private static readonly int IntensityId = Shader.PropertyToID("_Intensity");

        private Material _material;
        private RenderTargetIdentifier _source;

        public ChromaticAbberationPass(Material material)
        {
            _material = material;
        }

        public void Setup(RenderTargetIdentifier source)
        {
            _source = source;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            if (_material == null)
            {
                return;
            }

            // シーン上の ChromaticAbberation コンポーネントを探す
            ChromaticAbberation effect = Object.FindFirstObjectByType<ChromaticAbberation>();
            if (effect == null)
            {
                // コンポーネントがなければ何もしない
                return;
            }

            float intensity = effect.CurrentIntensity;
            if (intensity <= 0f)
            {
                // 強度0なら何もしない(パフォーマンス節約)
                return;
            }

            CommandBuffer cmd = CommandBufferPool.Get(ProfilerTag);

            // 強度をマテリアルへ渡す
            _material.SetFloat(IntensityId, intensity);

            RenderTextureDescriptor desc = renderingData.cameraData.cameraTargetDescriptor;
            desc.depthBufferBits = 0;

            // 一時RTを確保
            cmd.GetTemporaryRT(TempTargetId, desc, FilterMode.Bilinear);

            // 元のカラーを一時RTへコピー
            Blit(cmd, _source, TempTargetId);

            // 一時RTから元のカラーへ、色収差シェーダを通して描画
            Blit(cmd, TempTargetId, _source, _material, 0);

            // 一時RT解放
            cmd.ReleaseTemporaryRT(TempTargetId);

            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
    }
}

3. シェーダコード(ChromaticAbberation.shader)

RGBそれぞれのサンプル座標を少しだけずらして、色収差っぽいにじみを出します。


Shader "Hidden/Custom/ChromaticAbberation"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
        _Intensity ("Intensity", Float) = 0.01
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }

        Pass
        {
            Name "ChromaticAbberationPass"

            ZTest Always
            ZWrite Off
            Cull Off
            Blend One Zero

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv         : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv          : TEXCOORD0;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            float _Intensity;

            Varyings Vert (Attributes input)
            {
                Varyings output;
                output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = input.uv;
                return output;
            }

            half4 Frag (Varyings input) : SV_Target
            {
                float2 uv = input.uv;

                // 画面中心からの距離に応じてずらし量を増やす(中央はほぼ影響なし、端ほど強く)
                float2 center = float2(0.5, 0.5);
                float2 dir = uv - center;
                float dist = length(dir);

                float2 offset = normalize(dir) * _Intensity * dist;

                // RGB それぞれのサンプル位置を少しずらす
                float3 col;
                col.r = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + offset).r;
                col.g = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv).g;
                col.b = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - offset).b;

                return half4(col, 1.0);
            }
            ENDHLSL
        }
    }

    FallBack Off
}

使い方の手順

  1. プロジェクトの準備
    • Unity6 でプロジェクトを作成し、レンダーパイプラインに URP を使用していることを確認します。
    • Assets フォルダ内に適当なフォルダ(例: Assets/ChromaticAbberation)を作成します。
    • そこに以下の3ファイルを作成し、上記コードをそれぞれコピペします。
      • ChromaticAbberation.cs
      • ChromaticAbberationRenderFeature.cs
      • ChromaticAbberation.shader
  2. Render Feature の設定
    • Project ウィンドウで URP の Renderer Asset(例: ForwardRendererUniversalRenderer)を選択します。
    • Inspector の「Renderer Features」欄で「Add Renderer Feature」ボタンを押し、
      ChromaticAbberationRenderFeature を追加します。
    • 追加された Feature の Shader スロットに、作成した ChromaticAbberation.shader をドラッグ&ドロップで割り当てます。
  3. 色収差コンポーネントをカメラに付ける
    • シーン内のメインカメラ(例: Main Camera)を選択します。
    • 「Add Component」で ChromaticAbberation を追加します。
    • Max IntensityDurationFade Curve を好みの値に調整しておきます。
      (まずは Max Intensity = 0.015, Duration = 0.3 くらいがおすすめです)
  4. ゲームロジックから呼び出す(具体例)
    例えば「プレイヤーが敵に触れたときに色収差を発生させる」場合は、プレイヤー側のスクリプトで以下のようにします。
    
    using UnityEngine;
    
    [RequireComponent(typeof(Collider))]
    public class PlayerDamageExample : MonoBehaviour
    {
        [SerializeField] private ChromaticAbberation chromaticAbberation;
    
        private void Reset()
        {
            // 自動でカメラ上の ChromaticAbberation を探してセットする簡易実装
            if (chromaticAbberation == null)
            {
                chromaticAbberation = FindFirstObjectByType<ChromaticAbberation>();
            }
        }
    
        private void OnTriggerEnter(Collider other)
        {
            // 敵に当たったらダメージ演出として色収差を再生
            if (other.CompareTag("Enemy"))
            {
                if (chromaticAbberation != null)
                {
                    chromaticAbberation.PlayEffect();
                }
    
                // ここでHPを減らすなどの処理を行う
                // TakeDamage(10);
            }
        }
    }
    
    • プレイヤー: 上記 PlayerDamageExample をプレイヤーに付ける。
    • : Enemy タグを付け、Is Trigger な Collider を持たせる。
    • 動く床: 「着地の衝撃で色収差を少しだけ発生させる」といった演出も、
      着地判定のタイミングで chromaticAbberation.PlayEffect(0.008f); のように呼ぶだけでOKです。

メリットと応用

ChromaticAbberation をコンポーネントとして分離することで、次のようなメリットがあります。

  • ダメージ処理と画面演出が分離される
    プレイヤーや敵のスクリプトは「いつダメージを受けたか」だけを知っていればよく、
    画面をどう歪ませるかといった実装詳細は ChromaticAbberation 側に閉じ込められます。
  • プレハブ管理が楽になる
    メインカメラのプレハブに ChromaticAbberation を付けておけば、
    どのシーンでも同じダメージ演出を再利用できます。
    シーンごとにコピーしたスクリプトをいじる必要がなくなります。
  • レベルデザイン時に「演出だけ」差し替えられる
    たとえば「このボス戦だけは色収差を強めにしたい」場合、
    ボス戦シーンのカメラの Max IntensityDuration を少し変えるだけで調整できます。
    ダメージロジックには一切手を入れなくて済むので、安全に演出をいじれます。

さらに、色収差コンポーネント自体も小さくまとまっているので、
別の演出と組み合わせた拡張もしやすくなります。

改造案:色収差と同時に画面を白くフラッシュさせる

たとえば「ダメージ時に色収差+白フラッシュ」を同時に行いたい場合、
こんな感じで ChromaticAbberation に簡易的なフラッシュ値を追加することもできます。


/// 色収差と同時に、画面全体のホワイトアウト量を返す例
public float GetFlashAmount()
{
    // 強度が高いほどフラッシュも強くする(適当な係数で調整)
    // 0〜1 の範囲にクランプ
    float flash = CurrentIntensity * 80f;
    return Mathf.Clamp01(flash);
}

この GetFlashAmount() を別のフルスクリーンシェーダに渡して、
「色収差+白フェード」や「色収差+コントラストアップ」など、演出をどんどん足していくこともできます。
大事なのは、「色収差の強度を管理する責務」だけをこのコンポーネントに持たせることですね。

こうして小さなコンポーネントを組み合わせていくと、
後からのメンテナンスや演出調整がかなり楽になります。ぜひ自分のプロジェクト用にカスタマイズしてみてください。