Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

【Unity】URPを拡張して、Normal描画パスを追加してみた

はじめに

Unityの従来のレンダリングパイプラインでは、 _CameraDepthNormalsTexture を利用することで画面のノーマル情報を取得できます.

test.shader
sampler2D _CameraDepthNormalsTexture;

この_CameraDepthNormalsTexture ですが、残念ながら URP (Universal Render Pipeline)ではサポートされていません

作ったもの

今回は勉強がてら、URPのレンダリングパイプラインを拡張してノーマル情報をとれるようにしてみました。

シェーダーに _CameraWorldNormalTexture プロパティを追加すると、ワールドスペースの法線情報が取得できます。
image.png

環境

Universal RP 8.2.0
Unity 2020.1.2f1

実装方法

  1. URPのLit.shader を改造して、メッシュ法線をレンダリングするパス(NormalOnly)を追加
  2. ScriptableRendererFeature を利用して、自作パス(NormalOnly)を実行し、Normalのみレンダリング
  3. レンダーターゲットに _CameraWorldNormalTexture を指定

シェーダーに_CameraWorldNormalTexture を定義することで、法線のレンダリング結果を取得できるようになります。

STEP1 : URPを編集できるようにする

今回はURPパッケージの中身に手を加えたいので、URPを編集可能な状態にします。

Universal RPフォルダを、Assetsフォルダと同階層のPackagesフォルダの中にコピーするだけです。

Unityプロジェクト
├─Assets
├─Library
├─Packages
└─ProjectSettings

image.png

参考 : ローカルパッケージのインストール
https://docs.unity3d.com/ja/2019.4/Manual/upm-ui-local.html

Packagesフォルダの中にUniversal RPパッケージを配置すると、Package Manager上で Universal RP に In Development ラベルが付きます。
image.png

以上で、Universal RP を編集する準備が整いました。

STEP2 : ノーマル描画HLSLの追加

まずはNormalOnlyPass.hlsl を作成します。
これは3DモデルのNormalだけを描画するHLSLシェーダーです。

NormalOnlyPass.hlsl
#ifndef UNIVERSAL_DEPTH_ONLY_PASS_INCLUDED
#define UNIVERSAL_DEPTH_ONLY_PASS_INCLUDED

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

struct Attributes
{
    float4 positionOS   : POSITION;
    float3 normalOS     : NORMAL;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings
{
    float4 positionCS   : SV_POSITION;
    float3 normalWS     : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

Varyings NormalOnlyVertex(Attributes input)
{
    Varyings output = (Varyings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

    output.positionCS = TransformObjectToHClip(input.positionOS.xyz); 
    output.normalWS = TransformObjectToWorldNormal(input.normalOS.xyz); // object space normal -> world space normal

    return output;
}

half4 NormalOnlyFragment(Varyings input) : SV_TARGET
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    return half4(input.normalWS, 1);
}
#endif

NormalOnlyPass.hlslの追加先

作成した NormalOnlyPass.hlsl は Universal RP フォルダ内の Shaders フォルダの中に追加します。
同じ階層にはLit.shaderDepthOnlyPass.hlsl などがあります。

image.png

STEP3 : Lit.shaderにPassを追加

先ほど追加した NormalOnlyPass.hlsl は .shader ファイルに埋め込んで実行させる必要があります。
今回は、URPで最もスタンダードな Lit.shader から NormalOnlyPass.hlsl を実行させたいと思います。

補足 : URPプロジェクトのSampleSceneの3dモデルではLit.shaderが使われています。
image.png

Lit.shader に以下のPassを追加します。

Lit.shaderに追加するPass
Pass
{
    Name "NormalOnly"
    Tags{"LightMode" = "NormalOnly"}

    ZWrite On
    Cull[_Cull]

    HLSLPROGRAM
    // Required to compile gles 2.0 with standard srp library
    #pragma prefer_hlslcc gles
    #pragma exclude_renderers d3d11_9x
    #pragma target 2.0

    #pragma vertex NormalOnlyVertex
    #pragma fragment NormalOnlyFragment

    //--------------------------------------
    // GPU Instancing
    #pragma multi_compile_instancing

    #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/NormalOnlyPass.hlsl"
    ENDHLSL
}

上記Passの Tags{"LightMode" = "NormalOnly"} はレンダラーからレンダリング対象としてPassを識別させる際に必要な情報になります。

STEP4 : レンダーパス(ScriptableRendererPass)を追加

.shaderに埋め込んだPassはそのままでは実行されません。Unityのレンダラーにレンダリングを実行させる必要があります。

レンダリングを実行させるには、 ScriptableRendererFeatureScriptableRenderPass を利用します。

まずは NormalOnlyPass.cs(ScriptableRenderPass) を追加します。
この NormalOnlyPass.cs はLit.shaderが持っているNormalOnlyパスに対し、レンダリング命令を発行するものです。

NormalOnlyPass.cs
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class NormalOnlyPass : ScriptableRenderPass
{
    private const string m_ProfilerTag = "[DEV] Normal Only Pass"; // フレームデバッガ―上の名前表示
    private ProfilingSampler m_ProfilingSampler = new ProfilingSampler(m_ProfilerTag);
    private ShaderTagId m_ShaderTagId = new ShaderTagId("NormalOnly"); // NormalOnlyタグを持っているシェーダーパスを実行する
    private FilteringSettings m_FilteringSettings; // レンダリングのフィルタリング設定
    private RenderTargetHandle depthAttachmentHandle; // RenderTextureの識別用struct
    private RenderTextureDescriptor descriptor; // 作成するRenderTextureの情報を入れるstruct

    /// <summary>
    /// Create Normal Only Pass
    /// </summary>
    public NormalOnlyPass(RenderPassEvent evt, RenderQueueRange renderQueueRange, LayerMask layerMask)
    {
        m_FilteringSettings = new FilteringSettings(renderQueueRange, layerMask);
        renderPassEvent = evt;
    }

    /// <summary>
    /// Configure the pass
    /// </summary>
    public void Setup(
        RenderTextureDescriptor baseDescriptor,
        RenderTargetHandle depthAttachmentHandle)
    {
        this.depthAttachmentHandle = depthAttachmentHandle;
        baseDescriptor.colorFormat = RenderTextureFormat.ARGB32; // ARGB32は RGBAチャンネルそれぞれに8bitずつ入るテクスチャフォーマット
        baseDescriptor.depthBufferBits = 32; // デプスバッファを32bitにする(Zテストに必要)

        baseDescriptor.msaaSamples = 1; // MSAAは使用しない
        descriptor = baseDescriptor;
    }

    // This method is called before executing the render pass.
    // It can be used to configure render targets and their clear state. Also to create temporary render target textures.
    // When empty this render pass will render to the active camera render target.
    // You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
    // The render pipeline will ensure target setup and clearing happens in an performance manner.
    public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    {
        // RenderTextureの確保
        cmd.GetTemporaryRT(depthAttachmentHandle.id, descriptor, FilterMode.Point);
        ConfigureTarget(depthAttachmentHandle.Identifier());
        ConfigureClear(ClearFlag.All, Color.black);
    }

    // Here you can implement the rendering logic.
    // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
    // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
    // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag);
        using (new ProfilingScope(cmd, m_ProfilingSampler))
        {
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            var sortFlags = renderingData.cameraData.defaultOpaqueSortFlags;
            var drawSettings = CreateDrawingSettings(m_ShaderTagId, ref renderingData, sortFlags);
            drawSettings.perObjectData = PerObjectData.None;

            context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref m_FilteringSettings);

        }
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    /// Cleanup any allocated resources that were created during the execution of this render pass.
    public override void FrameCleanup(CommandBuffer cmd)
    {
        if (cmd == null)
            throw new ArgumentNullException("cmd");

        // RenderTextureの解放
        if (depthAttachmentHandle  != RenderTargetHandle.CameraTarget)
        {
            cmd.ReleaseTemporaryRT(depthAttachmentHandle.id);
            depthAttachmentHandle = RenderTargetHandle.CameraTarget;
        }
    }
}

STEP5 : ScriptableRendererFeatureを追加

作成したNormalOnlyPassはそのままでは実行されません。
Unityのレンダラーに認識させる必要があります。

認識させるためには以下のような手順を踏みます
1. ScriptableRendererFeature を作成
2. ScriptableRendererFeatureNormalOnlyPass を組み込んで実行させる
3. ForwardRendererDataScriptableRendererFeature を登録

ScriptableRendererFeatureの作成

以下の MyRendererFeature.cs を作成し、Assets/フォルダ以下の好きな場所に配置します。

MyRendererFeature.cs
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class MyRendererFeature : ScriptableRendererFeature
{
    [SerializeField] private MyFeatureSettings settings = new MyFeatureSettings();
    private NormalOnlyPass normalOnlyPass = null;
    private LayerMask layerMask;
    private RenderTargetHandle m_NormalTexture;

    [Serializable]
    public class MyFeatureSettings
    {
        public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingOpaques; 
        public LayerMask layerMask = -1; // Everything
    }

    public override void Create()
    {
        normalOnlyPass = new NormalOnlyPass(settings.renderPassEvent, RenderQueueRange.opaque, settings.layerMask);
        m_NormalTexture.Init("_CameraWorldNormalTexture"); // 内部では Shader.PropertyToID() が呼ばれている
    }

    // Here you can inject one or multiple render passes in the renderer.
    // This method is called when setting up the renderer once per-camera.
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        var cameraData = renderingData.cameraData;
        if (cameraData.isPreviewCamera || cameraData.isSceneViewCamera) return;

        var cameraTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;
        normalOnlyPass.Setup(cameraTargetDescriptor, m_NormalTexture);
        renderer.EnqueuePass(normalOnlyPass);
    }
}

補足 : レンダリング先の指定について

補足 : レンダリング先の指定について

以下のコードで、シェーダープロパティの_CameraWorldNormalTexture を作成しています。
Init メソッドの内部では Shader.PropertyToID() が呼ばれているので、実際にはint型のIDとしてシェーダープロパティが格納されます。

MyRendererFeature.cs
m_NormalTexture.Init("_CameraWorldNormalTexture"); // 内部では Shader.PropertyToID() が呼ばれている

以下のコードではNormalOnlyPassm_NormalTexture を登録しています。
これにより、_CameraWorldNormalTexture に対してワールド法線のレンダリングが行われるようになります。

MyRendererFeature.cs
normalOnlyPass.Setup(cameraTargetDescriptor, m_NormalTexture);
renderer.EnqueuePass(normalOnlyPass);

RendererFeatureの登録

MyRendererFeature.cs を作成したら、
次は ForwardRendererData アセット に MyRendererFeature を登録します。

image.png
image.png

以上で URPの拡張は終わりです。

Frame Debuggerで描画を確認する

Frame Debugger を見ると、 NormalOnlyPass のレンダリング結果を見ることができます。

Frame Debuggerの起動

Window -> Analysis -> Frame Debugger を選択
image.png

ウィンドウ左上のEnable をクリック
image.png

ドローコールの一覧が表示されます。
image.png

[DEV] Normal Only Pass という項目を選択すると、今回の NormalOnlyPass が描画したものを見ることができます。
image.png

モデルの法線情報だけが表示されていることが確認できます。ここに表示されているのはワールド空間の法線情報です。
image.png

ShaderGraphでノーマル情報を取得してみる

フレームデバッガーの上の方に注目すると、RenderTarget _CameraWorldNormalTexture という表示があります。

「この描画パスのレンダリングのターゲットは _CameraWorldNormalTexture です」  ということを意味するメッセージです。

この _CameraWorldNormalTexture をShaderGraphから取得してみましょう。

image.png

ShaderGraphの作成

メニューから
Create -> Shader -> Unlit Graph
を選択して、Unlitのシェーダーグラフを作成します。
image.png

_CameraWorldNormalTexture の追加

シェーダープロパティに以下のようなTexture2Dプロパティを追加します。
image.png

NormalTexをSample Texture 2Dノードに接続し、Unlit MasterのColorへ出力します。
image.png

Unlit Master の設定はデフォルトのままで大丈夫です。
image.png

NormalOnlyPassにてワールド法線のレンダリングのタイミングを RenderPassEvent.BeforeRenderingOpaques に設定しているため、
色が正しく取得できるはずです。

シェーダーグラフ絡めてリアルを作成し、適当なオブジェクトに貼り付けると、GameViewでワールド法線が表示されます。
image.png

補足 : Sceneビューでワールドノーマルが表示されない理由について

Sceneビューだと、NormalOnlyPassの描画がチラチラしてしまう問題が発生してしまうため、
シーンカメラ(プレビューカメラ)ではNormalOnlyPassをスキップしています。

MyRendererFeature.cs
// Here you can inject one or multiple render passes in the renderer.
// This method is called when setting up the renderer once per-camera.
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
    var cameraData = renderingData.cameraData;
    if (cameraData.isPreviewCamera || cameraData.isSceneViewCamera) return;

    var cameraTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;
    normalOnlyPass.Setup(cameraTargetDescriptor, m_NormalTexture);
    renderer.EnqueuePass(normalOnlyPass);
}
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