-->


Shader "UI/Glow3Pass"
{
Properties
{
[HideInInspector] _MainTex("-", 2D) = "white"{} //uGUI及びGraphics.Blitが標準で扱うテクスチャ
[HideInInspector] _CustomTex("-", 2D) = "white"{}
[HideInInspector] _Luminance("-", float) = 0
[HideInInspector] _BlurCount("-", float) = 0
[HideInInspector] _BlurDistance("-", float) = 0
[HideInInspector] _BlurPower("-", float) = 0
}
SubShader
{
Tags
{
"RenderPipeline" = "UniversalPipeline"
"Queue" = "Transparent"
}
Cull Off
ZWrite Off
HLSLINCLUDE
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
//uGUI Image > Source Imageにアタッチされた画像を参照できるようにする
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
//最終的に画面に出力されるテクスチャを宣言、中身のデータはC#側から渡される
TEXTURE2D(_CustomTex);
SAMPLER(sampler_CustomTex);
//C#から渡された_CustomTexのサイズを参照できるようにする
float4 _CustomTex_TexelSize;
//SRP Batcherを適用、動的に変更される数値はこの中に定義
CBUFFER_START(UnityPerMaterial)
half _Luminance;
int _BlurCount;
half _BlurDistance;
half _BlurPower;
CBUFFER_END
static const half MaxLuminance = 10;
static const half HalfLuminance = MaxLuminance / 2;
static const half MaxBlurPower = 10;
struct Attributes
{
half2 uv : TEXCOORD0;
float4 positionOS : POSITION;
half4 color : COLOR;
};
struct Varyings
{
half2 uv : TEXCOORD0;
float4 positionCS : SV_POSITION;
half4 color : COLOR;
};
Varyings vert (Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = input.uv;
output.color = input.color;
return output;
}
//第二引数で指定した方向に画像をぼかす
half4 executeBlur(half2 uv, half2 direction)
{
//_BlurDistanceに関しては_Luminanceの値も影響させる
//_Luminanceの値が大きいほど、一定程度までBlurの距離もどんどん伸びる
half clampLuminance = clamp(_Luminance, 0, HalfLuminance) / HalfLuminance;
half fixBlurDistance = _BlurDistance * clampLuminance;
half4 color = 0;
for (int shift = -_BlurCount; shift <= _BlurCount; shift++)
{
half2 offset = direction * shift * fixBlurDistance * _CustomTex_TexelSize;
color += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + offset);
}
//加算した回数分だけ割って平均色を出す
color /= (_BlurCount * 2 + 1);
//アルファ値に関してはBlorPowerに合わせて適切に弱める
color.a *= (_BlurPower / MaxBlurPower);
return color;
}
//直線的なoriginalColor > finalColorの上昇から、Easeをかけた曲線的な上昇に変換する
half3 cubicIn(half curParam, half maxParam, half3 originCol, half3 finalCol)
{
finalCol -= originCol;
curParam /= maxParam;
return finalCol * curParam * curParam * curParam + originCol;
}
ENDHLSL
//Pass0: 一番上のPassはuGUIのCanvasRendererから自動的に呼び出される、処理的には下のPass1,2より後に実行される
Pass
{
//一番最後は背景とブレンドして透過処理を行う
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
//オリジナルの画像を上に被せた上で、全てのピクセル色の輝度を上げる
half4 frag (Varyings input) : SV_Target
{
//CanvasRendererから呼ばれた際は、_MainTexにはPass1, 2実行前の純粋な元の画像が代入されている
//この_MainTexから指定したUV座標のピクセル色を取得
half4 original = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
//Pass1,2でBlur処理を通した画像から、指定したUV座標のピクセル色を取得
half4 blurred = SAMPLE_TEXTURE2D(_CustomTex, sampler_CustomTex, input.uv);
//Blur画像の上に元の画像を重ねる
//例えば指定UV座標のalphaが1なら、完全に元の画像のピクセルを描画、0なら完全なBlur画像を描画
//0.5なら両者の中間のピクセル色が描画される
half4 combine = lerp(blurred, original, original.a);
//まず掛け算を使って、RGBの値をそれぞれの強度に応じて相応に増加させる
//この際輝度の上がり方が急すぎるので、Easeを使って少し序盤の輝度を上昇力を弱める
half3 finalCol = combine.rgb + (combine.rgb * MaxLuminance * 2);
combine.rgb = cubicIn(_Luminance, MaxLuminance, combine.rgb, finalCol);
//_Luminanceが7を超えてからは、足し算で純粋にRGBを増やし白飛びさせる
half addWhite = saturate((_Luminance - 7) / 3);
combine.rgb += addWhite;
return combine;
}
ENDHLSL
}
//Pass1: C#から明示的に呼び出す
Pass
{
//背景は考慮せず画像単体で描画処理
Blend One Zero
HLSLPROGRAM
//x軸にBlurをかける
half4 frag (Varyings input) : SV_Target
{
return executeBlur(input.uv, half2(1, 0));
}
ENDHLSL
}
//Pass2: C#から明示的に呼び出す
Pass
{
//背景は考慮せず画像単体で描画処理
Blend One Zero
HLSLPROGRAM
//y軸にBlurをかける
half4 frag (Varyings input) : SV_Target
{
return executeBlur(input.uv, half2(0, 1));
}
ENDHLSL
}
}
}

using System;
using UnityEngine;
using UnityEngine.UI;
[ExecuteAlways]
/// <summary>
/// RenderTextureをシェーダーに流し込んで画像を発光させる処理
/// </summary>
public class Glow3PassController : MonoBehaviour
{
[Serializable]
/// <summary>
/// シェーダーに渡すパラメータはこちらで管理
/// </summary>
private class Parameters
{
[SerializeField, Range(0, 10)] private float luminance = 5;
public float Luminance => luminance;
[SerializeField, Range(0, 10)] private int blurCount = 5;
public int BlurCount => blurCount;
[SerializeField, Range(0, 10)] private float blurDistance = 3;
public float BlurDistance => blurDistance;
[SerializeField, Range(0, 10)] private float blurPower = 5;
public float BlurPower => blurPower;
// 変更があったときだけ更新するためのキャッシュ
private float _lastLuminance;
private int _lastBlurCount;
private float _lastBlurPower;
private float _lastBlurDistance;
/// <summary>
/// インスペクターのパラメータに変化があった場合trueを返す
/// </summary>
public bool CahngedParameter => !Mathf.Approximately(luminance, _lastLuminance) || blurCount != _lastBlurCount ||
!Mathf.Approximately(blurPower, _lastBlurPower) || !Mathf.Approximately(blurDistance, _lastBlurDistance);
/// <summary>
/// キャッシュに最新のパラメーター値を反映させる
/// </summary>
public void UpdateParameter()
{
_lastLuminance = luminance;
_lastBlurCount = blurCount;
_lastBlurPower = blurPower;
_lastBlurDistance = blurDistance;
}
}
[SerializeField] private Image targetImage;
[SerializeField] private Shader targetShader;
[SerializeField] private Parameters parameters;
private Texture2D sourceTexture;
private Material createdMaterial;
private RenderTexture tmpRenderTexture1;
private RenderTexture tmpRenderTexture2;
/// <summary>
/// 初期化処理の必要性がある場合trueを返す
/// </summary>
private bool NeedsInitialization => sourceTexture == null || sourceTexture != targetImage.sprite.texture ||
createdMaterial == null || targetImage.material != createdMaterial ||
tmpRenderTexture1 == null || tmpRenderTexture2 == null;
/// <summary>
/// 初期化処理
/// </summary>
private void Init()
{
Release();
sourceTexture = targetImage.sprite.texture;
createdMaterial = new (targetShader);
createdMaterial.hideFlags = HideFlags.DontSave;
targetImage.material = createdMaterial;
//処理負荷軽減のため、大元の画像サイズから1/2のサイズでtmpRenderTexture1, 2を作成
//tmpRenderTexture1, 2は両方ともぼかし処理のための画像なので、サイズが小さくても見た目に違和感は出ない
tmpRenderTexture1 = new RenderTexture(sourceTexture.width / 2, sourceTexture.height /2, 0);
tmpRenderTexture2 = new RenderTexture(sourceTexture.width / 2, sourceTexture.height / 2, 0);
//事前にBlitで更新される予定のtmpRenderTexture2を、_CustomTexの名前でシェーダー側と紐づけておく
//後はCanvasRender内の処理で自動的にPass0を実行させて、その_CustomTexを画像に反映させる
targetImage.material.SetTexture("_CustomTex", tmpRenderTexture2);
//初期化後は問答無用でUpdateTextureを呼ぶ
UpdateTexture();
}
/// <summary>
/// 最初にUnityが1回だけ呼ぶ処理
/// </summary>
private void Start()
{
if (NeedsInitialization)
{
Init();
}
}
/// <summary>
/// Unityから毎フレーム呼ばれる処理
/// </summary>
private void Update()
{
if (targetImage == null || targetImage.sprite == null || targetShader == null)
{
return;
}
if (NeedsInitialization)
{
Init();
}
else if(parameters.CahngedParameter)
{
UpdateTexture();
}
}
/// <summary>
/// 対象のシェーダーのPass1, 2を実行させる
/// </summary>
private void UpdateTexture()
{
parameters.UpdateParameter();
//輝度をセット
createdMaterial.SetFloat("_Luminance", parameters.Luminance);
//Blur設定をセット
createdMaterial.SetInt("_BlurCount", parameters.BlurCount);
createdMaterial.SetFloat("_BlurPower", parameters.BlurPower);
createdMaterial.SetFloat("_BlurDistance", parameters.BlurDistance);
//第1引数の画像を元に、Blitでシェーダー内のPass1を実行させ、結果を第2引数に書き込む
//Blitの仕様上、この書き込み処理は、第3引数のMaterial内の_MainTexを通して行われる
Graphics.Blit(sourceTexture, tmpRenderTexture1, createdMaterial, 1);
//Pass1の結果を元にPass2を実行
Graphics.Blit(tmpRenderTexture1, tmpRenderTexture2, createdMaterial, 2);
}
/// <summary>
/// 作成したRenderTextureやMaterialのメモリを解放する
/// </summary>
private void Release()
{
if (tmpRenderTexture1 != null)
{
tmpRenderTexture1.Release();
tmpRenderTexture2.Release();
}
if (createdMaterial != null)
{
DestroyImmediate(createdMaterial);
}
}
/// <summary>
/// このGameObjectを非表示にした時の処理
/// </summary>
private void OnDisable()
{
Release();
}
}



