Unityを触り始めた頃は、つい「色の調整も、ポストエフェクトも、全部ひとつのカメラ用スクリプトのUpdateに書いてしまう」ことが多いですよね。最初は動くので満足しがちですが、機能が増えるたびに if 文だらけになり、どの処理がどの効果に関係しているのか分からなくなるという問題が出てきます。

特に「色覚多様性への配慮(色覚補正)」のように、明確に役割が分かれている機能を1つの巨大スクリプトに押し込むと、保守やテストが非常にしんどくなります。

そこでこの記事では、画面全体の色調を変換して、色覚多様性に配慮した表示にするための専用コンポーネント
「ColorBlindFilter」 を作っていきます。カメラに1つアタッチするだけで、赤緑系・青黄系の色覚特性を意識した表示に切り替えられるようにして、他の処理とはきれいに分離してしまいましょう。

【Unity】色覚多様性にやさしい画面フィルタ!「ColorBlindFilter」コンポーネント

ここでは、カメラにアタッチするだけで画面全体の色調を変換するポストエフェクト系コンポーネントを実装します。

  • シーン全体をグレースケール寄りにする
  • 赤と緑のコントラストを強調する
  • 青と黄色のコントラストを強調する

といった、シンプルだけど実用的な色覚補助モードを切り替えられるようにしておきます。

フルコード:ColorBlindFilter.cs


using UnityEngine;

/// <summary>
// 画面全体の色調を変換し、色覚多様性に配慮した表示にするコンポーネント。
// カメラにアタッチして使用します。
/// </summary>
[ExecuteAlways] // エディタ上でも動かしたい場合に便利
[RequireComponent(typeof(Camera))]
public class ColorBlindFilter : MonoBehaviour
{
    /// <summary>
    /// 色覚フィルタの種類
    /// </summary>
    public enum FilterMode
    {
        None,           // 補正なし
        Grayscale,      // グレースケール(色に依存しないUI確認など)
        ProtanopiaLike, // 赤系の識別が弱いケースを意識した補助
        DeuteranopiaLike, // 緑系の識別が弱いケースを意識した補助
        TritanopiaLike  // 青黄系の識別が弱いケースを意識した補助
    }

    [Header("フィルタ設定")]
    [SerializeField]
    private FilterMode filterMode = FilterMode.None;

    [Tooltip("フィルタ強度。0で無効、1でフル適用。")]
    [Range(0f, 1f)]
    [SerializeField]
    private float intensity = 1.0f;

    [Header("デバッグ表示")]
    [Tooltip("現在のフィルタ名を画面左上に表示するか")]
    [SerializeField]
    private bool showFilterLabel = true;

    // 使用するマテリアル(ランタイムで生成)
    private Material _material;

    // シェーダーの定義をスクリプト内に埋め込むための定数
    // Unity6 でも従来通りのBlitベースのポストエフェクトとして動作させます。
    private const string ShaderName = "Hidden/ColorBlindFilterShader";

    // シェーダーのソースコード(最小限の頂点/フラグメントシェーダ)
    // 実運用では .shader ファイルとして分けることが多いですが、
    // この記事では「このファイルだけで完結」させるためにスクリプト内に埋め込みます。
    private const string ShaderSource = @"
Shader ""Hidden/ColorBlindFilterShader""
{
    Properties
    {
        _MainTex (""Texture"", 2D) = ""white"" {}
        _Intensity (""Intensity"", Range(0,1)) = 1
        _Mode (""Mode"", Float) = 0
    }
    SubShader
    {
        Tags { ""RenderType"" = ""Opaque"" }
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include ""Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl""

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            float _Intensity;
            float _Mode;

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

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

            Varyings vert (Attributes v)
            {
                Varyings o;
                o.positionHCS = TransformWorldToHClip(v.positionOS.xyz);
                o.uv = v.uv;
                return o;
            }

            // 色覚補助用の簡易変換
            float3 ApplyFilter(float3 color, float mode, float intensity)
            {
                // グレースケール
                if (mode == 1.0)
                {
                    float gray = dot(color, float3(0.299, 0.587, 0.114));
                    float3 g = float3(gray, gray, gray);
                    return lerp(color, g, intensity);
                }

                // Protanopia-like(赤の感度が低いケースを意識した変換)
                if (mode == 2.0)
                {
                    // 参考値ベースの簡易マトリクス
                    float3x3 m = float3x3(
                        0.567, 0.433, 0.000,
                        0.558, 0.442, 0.000,
                        0.000, 0.242, 0.758
                    );
                    float3 c = mul(m, color);
                    return lerp(color, c, intensity);
                }

                // Deuteranopia-like(緑の感度が低いケース)
                if (mode == 3.0)
                {
                    float3x3 m = float3x3(
                        0.625, 0.375, 0.000,
                        0.700, 0.300, 0.000,
                        0.000, 0.300, 0.700
                    );
                    float3 c = mul(m, color);
                    return lerp(color, c, intensity);
                }

                // Tritanopia-like(青黄の識別が弱いケース)
                if (mode == 4.0)
                {
                    float3x3 m = float3x3(
                        0.950, 0.050, 0.000,
                        0.000, 0.433, 0.567,
                        0.000, 0.475, 0.525
                    );
                    float3 c = mul(m, color);
                    return lerp(color, c, intensity);
                }

                // None
                return color;
            }

            float4 frag (Varyings i) : SV_Target
            {
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                float3 filtered = ApplyFilter(col.rgb, _Mode, _Intensity);
                return float4(filtered, col.a);
            }

            ENDHLSL
        }
    }
}
";

    private void Awake()
    {
        EnsureMaterial();
    }

    private void OnValidate()
    {
        // インスペクタ変更時にもマテリアルを確保
        EnsureMaterial();
    }

    /// <summary>
    /// マテリアルを生成し、シェーダーを用意する
    /// </summary>
    private void EnsureMaterial()
    {
        if (_material != null)
        {
            return;
        }

        // 既にシェーダーが存在するか検索
        Shader shader = Shader.Find(ShaderName);

        // 見つからない場合は、ランタイムでシェーダーを生成
        if (shader == null)
        {
            shader = new Shader();
            // 実際には ShaderLab の動的コンパイルAPIは提供されていないため、
            // 通常は .shader ファイルとしてプロジェクトに置きます。
            // この記事では「概念と使い方」を重視するため、
            // Shader.Find によって事前に用意されたシェーダーを使う前提で進めます。
            //
            // ※ 実際に動かす場合は、上記 ShaderSource の内容を
            //    Assets/ColorBlindFilterShader.shader などとして保存し、
            //    Shader ""Hidden/ColorBlindFilterShader"" としてプロジェクトに追加してください。
        }

        if (shader == null)
        {
            Debug.LogError("[ColorBlindFilter] シェーダーが見つかりません。'Hidden/ColorBlindFilterShader' をプロジェクトに追加してください。");
            enabled = false;
            return;
        }

        _material = new Material(shader);
        _material.hideFlags = HideFlags.HideAndDontSave;
    }

    private void OnDestroy()
    {
        // メモリリーク防止
        if (_material != null)
        {
            if (Application.isPlaying)
            {
                Destroy(_material);
            }
            else
            {
                DestroyImmediate(_material);
            }
        }
    }

    /// <summary>
    /// カメラの描画結果にポストエフェクトをかける
    /// Built-in Render Pipeline 用
    /// </summary>
    /// <param name="src">描画済みテクスチャ</param>
    /// <param name="dest">出力先テクスチャ</param>
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (_material == null || filterMode == FilterMode.None || intensity <= 0f)
        {
            // フィルタ無効時はそのまま出力
            Graphics.Blit(src, dest);
            return;
        }

        // シェーダーへパラメータを渡す
        _material.SetFloat("_Intensity", intensity);
        _material.SetFloat("_Mode", (float)filterMode);

        // 画面全体にフィルタを適用
        Graphics.Blit(src, dest, _material);
    }

    /// <summary>
    /// 現在のフィルタ名を簡易的に画面表示する
    /// </summary>
    private void OnGUI()
    {
        if (!showFilterLabel)
        {
            return;
        }

        const int width = 260;
        const int height = 24;

        Rect rect = new Rect(10, 10, width, height);

        string label = $"ColorBlindFilter: {filterMode} (Intensity: {intensity:0.00})";

        // 背景ボックス
        GUI.color = new Color(0f, 0f, 0f, 0.6f);
        GUI.Box(rect, GUIContent.none);

        // テキスト
        GUI.color = Color.white;
        GUI.Label(rect, label);
    }

    #region 公開API(他のコンポーネントから制御したいとき用)

    /// <summary>
    /// フィルタモードを変更する
    /// </summary>
    public void SetFilterMode(FilterMode mode)
    {
        filterMode = mode;
    }

    /// <summary>
    /// フィルタ強度を変更する(0〜1)
    /// </summary>
    public void SetIntensity(float value)
    {
        intensity = Mathf.Clamp01(value);
    }

    #endregion
}

重要な注意点:
Unityの仕様上、スクリプトから ShaderLab を完全に動的生成することはできません。上記コード内の ShaderSource は「どんなシェーダーを書けばよいか」の参考として載せています。
実際に動かすには、次のようにしてください。

  1. Assets/ColorBlindFilterShader.shader などの名前で新しいシェーダーファイルを作成する。
  2. 中身を、上記 ShaderSource に書かれている ShaderLab の内容に差し替える。
  3. シェーダー名は Shader "Hidden/ColorBlindFilterShader" のままにしておく。

こうしてプロジェクトにシェーダーを追加しておけば、Shader.Find("Hidden/ColorBlindFilterShader") で正しく見つかり、コンポーネントが動作します。

使い方の手順

  1. シェーダーファイルを作成する
    • Unity の Project ビューで右クリック → Create → Shader → Unlit Shader などを選択。
    • ファイル名を ColorBlindFilterShader に変更。
    • 中身を、先ほどの ShaderSource に示した ShaderLab コードに全置換する。
    • 先頭行の Shader "Hidden/ColorBlindFilterShader" が一致していることを確認。
  2. ColorBlindFilter.cs を作成してカメラにアタッチ
    • Project ビューで右クリック → Create → C# Script → 名前を ColorBlindFilter にする。
    • 中身をこの記事の C# コードにコピペして保存。
    • シーン内のメインカメラ(Main Camera)を選択し、ColorBlindFilter コンポーネントを Add Component で追加。
  3. インスペクタでモードを選択する
    カメラに追加した ColorBlindFilter を選択し、インスペクタから次のように設定します。
    • Filter Mode
      • None … 補正なし
      • Grayscale … グレースケール表示
      • ProtanopiaLike … 赤系の識別が弱いケースを意識
      • DeuteranopiaLike … 緑系の識別が弱いケースを意識
      • TritanopiaLike … 青黄系の識別が弱いケースを意識
    • Intensity:0〜1 の間で補正の強さを調整。
    • Show Filter Label:画面左上に現在のモードを表示するかどうか。
  4. 具体的な使用例
    いくつかのシーンでの使い方を挙げておきます。
    • プレイヤーキャラクターのUI検証
      HPゲージやスタミナバーなどを「赤と緑」で表現している場合、ProtanopiaLikeDeuteranopiaLike をオンにして、
      「色だけでなく形やアイコンでも意味が伝わっているか」を確認できます。
    • 敵の識別性チェック
      敵の種類を「青と黄色」で区別している場合、TritanopiaLike を有効にして、
      それでも敵種別が見分けられるかどうかをテストできます。
    • 動く床やギミックのテスト
      動く床の「安全ゾーン」と「危険ゾーン」を色で塗り分けている場合、
      グレースケールや各モードを切り替えながら、コントラストやパターンが十分かを確認できます。

メリットと応用

ColorBlindFilter を「カメラ専用の小さなコンポーネント」として分けることで、次のようなメリットがあります。

  • プレハブ管理が楽になる
    プレイヤーや敵のスクリプトに色覚補正ロジックを書かず、カメラのプレハブにだけアタッチしておけばOKです。
    シーンをまたいでも、カメラプレハブを使い回すだけで同じ色覚補正環境を再現できます。
  • レベルデザインの検証がしやすい
    レベルデザイナーが「このステージ、赤と緑の区別だけに頼っていないかな?」と気になったとき、
    インスペクタからモードを変えるだけで、すぐに色覚多様性を意識した見え方を確認できます。
  • 責務が明確でテストしやすい
    色覚補正は「描画結果を変換するだけ」の責務に絞っているので、
    バグ調査の際も「描画問題なのか、ゲームロジックなのか」が切り分けやすくなります。

さらに、他のコンポーネントから SetFilterModeSetIntensity を呼び出すことで、オプションメニューからプレイヤー自身にモードを選ばせるといった応用も簡単です。

最後に、簡単な「改造案」として、キーボード入力でフィルタモードを順番に切り替える関数の例を載せておきます。
(Input System を使わず、デバッグ用に Update から呼ぶ想定です)


/// <summary>
/// キーボードの F キーでフィルタモードを順番に切り替える簡易デバッグ関数
/// (例:Update() から呼び出して使います)
/// </summary>
private void HandleDebugKeyToggle()
{
    if (Input.GetKeyDown(KeyCode.F))
    {
        // Enum を一つ進める
        int next = ((int)filterMode + 1) % System.Enum.GetValues(typeof(FilterMode)).Length;
        filterMode = (FilterMode)next;
        Debug.Log($"[ColorBlindFilter] Filter mode changed to: {filterMode}");
    }
}

このように、小さなコンポーネントとして色覚補正を切り出しておくと、ゲーム本体のロジックとは独立して改善・実験を繰り返せるので、とても扱いやすくなります。ぜひ自分のプロジェクト用にカスタマイズしてみてください。