-->

【Unity Shader】UIノイズゆらぎシェーダー

【Unity Shader】UIノイズゆらぎシェーダー
 ※コードが古くなっていたのでURP用にコードを改修しました。(2026/08/16) 


水面のゆらぎ表現が出来る、画面がゆらゆら揺らぐようなシェーダーが作りたいなと思ってしばらく研究を続けていました。
幸いこういった表現は既にネットにいくつか記事が転がっており、それを元にuGUI用に調整することができたので共有します。

似たカテゴリで ウェーブシェーダーもオススメ。
後にこのシェーダーを活かして 陽炎シェーダーも作ってるのでそちらもどうぞ。

ノイズゆらぎシェーダー概要

今回のシェーダーを実装したサンプル動画は次の通りです。
左側のドラゴンに関しては、 シルエットシェーダーを併用してちょっと水色に画像を上書きしています。


この演出を作るに当たってのHierarchyの構成は下のような感じです。
 WaterRipple 
に今回作成したシェーダーをアタッチしています。
揺らぎを与えたいUIは、シェーダーを描画しているカメラとは別のカメラで描画してあげる必要があります。


水面の揺らぎのために作ったシェーダーですが、別に炎の揺らぎに対しても使えます。

RenderFeatureの登録

今回作るシェーダーは個々の画像に対してでなく、カメラに映った最終的な描画結果をまとめて変化させる技術になります。
このためシェーダーを実装する前に、まずC#側で専用のRenderFeatureを登録する必要があります。

 URPGrabRenderFeature.cs 
というUnityのC#スクリプトを作り中身を丸コピしてください。
コードの中身は以前公開した Render Graphの解説ページと全く一緒になります。
Copy Code

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;
using UnityEngine.Rendering.Universal;

/// <summary>
/// Universal Render Dataに登録させるポストプロセス処理のクラス
/// </summary>
public class URPGrabRenderFeature : ScriptableRendererFeature
{
    //介入する描画処理がGameViewのカメラかどうか。シェーダー側では基本GameViewのカメラにだけ処理を実行させる
    //(後述の_URPGrabTextureはGameViewから画面をキャプチャするので、それ以外のViewで使用するとおかしくなる)
    private readonly int _isGameViewId = Shader.PropertyToID("_IsGameView");

    private URPGrabRenderPass _pass;

    /// <summary>
    /// URPGrabRenderPassのインスタンスを生成
    /// </summary>
    public override void Create() => _pass = new URPGrabRenderPass();

    /// <summary>
    /// Unityに対しURPGrabRenderPass.RecordRenderGraphのロジックをカメラ描画工程に追加するように依頼
    /// </summary>
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        // この描画工程がGameViewのカメラかそれ以外かの情報だけは、この段階で全シェーダーに即時送信する
        bool isGameViewCamera = renderingData.cameraData.cameraType == CameraType.Game;
        Shader.SetGlobalInt(_isGameViewId, isGameViewCamera? 1 : 0);

        //描画工程を追加
        renderer.EnqueuePass(_pass);
    }
}

/// <summary>
///具体的なロジック処理クラス
/// </summary>
public class URPGrabRenderPass : ScriptableRenderPass
{
    private readonly int _textureId = Shader.PropertyToID("_URPGrabTexture");
    private readonly int _texelSizeId = Shader.PropertyToID("_URPGrabTexture_TexelSize");

    /// <summary>
    /// 初期化処理、このポストプロセスの実行タイミングを定義しているだけ
    /// </summary>
    public URPGrabRenderPass()
    {
        //カメラ描画処理が全て終わってからURPGrabRenderPass.RecordRenderGraphを実行させる事をUnityに伝える
        renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    }

    /// <summary>
    /// 追加される新しい工程に必要なデータ(Context)を受け渡すのためクラス
    /// </summary>
    private class PassData
    {
        public TextureHandle destination; //テクスチャコピー先のメモリ領域
        public int textureId;
        public int texelSizeId;
        public Vector4 texelSize;
    }

    /// <summary>
    ///カメラの画角に映っている情報をテクスチャとしてシェーダーに渡す処理
    /// </summary>
    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        //今この瞬間にカメラに書き込んでいる画面の色や深度のデータを取得
        UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
        //resourceDataからカメラが描画しているテクスチャのメモリ領域を取得
        TextureHandle originalTextureHandle = resourceData.activeColorTexture;

        //これから生成するテクスチャの設計図を作成
        TextureDesc grabTextureDesc;
        try
        {
            //テクスチャの設計図をコピー
            grabTextureDesc = renderGraph.GetTextureDesc(originalTextureHandle);
        }
        catch
        {
            return;
        }
        //設計図を出力用に上書き
        grabTextureDesc.name = "URPGrabTexture";
        grabTextureDesc.clearBuffer = false;
        grabTextureDesc.msaaSamples = MSAASamples.None;
        grabTextureDesc.depthBufferBits = 0;
        var texelSize = new Vector4(1.0f / grabTextureDesc.width, 1.0f / grabTextureDesc.height,
                                            grabTextureDesc.width, grabTextureDesc.height);
        //設計図から新しいメモリ領域を作成
        TextureHandle grabTextureHandle = renderGraph.CreateTexture(grabTextureDesc);

        //Blitで実物のテクスチャデータを丸々コピー
        renderGraph.AddBlitPass(originalTextureHandle, grabTextureHandle, Vector2.one, Vector2.zero, passName: "BlitCopyToGrabTexture");

        using (var builder = renderGraph.AddRasterRenderPass("SetGrabTextureToGlobalState", out PassData passData))
        {
            //読み込み対象をbuilderに登録
            builder.UseTexture(grabTextureHandle, AccessFlags.Read);
            //この描画工程の中でのみ「グローバル変数(全シェーダー共通の設定)」を書き換えることを許可する
            builder.AllowGlobalStateModification(true);

            //コピーしたを画像をpassDataに代入
            passData.destination = grabTextureHandle;
            passData.textureId = _textureId;
            passData.texelSizeId = _texelSizeId;
            passData.texelSize = texelSize;

            //予約されたスケジュールが実行された時の処理
            builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
            {
                //GPUに対して「このテクスチャ(destination)を、このIDの名前で公開せよ」と命令を出す
                //これにより、全シェーダーの _URPGrabTexture に中身が流し込まれる
                context.cmd.SetGlobalTexture(data.textureId, data.destination);
                context.cmd.SetGlobalVector(data.texelSizeId, data.texelSize);
            });
        }
    }
}
ファイルを作成したら、プロジェクトで使用している
 Scriptable Renderer Data 
のインスペクターを確認してください。
確認方法はこちらになります。 p.74 : 【Unity Shader 基礎】URP環境の確認の仕方

インスペクターの一番下、 Add Renderer Feature から作成したURPGrabRenderFeatureを追加してください。


これでURPのポストプロセス処理に、カメラの全画面の色情報をシェーダーに流し込む工程が追加されました。
あらゆるシェーダーコードで、
 _URPGrabTexture 
というテクスチャを参照することが可能です。

ノイズゆらぎシェーダーの実装

以下シェーダー側のコード全文です。
.shader拡張子であればファイル名は何でもいいですが、とりあえず
 NoiseWave.shader 
というファイルを作り、中身を全てコピペしてください。
Copy Code

Shader "URPGrab/NoiseWave"
{
    Properties
    {
        [HideInInspector]_MainTex("-",2D)="white"{}
        _FluctPow("FluctPow", Range(0.01, 0.1)) = 0.05 //歪みの強さ
        _AnimSpeed("AnimSpeed", Range(0.1, 10)) = 0.2 //歪みが変化するスピード

        [Space(10)]
        [Toggle] _UseAutoAnim("UseAutoAnim", float) = 0 //経過時間によって自動でアニメーションをするか
        _ManualAnimVal("ManualAnimVal", float) = 0 //AutoAnimを使わない場合の、アニメの移動値
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Transparent"
            "RenderPipeline" = "UniversalPipeline"
        }
        Cull Off
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            HLSLPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

                struct Attributes
                {
                    half2 uv : TEXCOORD0;
                    float4 positionOS : POSITION; //オブジェクト空間座標(対象の大元の座標と寸法)
                };

                struct Varyings
                {
                    half2 uv : TEXCOORD0;
                    float4 positionCS : SV_POSITION; //クリップ空間座表(カメラの四角い画角内での対象の座標)
                    float4 positionVP : TEXCOORD1; //ビューポート空間座標(クリップ空間座表 -w ~ +w を 0 ~ w の範囲に修正した座標)
                };

                //URPGrabScreenFeatureから全画面の色情報などを受け取る
                int _IsGameView;
                TEXTURE2D(_URPGrabTexture);
                SAMPLER(sampler_URPGrabTexture);
                half4 _URPGrabTexture_TexelSize;

                //SRP Batcherを適用
                CBUFFER_START(UnityPerMaterial)
                    //Propertiesに対応する変数
                    float _FluctPow;
                    float _AnimSpeed;
                    float _UseAutoAnim;
                    float _ManualAnimVal;
                CBUFFER_END

                //ランダムノイズ(粗いノイズ、0~1の値がランダムに広がる)
                float random (half2 p)
                {
                    p = half2(dot(p, float2(127.1, 311.7)),
                        dot(p, half2(269.5, 183.3)));

                    return -1.0 + 2.0 * frac(sin(p) * 43758.5453123);
                }

                //パーリンノイズ(隣接する値が少しずつ異なる滑かなノイズ、ランダムノイズを参照する)
                float perlinNoise(half2 st)
                {
                    half2 p = floor(st);
                    half2 f = frac(st);
                    half2 u = f * f * (3.0 - 2.0 * f);

                    float v00 = random(p + half2(0, 0));
                    float v10 = random(p + half2(1, 0));
                    float v01 = random(p + half2(0, 1));
                    float v11 = random(p + half2(1, 1));

                    return lerp(lerp(dot(v00, f - half2(0, 0)), dot(v10, f - half2(1, 0)), u.x),
                        lerp(dot(v01, f - half2(0, 1)), dot(v11, f - half2(1, 1)), u.x),
                        u.y) + 0.5f;
                }

                //fBmノイズ(究極に滑らかなノイズ、パーリンノイズを参照する)
                float fBm (half2 st)
                {
                    float f = 0;
                    half2 q = st;

                    f += 0.5000*perlinNoise( q ); q = q*2.01;
                    f += 0.2500*perlinNoise( q ); q = q*2.02;
                    f += 0.1250*perlinNoise( q ); q = q*2.03;
                    f += 0.0625*perlinNoise( q ); q = q*2.01;

                    return f;
                }

                //頂点シェーダー
                Varyings vert(Attributes input)
                {
                    Varyings output;

                    //uv座標を代入
                    output.uv = input.uv;
                    //オブジェクト空間座標をクリップ空間、ビューポート空間座標にそれぞれ変換
                    output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                    output.positionVP = ComputeScreenPos(output.positionCS);

                    return output;
                }

                //ピクセルシェーダー
                half4 frag (Varyings input) : SV_Target
                {
                    //GameViewのカメラ以外に対しては半透明ピンクを返すだけ
                    if (_IsGameView == 0)
                    {
                        return half4(1, 0, 1, 0.2);
                    }

                    //ビューポート空間座標から奥行を省いて座標を平面化する
                    float2 screenUV = input.positionVP.xy / input.positionVP.w;

                    float2 nUv = screenUV;
                    //UseAutoAnimがOnの時は時間経過によってノイズの位置をずらす、Offの時はManualAnimValを参照する
                    nUv.y += lerp(_ManualAnimVal * _AnimSpeed, _Time.y * _AnimSpeed, _UseAutoAnim);

                    //fBmノイズで滑らかなノイズを作成した上で、値の範囲を0~1から-1~1に拡大
                    float noise = 2 * fBm(nUv) -1 ;

                    //画面の上下端では歪みを実行させないためのnoise配合率を算出
                    half isUvBotom = step(screenUV.y, 0.1);
                    half rate1 = isUvBotom * (screenUV.y / 0.1) + (1 - isUvBotom) * 1;
                    half isUvUp = 1 - step(screenUV.y, 0.9);
                    half rate2 = isUvUp * ((0.1 - (screenUV.y - 0.9)) / 0.1) + (1 - isUvUp) * 1;
                    half noiseRate = rate1 * rate2;

                    //noiseの値は-1~1の範囲に収まっている
                    //これに_FluctPowを掛けると、最小で-0.01~0.01、最大で-0.1~0.1の範囲に収まる
                    //その修正された値で、対象画像のuv座標のy軸だけ移動させ、描画するピクセルをズラす
                    screenUV.y += noiseRate * (noise * _FluctPow);

                    //Grabした画像に変形させたuv座標を適用させ歪ませる
                    return SAMPLE_TEXTURE2D(_URPGrabTexture, sampler_URPGrabTexture, screenUV);
                }
            ENDHLSL
        }
    }
}
シェーダーファイルを作成したら、次にMaterialを新規作成し、Shaderの欄に
 URPGrab/NoiseWave 
を指定します。

この辺りの紐づけの仕様についてはこちらでも解説してあります。 p.63 : 【Unity Shader 基礎】Imageにシェーダーを反映させる方法

Unityにシェーダーを反映

Hierarchy上で_URPGrabTexture専用のOverlay CameraとCanvasを追加します。
作成したカメラは、Base CameraのStackの一番下に登録してください。


作成したCanvasにuGUI Imageを配置して、ストレッチで全画面に伸ばします。
ImageのMaterialには先ほど作ったマテリアルをアタッチします。


これで実装完了です。
マテリアルのインスペクターの値を操作することでシェーダー効果を制御できます。

ノイズゆらぎシェーダーの使い方

プロパティとして4つの値が外出しされています。
Copy Code

    Properties
    {
        [HideInInspector]_MainTex("-",2D)="white"{}
        _FluctPow("FluctPow", Range(0.01, 0.1)) = 0.05 //歪みの強さ
        _AnimSpeed("AnimSpeed", Range(0.1, 10)) = 0.2 //歪みが変化するスピード

        [Space(10)]
        [Toggle] _UseAutoAnim("UseAutoAnim", float) = 0 //経過時間によって自動でアニメーションをするか
        _ManualAnimVal("ManualAnimVal", float) = 0 //AutoAnimを使わない場合の、アニメの移動値
    }
ゆがみシェーダーのプロパティ

 FluctPow 
を操作すると画像に対する歪みの強さを変更できます。
下は最小値0.01と最大値0.1に設定した時の比較動画。


 AnimSpeed 
を操作すると歪みの変化スピードを変更できます。
下は最小値の0.1と最大値の10に設定した時の比較動画。


 UseAutoAnim 
のトグルがOnの場合、UnityをPlay状態にしてるだけで勝手にゆらぎアニメーションを実行します。
コードで言うと下の部分で、
 _Time.y 
というUnity側で用意している変数を使い、シーン読み込みからの経過時間でゆがみを変化させています。
Copy Code

                    //UseAutoAnimがOnの時は時間経過によってノイズの位置をずらす、Offの時はManualAnimValを参照する
                    nUv.y += lerp(_ManualAnimVal * _AnimSpeed, _Time.y * _AnimSpeed, _UseAutoAnim);

Unityのドキュメントを読むと、_Timeは4次元の変数で、1~4次元目まで経過時間を少し割ったり掛けたりしてるだけになります。
 _Time 
: float4 : ステージのロードからの時間 (t/20, t, t*2, t*3)。シェーダー内でアニメーション化を行うために使用します。
_Time.yは名前の印象から受けるy軸とかは関係なくて、単純に2次元目のtを指定してるだけみたいです。

UseAutoAnimは初期状態にOnになっていて、もしOffにした場合は、勝手にアニメーションは実行してくれません。
この場合は
 ManualAnimVal 
を操作することで手動でアニメーションを実行します。


勿論 マテリアルにはC#からアクセス可能なので、C#のコード上からアニメーションも実行できます。下は一例。
(アニメーションに Tweenを使っています)
Copy Code

    DOTween.To(() => star.material.GetFloat("_ManualAnimVal"), (val) => 
    {
        star.material.SetFloat("_ManualAnimVal", val);
    }, 50, 3f);

1
1