Skip to content

第11回 描画バッファとポストエフェクト

前回: 第10回 シェーダー連携1

注意

この授業には提出課題があります

提出方法は 授業内で説明します

前回の振り返り

メッシュ上で斬撃を動かしました

  • 第10回では、メッシュの UV を使って 3D 空間上の斬撃エフェクトを作りました
  • PhaseSpeedIntensity をスクリプトから操作し、固定メッシュ上で光が走るように見せました
  • Sample Gradient とノイズを組み合わせ、色、透明度、内部の密度を調整しました
  • 今回は、メッシュ内の見た目だけでなく、描画済みの画面やポストプロセスと連携する表現を扱います

今回の授業の目的

描画結果を使ってより複雑な表現を作る

  • 画面に描かれた映像をシェーダーで加工して歪ませる
  • 高輝度な色を Bloom で光らせる
  • MaterialPropertyBlock で Renderer ごとの値を制御して一瞬だけ強い輝度や色を出す
  • シェーダーが何を出力し、その出力が後段の描画処理でどう使われるかを理解することが目標です

今回の見取り図

先に全体像をつかむ

text
通常の描画
  -> Color Buffer に色が書き込まれる
  -> その色を Scene Color として読み直す
  -> 明るい部分を Bloom が拾う
  -> 最終的な画面に合成される
  • 歪みは「読む」処理です
  • 発光とフラッシュは「書く」処理です
  • Bloom は、書かれた高輝度を後段で使う処理です
  • このあと各用語を順番に整理してから、実装手順に入ります

描画バッファとは

描画中の画面の色情報を保持するバッファ

  • ゲーム画面は、いきなり最終画像として完成するわけではありません
  • カメラから見たオブジェクトの色、深度、法線などが、描画の途中結果としてバッファに書き込まれます
  • そのうちの画面の色を保持するものが Color Buffer です
  • ポストプロセスやスクリーンスペース表現は、この描画情報を利用して最終的な見た目を加工します

ポストプロセスとは

カメラが描いたあとに画面全体へかける後処理

  • Post Processing は、描画の最後に画面全体へかける処理です
  • 写真や動画編集で、撮影後の画像に色補正やぼかしを加える処理と同じく、ゲーム画面の最終的な色味や明るさを加工するために使います
  • たとえば Bloom、Color Adjustments、Vignette、Depth of Field、Motion Blur などが代表的です
  • エフェクト制作では、シェーダーが出力した色をポストプロセスでさらに加工することで、よりインパクトのある表現を作ることができます

Unity の Volume

ポストプロセスの設定をシーンに置く仕組み

  • URP では、ポストプロセスの多くを Volume で設定します
  • Global Volume は、シーン全体に同じポストプロセス設定を適用します
  • Local Volume は、特定範囲に入ったときだけ設定を変える用途に使えます
    例えば屋内と屋外で表現を切り替える場合などに使用されます
  • Volume には Profile を割り当て、その中に Bloom などの項目を追加します
  • カメラ側で Post Processing が有効でないと、Volume を置いても効果が反映されませんので注意してください

ポストプロセスの注意

画面全体に影響し、対象を選びづらい

  • Bloom のようなポストプロセスは、基本的に画面全体の描画結果を見て処理します
    そのため、単体のエフェクトのみではなく、背景、UI、他のエフェクトなども同時に加工される場合があります
  • 特定の対象だけに影響させたい場合は、描画段階の分離やカスタムレンダーパスが必要です
  • その設定は入門段階では複雑になりやすいため、今回は扱いません
  • 画面全体を強く加工すると派手になりますが、情報が読みにくくなることもあります
  • エフェクト用の明るさと、ゲーム画面全体の見やすさを同時に調整する必要があります
  • 今回はその中でも、エフェクト制作で使いやすい Bloom に絞って扱います

今回扱うバッファの使い方

読む処理と書く処理を分けて整理する

表現何をするか使う考え方
歪み描画済みの色をずらして読むScreen UV、Scene Color
発光通常より明るい色を書き込むEmission、HDR Color
Bloom明るい部分を抽出してにじませるPost Processing
フラッシュ一瞬だけ高輝度や白寄せの値を書くMaterialPropertyBlock
  • 歪みは、主に「読む」表現です
  • 発光とフラッシュは、主に「書く」表現です
  • Bloom は、書き込まれた結果を後段で「再利用」する表現です

UV と Screen UV の違い

メッシュ基準か、画面基準か

  • 前回の斬撃では、メッシュの UV を使いました
  • UV はメッシュ状に定義された座標で、メッシュの形や UV 展開時の形に依存します
  • 今回使う Screen UV は、画面上の位置を基準にした座標です
    画面左下から右上へ向かって値が変化し、カメラに映った結果を扱うときに使います
  • つまり、前回は「メッシュ上のどこにあるか」を基準としましたが、今回は「画面上のどこにあるか」を基準とします

スクリーンスペース表現

画面上の座標で処理する

text
Screen Position
  -> 画面上の UV を得る
  -> Scene Color を読む
  -> 読む位置をずらす
  -> 画面が歪んだように見える
  • スクリーンスペース表現は、オブジェクトではなく画面上の座標を使います
  • 画面全体のエフェクト、歪み、ポストエフェクト、UI 的な見た目と相性が良いです
  • ただしカメラや描画順の影響を受けるため、通常のシェーダーとは注意すべき点が異なります

Scene Color

描画済みの画面色を取得する

  • Shader Graph の Scene Color ノードは、すでに描画された画面の色を取得するためのノードです
  • 歪み表現では、Scene Color をそのまま取得するのではなく、少しずらした座標で取得します
  • ずらした位置で取得した色を書き込むことで、画面が揺らいだように見せることができます
  • 熱気、衝撃波、空間の切れ目、魔法のゆらぎなど、光の屈折の表現に利用可能です

Scene Color を使う準備

URP の Opaque Texture を有効にする

  • Scene Color ノードを使うには、URP Asset の Opaque Texture が必要になります
    Project Settings > Graphics または URP Asset を確認し、必要であれば Opaque Texture を有効にしてください
  • 歪み用マテリアルは Transparent 系の設定にします
  • Scene Color は基本的に、すでに描画済みの不透明オブジェクトを参照します
  • 透明エフェクト同士が重なる場合は、どのエフェクトが先に描画されるかを意識する必要があります
    必要に応じて Sorting Priority などで順序を調整します

歪み表現

画面色をずらして読み直す

text
Screen UV
  + Noise または Normal Map によるずれ
  -> Scene Color をサンプリング
  -> 歪んだ背景として出力
  • 歪みは、背景画像そのものを変形しているわけではありません
  • 画面に描かれた色を、少しずれた座標で読み直すことで歪んで見せています
  • ノイズや法線マップを使って UV をずらすと、熱気や衝撃波のような見た目を作れます
  • ずらす量が大きすぎると画面が崩れて見えるため、値は小さく扱います

歪み Shader Graph の作り方

Screen UV に小さなオフセットを加える

  1. URP Unlit Shader Graph を作成する
  2. Surface を Transparent にする
  3. Screen Position ノードを追加する
  4. XY 成分を取り出して Screen UV として使う
  5. Gradient Noise または Normal Map から 2D のずれを作る
  6. ずれに _DistortionStrength を掛ける
  7. Screen UV + ずれScene Color の UV に入れる
  8. 出力色を Base Color に接続し、Alpha を弱めにする
  • _DistortionStrength は最初 0.005 から 0.03 程度の小さい値で試します
  • 画面全体が大きくずれると酔いやすく、何が起きたか分かりにくくなります

歪みの形を制限する

マスクで必要な場所だけ歪ませる

  • 歪みはメッシュ全体に出すのではなく、斬撃や衝撃波の周辺だけに出します
  • UV や頂点カラー、テクスチャーを使って Alpha マスクを作ります
  • 外側に向かって Alpha が 0 になるようにすると、背景との境界が自然になります
  • 前回作った Sample Gradient の Alpha を歪み強度にも使うと、斬撃の形と歪みの形を合わせやすくなります

輝度を書き込む

発光は明るい色を出力することから始まる

  • シェーダーの Emission は、マテリアル自身が明るい色を出しているように見せるための出力です
  • ただし Emission を設定しただけで、周囲のオブジェクトを実際に照らすわけではありません
  • エフェクトでは、通常より明るい HDR カラーを画面へ書き込みます
  • その明るい部分を Bloom が拾うことで、画面上で光がにじんだように見えます

Emission を Shader Graph で作る

色と強度を分ける

Shader Graph に以下のプロパティを追加します。

プロパティ用途
_BaseColorColor基本色
_EmissionColorColor発光色
_EmissionIntensityFloat発光の強さ
_AlphaFloat透明度
  • EmissionColor * EmissionIntensity を Emission に接続します
  • 斬撃本体の色にも同じ値を少し加えると、発光している印象を作りやすくなります
  • EmissionIntensity はスクリプトや AnimationCurve で時間変化させます

Bloom

書き込まれた高輝度を後段で再利用する

  • Bloom は、明るい部分を抽出してぼかし、元の画面へ合成するポストプロセスです
  • Bloom はシェーダー単体の機能ではなく、カメラが描いた結果に対する後処理です
  • そのため、Bloom を効かせたい場所には、Bloom が拾えるだけの明るい値を書き込む必要があります
  • エフェクト制作では、Emission と Bloom をセットで考えると調整しやすくなります

Bloom の設定

Volume で画面効果を有効にする

  1. シーンに Global Volume を作成する
  2. Profile を作成し、Bloom を追加する
  3. IntensityThreshold を調整する
  4. URP Asset で HDR が有効か確認する
  5. カメラ側で Post Processing が有効か確認する
  • Bloom は画面全体に影響するため、強すぎると UI や背景まで白くにじみます
  • まず弱めに設定し、マテリアル側の Emission 強度で調整するのがおすすめです

ヒットフラッシュ

一瞬だけ強い信号を書き込む

  • ヒットした対象を一瞬だけ白くすると、攻撃が当たったことが分かりやすくなります
  • この回では、フラッシュを「ヒットの瞬間だけ描画結果へ強い輝度や色を書き込む処理」として扱います
  • 炎なら赤、雷なら黄色、氷なら水色のように、属性色へ寄せても効果的です
  • フラッシュは長すぎると点滅に見えるため、0.05 秒から 0.15 秒程度の短い時間で使います

フラッシュ用 Shader Graph

元の色とフラッシュ色を Lerp する

Shader Graph 側に以下のプロパティを用意します。

プロパティ用途
_FlashColorColorフラッシュ時の色
_FlashAmountFloatフラッシュの混ざり具合
text
BaseColor
  -> Lerp A
FlashColor
  -> Lerp B
FlashAmount
  -> Lerp T
Lerp Result
  -> Base Color
  • _FlashAmount0 なら通常色、1 ならフラッシュ色になります
  • Lit Shader Graph なら Emission にも少し足すと、Bloom に反応するフラッシュにできます
  • キャラクターの既存マテリアルに追加する場合は、元の見た目を壊さない範囲で組み込みます

MaterialPropertyBlock

マテリアルを複製せずに Renderer ごとの値を変える

  • 対象のマテリアルを直接書き換えると、同じマテリアルを使っている他の Renderer にも影響することがあります
  • renderer.material を使うと、実行時にマテリアルが複製されることがあります
  • 大量の敵やエフェクトでこれを行うと、メモリや管理の問題につながります
  • MaterialPropertyBlock を使うと、共有マテリアルはそのままに Renderer ごとのプロパティだけを変更できます
  • エフェクトの一時的な発光、透明度、フラッシュ制御に向いています

フラッシュ制御コード

Renderer の値を一時的に変更する

csharp
using System.Collections;
using UnityEngine;

public class HitFlash : MonoBehaviour
{
    [SerializeField] private Renderer targetRenderer;
    [SerializeField] private Color flashColor = Color.white;
    [SerializeField, Min(0f)] private float duration = 0.08f;

    private static readonly int FlashColorId =
        Shader.PropertyToID("_FlashColor");
    private static readonly int FlashAmountId =
        Shader.PropertyToID("_FlashAmount");

    private MaterialPropertyBlock _propertyBlock;
    private Coroutine _routine;

    private void Awake()
    {
        if (targetRenderer == null)
            targetRenderer = GetComponentInChildren<Renderer>();

        _propertyBlock = new MaterialPropertyBlock();
    }

    public void Play()
    {
        if (_routine != null)
            StopCoroutine(_routine);

        _routine = StartCoroutine(FlashRoutine());
    }

    private IEnumerator FlashRoutine()
    {
        var elapsed = 0f;
        Apply(1f);

        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            var t = duration <= 0f ? 1f : Mathf.Clamp01(elapsed / duration);
            var amount = 1f - t;
            Apply(amount);
            yield return null;
        }

        Apply(0f);
        _routine = null;
    }

    private void Apply(float amount)
    {
        if (targetRenderer == null)
            return;

        targetRenderer.GetPropertyBlock(_propertyBlock);
        _propertyBlock.SetColor(FlashColorId, flashColor);
        _propertyBlock.SetFloat(FlashAmountId, amount);
        targetRenderer.SetPropertyBlock(_propertyBlock);
    }
}

斬撃とバッファ利用を組み合わせる

前回のエフェクトを画面側の処理につなげる

text
斬撃本体
  -> メッシュ UV で形と流れを作る
  -> Emission で高輝度を書き込む
  -> Bloom が後段で拾う

歪みメッシュ
  -> Screen UV で Scene Color を読む
  -> 読む位置をずらす
  -> 背景が揺らいだように見える
  • 斬撃本体は、攻撃の形と方向を見せます
  • 歪み用メッシュは、同じ場所で画面色をずらして読み直します
  • ヒット対象は、フラッシュで一瞬だけ強い値を書き込みます
  • それらが最終的に同じ画面上で合成され、1つのヒット演出になります

斬撃と歪みを同時に制御する

複数 Renderer のプロパティをまとめて動かす

csharp
using UnityEngine;

public class ShaderLinkedSlash : MonoBehaviour
{
    [SerializeField] private Renderer slashRenderer;
    [SerializeField] private Renderer distortionRenderer;
    [SerializeField, Min(0f)] private float lifeTime = 0.35f;
    [SerializeField] private AnimationCurve intensityCurve =
        new AnimationCurve(
            new Keyframe(0f, 0f),
            new Keyframe(0.15f, 1f),
            new Keyframe(1f, 0f));
    [SerializeField] private AnimationCurve distortionCurve =
        new AnimationCurve(
            new Keyframe(0f, 0f),
            new Keyframe(0.1f, 1f),
            new Keyframe(0.7f, 0f));
    [SerializeField] private AnimationCurve phaseCurve =
        AnimationCurve.Linear(0f, 0f, 1f, 1f);

    private static readonly int PhaseId = Shader.PropertyToID("_Phase");
    private static readonly int IntensityId = Shader.PropertyToID("_Intensity");
    private static readonly int EmissionIntensityId =
        Shader.PropertyToID("_EmissionIntensity");
    private static readonly int DistortionStrengthId =
        Shader.PropertyToID("_DistortionStrength");

    private MaterialPropertyBlock _slashBlock;
    private MaterialPropertyBlock _distortionBlock;
    private float _elapsedTime;
    private bool _playing;

    private void Awake()
    {
        _slashBlock = new MaterialPropertyBlock();
        _distortionBlock = new MaterialPropertyBlock();
    }

    private void OnEnable()
    {
        Play();
    }

    private void Update()
    {
        if (!_playing)
            return;

        _elapsedTime += Time.deltaTime;
        var t = lifeTime <= 0f ? 1f : Mathf.Clamp01(_elapsedTime / lifeTime);

        Apply(t);

        if (t >= 1f)
            _playing = false;
    }

    public void Play()
    {
        _elapsedTime = 0f;
        _playing = true;
        Apply(0f);
    }

    private void Apply(float normalizedTime)
    {
        var phase = phaseCurve.Evaluate(normalizedTime);
        var intensity = intensityCurve.Evaluate(normalizedTime);
        var distortion = distortionCurve.Evaluate(normalizedTime);

        if (slashRenderer != null)
        {
            slashRenderer.GetPropertyBlock(_slashBlock);
            _slashBlock.SetFloat(PhaseId, phase);
            _slashBlock.SetFloat(IntensityId, intensity);
            _slashBlock.SetFloat(EmissionIntensityId, intensity);
            slashRenderer.SetPropertyBlock(_slashBlock);
        }

        if (distortionRenderer != null)
        {
            distortionRenderer.GetPropertyBlock(_distortionBlock);
            _distortionBlock.SetFloat(PhaseId, phase);
            _distortionBlock.SetFloat(DistortionStrengthId, distortion);
            distortionRenderer.SetPropertyBlock(_distortionBlock);
        }
    }
}

カーブの調整

同じ素材でも印象は時間変化で決まる

使い方の目安
Phase攻撃方向に沿って 0 から 1 へ短時間で動かす
Intensity出始めに強く、後半で素早く落とす
EmissionIntensityヒットの瞬間だけ強くする
DistortionStrength最初だけ出して、長く残さない
FlashAmount0.05 秒から 0.15 秒程度で消す
  • 発光と歪みは、長く残るほど重く、邪魔に見えやすくなります
  • 攻撃が当たった瞬間だけ強く、後半は形が残る程度にすると扱いやすいです

描画順の注意

透明エフェクトは順番で見え方が変わる

  • 透明マテリアルは描画順の影響を受けます
  • 歪み、斬撃本体、火花を重ねる場合、どれを前に出すかで見え方が変わります
  • Sorting Priority や Renderer の設定で順序を調整します
  • Scene Color を使う歪みは、何がすでに描かれているかに依存します
  • 問題が起きたら、まず要素を1つずつ非表示にして原因を切り分けます

負荷の注意

画面全体のコストを意識する

  • Bloom は画面全体にかかるポストプロセスです
  • 透明エフェクトは重なりが増えるほど描画負荷が増えます
  • Scene Color を使う歪みは、画面色の参照が必要です
  • ノイズを複数使う Shader Graph は見た目以上に重くなることがあります
  • エフェクトを大量に出すゲームでは、見た目だけでなく同時発生数も考えて調整します

実習

描画バッファを利用したヒット演出を作る

前回作った斬撃に、Scene Color、Emission、Bloom、フラッシュを接続します

実習1:Bloom と Emission を確認する

高輝度を書き込んで後段で光らせる

  1. Global Volume を作成し、Bloom を追加する
  2. URP とカメラの Post Processing 設定を確認する
  3. 前回の斬撃 Shader Graph に Emission を追加する
  4. _EmissionIntensity をプロパティ化する
  5. 値を上げたときに斬撃が明るくにじむか確認する
  • 画面全体が白くなる場合は、Bloom の IntensityThreshold を下げます

実習2:歪み用マテリアルを作る

Scene Color をずらして背景を歪ませる

  1. 歪み用の URP Unlit Shader Graph を作成する
  2. Screen PositionScene Color を使う
  3. ノイズで Screen UV を少しずらす
  4. _DistortionStrength をプロパティ化する
  5. 斬撃メッシュ、または少し大きい別メッシュに適用する
  • 歪みが見えない場合は、URP Asset の Opaque Texture と描画対象の位置関係を確認します

実習3:ヒット対象をフラッシュさせる

ヒットの瞬間だけ強い値を書き込む

  1. 対象の Shader Graph に _FlashColor_FlashAmount を追加する
  2. Lerp で通常色とフラッシュ色を混ぜる
  3. 必要なら Emission にもフラッシュ色を少し加える
  4. HitFlash スクリプトを対象に追加する
  5. Play() を呼んで一瞬だけ色が変わるか確認する
  • 共有マテリアルを直接変更しないように、スクリプトでは MaterialPropertyBlock を使います

実習4:1つの攻撃として再生する

読む処理と書く処理を組み合わせる

  1. 前回の斬撃メッシュを用意する
  2. 歪み用メッシュを斬撃の少し外側に重ねる
  3. ヒット位置に短寿命の Particle System を置く
  4. ShaderLinkedSlash で斬撃と歪みを制御する
  5. 攻撃入力やテスト用キーから、斬撃、歪み、フラッシュ、火花を同時に再生する
  • まずは手動再生で確認し、最後にゲーム側の攻撃処理へ接続します

授業内課題

描画バッファを利用したヒット演出を作成してください

画面色の読み直し、または高輝度の書き込みを使います

課題の条件

以下を満たしてください

  • 前回作成した斬撃、または新規作成したメッシュエフェクトを使っている
  • Emission と Bloom、または HDR カラーによる発光表現を使っている
  • Scene Color による歪み、またはヒット対象のフラッシュを追加している
  • MaterialPropertyBlock またはスクリプトから Shader Graph のプロパティを操作している
  • 出現から消滅までの時間変化を AnimationCurve などで調整している
  • スクリーンショットまたは短い動画で提出する

確認観点

どのバッファをどう使っているか説明する

  • 攻撃の方向や範囲が分かるか
  • Scene Color を読んでいる部分はどこか
  • 高輝度を書き込んでいる部分はどこか
  • Bloom が強すぎて形が消えていないか
  • 歪みが背景を壊しすぎていないか
  • フラッシュが長すぎて点滅に見えていないか
  • どのプロパティをスクリプトから制御しているか説明できるか

よくある問題

原因を切り分ける

問題確認する場所
Bloom が出ないVolume、カメラ、URP の HDR、Emission 強度
歪みが出ないOpaque Texture、Scene Color、透明設定、描画順
対象が全部同じ色になる共有マテリアルを直接変更していないか
斬撃が見えないAlpha、Blend、Sorting、メッシュの向き
画面が白く飛ぶEmission、Bloom Intensity、Additive の重ねすぎ
重い透明エフェクトの重なり、ノイズ数、同時発生数

余裕があれば挑戦

さらにゲームらしくする

  • 攻撃属性ごとに色、歪み、火花の種類を変える
  • カメラシェイクや一瞬のヒットストップを追加する
  • 敵の残り HP に応じてフラッシュ色を変える
  • 斬撃の発生方向をキャラクターの向きに合わせる
  • 命中時と空振り時でエフェクトの量を変える

提出時の説明

調整の意図を添えてください

  • どの攻撃、どの属性を表現したか
  • Scene Color、Emission、Bloom、フラッシュのどれを使ったか
  • 一番強く見せたい瞬間をどこにしたか
  • どの Shader Graph プロパティをスクリプトから操作したか
  • 重くなりすぎないように何を調整したか
  • 改善したい点

今回のまとめ

描画結果を読む、書く、後段で使う

  • 歪みは、描画済みの Scene Color をずらして読み直す表現です
  • Emission は、Bloom が拾える高輝度を画面へ書き込む表現です
  • Bloom は、書き込まれた高輝度をポストプロセスで再利用する表現です
  • フラッシュは、ヒットの瞬間だけ強い輝度や色を書き込む演出です
  • 描画バッファの利用と再利用を意識すると、歪み、発光、フラッシュを同じ文脈で設計できます

おつかれさまでした!

次回予告

制作課題に向けて、これまでのエフェクトを組み合わせていきます