【Unity】セルシェーディングを1から作ってみるメモ その3 脱Post-process【Shader】

はじめに

前回

アウトラインに Unity の Post-process を利用していましたが、アルファに情報を保持し続けるのは色々厳しいので、CommandBuffer で良きタイミングでアルファ情報を取得してアウトラインを書くことにしました。

対応方法

メインカメラにコマンドを追加し、アルファ情報を抜き出しテクスチャー化

その後前回と同じ方法でアウトラインを書きます。この時点でアルファは不要になるので、半透明や HDR MSAA など干渉していた部分が解決します。

アルファを抜き出す

アルファ情報を抜き出してテクスチャにする shader です。


Shader "Hidden/Neko/AlphaSnap"
{
	Properties
	{
		_MainTex("Main Texture", 2D) = "white" {}
	}//Properties

    SubShader
    {
		Tags
		{
			"Queue" = "Geometry+1"
			"RenderType" = "Opaque"
		}
        Cull Off
        ZWrite Off
        ZTest Always

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

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

			float4 _MainTex_ST;
			sampler2D _MainTex;

			v2f vert( appdata_base v)
			{
				v2f o = (v2f)0;
				o.vertex = UnityObjectToClipPos( v.vertex);
				o.texcoord = float2( v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw);;
				return o;
			}

			float frag( v2f i) : SV_Target
			{
				float alpha = tex2D( _MainTex, i.texcoord).a;
				return alpha;
			}

			ENDCG
		}//Pass
    }//SubShader
}//Shader

特に難しいことはしていません。メインテクスチャからアルファを出力してます。

アウトラインシェーダー

こちらは前回の Post-process の移植になります。
プロパティを追加し、深度・法線・アルファテクスチャを参照しつつ描画しています。

前回とほぼ同じです。どこかのサイト(忘れた)でテクスチャのフェッチは先に済ませたほうが良いというのを見たので、先に一気に見ています。

/*
	参考サイト

	Outline Shader
	https://roystan.net/articles/outline-shader.html
*/

Shader "Hidden/Neko/DNBOutline"
{
	Properties
	{
		[HideInInspector]_MainTex("Main Texture", 2D) = "white" {}
		_DepthThreshold( "Depth Threshold", Range(0, 1.0)) = 0.05
		_DepthNormalThreshold( "Depth Normal Threshold", Range(0, 1.0)) = 0.5
		_DepthNormalThresholdScale( "Depth Normal Threshold", Float) = 7
		_NormalThreshold( "Normal Threshold", Range(0, 1.0)) = 0.3
		_BrightnessThreshold( "Brightness Threshold", Range(0, 1.0)) = 0.005
		_Color( "Color", Color ) = ( 0, 0, 0, 1)
		_Scale( "Scale", Range(0, 5)) = 2
	}//Properties
	SubShader
	{
		Cull Off
		ZWrite Off
		ZTest Always

		Pass
		{
			CGPROGRAM
			#pragma vertex Vert
			#pragma fragment Frag
			#include "UnityCG.cginc"

			sampler2D _MainTex;
			sampler2D _CameraDepthNormalsTexture;
			sampler2D _CameraDepthTexture;
			sampler2D _BrightnessTexture;

			float4 _MainTex_ST;
			float4 _MainTex_TexelSize;

			float4 alphaBlend(float4 top, float4 bottom)
			{
				float3 color = (top.rgb * top.a) + (bottom.rgb * (1 - top.a));
				float alpha = top.a + bottom.a * (1 - top.a);

				return float4(color, alpha);
			}

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

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float2 texcoord : TEXCOORD0;
				float3 viewSpaceDir : TEXCOORD1;
				float2 snapuv : TEXCOORD2;
			};

			v2f Vert( appdata v)
			{
				v2f o = (v2f)0;
				o.vertex = UnityObjectToClipPos( v.vertex);
				o.texcoord = float2( v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw);
				o.snapuv = float2( v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw);
				o.viewSpaceDir = mul( unity_CameraInvProjection, o.vertex).xyz;
				if( unity_MatrixVP._24 < 0)
				{
					o.snapuv = o.snapuv * float2( 1.0, -1.0) + float2( 0.0, 1.0);
				}
				return o;
			}

			float _Scale;
			float _DepthThreshold;
			float _NormalThreshold;
			float4 _Color;
			float _DepthNormalThreshold;
			float _DepthNormalThresholdScale;
			float _BrightnessThreshold;

			float4 Frag( v2f i) : SV_Target
			{
				float halfScaleFloor = floor(_Scale * 0.5);
				float halfScaleCeil = ceil(_Scale * 0.5);

				float2 bottomLeftUV = i.texcoord - float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleFloor;
				float2 topRightUV = i.texcoord + float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleCeil;  
				float2 bottomRightUV = i.texcoord + float2(_MainTex_TexelSize.x * halfScaleCeil, -_MainTex_TexelSize.y * halfScaleFloor);
				float2 topLeftUV = i.texcoord + float2(-_MainTex_TexelSize.x * halfScaleFloor, _MainTex_TexelSize.y * halfScaleCeil);

				float2 bottomLeftUVa = i.snapuv - float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleFloor;
				float2 topRightUVa = i.snapuv + float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleCeil;  
				float2 bottomRightUVa = i.snapuv + float2(_MainTex_TexelSize.x * halfScaleCeil, -_MainTex_TexelSize.y * halfScaleFloor);
				float2 topLeftUVa = i.snapuv + float2(-_MainTex_TexelSize.x * halfScaleFloor, _MainTex_TexelSize.y * halfScaleCeil);

				float3 normal0 = tex2D( _CameraDepthNormalsTexture, bottomLeftUV).rgb;
				float3 normal1 = tex2D( _CameraDepthNormalsTexture, topRightUV).rgb;
				float3 normal2 = tex2D( _CameraDepthNormalsTexture, bottomRightUV).rgb;
				float3 normal3 = tex2D( _CameraDepthNormalsTexture, topLeftUV).rgb;
				float depth0 = UNITY_SAMPLE_DEPTH( tex2D( _CameraDepthTexture, bottomLeftUV));
				float depth1 = UNITY_SAMPLE_DEPTH( tex2D( _CameraDepthTexture, topRightUV));
				float depth2 = UNITY_SAMPLE_DEPTH( tex2D( _CameraDepthTexture, bottomRightUV));
				float depth3 = UNITY_SAMPLE_DEPTH( tex2D( _CameraDepthTexture, topLeftUV));
				float alpha0 = tex2D( _BrightnessTexture, bottomLeftUVa).r;
				float alpha1 = tex2D( _BrightnessTexture, topRightUVa).r;
				float alpha2 = tex2D( _BrightnessTexture, bottomRightUVa).r;
				float alpha3 = tex2D( _BrightnessTexture, topLeftUVa).r;
				float4 color = tex2D( _MainTex, i.snapuv);

				float3 viewNormal = normal0 * 2.0 - 1.0;
				float NdotV = 1 - dot( viewNormal, -i.viewSpaceDir);
				float normalThreshold01 = saturate( (NdotV - _DepthNormalThreshold) / (1 - _DepthNormalThreshold));

				// ノーマルバッファからエッジを抽出
				float normalThreshold = normalThreshold01 * _DepthNormalThresholdScale + 1;
				float3 normalFiniteDifference0 = normal1 - normal0;
				float3 normalFiniteDifference1 = normal3 - normal2;
				float edgeNormal = sqrt( dot( normalFiniteDifference0, normalFiniteDifference0) + dot( normalFiniteDifference1, normalFiniteDifference1));
				edgeNormal = 1-step( edgeNormal, _NormalThreshold);

				// 深度バッファからエッジを抽出
				float depthThreshold = _DepthThreshold * depth0;
				float depthFiniteDifference0 = depth1 - depth0;
				float depthFiniteDifference1 = depth3 - depth2;
				float edgeDepth = sqrt( pow( depthFiniteDifference0, 2) + pow( depthFiniteDifference1, 2));
				edgeDepth = 1-step( edgeDepth, depthThreshold);

				// アルファからエッジを抽出
				float alphaFiniteDifference0 = alpha1 - alpha0;
				float alphaFiniteDifference1 = alpha3 - alpha2;
				float edgeAlpha = sqrt( pow( alphaFiniteDifference0, 2) + pow( alphaFiniteDifference1, 2));
				edgeAlpha = 1-step( edgeAlpha, _BrightnessThreshold);

				// エッジを合成
				float edge = max( max( edgeDepth, edgeNormal), edgeAlpha);
				float4 edgeColor = float4( _Color.rgb, _Color.a * edge);
				return alphaBlend( edgeColor, color);
			}
			ENDCG
		}//Pass
	}//SubShader
}//Shader
sampler2D _BrightnessTexture;

には アルファから抜き取ったテクスチャが設定されます。

問題というか、未だに理解不能なのが

if( unity_MatrixVP._24 < 0)
{
	o.snapuv = o.snapuv * float2( 1.0, -1.0) + float2( 0.0, 1.0);
}

の部分になります。

HDR と MSAA の組み合わせで DepthNormalBuffer と ColorBuffer の上下が逆になるようで、UNITY_UV_STARTS_AT_TOP では解決できないため 苦肉の策で uvを追加して MatrixVP の Y方向を見て変更しています。すべての環境で正常に出るかは不明 Windows では確認済みです。

デプスノーマルバッファとカラーバッファの上下が逆の絵

 予想ですが、デプスノーマルバッファはUnityがよろしく置換していて、直接取得したカラーバッファは環境依存のまま上下が逆になるのではないでしょうか?
 もしかしたらデプスノーマルマップも直接取得すれば行けるかもしれません。

もっと適切な方法があったらぜひ知りたいです。

カメラにコマンド追加

まずシェーダーを生成します。必要なプロパティを追加しています。

var alphaSnapShader = Shader.Find( "Hidden/Neko/AlphaSnap");
var dnbOutlineShader = Shader.Find( "Hidden/Neko/DNBOutline");
if( alphaSnapShader == null || dnbOutlineShader == null) return;

// シェーダー生成
m_AlphaSnapShaderMaterial = new Material( alphaSnapShader);
m_DNBOutlineShaderMaterial = new Material( dnbOutlineShader);
m_DNBOutlineShaderMaterial.SetInt( "_Scale", m_Size);
m_DNBOutlineShaderMaterial.SetFloat( "_DepthThreshold", m_DepthThreshold);
m_DNBOutlineShaderMaterial.SetFloat( "_NormalThreshold", m_NormalThreshold);
m_DNBOutlineShaderMaterial.SetFloat( "_DepthNormalThresholdScale", m_DepthNormalThresholdScale);
m_DNBOutlineShaderMaterial.SetFloat( "_DepthNormalThreshold", m_DepthNormalThreshold);
m_DNBOutlineShaderMaterial.SetFloat( "_BrightnessThreshold", m_BrightnessThreshold);
m_DNBOutlineShaderMaterial.SetColor( "_Color", m_Color);

次にコマンドバッファを生成します。

m_CommandBuffer = new CommandBuffer();
m_CommandBuffer.name = "DNBOutline";
int tempTextureIdentifier = Shader.PropertyToID( "_Temp");
int alphaTextureIdentifier = Shader.PropertyToID( "_Alpha");
m_CommandBuffer.GetTemporaryRT( tempTextureIdentifier, -1, -1);
m_CommandBuffer.GetTemporaryRT( alphaTextureIdentifier, -1, -1, 0, FilterMode.Bilinear, RenderTextureFormat.R8);
m_CommandBuffer.Blit( BuiltinRenderTextureType.CameraTarget, tempTextureIdentifier);
m_CommandBuffer.Blit( tempTextureIdentifier, alphaTextureIdentifier, m_AlphaSnapShaderMaterial);
m_CommandBuffer.SetGlobalTexture( "_BrightnessTexture", alphaTextureIdentifier);
m_CommandBuffer.Blit( tempTextureIdentifier, BuiltinRenderTextureType.CameraTarget, m_DNBOutlineShaderMaterial);
m_CommandBuffer.ReleaseTemporaryRT( alphaTextureIdentifier);
m_CommandBuffer.ReleaseTemporaryRT( tempTextureIdentifier);
m_Camera.AddCommandBuffer( CAMERA_EVENT, m_CommandBuffer);

順番に見ていくと

下記の部分で2枚テクスチャを生成しています。現在の画面のコピー用とアルファ保存用です。

m_CommandBuffer.GetTemporaryRT( tempTextureIdentifier, -1, -1);
m_CommandBuffer.GetTemporaryRT( alphaTextureIdentifier, -1, -1, 0, FilterMode.Bilinear, RenderTextureFormat.R8);

まずテンポラリテクスチャに現在の画面をコピーします。

m_CommandBuffer.Blit( BuiltinRenderTextureType.CameraTarget, tempTextureIdentifier);

次に “Hidden/Neko/AlphaSnap” を利用して アルファだけコピーします。そのテクスチャをアウトラインで利用できるように テクスチャとして設定します。

m_CommandBuffer.Blit( tempTextureIdentifier, alphaTextureIdentifier, m_AlphaSnapShaderMaterial);
m_CommandBuffer.SetGlobalTexture( "_BrightnessTexture", alphaTextureIdentifier);

その後アウトラインシェーダーでターゲットに描画していきます。

m_CommandBuffer.Blit( tempTextureIdentifier, BuiltinRenderTextureType.CameraTarget, m_DNBOutlineShaderMaterial);

最後はテンポラリを破棄します。

m_CommandBuffer.ReleaseTemporaryRT( alphaTextureIdentifier);
m_CommandBuffer.ReleaseTemporaryRT( tempTextureIdentifier);

という流れを カメラの AfterForwardOpaque のタイミングに登録します。

m_Camera.AddCommandBuffer( CAMERA_EVENT, m_CommandBuffer);

カメラのスクリプト

これをカメラコンポーネントにアタッチします。

using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

namespace test {

[ExecuteAlways]
[RequireComponent( typeof( Camera))]
public class DNBOutlineCameraBehaviour : MonoBehaviour
{
	const CameraEvent CAMERA_EVENT = CameraEvent.AfterForwardOpaque;

	[SerializeField]
	[Range( 0, 5)]
	private int m_Size = 2;

	[SerializeField]
	[Range( 0.0f, 1.0f)]
	private float m_DepthThreshold = 0.05f;

	[SerializeField]
	[Range( 0.0f, 1.0f)]
	private float m_NormalThreshold = 0.3f;

	[SerializeField]
	private float m_DepthNormalThresholdScale = 7.0f;

	[SerializeField]
	[Range( 0.0f, 1.0f)]
	private float m_DepthNormalThreshold = 0.5f;

	[SerializeField]
	[Range( 0.0f, 1.0f)]
	private float m_BrightnessThreshold = 0.005f;

	[SerializeField]
	private Color m_Color = Color.black;

	private Camera m_Camera;
	private CommandBuffer m_CommandBuffer = null;
	private Material m_AlphaSnapShaderMaterial = null;
	private Material m_DNBOutlineShaderMaterial = null;

	//----------------------------------------------------------------------------
	// この関数はスクリプトがロードされた時やインスペクターの値が変更されたときに呼び出されます(この呼出はエディター上のみ)
	private void OnValidate() => Setup();
	//----------------------------------------------------------------------------
	// この関数はオブジェクトが有効/アクティブになったときに呼び出されます
	private void OnEnable() => Setup();
	//----------------------------------------------------------------------------
	// この関数はオブジェクトが無効になると呼び出されます。
	private void OnDisable() => Teardown();
	//----------------------------------------------------------------------------
	private void Setup()
	{
		Teardown();

		m_Camera = gameObject?.GetComponent<Camera>();
		if( m_Camera == null) return;
		m_Camera.depthTextureMode = DepthTextureMode.DepthNormals;

		var alphaSnapShader = Shader.Find( "Hidden/Neko/AlphaSnap");
		var dnbOutlineShader = Shader.Find( "Hidden/Neko/DNBOutline");
		if( alphaSnapShader == null || dnbOutlineShader == null) return;

		// シェーダー生成
		m_AlphaSnapShaderMaterial = new Material( alphaSnapShader);
		m_DNBOutlineShaderMaterial = new Material( dnbOutlineShader);
		m_DNBOutlineShaderMaterial.SetInt( "_Scale", m_Size);
		m_DNBOutlineShaderMaterial.SetFloat( "_DepthThreshold", m_DepthThreshold);
		m_DNBOutlineShaderMaterial.SetFloat( "_NormalThreshold", m_NormalThreshold);
		m_DNBOutlineShaderMaterial.SetFloat( "_DepthNormalThresholdScale", m_DepthNormalThresholdScale);
		m_DNBOutlineShaderMaterial.SetFloat( "_DepthNormalThreshold", m_DepthNormalThreshold);
		m_DNBOutlineShaderMaterial.SetFloat( "_BrightnessThreshold", m_BrightnessThreshold);
		m_DNBOutlineShaderMaterial.SetColor( "_Color", m_Color);

		// コマンドバッファ生成
		m_CommandBuffer = new CommandBuffer();
		m_CommandBuffer.name = "DNBOutline";
		int tempTextureIdentifier = Shader.PropertyToID( "_Temp");
		int alphaTextureIdentifier = Shader.PropertyToID( "_Alpha");
		m_CommandBuffer.GetTemporaryRT( tempTextureIdentifier, -1, -1);
		m_CommandBuffer.GetTemporaryRT( alphaTextureIdentifier, -1, -1, 0, FilterMode.Bilinear, RenderTextureFormat.R8);
		m_CommandBuffer.Blit( BuiltinRenderTextureType.CameraTarget, tempTextureIdentifier);
		m_CommandBuffer.Blit( tempTextureIdentifier, alphaTextureIdentifier, m_AlphaSnapShaderMaterial);
		m_CommandBuffer.SetGlobalTexture( "_BrightnessTexture", alphaTextureIdentifier);
		m_CommandBuffer.Blit( tempTextureIdentifier, BuiltinRenderTextureType.CameraTarget, m_DNBOutlineShaderMaterial);
		m_CommandBuffer.ReleaseTemporaryRT( alphaTextureIdentifier);
		m_CommandBuffer.ReleaseTemporaryRT( tempTextureIdentifier);
		m_Camera.AddCommandBuffer( CAMERA_EVENT, m_CommandBuffer);
	}
	//----------------------------------------------------------------------------
	private void Teardown()
	{
		if( m_CommandBuffer != null)
		{
			m_Camera.RemoveCommandBuffer( CAMERA_EVENT, m_CommandBuffer);
			m_CommandBuffer = null;
		}
		if( m_AlphaSnapShaderMaterial != null)
		{
			DestroyImmediate( m_AlphaSnapShaderMaterial);
			m_AlphaSnapShaderMaterial = null;
		}
		if( m_DNBOutlineShaderMaterial != null)
		{
			DestroyImmediate( m_DNBOutlineShaderMaterial);
			m_DNBOutlineShaderMaterial = null;
		}
	}
} // class DNBOutlineCameraBehaviour

} // namespace test

[ExecuteAlways] をつけてるため、エディット中でも反映されますが、Sceneウィンドウには反映されません。必要であれば、すべてのカメラに登録するように改良するのが良いかと思います。

完成

↓髪のハイライトを改修しました。

この作品はユニティちゃんライセンス条項の元に提供されています
One Comment

Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です