Unityを触り始めたころは、つい何でもかんでも1つのスクリプトの Update() に書いてしまいがちですよね。プレイヤー操作、UI更新、エフェクト制御、シーン遷移…全部が1ファイルに詰め込まれていくと、動くには動くけど「どこを触ればいいのか分からない」「バグを直すと別のところが壊れる」といった状態になりやすくなります。

画面エフェクトもその典型で、フェードやモザイク、ポストエフェクトを1つの巨大クラスが全部握っていると、あとから演出を追加・変更するのがかなりつらくなります。

そこでこの記事では、「画面全体の解像度を一時的に落としてドット絵風にする」 ことだけに責務を絞ったコンポーネント 「PixelateEffect」 を作っていきます。画面遷移やイベント時の演出として、モザイク風のピクセル化エフェクトを簡単にオン・オフできるようにして、演出用の小さなコンポーネントとして再利用しやすい形にしてみましょう。

【Unity】一瞬でドット絵風演出!「PixelateEffect」コンポーネント

今回のコンポーネントは、カメラの描画結果を一度低解像度のレンダーテクスチャに描いてから、それをフルスクリーンで引き伸ばすことで「モザイク(ピクセル化)」を実現します。
Unity6 標準のレンダリング機能だけで動作し、外部パッケージには依存しません。

フルコード(C# / MonoBehaviour)


using UnityEngine;
using System.Collections;

/// <summary>
/// 画面全体をピクセル化(モザイク)するコンポーネント。
/// ・カメラの描画結果を低解像度の RenderTexture に書き出し、
///   それをフルスクリーンで拡大表示することでドット絵風に見せる。
/// ・演出タイミングで有効 / 無効を切り替えられるようにしておく。
/// </summary>
[RequireComponent(typeof(Camera))]
public class PixelateEffect : MonoBehaviour
{
    // ==== 基本設定 ====

    [Header("ピクセル化の解像度設定")]
    [Tooltip("画面の短辺を何ピクセル相当にするか。\n値を小さくするほど大きなドットになります。")]
    [SerializeField] private int basePixelCountOnShortSide = 80;

    [Tooltip("アスペクト比を維持したまま、縦横の解像度を整数に丸めるかどうか。")]
    [SerializeField] private bool keepAspectRatio = true;

    [Header("エフェクト制御")]
    [Tooltip("開始時からピクセル化を有効にするかどうか")]
    [SerializeField] private bool startEnabled = false;

    [Tooltip("有効 / 無効を切り替えるときに、スムーズに補間するかどうか")]
    [SerializeField] private bool smoothTransition = true;

    [Tooltip("スムーズに切り替える際の時間(秒)")]
    [SerializeField] private float transitionDuration = 0.3f;

    // ==== 内部状態 ====

    private Camera targetCamera;

    // 現在のピクセル化強度(0=なし, 1=フルピクセル化)
    private float currentIntensity = 0f;

    // 目標のピクセル化強度
    private float targetIntensity = 0f;

    // 描画に使う一時的な低解像度 RenderTexture
    private RenderTexture pixelatedRT;

    // 画面描画に使うマテリアル
    private Material pixelateMaterial;

    // シェーダーのプロパティID(文字列検索を避けるためキャッシュ)
    private static readonly int MainTexId = Shader.PropertyToID("_MainTex");
    private static readonly int IntensityId = Shader.PropertyToID("_Intensity");

    // コルーチン制御用
    private Coroutine transitionCoroutine;


    private void Awake()
    {
        targetCamera = GetComponent<Camera>();

        // シェーダーをコード内で定義し、マテリアルを生成する
        pixelateMaterial = CreatePixelateMaterial();

        // 開始時の有効状態を設定
        currentIntensity = startEnabled ? 1f : 0f;
        targetIntensity = currentIntensity;
    }

    private void OnEnable()
    {
        // 解像度に応じた RenderTexture を作成
        CreateOrResizeRenderTexture();
    }

    private void OnDisable()
    {
        ReleaseRenderTexture();
    }

    private void OnDestroy()
    {
        ReleaseRenderTexture();

        if (pixelateMaterial != null)
        {
            Destroy(pixelateMaterial);
            pixelateMaterial = null;
        }
    }

    /// <summary>
    /// 毎フレームの後処理で呼ばれる描画関数。
    /// カメラの描画結果をここでピクセル化して出力する。
    /// </summary>
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (pixelateMaterial == null)
        {
            // マテリアルがなければ素通し
            Graphics.Blit(src, dest);
            return;
        }

        // 目標強度が 0 なら、ピクセル化せずに素通し
        if (Mathf.Approximately(currentIntensity, 0f))
        {
            Graphics.Blit(src, dest);
            return;
        }

        // 解像度が変わっていたら RenderTexture を作り直す
        if (pixelatedRT == null || pixelatedRT.width == 0 || pixelatedRT.height == 0)
        {
            CreateOrResizeRenderTexture();
        }

        // まず、元の画像を低解像度の RenderTexture に描画
        Graphics.Blit(src, pixelatedRT);

        // シェーダーにテクスチャと強度を設定
        pixelateMaterial.SetTexture(MainTexId, pixelatedRT);
        pixelateMaterial.SetFloat(IntensityId, currentIntensity);

        // 低解像度テクスチャをフルスクリーンに拡大して描画
        Graphics.Blit(pixelatedRT, dest, pixelateMaterial);
    }

    private void Update()
    {
        // スムーズ切り替えをしない場合は、currentIntensity をそのまま使う
        if (!smoothTransition)
        {
            currentIntensity = targetIntensity;
            return;
        }

        // スムーズ切り替えをする場合は、Lerp で徐々に近づける
        if (!Mathf.Approximately(currentIntensity, targetIntensity))
        {
            float speed = transitionDuration > 0f ? (1f / transitionDuration) : 1f;
            currentIntensity = Mathf.MoveTowards(currentIntensity, targetIntensity, Time.unscaledDeltaTime * speed);
        }
    }

    /// <summary>
    /// ピクセル化エフェクトを有効化する(強度1へ)。
    /// </summary>
    public void EnablePixelate()
    {
        SetTargetIntensity(1f);
    }

    /// <summary>
    /// ピクセル化エフェクトを無効化する(強度0へ)。
    /// </summary>
    public void DisablePixelate()
    {
        SetTargetIntensity(0f);
    }

    /// <summary>
    /// 指定した強度にピクセル化を切り替える(0~1)。
    /// </summary>
    public void SetPixelateIntensity(float intensity)
    {
        SetTargetIntensity(Mathf.Clamp01(intensity));
    }

    /// <summary>
    /// 一定時間かけてピクセル化をオンにする(コルーチン版)。
    /// 例: StartCoroutine(PixelateIn(0.5f));
    /// </summary>
    public IEnumerator PixelateIn(float duration)
    {
        yield return TransitionTo(1f, duration);
    }

    /// <summary>
    /// 一定時間かけてピクセル化をオフにする(コルーチン版)。
    /// 例: StartCoroutine(PixelateOut(0.5f));
    /// </summary>
    public IEnumerator PixelateOut(float duration)
    {
        yield return TransitionTo(0f, duration);
    }

    /// <summary>
    /// 内部的な目標強度の設定と、スムーズ遷移コルーチンの制御。
    /// </summary>
    private void SetTargetIntensity(float intensity)
    {
        targetIntensity = intensity;

        if (!smoothTransition)
        {
            currentIntensity = targetIntensity;
            return;
        }

        // 既に遷移中なら止める
        if (transitionCoroutine != null)
        {
            StopCoroutine(transitionCoroutine);
            transitionCoroutine = null;
        }

        // 一定時間で目標値に到達させるコルーチンを開始
        if (transitionDuration > 0f)
        {
            transitionCoroutine = StartCoroutine(TransitionTo(targetIntensity, transitionDuration));
        }
        else
        {
            currentIntensity = targetIntensity;
        }
    }

    /// <summary>
    /// 指定した時間をかけて currentIntensity を target まで補間する。
    /// </summary>
    private IEnumerator TransitionTo(float target, float duration)
    {
        float start = currentIntensity;
        float time = 0f;

        // duration が 0 以下なら即時反映
        if (duration <= 0f)
        {
            currentIntensity = target;
            yield break;
        }

        while (time < duration)
        {
            time += Time.unscaledDeltaTime;
            float t = Mathf.Clamp01(time / duration);
            currentIntensity = Mathf.Lerp(start, target, t);
            yield return null;
        }

        currentIntensity = target;
    }

    /// <summary>
    /// 現在の画面サイズと設定に応じて RenderTexture を作成 / リサイズ。
    /// </summary>
    private void CreateOrResizeRenderTexture()
    {
        ReleaseRenderTexture();

        // 画面サイズからピクセル化後の解像度を計算
        int width, height;
        CalculatePixelatedResolution(out width, out height);

        if (width <= 0 || height <= 0)
        {
            return;
        }

        // RenderTexture を作成(Depth は不要なので 0)
        pixelatedRT = new RenderTexture(width, height, 0, RenderTextureFormat.Default);
        pixelatedRT.filterMode = FilterMode.Point; // ポイントサンプリングでカクカクにする
        pixelatedRT.useMipMap = false;
        pixelatedRT.autoGenerateMips = false;
        pixelatedRT.Create();
    }

    /// <summary>
    /// RenderTexture を破棄する。
    /// </summary>
    private void ReleaseRenderTexture()
    {
        if (pixelatedRT != null)
        {
            if (pixelatedRT.IsCreated())
            {
                pixelatedRT.Release();
            }
            Destroy(pixelatedRT);
            pixelatedRT = null;
        }
    }

    /// <summary>
    /// 画面の短辺を basePixelCountOnShortSide に合わせて、
    /// ピクセル化後の幅・高さを計算する。
    /// </summary>
    private void CalculatePixelatedResolution(out int width, out int height)
    {
        int screenWidth = Screen.width;
        int screenHeight = Screen.height;

        if (screenWidth <= 0 || screenHeight <= 0 || basePixelCountOnShortSide <= 0)
        {
            width = screenWidth;
            height = screenHeight;
            return;
        }

        // 画面の短辺を基準としたスケール係数
        int shortSide = Mathf.Min(screenWidth, screenHeight);
        float scale = (float)basePixelCountOnShortSide / shortSide;

        if (keepAspectRatio)
        {
            // アスペクト比を維持して整数化
            width = Mathf.Max(1, Mathf.RoundToInt(screenWidth * scale));
            height = Mathf.Max(1, Mathf.RoundToInt(screenHeight * scale));
        }
        else
        {
            // 短辺だけを指定ピクセル数に合わせ、長辺はそのままスケール
            if (screenWidth <= screenHeight)
            {
                width = basePixelCountOnShortSide;
                height = Mathf.Max(1, Mathf.RoundToInt(screenHeight * scale));
            }
            else
            {
                height = basePixelCountOnShortSide;
                width = Mathf.Max(1, Mathf.RoundToInt(screenWidth * scale));
            }
        }
    }

    /// <summary>
    /// ピクセル化用のシェーダーをコードから生成し、マテリアルを返す。
    /// Unity6 では ShaderLab も引き続き利用可能。
    /// </summary>
    private Material CreatePixelateMaterial()
    {
        // 非常にシンプルなフルスクリーン描画シェーダー
        // Intensity が 0 のときは元画像、1 のときは完全にピクセル化された画像を描画。
        string shaderCode = @"
Shader ""Hidden/PixelateEffectShader""
{
    Properties
    {
        _MainTex (""Texture"", 2D) = ""white"" {}
        _Intensity (""Intensity"", Range(0,1)) = 1
    }
    SubShader
    {
        Tags { ""RenderType""=""Opaque"" }
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include ""UnityCG.cginc""

            sampler2D _MainTex;
            float4 _MainTex_TexelSize; // (1/width, 1/height, width, height)
            float _Intensity;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // ピクセルの中心にサンプリング位置をスナップする
                float2 pixelCoord = i.uv / _MainTex_TexelSize.xy;
                float2 snapped = floor(pixelCoord) * _MainTex_TexelSize.xy + _MainTex_TexelSize.xy * 0.5;

                fixed4 pixelatedColor = tex2D(_MainTex, snapped);
                fixed4 originalColor = tex2D(_MainTex, i.uv);

                // Intensity で元画像とピクセル化画像をブレンド
                return lerp(originalColor, pixelatedColor, _Intensity);
            }
            ENDCG
        }
    }
}";
        // シェーダーを動的に生成
        Shader shader = ShaderUtil.CreateShaderFromString(shaderCode);
        if (shader == null)
        {
            Debug.LogError("PixelateEffect: シェーダーの生成に失敗しました。");
            return null;
        }

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

    /// <summary>
    /// Shader をコードから生成するためのユーティリティ。
    /// Unity のバージョンにより実装が変わる可能性がありますが、
    /// ここでは簡易的に Shader.Create をラップしています。
    /// </summary>
    private static class ShaderUtil
    {
        public static Shader CreateShaderFromString(string shaderSource)
        {
            // Unity 公式の API では ShaderLab を直接コンパイルするメソッドは公開されていませんが、
            // Shader.Create でランタイム生成が可能な環境を想定しています。
            // プロジェクトによっては、シェーダーを別ファイルで管理して
            // Shader.Find で取得する形にしてもOKです。
            return Shader.Create(shaderSource);
        }
    }
}

このコンポーネントは、カメラにアタッチするだけで「ピクセル化のオン・オフ」「強度の補間」「解像度の調整」ができるようになっています。責務は「ピクセル化されたフルスクリーン描画をすること」だけに絞ってあるので、トランジションのトリガーやイベント検知は別コンポーネントに任せる設計にしやすいですね。

使い方の手順

  1. ① カメラに PixelateEffect をアタッチする
    • Hierarchy でメインカメラ(例: Main Camera)を選択します。
    • Add Component ボタンから PixelateEffect を追加します。
    • インスペクター上で以下のパラメータを調整します:
      • Base Pixel Count On Short Side: 画面の短辺を何ピクセルにするか(例: 80〜160)。小さいほど荒いドットになります。
      • Keep Aspect Ratio: アスペクト比を維持したい場合は ON のままでOKです。
      • Start Enabled: ゲーム開始時からピクセル化した状態にしたい場合に ON。
      • Smooth TransitionTransition Duration: エフェクトのオン/オフをふわっと切り替えたい場合に使用します。
  2. ② プレイヤーやゲーム管理オブジェクトから呼び出す
    例えば「ステージ開始時に一瞬だけピクセル化してから戻す」演出をしたい場合、
    GameManager 的なオブジェクトに以下のようなスクリプトを追加します。
    
    using UnityEngine;
    
    public class StageStartPixelate : MonoBehaviour
    {
        [SerializeField] private PixelateEffect pixelateEffect;
        [SerializeField] private float inDuration = 0.2f;
        [SerializeField] private float holdDuration = 0.3f;
        [SerializeField] private float outDuration = 0.4f;
    
        private void Start()
        {
            if (pixelateEffect == null)
            {
                // シーン内のメインカメラから探してくる例
                Camera mainCam = Camera.main;
                if (mainCam != null)
                {
                    pixelateEffect = mainCam.GetComponent<PixelateEffect>();
                }
            }
    
            if (pixelateEffect != null)
            {
                // ステージ開始演出を再生
                StartCoroutine(PlayStageStartEffect());
            }
        }
    
        private System.Collections.IEnumerator PlayStageStartEffect()
        {
            // まずピクセル化をONにしていく
            yield return pixelateEffect.PixelateIn(inDuration);
            // 少しだけピクセル状態を維持
            yield return new WaitForSeconds(holdDuration);
            // ピクセル化をOFFに戻す
            yield return pixelateEffect.PixelateOut(outDuration);
        }
    }
    

    これで、ステージ開始時に「一瞬ドット絵になる」演出が簡単に追加できます。

  3. ③ イベント時の演出に使う(例: ダメージやアイテム取得)
    敵に大ダメージを受けたときや、レアアイテムを取得したときに画面をピクセル化する演出も簡単です。
    
    using UnityEngine;
    
    public class PlayerHitPixelate : MonoBehaviour
    {
        [SerializeField] private PixelateEffect pixelateEffect;
        [SerializeField] private float duration = 0.3f;
    
        // 例として、ダメージを受けたときに呼ぶメソッド
        public void OnDamaged()
        {
            if (pixelateEffect == null)
            {
                Camera mainCam = Camera.main;
                if (mainCam != null)
                {
                    pixelateEffect = mainCam.GetComponent<PixelateEffect>();
                }
            }
    
            if (pixelateEffect != null)
            {
                // コルーチンで一瞬だけピクセル化
                StartCoroutine(PixelateOnce());
            }
        }
    
        private System.Collections.IEnumerator PixelateOnce()
        {
            yield return pixelateEffect.PixelateIn(duration * 0.5f);
            yield return pixelateEffect.PixelateOut(duration * 0.5f);
        }
    }
    

    こういった「イベント検知 + 演出再生」の部分は別コンポーネントに切り出しておき、
    PixelateEffect はあくまで「描画の担当」にしておくと、責務が分かれて管理しやすくなります。

  4. ④ UI やシーン遷移の演出に組み込む
    • タイトル画面からゲームシーンに移動する際に、PixelateIn → シーンロード → PixelateOut の順でコルーチンを組むと、レトロゲーム風のトランジションになります。
    • ポーズメニューを開くときに画面を少しだけピクセル化して「ゲームが止まった感」を出すのもアリです。

メリットと応用

1. プレハブ・カメラごとに簡単に演出を差し替えられる
PixelateEffect はカメラに完結したコンポーネントなので、
「このカメラはピクセル化あり」「このカメラはピクセル化なし」といった切り替えがプレハブ単位で簡単にできます。
2D ゲームのメインカメラだけピクセル化して、UI 用のカメラはそのままにする、といった構成も取りやすいですね。

2. レベルデザイン時に「演出のパラメータ」を触るだけで調整できる
シーン遷移やイベント時の演出を、巨大な GameManager の中で if 文だらけにしてしまうと、
ちょっとした強度の調整だけでもコードを開いてビルドし直す必要が出てきます。
PixelateEffect のようにコンポーネントに切り出しておけば、
「Base Pixel Count On Short Side」や「Transition Duration」をインスペクターからいじるだけで演出の印象を変えられる ので、レベルデザインがかなり楽になります。

3. 他のエフェクトと組み合わせやすい
ピクセル化は「ポストプロセス系」のエフェクトなので、フェードイン・アウトやカメラシェイク、色調補正などと組み合わせると演出の幅が広がります。
それぞれを単機能コンポーネントとして分けておけば、
「このシーンではピクセル化 + シェイク」「このイベントではピクセル化だけ」といった組み合わせが柔軟にできます。

改造案:時間経過で自動的にドットサイズを変化させる

例えば「時間とともに徐々にドットが荒くなる / 細かくなる」ような表現をしたい場合、
PixelateEffect の外側に小さな制御コンポーネントを足すだけで実現できます。


using UnityEngine;

/// <summary>
/// ゲーム開始から時間経過に応じて、自動でピクセル化強度を変化させるサンプル。
/// 例: タイムリミットが近づくほど画面が荒くなる、など。
/// </summary>
public class PixelateOverTime : MonoBehaviour
{
    [SerializeField] private PixelateEffect pixelateEffect;
    [SerializeField] private float duration = 10f; // この時間で 0 → 1 に変化

    private float elapsed;

    private void Update()
    {
        if (pixelateEffect == null) return;

        elapsed += Time.deltaTime;
        float t = Mathf.Clamp01(elapsed / duration);

        // 経過時間に応じて強度を変化させる
        pixelateEffect.SetPixelateIntensity(t);
    }
}

このように、PixelateEffect 自体には「どういうタイミングで変化させるか」は書かず、
「描画の担当」に集中させておくと、演出のバリエーションを増やしたくなったときに小さなコンポーネントを足していくだけで済みます。
Godクラス化を避けつつ、コンポーネント指向で演出を積み上げていきましょう。