はじめに
Unityの従来のレンダリングパイプラインでは、 _CameraDepthNormalsTexture を利用することで画面のノーマル情報を取得できます.
sampler2D _CameraDepthNormalsTexture;
この_CameraDepthNormalsTexture ですが、残念ながら URP (Universal Render Pipeline)ではサポートされていません
作ったもの
今回は勉強がてら、URPのレンダリングパイプラインを拡張してノーマル情報をとれるようにしてみました。
シェーダーに _CameraWorldNormalTexture プロパティを追加すると、ワールドスペースの法線情報が取得できます。
環境
Universal RP 8.2.0
Unity 2020.1.2f1
実装方法
- URPの
Lit.shader
を改造して、メッシュ法線をレンダリングするパス(NormalOnly
)を追加 -
ScriptableRendererFeature
を利用して、自作パス(NormalOnly
)を実行し、Normalのみレンダリング - レンダーターゲットに
_CameraWorldNormalTexture
を指定
シェーダーに_CameraWorldNormalTexture
を定義することで、法線のレンダリング結果を取得できるようになります。
STEP1 : URPを編集できるようにする
今回はURPパッケージの中身に手を加えたいので、URPを編集可能な状態にします。
Universal RPフォルダを、Assetsフォルダと同階層のPackagesフォルダの中にコピーするだけです。
Unityプロジェクト
├─Assets
├─Library
├─Packages
└─ProjectSettings
参考 : ローカルパッケージのインストール
https://docs.unity3d.com/ja/2019.4/Manual/upm-ui-local.html
Packagesフォルダの中にUniversal RPパッケージを配置すると、Package Manager上で Universal RP に In Development ラベルが付きます。
以上で、Universal RP を編集する準備が整いました。
STEP2 : ノーマル描画HLSLの追加
まずはNormalOnlyPass.hlsl を作成します。
これは3DモデルのNormalだけを描画する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.shader
や DepthOnlyPass.hlsl
などがあります。
STEP3 : Lit.shaderにPassを追加
先ほど追加した NormalOnlyPass.hlsl
は .shader ファイルに埋め込んで実行させる必要があります。
今回は、URPで最もスタンダードな Lit.shader から NormalOnlyPass.hlsl
を実行させたいと思います。
補足 : URPプロジェクトのSampleSceneの3dモデルではLit.shaderが使われています。
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のレンダラーにレンダリングを実行させる必要があります。
レンダリングを実行させるには、 ScriptableRendererFeature と ScriptableRenderPass を利用します。
まずは NormalOnlyPass.cs
(ScriptableRenderPass) を追加します。
この NormalOnlyPass.cs
はLit.shaderが持っているNormalOnlyパスに対し、レンダリング命令を発行するものです。
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のレンダラーに認識させる必要があります。
認識させるためには以下のような手順を踏みます
-
ScriptableRendererFeature
を作成 -
ScriptableRendererFeature
にNormalOnlyPass
を組み込んで実行させる -
ForwardRendererData
にScriptableRendererFeature
を登録
ScriptableRendererFeatureの作成
以下の MyRendererFeature.cs を作成し、Assets/フォルダ以下の好きな場所に配置します。
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としてシェーダープロパティが格納されます。
m_NormalTexture.Init("_CameraWorldNormalTexture"); // 内部では Shader.PropertyToID() が呼ばれている
以下のコードではNormalOnlyPass
に m_NormalTexture
を登録しています。
これにより、_CameraWorldNormalTexture
に対してワールド法線のレンダリングが行われるようになります。
normalOnlyPass.Setup(cameraTargetDescriptor, m_NormalTexture);
renderer.EnqueuePass(normalOnlyPass);
RendererFeatureの登録
MyRendererFeature.cs
を作成したら、
次は ForwardRendererData
アセット に MyRendererFeature
を登録します。
以上で URPの拡張は終わりです。
Frame Debuggerで描画を確認する
Frame Debugger を見ると、 NormalOnlyPass
のレンダリング結果を見ることができます。
Frame Debuggerの起動
Window -> Analysis -> Frame Debugger を選択
[DEV] Normal Only Pass という項目を選択すると、今回の NormalOnlyPass
が描画したものを見ることができます。
モデルの法線情報だけが表示されていることが確認できます。ここに表示されているのはワールド空間の法線情報です。
ShaderGraphでノーマル情報を取得してみる
フレームデバッガーの上の方に注目すると、RenderTarget _CameraWorldNormalTexture という表示があります。
「この描画パスのレンダリングのターゲットは _CameraWorldNormalTexture
です」 ということを意味するメッセージです。
この _CameraWorldNormalTexture
をShaderGraphから取得してみましょう。
ShaderGraphの作成
メニューから
Create -> Shader -> Unlit Graph
を選択して、Unlitのシェーダーグラフを作成します。
##_CameraWorldNormalTexture の追加
シェーダープロパティに以下のようなTexture2Dプロパティを追加します。
NormalTexをSample Texture 2Dノードに接続し、Unlit MasterのColorへ出力します。
Unlit Master の設定はデフォルトのままで大丈夫です。
NormalOnlyPass
にてワールド法線のレンダリングのタイミングを RenderPassEvent.BeforeRenderingOpaques
に設定しているため、
色が正しく取得できるはずです。
シェーダーグラフ絡めてリアルを作成し、適当なオブジェクトに貼り付けると、GameViewでワールド法線が表示されます。
補足 : Sceneビューでワールドノーマルが表示されない理由について
Sceneビューだと、NormalOnlyPass
の描画がチラチラしてしまう問題が発生してしまうため、
シーンカメラ(プレビューカメラ)ではNormalOnlyPass
をスキップしています。
// 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);
}