【Unity】カスタムポストプロセスで画面を水が流れるエフェクトを作る

投稿者: | 2024-02-15

前の記事の画面を水が流れるエフェクトをカスタムポストプロセスで作ってみました。

水流のオブジェクトにプレイヤーがあたったときにエフェクトを表示します。

カスタムポストプロセスを作る

C# Post Process Volumeと、HDRP Post Processを作成します。

ポストプロセスボリュームを編集

ポストプロセスボリュームの「kShaderName」変数に作成したシェーダー名を入力します。またインジェクションポイントを「AfterPostProcess」に設定しました。

ポストプロセスボリュームでは、Renderメソッドでポストプロセスシェーダーで定義するプロパティに値をセットできます。

    public override void Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination)
    {
        if (m_Material == null)
            return;
        
        m_Material.SetFloat("_Intensity", intensity.value);
        m_Material.SetFloat("_Speed", speed.value);
        m_Material.SetFloat("_Strength", strength.value);      
        m_Material.SetVector("_Tiling", tiling.value);
        m_Material.SetTexture("_NormalMap", tex.value);
        m_Material.SetTexture("_MainTex", source);

フィールドを公開して、この値をインスペクタや他のスクリプトから設定できるようにしています。

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
using System;

[Serializable, VolumeComponentMenu("Post-processing/Custom/FullScreenWaterDistortion")]
public sealed class FullScreenWaterDistortion : CustomPostProcessVolumeComponent, IPostProcessComponent
{
    [Tooltip("Controls the intensity of the effect.")]
    public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f);
    public ClampedFloatParameter strength = new ClampedFloatParameter(0f, 0f, 1f);
    public ClampedFloatParameter speed = new ClampedFloatParameter(1f, 0f, 100f);
    public Vector2Parameter tiling = new(value: Vector2.zero);
    public Texture2DParameter tex = new(value: null);

クラス名の前にVolumeComponentMenu属性をつけると、VolumeコンポーネントのAdd Overrideのポップアップメニューにエフェクトを表示できるようになります。

[Serializable, VolumeComponentMenu("Post-processing/Custom/FullScreenWaterDistortion")]

リストにカスタムポストプロセスを追加

プロジェクト設定のカスタムポストプロセスのリストにエフェクトを追加します。Edit > Project Settings… でプロジェクト設定が表示されます。HDRP Global Settingsの「Custom Post Process Orders」の「+」ボタンから、作成したエフェクトを追加します。今回はポストプロセスボリュームで設定した「After Post Process」に追加しました。

シェーダーを編集

ポストプロセスシェーダーを編集します。

Shader "Hidden/Shader/FullScreenWaterStream3"
{
    Properties
    {
        // This property is necessary to make the CommandBuffer.Blit bind the source texture to _MainTex
        _MainTex("Main Texture", 2DArray) = "grey" {}
        _NormalMap("Normal Map", 2D) = "bump" {}
        _Intensity("Intensity", Range(0.0, 1.0)) = 1
        _Strength("Strength", float) = 0.03
        _Speed("Speed", float) = 1
        _Tiling("Tiling", Vector) = (1.0, 0.1, 0.0, 0.0)
    }

    HLSLINCLUDE

    #pragma target 4.5
    #pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch

    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/PostProcessing/Shaders/FXAA.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/PostProcessing/Shaders/RTUpscale.hlsl"

    struct Attributes
    {
        uint vertexID : SV_VertexID;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };

    struct Varyings
    {
        float4 positionCS : SV_POSITION;
        float2 texcoord   : TEXCOORD0;
        UNITY_VERTEX_OUTPUT_STEREO
    };

    Varyings Vert(Attributes input)
    {
        Varyings output;
        UNITY_SETUP_INSTANCE_ID(input);
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
        output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID);
        output.texcoord = GetFullScreenTriangleTexCoord(input.vertexID);
        return output;
    }

    // List of properties to control your post process effect
    float _Intensity;
    float _Strength;
    float4 _Tiling;
    float _Speed;
    TEXTURE2D_X(_MainTex);
    TEXTURE2D(_NormalMap);

    float4 CustomPostProcess(Varyings input) : SV_Target
    {
        UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
        
        // uvをスクロールさせる
        float2 uv = float2(input.texcoord.x, input.texcoord.y + _Time.x * _Speed) * _Tiling.xy;

        // テクスチャをサンプリング
        float4 normal = SAMPLE_TEXTURE2D(_NormalMap, s_linear_repeat_sampler, uv);

        // 強さをかける
        float2 offset = UnpackNormal(normal).xy * _Intensity * _Strength;

        // uvをずらしてシーンカラーを取得
        float3 sourceColor = SAMPLE_TEXTURE2D_X(_MainTex, s_linear_clamp_sampler, ClampAndScaleUVForBilinearPostProcessTexture(input.texcoord.xy + offset)).xyz;

        return float4(sourceColor, 1);
    }

    ENDHLSL

    SubShader
    {
        Tags{ "RenderPipeline" = "HDRenderPipeline" }
        Pass
        {
            Name "FullScreenWaterStream3"

            ZWrite Off
            ZTest Always
            Blend Off
            Cull Off

            HLSLPROGRAM
                #pragma fragment CustomPostProcess
                #pragma vertex Vert
            ENDHLSL
        }
    }
    Fallback Off
}

まず必要なプロパティを定義しています。

Shader "Hidden/Shader/FullScreenWaterStream3"
{
    Properties
    {
        // This property is necessary to make the CommandBuffer.Blit bind the source texture to _MainTex
        _MainTex("Main Texture", 2DArray) = "grey" {}
        _NormalMap("Normal Map", 2D) = "bump" {}
        _Intensity("Intensity", Range(0.0, 1.0)) = 1
        _Strength("Strength", float) = 0.03
        _Speed("Speed", float) = 1
        _Tiling("Tiling", Vector) = (1.0, 0.1, 0.0, 0.0)
    }

シェーダーでは、前の記事のシェーダーグラフと同様の処理をします。

    float4 CustomPostProcess(Varyings input) : SV_Target
    {
        UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
        
        // uvをスクロールさせる
        float2 uv = float2(input.texcoord.x, input.texcoord.y + _Time.x * _Speed) * _Tiling.xy;

        // テクスチャをサンプリング
        float4 normal = SAMPLE_TEXTURE2D(_NormalMap, s_linear_repeat_sampler, uv);

        // 強さをかける
        float2 offset = UnpackNormal(normal).xy * _Intensity * _Strength;

        // uvをずらしてシーンカラーを取得
        float3 sourceColor = SAMPLE_TEXTURE2D_X(_MainTex, s_linear_clamp_sampler, ClampAndScaleUVForBilinearPostProcessTexture(input.texcoord.xy + offset)).xyz;

        return float4(sourceColor, 1);
    }

_Timeの値をテクスチャ座標のyに足してスクロールさせます。その座標でノーマルマップをサンプリングしています。

        
        // uvをスクロールさせる
        float2 uv = float2(input.texcoord.x, input.texcoord.y + _Time.x * _Speed) * _Tiling.xy;

        // テクスチャをサンプリング
        float4 normal = SAMPLE_TEXTURE2D(_NormalMap, s_linear_repeat_sampler, uv);

UnpackNormal関数を使ってテクスチャから法線情報を取得しています。

        // 強さをかける
        float2 offset = UnpackNormal(normal).xy * _Intensity * _Strength;

ノーマルマップの値によって元のテクスチャ座標をずらしてソーステクスチャをサンプリングしています。これによってカメラの像像に歪みが生じます。

        float3 sourceColor = SAMPLE_TEXTURE2D_X(_MainTex, s_linear_clamp_sampler, ClampAndScaleUVForBilinearPostProcessTexture(input.texcoord.xy + offset)).xyz;

        return float4(sourceColor, 1);

Volumeオーバーライドを追加

Volumeコンポーネントの「Add Override」ボタンから追加したエフェクトを選択します。プロファイルは「New」ボタンで新規作成できます。

オーバーライドしたいプロパティの横のチェックを入れて、値を入力します。ALLをクリックすると、すべてのチェックが入ります。ノーマルマップもアタッチしています。

ノーマルマップを作成

ノーマルマップは水の模様のタイリングテクスチャから作成しました。GIMPのフィルターで簡単に法線マップにできます。

Unityにインポートしたら、インポート設定のTexture Typeを「Normal map」に設定します。

エフェクトの再生/停止

プレイヤーが流水のトリガーコライダーに出入りすることでエフェクトの表示を切り替えます。

Volumeコンポーネントを持つゲームオブジェクトにスクリプトを追加します。ポストプロセスボリュームで公開したフィールドに値を入れることで、シェーダーのプロパティの値を変えられます。

using System.Collections;
using UnityEngine;
using UnityEngine.Rendering;

public class StreamingWaterDistortion2 : WaterEffectBase
{
    // ポストプロセスボリューム
    FullScreenWaterDistortion waterDistortion;

    // Start is called before the first frame update
    void Start()
    {
       var volume = GetComponent<Volume>();

        // ポストプロセスボリュームを取得
        volume.profile.TryGet(out waterDistortion);
    }

    public override void StartWaterEffect()
    {
        // エフェクトを最大にする
        waterDistortion.intensity.value = 1f;
    }

    public override void FadeOutWaterEffect()
    {
        // コルーチンを開始
        StartCoroutine("FadeOutEffect");
    }

     IEnumerator FadeOutEffect()
    {

        // Intensityを徐々に下げる
        while (waterDistortion.intensity.value > 0f)
        {
            waterDistortion.intensity.value -= Time.deltaTime * 0.5f;
            
            yield return null;
        }

        waterDistortion.intensity.value = 0f;
    }
}

まずStartメソッドでVolumeコンポーネントから、ポストプロセスボリュームを取得します。

using System.Collections;
using UnityEngine;
using UnityEngine.Rendering;

public class StreamingWaterDistortion2 : WaterEffectBase
{
    // ポストプロセスボリューム
    FullScreenWaterDistortion waterDistortion;

    // Start is called before the first frame update
    void Start()
    {
       var volume = GetComponent<Volume>();

        // ポストプロセスボリュームを取得
        volume.profile.TryGet(out waterDistortion);
    }

エフェクトを表示するメソッドとフェードアウトを開始するメソッドがあります。

    public override void StartWaterEffect()
    {
        // エフェクトを最大にする
        waterDistortion.intensity.value = 1f;
    }

    public override void FadeOutWaterEffect()
    {
        // コルーチンを開始
        StartCoroutine("FadeOutEffect");
    }

intensityを上げることでエフェクトを表示しています。intensityなどのプロパティはポストプロセスボリュームで公開されています。

// ポストプロセスボリューム
public sealed class FullScreenWaterDistortion : CustomPostProcessVolumeComponent, IPostProcessComponent
{
    [Tooltip("Controls the intensity of the effect.")]
    public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f);

コルーチンを使ってこの値を徐々に小さくすることでフェードアウトさせています。

    IEnumerator FadeOutEffect()
    {

        // Intensityを徐々に下げる
        while (waterDistortion.intensity.value > 0f)
        {
            waterDistortion.intensity.value -= Time.deltaTime * 0.5f;
            
            yield return null;
        }

        waterDistortion.intensity.value = 0f;
    }
}

水流にあたったときに画面エフェクトを表示

水流のオブジェクトと重なるようにBoxコライダーを配置しています。

コライダーはIs Triggerのチェックが入っています。専用のタグで識別します。プライヤーと衝突できるレイヤーも設定されています。

プレイヤーにスクリプトをつける

プレイヤーのスクリプトでBoxコライダーの当たり判定をします。タグで識別して、エフェクトの再生やフェードアウトのメソッドを呼んでいます。

using UnityEngine;


public class WaterInteractionHandler : MonoBehaviour
{ 
    [SerializeField] WaterEffectBase effect;

    private void OnTriggerEnter(Collider other)
    {
        if (other.tag != "WaterStream") return;

        // エフェクトを開始
        effect.StartWaterEffect();
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.tag != "WaterStream") return;

        // エフェクトをフェードアウト
        effect.FadeOutWaterEffect();
    }
}

これでカスタムポストプロセスで画面を流れる水のエフェクトが作れました。

コメントを残す

メールアドレスが公開されることはありません。