Help us understand the problem. What is going on with this article?

【Unity】UniversalRPでカスタムポストプロセスを作る【ZoomBlur】

概要

本記事はUnity #2 Advent Calendar 2019 の15日目の記事です。
今回UnityのUniversalRenderPipeline(以下、UniversalRP)を使用してZoomBlur【咆哮のポストエフェクト】を実装しました。
ZoomBlur.gif

この素晴らしいドラゴンはアセットストアから無料でダウンロードしました。感謝:relaxed:

環境

下記の環境で実装しております。

  • Unity2019.3.0b12
  • Windows10
  • UniversalRP 7.1.5

UniversalRPのVolumeとPostProcessingStackv2の比較

ppsv2に相当する機能が、UniversalRPではVolumeという名前に変わっていました。
他にもクラス名を比較すると下記のような対応関係になっています。

ppsv2 UniversalRenderPipeline
PostProcessEffectSettings VolumeComponent
PostProcessProfile VolumeProfile
PostProcessManager VolumeManager

ppsv2ではPostProcessEffectSettingsPostProcessEffectRendererを継承してカスタムエフェクトを作成しました。
参考:Writing-Custom-Effects

UniversalRPでは公式的にカスタムエフェクトの実装方法は提供されていませんが
下記のような手法で実装できました。

実装

まずはVolumeComponentを継承してカスタムエフェクトのパラメータを定義してみましょう。

VolumeComponent

VolumeComponentを継承してParameterを定義します。

ZoomBlur.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class ZoomBlur : VolumeComponent, IPostProcessComponent
{
    [Range(0f, 100f), Tooltip("強くすることで強いBlurがかかります。")]
    public FloatParameter focusPower = new FloatParameter(0f);

    [Range(0, 10), Tooltip("値が大きいほど綺麗にでますが負荷が高まるので注意してください。")]
    public IntParameter focusDetail = new IntParameter(5);

    [Tooltip("ブラーの中心座標。スクリーンの中心を(0,0)としています。")]
    public Vector2Parameter focusScreenPosition = new Vector2Parameter(Vector2.zero);

    public bool IsActive() => focusPower.value > 0f;

    public bool IsTileCompatible() => false;
}

ScriptableRendererFeatureとScriptableRenderPass

次に独自の描画パスを差し込めるようにするために2つのクラスとシェーダーを実装します。

ZoomBlurRenderFeature.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class ZoomBlurRenderFeature : ScriptableRendererFeature
{
    ZoomBlurPass zoomBlurPass;

    public override void Create()
    {
        zoomBlurPass = new ZoomBlurPass(RenderPassEvent.BeforeRenderingPostProcessing);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        zoomBlurPass.Setup(renderer.cameraColorTarget);
        renderer.EnqueuePass(zoomBlurPass);
    }
}
ZoomBlurPass.cs
public class ZoomBlurPass : ScriptableRenderPass
{
    static readonly string k_RenderTag = "Render ZoomBlur Effects";
    static readonly int MainTexId = Shader.PropertyToID("_MainTex");
    static readonly int TempTargetId = Shader.PropertyToID("_TempTargetZoomBlur");
    static readonly int FocusPowerId = Shader.PropertyToID("_FocusPower");
    static readonly int FocusDetailId = Shader.PropertyToID("_FocusDetail");
    static readonly int FocusScreenPositionId = Shader.PropertyToID("_FocusScreenPosition");
    ZoomBlur zoomBlur;
    Material zoomBlurMaterial;
    RenderTargetIdentifier currentTarget;

    public ZoomBlurPass(RenderPassEvent evt)
    {
        renderPassEvent = evt;
        var shader = Shader.Find("PostEffect/ZoomBlur");
        if (shader == null)
        {
            Debug.LogError("Shader not found.");
            return;
        }
        zoomBlurMaterial = CoreUtils.CreateEngineMaterial(shader);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (zoomBlurMaterial == null)
        {
            Debug.LogError("Material not created.");
            return;
        }

        if (!renderingData.cameraData.postProcessEnabled) return;

        var stack = VolumeManager.instance.stack;
        zoomBlur = stack.GetComponent<ZoomBlur>();
        if (zoomBlur == null) { return; }
        if (!zoomBlur.IsActive()) { return; }

        var cmd = CommandBufferPool.Get(k_RenderTag);
        Render(cmd, ref renderingData);
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    public void Setup(in RenderTargetIdentifier currentTarget)
    {
        this.currentTarget = currentTarget;
    }

    void Render(CommandBuffer cmd, ref RenderingData renderingData)
    {
        ref var cameraData = ref renderingData.cameraData;
        var source = currentTarget;
        int destination = TempTargetId;

        var w = cameraData.camera.scaledPixelWidth;
        var h = cameraData.camera.scaledPixelHeight;
        zoomBlurMaterial.SetFloat(FocusPowerId, zoomBlur.focusPower.value);
        zoomBlurMaterial.SetInt(FocusDetailId, zoomBlur.focusDetail.value);
        zoomBlurMaterial.SetVector(FocusScreenPositionId, zoomBlur.focusScreenPosition.value);

        int shaderPass = 0;
        cmd.SetGlobalTexture(MainTexId, source);
        cmd.GetTemporaryRT(destination, w, h, 0, FilterMode.Point, RenderTextureFormat.Default);
        cmd.Blit(source, destination);
        cmd.Blit(destination, source, zoomBlurMaterial, shaderPass);
    }
}

シェーダー

シェーダーはShaderToyをUnity用にカスタムして使わせていただきました。

ZoomBlur.shader
Shader "PostEffect/ZoomBlur"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        Tags { "RenderPipeline" = "UniversalPipeline"}
        Pass
        {
            CGPROGRAM
                #pragma vertex Vert
                #pragma fragment Frag

                sampler2D _MainTex;
                float2 _FocusScreenPosition;
                float _FocusPower;
                 int _FocusDetail;

                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                };

                struct v2f
                {
                    float2 uv : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                };

                v2f Vert(appdata v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uv = v.uv;
                    return o;
                }

                float4 Frag (v2f i) : SV_Target
                {
                    float2 screenPoint = _FocusScreenPosition + _ScreenParams.xy/2;
                    float2 uv = i.uv;
                    float2 mousePos = (screenPoint.xy / _ScreenParams.xy);
                    float2 focus = uv - mousePos;
                    float4 outColor = float4(0, 0, 0, 1);
                    for (int i=0; i<_FocusDetail; i++) {
                        float power = 1.0 - _FocusPower * (1.0/_ScreenParams.x) * float(i);
                        outColor.rgb += tex2D(_MainTex , focus * power + mousePos).rgb;
                    }
                    outColor.rgb *= 1.0 / float(_FocusDetail);
                    return outColor;
                }
            ENDCG
        }
    }
}

セットアップ

Main CameraのPost Processingはチェックを忘れずに入れておきます。

2019-12-08_23h36_51.png

Volumeの追加

ヒエラルキーの右クリック/Volume/Global Volumeでオブジェクトを追加します。
2019-12-09_11h30_53.png

Add OverrideにてZoom Blurを追加します。
プロファイルが設定されていなければNewボタンでプロファイルアセットを作成します。
2019-12-08_23h32_33.png
(Bloomは趣味でつけてます)

RenderPipelineアセットの作成とRenderFeatureの登録

UniversalRenderPipelineAssetの作成をします。
Create/Rendering/Universal Render Pipeline/Pipeline Asset(Forward Renderer)
2019-12-09_11h33_41.png

同時に作成されるUniversalRenderPipelineAsset_Renderer+ボタンでZoom Blur Render Featureを登録します。
2019-12-09_11h36_24.png

ProjectSettings/Graphics/Scriptable Render Pipeline SettingsUniversalRenderPipelineAssetをセットします。
2019-12-09_11h41_03.png

これで独自のポストプロセス描画パスが反映されるようになりました。

アニメーター連携

アニメーターから連携してZoomBlurのパラメータを操作できるように
コントローラとなるコンポーネントを作成します。

ZoomBlurController.cs
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteAlways]
public class ZoomBlurController : MonoBehaviour
{
    public VolumeProfile volumeProfile; // プロジェクトに作ったPostProcessVolume Profileをアタッチします
    [Range(0f, 100f)]
    public float focusPower = 10f;
    [Range(0, 10)]
    public int focusDetail = 5;
    public Vector2 focusScreenPosition = Vector2.zero;
    ZoomBlur zoomBlur;

    void Update()
    {
        if (volumeProfile == null) return;
        if (zoomBlur == null) volumeProfile.TryGet<ZoomBlur>(out zoomBlur);
        if (zoomBlur == null) return;

        zoomBlur.focusPower.value = focusPower;
        zoomBlur.focusDetail.value = focusDetail;
        zoomBlur.focusScreenPosition.value = focusScreenPosition;
    }
}

2019-12-14_17h23_50.png

プロジェクトデータ

サンプルプロジェクトデータをGitHubにアップしましたので
ご自由にお使いください。

参考サイト様

追記

環境

  • Unity2019.3.3f1
  • UniversalRP 7.2.1

ppv2互換性について

  • UniversalRenderPipelineAssetPost-processingIntegratedのほかに、Post Processing V2を選択可能です。
    2020-03-08_17h26_29.png
    2020-03-08_17h27_08.png

  • Post Processing V2を選択すると警告がでています。将来的には廃止するのであくまで互換性維持のために用いるべきとでています。

  • ppv2を使用すると、UniversalRP 7.2.0から同じく使用可能になった Camera Stackingが 使用不可 になるようです。
    2020-03-08_17h38_58.png

gumi
Python、Erlang、Elixir などちょっと変わった技術でゲームをつくったりする会社。プログラマだけじゃなく、企画、デザイン、イラストなど開発全般揃ってます。
http://gu3.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした