-->

【Unity Shader】UIサインウェーブシェーダー

【Unity Shader】UIサインウェーブシェーダー
関連ページ
  ※コードが古くなっていたのでURP用にコードを改修しました。(2026/09/12)  

カーテンシェーダーを作る過程でサイン波とコサイン波という大変便利な関数を見つけました。
結果的にカーテンシェーダーでは採用しませんでしたが、これを使って良い演出が作れたのでそれを共有します。

サインウェーブシェーダー概要

昔のSFCのドラゴンクエストでは、ワープする際にx軸に画面がゆらゆらと揺らぐ演出を使っていました。
下はそのSFCドラゴンクエスト1.2のワープ演出になります。


シェーダーのサイン波を知って、この関数を使えば簡単にドラクエの演出を模倣できそうだと思いました。
下が出来上がったウェーブシェーダーのサンプル動画です。
プロパティの設定によってはy軸にウェーブをかけることも可能です。

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 
の拡張子のファイルを作り、中身を全てコピペしてください。
Copy Code

Shader "URPGrab/SineWave"
{
    Properties
    {
        [HideInInspector]_MainTex("-",2D)="white"{}

        //インスペクターからVariantを選択させるため設定
        [KeywordEnum(X, Y)] _DIRECTION("Direction", Float) = 0

        //使用する波形をトグルとして表示、実際はfloat値の0か1が保存される
        [Toggle] _UsePlusSine("UsePlusSine", float) = 1
        [Toggle] _UseMinusSine("UseMinusSine", float) = 1

        [Space(10)]
        _Twist ("Twist", Range(0, 100)) = 0 //波形の細かさ
        _Suppression("Suppression", Range(1, 50)) = 10 //サイン波の波形の振れ幅の抑制値
    }

    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"

                //Variantのキーワードを定義
                #pragma shader_feature _DIRECTION_X _DIRECTION_Y

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

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

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

                //SRP Batcherを適用して処理を高速化
                CBUFFER_START(UnityPerMaterial)
                    //Propertiesに対応する変数
                    float _UsePlusSine;
                    float _UseMinusSine;
                    float _Twist;
                    float _Suppression;
                CBUFFER_END

                //プラスのサイン波を使って、元のUV座標からx軸座標のみを歪めて返す
                half2 plusSineUvDistortX(float2 screenUV)
                {
                    half twist = sin(screenUV.y * _Twist) / _Suppression;
                    half resultX = screenUV.x + twist;
                    half2 fixUv = half2(resultX , screenUV.y);
                    return fixUv;
                }

                //マイナスのサイン波を使って、元のUV座標からx軸座標のみを歪めて返す
                half2 minusSineUvDistortX(float2 screenUV)
                {
                    half twist = -sin(screenUV.y * _Twist) / _Suppression;
                    half resultX = screenUV.x + twist;
                    half2 fixUv = half2(resultX , screenUV.y);
                    return fixUv;
                }

                //プラスのサイン波を使って、元のUV座標からy軸座標のみを歪めて返す
                half2 plusSineUvDistortY(float2 screenUV)
                {
                    half twist = sin(screenUV.x * _Twist) / _Suppression;
                    half resultY = screenUV.y + twist;
                    half2 fixUv = half2(screenUV.x, resultY);
                    return fixUv;
                }

                //マイナスのサイン波を使って、元のUV座標からy軸座標のみを歪めて返す
                half2 minusSineUvDistortY(float2 screenUV)
                {
                    half twist = -sin(screenUV.x * _Twist) / _Suppression;
                    half resultY = screenUV.y + twist;
                    half2 fixUv = half2(screenUV.x, resultY);
                    return fixUv;
                }

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

                    //オブジェクト空間座標をクリップ空間、ビューポート空間座標にそれぞれ変換
                    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(0, 1, 1, 0.2);
                    }

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

                    //PlusSineもMinusSineも使わないなら、元のUV座標をそのまま使ってピクセル色を返す
                    if (_UsePlusSine + _UseMinusSine == 0)
                    {
                        return SAMPLE_TEXTURE2D(_URPGrabTexture, sampler_URPGrabTexture, screenUV);
                    }

                    //プラスとマイナスの2つの波形を使って歪ませたUV座標を取得
                    #if _DIRECTION_X
                        half2 plusSineUv = plusSineUvDistortX(screenUV);
                        half2 minusSineUv = minusSineUvDistortX(screenUV);
                    #else
                        half2 plusSineUv = plusSineUvDistortY(screenUV);
                        half2 minusSineUv = minusSineUvDistortY(screenUV);
                    #endif

                    //歪ませた座標を使って、_URPGrabTextureからピクセル色を取得
                    half4 col1 = SAMPLE_TEXTURE2D(_URPGrabTexture, sampler_URPGrabTexture, plusSineUv) * _UsePlusSine;
                    half4 col2 = SAMPLE_TEXTURE2D(_URPGrabTexture, sampler_URPGrabTexture, minusSineUv) * _UseMinusSine;

                    //それぞれの波系の合成率を取得、1種類なら1、2種類使ってれば0.5になる
                    half division = 1 / (_UsePlusSine + _UseMinusSine);
                    //それぞれの波形に合成率を掛ける
                    col1 = col1 * division;
                    col2 = col2 * division;
                    //波形を合成して返す
                    return col1 + col2;
                }
            ENDHLSL
        }
    }
}

シェーダーファイルを作成したら、Materialを新規作成し、Shaderの欄に
 URPGrab/SineWave 
を指定します。

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

URPGrab用のCameraとCanvasを追加

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



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


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

プロパティを解説

用意してあるプロパティは5つです。

 Direction 
: ウェーブを与える方向。X軸とY軸のどちらかを選べる。
 UsePlusSine 
: プラスのサイン波を画面に反映させるかどうか。
 UseMinusSine 
: マイナスのサイン波を画面に反映させるかどうか。
 Twist 
: 波の細かさの値。0から100まで選択可能で、値を上げるほど細かい波になる。
 Suppression 
: 波の揺れ幅の抑制値。0~50まで選択可能で、値を上げるほど揺れ幅が小さくなる。

Direction

UsePlusSineとUseMinusSine

2つは逆の方向に波形作ります。両方ともOnにすると2つの波形がブレンドされて以下のような演出になります。

Twist

Suppression

コードを少し解説

まずシェーダーのHLSL言語にはsin関数という、波形を作るのに大変便利な関数があります。
この関数に対していかなる数値を与えても、必ず-1から1の間の値を返します。
しかもその-1から1に行ったり来たりする値は、綺麗なカーブを描いたものが返ってきます。

今回やっていることは、対象ピクセルのuv座標のy軸をsin関数に渡し、返ってきた値をx軸にプラスさせるという手法をとっています。
DirectionにYを指定していればその逆です。
サイン波の解説

サイン波に関するコードは下の通りです。
Copy Code

                //プラスのサイン波を使って、元のUV座標からx軸座標のみを歪めて返す
                half2 plusSineUvDistortX(float2 screenUV)
                {
                    half twist = sin(screenUV.y * _Twist) / _Suppression;
                    half resultX = screenUV.x + twist;
                    half2 fixUv = half2(resultX , screenUV.y);
                    return fixUv;
                }

                //マイナスのサイン波を使って、元のUV座標からx軸座標のみを歪めて返す
                half2 minusSineUvDistortX(float2 screenUV)
                {
                    half twist = -sin(screenUV.y * _Twist) / _Suppression;
                    half resultX = screenUV.x + twist;
                    half2 fixUv = half2(resultX , screenUV.y);
                    return fixUv;
                }
 sin(screenUV.y * _Twist) / _Suppression 
を見ての通り、実際にはsin関数に対してはy軸をそのまま渡しているわけではないです。
_Twistの値をかけて、ユーザーが波形の細かさを操作できるようにしてあります。
更に_Suppressionの値で割ることで、波の揺れ幅を抑制してあります。
1
1