BLOG

はじめに

最近ゲームをチマチマ作るにあたり、シェーダーのカスタマイズを施す機会が増えてきました。 シェーダーのカスタマイズって既存のもののライティングを微調整したり、最後に効果を付け足したりする程度なのでサクッと済ませたいのですが、 「あの効果ってどうやって作るんだっけ…」って手が止まりがちだったので即刻導入できるようにコードの断片集を作りました。

まず最初にシンプルなUnlitシェーダーの例を挙げ、レシピはそれとの差分という形でここには掲示しています。 コードではURPでの利用を想定していますが、マルチライトなどの特殊なものを除けばエッセンスは他のRP、プラットフォームでも使い回せると思います。

また、この例はすべてUniversalForwardタグのパスしか記載していないので、場合に応じて他のパスを追加してください。その際、CBufferの変更に注意してください。 シェーダーのパスやCBufferなどについては(これは執筆中です)の記事で解説しているので、そちらも参考にしてください。

00 テンプレート

Shader "URP_Samples/Template"
{
	Properties
		{
			[MainTexture] _BaseMap("Texture", 2D) = "white" {}
			[MainColor]   _BaseColor("Color", Color) = (1, 1, 1, 1)
		}
	SubShader
	{
		Tags {"RenderType" = "Opaque" "IgnoreProjector" = "True" "RenderPipeline" = "UniversalPipeline" "ShaderModel"="4.5"}
		LOD 100

		Blend One Zero
		ZWrite On
		Cull Back

		Pass
		{
			Name "FowerdLit"
			Tags{"LightMode" = "UniversalForward"}

			HLSLPROGRAM
			#pragma exclude_renderers gles gles3 glcore
			#pragma target 4.5

			#pragma vertex vert
			#pragma fragment frag

			// -------------------------------------
			// Unity defined keywords
			#pragma multi_compile_fog

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

			TEXTURE2D(_BaseMap);
			SAMPLER(sampler_BaseMap);

			CBUFFER_START(UnityPerMaterial)
				float4 _BaseMap_ST;
				half4 _BaseColor;
			CBUFFER_END

			struct Attributes
			{
				float4 positionOS       : POSITION;
				float3 normalOS         : NORMAL;
			    float4 tangentOS        : TANGENT;
				float2 uv               : TEXCOORD0;
			};

			struct Varyings
			{
				float2 uv         : TEXCOORD0;
			    float3 normalWS   : TEXCOORD1;
				float  fogCoord   : TEXCOORD2;
				float4 positionCS : SV_POSITION;

				UNITY_VERTEX_OUTPUT_STEREO
			};

			Varyings vert(Attributes input)
			{
				Varyings output = (Varyings)0;

				UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

				VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
				output.positionCS = vertexInput.positionCS;
				output.uv = TRANSFORM_TEX(input.uv, _BaseMap);

				VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);
			    output.normalWS = normalInput.normalWS;

				return output;
			}

			half4 frag(Varyings input) : SV_Target
			{
				UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

				half2 uv = input.uv;
				half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
				half3 color = texColor.rgb * _BaseColor.rgb;
				half alpha = texColor.a * _BaseColor.a;

				color = MixFog(color, input.fogCoord);

				return half4(color, alpha);
			}
			ENDHLSL
		}
	}

	FallBack "Hidden/Universal Render Pipeline/FallbackError"
}

Source(Github)

01 シェード

いわゆるランバート反射モデルです。
影付けが強くなりすぎないように影響を半分にしたハーフランバートが用いられることもあります(コメントアウト)。

--- 00Template.shader	2021-11-30 18:20:34.114846200 +0900
+++ 01Diffuse.shader	2021-11-30 18:20:38.760747900 +0900
@@ -1,4 +1,4 @@
-Shader "URP_Samples/Template"
+Shader "URP_Samples/Diffuse"
 {
 	Properties
 		{
@@ -52,8 +52,9 @@
 			struct Varyings
 			{
 				float2 uv         : TEXCOORD0;
-				float3 normalWS   : TEXCOORD1;
-				float  fogCoord   : TEXCOORD2;
+				float3 positionWS : TEXCOORD1;
+				float3 normalWS   : TEXCOORD2;
+				float  fogCoord   : TEXCOORD3;
 				float4 positionCS : SV_POSITION;
 
 				UNITY_VERTEX_OUTPUT_STEREO
@@ -67,7 +68,9 @@
 
 				VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
 				output.positionCS = vertexInput.positionCS;
+				output.positionWS = vertexInput.positionWS;
 				output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
+				output.fogCoord = ComputeFogFactor(vertexInput.positionCS.z);
 
 				VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);
 				output.normalWS = normalInput.normalWS;
@@ -79,11 +82,19 @@
 			{
 				UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
 
+				float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS);
+
 				half2 uv = input.uv;
 				half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
 				half3 color = texColor.rgb * _BaseColor.rgb;
 				half alpha = texColor.a * _BaseColor.a;
 
+				Light mainLight = GetMainLight(shadowCoord);
+
+				float NoL = dot(input.normalWS, mainLight.direction);
+				color *= NoL; // Full
+				//color *= (NoL*0.5)+0.5; // Half
+
 				color = MixFog(color, input.fogCoord);
 
 				return half4(color, alpha);

Source(Github)

02 シェードの2値化 (トゥーンシェード)

01のシェードを閾値で2分することによってくっきりと明暗を分けます。
影部分の色は_ShadeColorの乗算により作られます。
ここでは_ShadeToonyパラメーターで境界をどの程度スムーズにするかを設定できるようにしています。
また、_ShadeShiftで境界のオフセットを設定できます。

--- 00Template.shader	2021-11-30 18:20:34.114846200 +0900
+++ 02Toon.shader	2021-11-30 18:40:11.821097400 +0900
@@ -1,9 +1,12 @@
-Shader "URP_Samples/Template"
+Shader "URP_Samples/Toon"
 {
 	Properties
 		{
 			[MainTexture] _BaseMap("Texture", 2D) = "white" {}
 			[MainColor]   _BaseColor("Color", Color) = (1, 1, 1, 1)
+			_ShadeColor("ShadeColor", Color) = (0.8, 0.8, 0.8, 1)
+			_ShadeToony("Shade Toony", Range(0.0, 1.0)) = 0.9
+			_ShadeShift("Shade Shift", Range(-1.0, 1.0)) = 0
 		}
 	SubShader
 	{
@@ -39,6 +42,9 @@
 			CBUFFER_START(UnityPerMaterial)
 				float4 _BaseMap_ST;
 				half4 _BaseColor;
+				half4 _ShadeColor;
+				half _ShadeToony;
+				half _ShadeShift;
 			CBUFFER_END
 
 			struct Attributes
@@ -47,15 +53,18 @@
 				float3 normalOS         : NORMAL;
 				float4 tangentOS        : TANGENT;
 				float2 uv               : TEXCOORD0;
+				UNITY_VERTEX_INPUT_INSTANCE_ID
 			};
 
 			struct Varyings
 			{
 				float2 uv         : TEXCOORD0;
-				float3 normalWS   : TEXCOORD1;
-				float  fogCoord   : TEXCOORD2;
+				float3 positionWS : TEXCOORD1;
+				float3 normalWS   : TEXCOORD2;
+				float  fogCoord   : TEXCOORD3;
 				float4 positionCS : SV_POSITION;
 
+				UNITY_VERTEX_INPUT_INSTANCE_ID
 				UNITY_VERTEX_OUTPUT_STEREO
 			};
 
@@ -63,11 +72,15 @@
 			{
 				Varyings output = (Varyings)0;
 
+				UNITY_SETUP_INSTANCE_ID(input);
+				UNITY_TRANSFER_INSTANCE_ID(input, output);
 				UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
 
 				VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
 				output.positionCS = vertexInput.positionCS;
+				output.positionWS = vertexInput.positionWS;
 				output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
+				output.fogCoord = ComputeFogFactor(vertexInput.positionCS.z);
 
 				VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);
 				output.normalWS = normalInput.normalWS;
@@ -77,13 +90,23 @@
 
 			half4 frag(Varyings input) : SV_Target
 			{
+				UNITY_SETUP_INSTANCE_ID(input);
 				UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
 
+				float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS);
+
 				half2 uv = input.uv;
 				half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
 				half3 color = texColor.rgb * _BaseColor.rgb;
 				half alpha = texColor.a * _BaseColor.a;
 
+				Light mainLight = GetMainLight(shadowCoord);
+
+				float NoL = dot(input.normalWS, mainLight.direction);
+				half thresholdL = (_ShadeShift - (1-_ShadeToony))/2 + 0.5;
+				half thresholdH = (_ShadeShift + (1-_ShadeToony))/2 + 0.5;
+				color *= lerp(_ShadeColor.rgb, half3(1, 1, 1), smoothstep(thresholdL, thresholdH, NoL));
+
 				color = MixFog(color, input.fogCoord);
 
 				return half4(color, alpha);

Source(Github)

03 スペキュラー (鏡面反射)

いわゆるPhongの反射モデルです。
この例ではメインライトの色を反射光に加算して(メタリックに近い)いますが、他のブレンドに変えても良いでしょう。

--- 00Template.shader	2021-11-30 18:20:34.114846200 +0900
+++ 03Specular.shader	2021-11-30 18:40:15.772635200 +0900
@@ -1,9 +1,10 @@
-Shader "URP_Samples/Template"
+Shader "URP_Samples/Specular"
 {
 	Properties
 		{
 			[MainTexture] _BaseMap("Texture", 2D) = "white" {}
 			[MainColor]   _BaseColor("Color", Color) = (1, 1, 1, 1)
+			_SpecPower ("Specular Power", Range(0,60)) = 3
 		}
 	SubShader
 	{
@@ -39,6 +40,7 @@
 			CBUFFER_START(UnityPerMaterial)
 				float4 _BaseMap_ST;
 				half4 _BaseColor;
+				half _SpecPower;
 			CBUFFER_END
 
 			struct Attributes
@@ -52,8 +54,10 @@
 			struct Varyings
 			{
 				float2 uv         : TEXCOORD0;
-				float3 normalWS   : TEXCOORD1;
-				float  fogCoord   : TEXCOORD2;
+				float3 positionWS : TEXCOORD1;
+				float3 normalWS   : TEXCOORD2;
+				float3 viewDirWS  : TEXCOORD3;
+				float  fogCoord   : TEXCOORD4;
 				float4 positionCS : SV_POSITION;
 
 				UNITY_VERTEX_OUTPUT_STEREO
@@ -67,7 +71,10 @@
 
 				VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
 				output.positionCS = vertexInput.positionCS;
+				output.positionWS = vertexInput.positionWS;
 				output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
+				output.fogCoord = ComputeFogFactor(vertexInput.positionCS.z);
+				output.viewDirWS = GetWorldSpaceViewDir(vertexInput.positionWS);
 
 				VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);
 				output.normalWS = normalInput.normalWS;
@@ -79,11 +86,21 @@
 			{
 				UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
 
+				float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS);
+
 				half2 uv = input.uv;
 				half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
 				half3 color = texColor.rgb * _BaseColor.rgb;
 				half alpha = texColor.a * _BaseColor.a;
 
+				Light mainLight = GetMainLight(shadowCoord);
+
+				float3 halfVector = normalize(normalize(mainLight.direction) + normalize(input.viewDirWS)); 
+				float NdotH = saturate(dot(input.normalWS, halfVector)); 
+				float spec = pow(NdotH, _SpecPower);
+
+				color += mainLight.color.rgb * spec * mainLight.distanceAttenuation;
+
 				color = MixFog(color, input.fogCoord);
 
 				return half4(color, alpha);

Source(Github)

04 マルチライト

複数のライトの情報を受け取り、01の方法で陰影をつけたものを加算ブレンドしています。

--- 00Template.shader	2021-11-30 18:20:34.114846200 +0900
+++ 04MultiLight.shader	2021-11-30 18:40:19.243612700 +0900
@@ -1,4 +1,4 @@
-Shader "URP_Samples/Template"
+Shader "URP_Samples/Diffuse"
 {
 	Properties
 		{
@@ -52,8 +52,9 @@
 			struct Varyings
 			{
 				float2 uv         : TEXCOORD0;
-				float3 normalWS   : TEXCOORD1;
-				float  fogCoord   : TEXCOORD2;
+				float3 positionWS : TEXCOORD1;
+				float3 normalWS   : TEXCOORD2;
+				float  fogCoord   : TEXCOORD3;
 				float4 positionCS : SV_POSITION;
 
 				UNITY_VERTEX_OUTPUT_STEREO
@@ -67,7 +68,9 @@
 
 				VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
 				output.positionCS = vertexInput.positionCS;
+				output.positionWS = vertexInput.positionWS;
 				output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
+				output.fogCoord = ComputeFogFactor(vertexInput.positionCS.z);
 
 				VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);
 				output.normalWS = normalInput.normalWS;
@@ -79,11 +82,26 @@
 			{
 				UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
 
+				float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS);
+
 				half2 uv = input.uv;
 				half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
-				half3 color = texColor.rgb * _BaseColor.rgb;
+				half3 baseColor = texColor.rgb * _BaseColor.rgb;
 				half alpha = texColor.a * _BaseColor.a;
 
+				Light mainLight = GetMainLight(shadowCoord);
+
+				float NoL = dot(input.normalWS, mainLight.direction);
+				half3 color = baseColor * mainLight.color * mainLight.distanceAttenuation * NoL;
+
+				uint pixelLightCount = GetAdditionalLightsCount();
+				for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex) {
+					Light light = GetAdditionalLight(lightIndex, input.positionWS);
+					half add_NoL = saturate(dot(input.normalWS, light.direction));
+
+					color +=  baseColor * light.color * light.distanceAttenuation * add_NoL;
+				}
+
 				color = MixFog(color, input.fogCoord);
 
 				return half4(color, alpha);

Source(Github)

05 複数テクスチャの利用 (AOブレンドなど)

この例ではシェーダーに入力するテクスチャを増やす方法を示しています。 ここでは2つのテクスチャの色を乗算(想定としてはベースカラーのテクスチャにオクルージョンテクスチャを適用する)していますが、 これに限らず例えばUVの位置や追加のパラメーターによってテクスチャを変えるなど、頻繁に求められるものだと思います。

--- 00Template.shader	2021-11-30 18:20:34.114846200 +0900
+++ 05Blend2Tex.shader	2021-11-30 18:40:23.195437300 +0900
@@ -1,9 +1,10 @@
-Shader "URP_Samples/Template"
+Shader "URP_Samples/Blend2Tex"
 {
 	Properties
 		{
 			[MainTexture] _BaseMap("Texture", 2D) = "white" {}
 			[MainColor]   _BaseColor("Color", Color) = (1, 1, 1, 1)
+			_SecondMap("Second Tex", 2D) = "white" {}
 		}
 	SubShader
 	{
@@ -35,9 +36,12 @@
 
 			TEXTURE2D(_BaseMap);
 			SAMPLER(sampler_BaseMap);
+			TEXTURE2D(_SecondMap);
+			SAMPLER(sampler_SecondMap);
 
 			CBUFFER_START(UnityPerMaterial)
 				float4 _BaseMap_ST;
+				float4 _SecondMap_ST;
 				half4 _BaseColor;
 			CBUFFER_END
 
@@ -81,7 +85,8 @@
 
 				half2 uv = input.uv;
 				half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
-				half3 color = texColor.rgb * _BaseColor.rgb;
+				half4 secondColor = SAMPLE_TEXTURE2D(_SecondMap, sampler_SecondMap, uv);
+				half3 color = texColor.rgb * _BaseColor.rgb * secondColor.rgb;
 				half alpha = texColor.a * _BaseColor.a;
 
 				color = MixFog(color, input.fogCoord);

Source(Github)

06 Matcap

法線をビュースペース変換することによってMatcapテクスチャ上のuvを求めています。 この例では視錐台カメラの補正はかけていない簡易的なものなので、視線の回転によって表示が微妙にズレてしまいます。(誰か補正のかけ方を教えてください)

--- 00Template.shader	2021-11-30 18:20:34.114846200 +0900
+++ 06Matcap.shader	2021-11-30 18:40:27.650560600 +0900
@@ -1,9 +1,10 @@
-Shader "URP_Samples/Template"
+Shader "URP_Samples/Matcap"
 {
 	Properties
 		{
 			[MainTexture] _BaseMap("Texture", 2D) = "white" {}
 			[MainColor]   _BaseColor("Color", Color) = (1, 1, 1, 1)
+			_SpecPower ("Specular Power", Range(0,60)) = 3
 		}
 	SubShader
 	{
@@ -39,6 +40,7 @@
 			CBUFFER_START(UnityPerMaterial)
 				float4 _BaseMap_ST;
 				half4 _BaseColor;
+				half _SpecPower;
 			CBUFFER_END
 
 			struct Attributes
@@ -53,7 +55,8 @@
 			{
 				float2 uv         : TEXCOORD0;
 				float3 normalWS   : TEXCOORD1;
-				float  fogCoord   : TEXCOORD2;
+				float3 viewDirWS  : TEXCOORD2;
+				float  fogCoord   : TEXCOORD3;
 				float4 positionCS : SV_POSITION;
 
 				UNITY_VERTEX_OUTPUT_STEREO
@@ -67,16 +70,22 @@
 
 				VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
 				output.positionCS = vertexInput.positionCS;
-				output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
+				output.fogCoord = ComputeFogFactor(vertexInput.positionCS.z);
+				output.viewDirWS = GetWorldSpaceViewDir(vertexInput.positionWS);
 
 				VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);
 				output.normalWS = normalInput.normalWS;
 
+				output.uv = TransformWorldToViewDir(normalInput.normalWS).xy;
+				output.uv = output.uv * 0.5 + 0.5;
+				output.uv = TRANSFORM_TEX(output.uv, _BaseMap);
+
 				return output;
 			}
 
 			half4 frag(Varyings input) : SV_Target
 			{
+				UNITY_SETUP_INSTANCE_ID(input);
 				UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
 
 				half2 uv = input.uv;

Source(Github)

07 フレネル/リムライト

フレネルは本来間接光による鏡面反射が、視線と表面の法線が直行するような部分で強く見られる現象を再現するものですが、「よわいアウトライン」としてキャラクター表現などでも多く使われているようです。
ここではそれに乗っ取り、実際に環境マップをサンプリングするのではなくそのまま加算で縁を明るくしています。お好きな方法でブレンドしてください。

--- 00Template.shader	2021-11-30 18:20:34.114846200 +0900
+++ 07Frenel.shader	2021-11-30 18:40:32.506565400 +0900
@@ -1,9 +1,10 @@
-Shader "URP_Samples/Template"
+Shader "URP_Samples/Frenel"
 {
 	Properties
 		{
 			[MainTexture] _BaseMap("Texture", 2D) = "white" {}
 			[MainColor]   _BaseColor("Color", Color) = (1, 1, 1, 1)
+			_F0 ("F0", Range(0.0, 1.0)) = 0.02
 		}
 	SubShader
 	{
@@ -39,6 +40,7 @@
 			CBUFFER_START(UnityPerMaterial)
 				float4 _BaseMap_ST;
 				half4 _BaseColor;
+				half _F0;
 			CBUFFER_END
 
 			struct Attributes
@@ -53,7 +55,8 @@
 			{
 				float2 uv         : TEXCOORD0;
 				float3 normalWS   : TEXCOORD1;
-				float  fogCoord   : TEXCOORD2;
+				float3 viewDirWS  : TEXCOORD2;
+				float  fogCoord   : TEXCOORD3;
 				float4 positionCS : SV_POSITION;
 
 				UNITY_VERTEX_OUTPUT_STEREO
@@ -68,6 +71,8 @@
 				VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
 				output.positionCS = vertexInput.positionCS;
 				output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
+				output.fogCoord = ComputeFogFactor(vertexInput.positionCS.z);
+				output.viewDirWS = GetWorldSpaceViewDir(vertexInput.positionWS);
 
 				VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);
 				output.normalWS = normalInput.normalWS;
@@ -84,6 +89,9 @@
 				half3 color = texColor.rgb * _BaseColor.rgb;
 				half alpha = texColor.a * _BaseColor.a;
 
+				half frenel = _F0 + (1.0 - _F0) * pow(1 - dot(normalize(input.viewDirWS), input.normalWS), 5);
+				color += frenel;
+
 				color = MixFog(color, input.fogCoord);
 
 				return half4(color, alpha);

Source(Github)

08 アウトライン

2パスかけて描画することが一般的ですが、ここではジオメトリシェーダーを用いて面数を2倍に増やすことで1パスでアウトラインをつけています。 これは簡易的なものなので、高ポリゴンモデルに使うとパフォーマンス上問題があるかもしれません。

--- 00Template.shader	2021-11-30 18:20:34.114846200 +0900
+++ 08Outline.shader	2021-11-30 18:40:36.202541600 +0900
@@ -1,9 +1,11 @@
-Shader "URP_Samples/Template"
+Shader "URP_Samples/Outline"
 {
 	Properties
 		{
 			[MainTexture] _BaseMap("Texture", 2D) = "white" {}
 			[MainColor]   _BaseColor("Color", Color) = (1, 1, 1, 1)
+			_OutlineWidth ("Outline Width", Range(0,0.1)) = 0.01
+			_OutlineColor ("Outline Color", Color) = (0, 0, 0, 1)
 		}
 	SubShader
 	{
@@ -22,8 +24,10 @@
 			HLSLPROGRAM
 			#pragma exclude_renderers gles gles3 glcore
 			#pragma target 4.5
+			#pragma require geometry
 
 			#pragma vertex vert
+			#pragma geometry geom
 			#pragma fragment frag
 
 			// -------------------------------------
@@ -39,6 +43,8 @@
 			CBUFFER_START(UnityPerMaterial)
 				float4 _BaseMap_ST;
 				half4 _BaseColor;
+				half _OutlineWidth;
+				float4 _OutlineColor;
 			CBUFFER_END
 
 			struct Attributes
@@ -54,6 +60,8 @@
 				float2 uv         : TEXCOORD0;
 				float3 normalWS   : TEXCOORD1;
 				float  fogCoord   : TEXCOORD2;
+				float4 outline    : TEXCOORD3;
+				float3 positionWS : TEXCOORD4;
 				float4 positionCS : SV_POSITION;
 
 				UNITY_VERTEX_OUTPUT_STEREO
@@ -67,7 +75,10 @@
 
 				VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
 				output.positionCS = vertexInput.positionCS;
+				output.positionWS = vertexInput.positionWS;
 				output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
+				output.fogCoord = ComputeFogFactor(vertexInput.positionCS.z);
+				output.outline = float4(0, 0, 0, 0);
 
 				VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);
 				output.normalWS = normalInput.normalWS;
@@ -75,13 +86,33 @@
 				return output;
 			}
 
+			[maxvertexcount(6)]
+			void geom(triangle Varyings input[3], inout TriangleStream<Varyings> outputStream)
+			{
+				[unroll(3)]
+				for (int i = 0; i < 3; ++i) {
+					outputStream.Append(input[i]);
+				}
+
+				outputStream.RestartStrip();
+
+				[unroll(3)]
+				for (int j = 2; j >= 0; --j) {
+					input[j].positionCS = TransformWorldToHClip(input[j].positionWS += (_OutlineWidth)*input[j].normalWS);
+					input[j].outline = float4(_OutlineColor.rgb, 1.0f);
+					outputStream.Append(input[j]);
+				}
+			}
+
 			half4 frag(Varyings input) : SV_Target
 			{
 				UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
 
 				half2 uv = input.uv;
 				half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
-				half3 color = texColor.rgb * _BaseColor.rgb;
+
+				half3 color = (input.outline.a <= 0.5) ? texColor.rgb * _BaseColor.rgb : input.outline.rgb;
+
 				half alpha = texColor.a * _BaseColor.a;
 
 				color = MixFog(color, input.fogCoord);

Source(Github)

09 ディザ抜き

不透明描画でもピクセルを間引きすることで半透明描画表現を行う方法です。 ベイヤー行列をテクスチャに焼いたものをサンプリングし、比較することでどのピクセルを抜くかどうかを判定します。

ここではベイヤー行列のテクスチャを公開できないので、_DitherMapに設定するテクスチャは「ディザ テクスチャ」等で検索するか自作して頂けると助かります。

--- 00Template.shader	2021-11-30 18:20:34.114846200 +0900
+++ 09Dither.shader	2021-11-30 18:40:40.202542000 +0900
@@ -1,9 +1,11 @@
-Shader "URP_Samples/Template"
+Shader "URP_Samples/Dither"
 {
 	Properties
 		{
 			[MainTexture] _BaseMap("Texture", 2D) = "white" {}
 			[MainColor]   _BaseColor("Color", Color) = (1, 1, 1, 1)
+			[NoScaleOffset] _DitherMap("DitherMap", 2D) = "white" {}
+			_Opacity("Opacity", Range(0, 1)) = 1
 		}
 	SubShader
 	{
@@ -35,10 +37,15 @@
 
 			TEXTURE2D(_BaseMap);
 			SAMPLER(sampler_BaseMap);
+			TEXTURE2D(_DitherMap);
+			SAMPLER(sampler_DitherMap);
 
 			CBUFFER_START(UnityPerMaterial)
 				float4 _BaseMap_ST;
 				half4 _BaseColor;
+				half _Cutoff;
+				half _Surface;
+				float _Opacity;
 			CBUFFER_END
 
 			struct Attributes
@@ -79,6 +86,10 @@
 			{
 				UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
 
+				float2 ditherUV = input.positionCS.xy / 4;
+				float dither = SAMPLE_TEXTURE2D(_DitherMap, sampler_DitherMap, ditherUV).r;
+				if (_Opacity - dither < 0) discard;
+
 				half2 uv = input.uv;
 				half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
 				half3 color = texColor.rgb * _BaseColor.rgb;

Source(Github)

10 三面投影 (Triplaner)

モデルのUVを使わず、オブジェクトのワールド座標をもとにテクスチャーを貼り付ける(投影する)手法です。
主に地形や、部屋の壁紙などに使われます。
曲面や座標軸に直行していない面では、テクスチャによってはうまくごまかせることもありますが、基本的には苦手です。

また、これをノーマルマップ付きのものに拡張する場合は、タンジェントの取り扱いに気をつけてください。

--- 00Template.shader	2021-11-30 18:20:34.114846200 +0900
+++ 10Triplanar.shader	2021-11-30 18:40:43.898760400 +0900
@@ -1,4 +1,4 @@
-Shader "URP_Samples/Template"
+Shader "URP_Samples/Unlit"
 {
 	Properties
 		{
@@ -51,7 +51,7 @@
 
 			struct Varyings
 			{
-				float2 uv         : TEXCOORD0;
+				float3 positionWS : TEXCOORD0;
 				float3 normalWS   : TEXCOORD1;
 				float  fogCoord   : TEXCOORD2;
 				float4 positionCS : SV_POSITION;
@@ -66,8 +66,8 @@
 				UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
 
 				VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
+				output.positionWS = vertexInput.positionWS;
 				output.positionCS = vertexInput.positionCS;
-				output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
 
 				VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);
 				output.normalWS = normalInput.normalWS;
@@ -79,14 +79,18 @@
 			{
 				UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
 
-				half2 uv = input.uv;
-				half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
-				half3 color = texColor.rgb * _BaseColor.rgb;
-				half alpha = texColor.a * _BaseColor.a;
+				float3 tripUV = input.positionWS;
+				float3 tripFactor = normalize(abs(input.normalWS));
 
-				color = MixFog(color, input.fogCoord);
+				half4 albedoX = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, tripUV.zy * float2(sign(input.normalWS.x), 1.0));
+				half4 albedoY = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, tripUV.xz * float2(sign(input.normalWS.y), 1.0));
+				half4 albedoZ = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, tripUV.xy * float2(-sign(input.normalWS.z), 1.0));
 
-				return half4(color, alpha);
+				half4 color = (albedoX * tripFactor.x + albedoY * tripFactor.y + albedoZ * tripFactor.z) * _BaseColor;
+
+				color.rgb = MixFog(color.rgb, input.fogCoord);
+
+				return color;
 			}
 			ENDHLSL
 		}

Source(Github)

追加予定

  • ノーマルマップ適用
  • 影の受け取り
  • SSAOの受け取り
  • 環境の受け取り