はじめに
ファーシェーダといえばシェル法やフィン法を用いてジオメトリシェーダで作成するのが一般的ですが、モバイル業界では (取り分けiOS(metal)ではジオメトリシェーダ非対応な為) 未だジオメトリシェーダが使用できないという状況にあります。そこで今回はシェル法をマルチパスで書いて、尚且つURPに対応させてみたいと思います。
概要
シェル法といえば、元のプリミティブ (ポリゴン) を法線方向に移動させて何層も殻 (シェル) を被せるように描画する方法ですが、層の数だけパスを記述する必要があります。さらに、URP (Universal Render Pipeline) 環境では、パスの追加にはパイプライン側の拡張も必要なので合わせて行なっていきたいと思います。ちなみに検証に使用した環境は以下の通りです。
- Unity 2020.3.21f1
- Universal RP 10.6.0
パイプライン側の拡張
パスの追加には、ScriptableRenderPass クラスと ScriptableRendererFeature クラスをそれぞれ継承したクラスを用意します。以下は必要最低限の抜粋になります。
public class MultiPassFurRenderPass : ScriptableRenderPass
{
// Fur部分(Shell)のパスの数
private const int MultiPassCount = 8;
// Shaderに記述するタグ名
private const string LightModeTag = "MultiPass";
private ShaderTagId[] tagIdList = null;
// レンダリングするタイミング
private readonly RenderPassEvent _renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
// 対象とするRenderQueue
private readonly RenderQueueRange _renderQueueRange = RenderQueueRange.transparent;
// Constructor
public MultiPassFurRenderPass()
{
renderPassEvent = _renderPassEvent;
tagIdList = new ShaderTagId[MultiPassCount + 1];
for (var i = 0; i < MultiPassCount + 1; i++)
{
var name = LightModeTag + i.ToString();
tagIdList[i] = new ShaderTagId(name);
}
}
// レンダリング処理
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
// パスの描画
for (var i = 0; i < MultiPassCount + 1; i++)
{
var sortFlags = renderingData.cameraData.defaultOpaqueSortFlags;
var dSettings = CreateDrawingSettings(tagIdList[i], ref renderingData, sortFlags);
var fSettings = new FilteringSettings(_renderQueueRange, -1);
dSettings.perObjectData = PerObjectData.None;
context.DrawRenderers(renderingData.cullResults, ref dSettings, ref fSettings);
}
}
}
public class MultiPassFurRendererFeature : ScriptableRendererFeature
{
private MultiPassFurRenderPass _renderPass = null;
public override void Create()
{
// パスを生成
_renderPass = new MultiPassFurRenderPass();
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (_renderPass == null)
{
return;
}
// レンダリングパイプラインにパスを追加
renderer.EnqueuePass(_renderPass);
}
}
一番のポイントは、MultiPassFurRenderPass.cs 内にて使用している ShaderTagId の配列です。これを生成する時に使用している文字列 (ここでは 「MultiPass」 + 「数字」) がシェーダのパス内で記述する「LightMode」タグの識別に使用されます。
作成した2つのソースを Assets/ フォルダ配下の任意の場所に配置します。
その後、ForwardRenderer.asset を選択しインスペクタ上で MultiPassFurRendererFeature を追加します。
これでパイプライン側の拡張は完了です。
シェーダ側の実装
シェーダ側は以下の通り、本体である MultiPassFur.shader と、頂点シェーダ及びフラグメントシェーダの実装が含まれる MultiPassFurCore.hlsl の2ファイルとなります。
Shader "Extra/MultiPassFur"
{
Properties
{
_MainTex ("Base Map", 2D) = "black" {}
_FurTex ("Fur Map", 2D) = "black" {}
_Diffuse ("Diffuse value", Range(0.0, 1.0)) = 1.0
_FurLength ("Fur length", Range(0.0, 1.0)) = 0.5
_CutOff ("Alpha cutoff", Range(0.0, 1.0)) = 0.5
_Blur ("Blur", Range(0.0, 1.0)) = 0.5
_Thickness ("Thickness", Range(0.0, 0.5)) = 0.25
}
SubShader
{
Tags
{
"RenderType" = "Transparent"
"RenderPipeline" = "UniversalPipeline"
"UniversalMaterialType" = "Lit"
"IgnoreProjector" = "True"
"Queue" = "Transparent"
}
ZWrite On
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
Name "MultiPass0"
Tags { "LightMode" = "MultiPass0" }
HLSLPROGRAM
#pragma target 3.0
#pragma vertex vertFirst
#pragma fragment fragFirst
#define FURSTEP 0.0
#include "MultiPassFurCore.hlsl"
ENDHLSL
}
Pass
{
Name "MultiPass1"
Tags { "LightMode" = "MultiPass1" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#define FURSTEP 0.125
#include "MultiPassFurCore.hlsl"
ENDHLSL
}
Pass
{
Name "MultiPass2"
Tags { "LightMode" = "MultiPass2" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#define FURSTEP 0.25
#include "MultiPassFurCore.hlsl"
ENDHLSL
}
Pass
{
Name "MultiPass3"
Tags { "LightMode" = "MultiPass3" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#define FURSTEP 0.375
#include "MultiPassFurCore.hlsl"
ENDHLSL
}
Pass
{
Name "MultiPass4"
Tags { "LightMode" = "MultiPass4" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#define FURSTEP 0.5
#include "MultiPassFurCore.hlsl"
ENDHLSL
}
Pass
{
Name "MultiPass5"
Tags { "LightMode" = "MultiPass5" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#define FURSTEP 0.625
#include "MultiPassFurCore.hlsl"
ENDHLSL
}
Pass
{
Name "MultiPass6"
Tags { "LightMode" = "MultiPass6" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#define FURSTEP 0.75
#include "MultiPassFurCore.hlsl"
ENDHLSL
}
Pass
{
Name "MultiPass7"
Tags { "LightMode" = "MultiPass7" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#define FURSTEP 0.875
#include "MultiPassFurCore.hlsl"
ENDHLSL
}
Pass
{
Name "MultiPass8"
Tags { "LightMode" = "MultiPass8" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#define FURSTEP 1.0
#include "MultiPassFurCore.hlsl"
ENDHLSL
}
}
}
シェーダプロパティである2つのテクスチャはそれぞれ、ベースの色となる _MainTex (Base Map) と、ファーパターンとなる _FurTex (Fur Map) を指定します。(Fur Map は一般的なグレースケールのノイズテクスチャが使用できます)
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2fFirst
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
half4 diff : COLOR;
};
struct v2f
{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv1 : TEXCOORD1;
half4 diff : COLOR;
};
sampler2D _MainTex;
sampler2D _FurTex;
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float4 _FurTex_ST;
float _Diffuse;
float _FurLength;
float _Blur;
float _CutOff;
float _Thickness;
CBUFFER_END
half LambertDiffuse(float3 worldNormal)
{
float3 lightDir = normalize(_MainLightPosition.xyz);
float NdotL = max(0, dot(worldNormal, lightDir));
return (half)(NdotL * _Diffuse);
}
v2fFirst vertFirst(appdata v)
{
v2fFirst o;
o.pos = TransformObjectToHClip(v.vertex.xyz);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.diff.rgb = half3(0.0, 0.0, 0.0);
float3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
o.diff.a = LambertDiffuse(worldNormal);
return o;
}
half4 fragFirst(v2fFirst i) : SV_Target
{
half4 col = tex2D(_MainTex, i.uv);
col.rgb *= i.diff.a;
return col;
}
v2f vert(appdata v)
{
v2f o;
float scaleFactor = 0.10;
v.vertex.xyz += v.normal * (_FurLength * FURSTEP * scaleFactor);
o.pos = TransformObjectToHClip(v.vertex.xyz);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv1 = TRANSFORM_TEX(v.texcoord, _FurTex);
float3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
half dc = LambertDiffuse(worldNormal);
o.diff = half4(dc, dc, dc, 1 - (FURSTEP * FURSTEP));
float4 worldPos = mul(unity_WorldToObject, v.vertex);
o.diff.a += dot(normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz), worldNormal) - _Blur;
return o;
}
half4 frag(v2f i) : SV_Target
{
half4 col = tex2D(_MainTex, i.uv);
half alpha = tex2D(_FurTex, i.uv1).r;
col *= i.diff;
col.a *= step(lerp(_CutOff, _CutOff + _Thickness, FURSTEP), alpha);
return col;
}
URPでの注意点としては主に以下のようなものがあります。
- Cg言語 → HLSL言語 へ変更された (CGPROGRAM〜ENDCG → HLSLPROGRAM〜ENDHLSL)
- SubShaderへのタグ追加 ("RenderPipeline" = "UniversalPipeline")
- Passへのタグ追加 ("LightMode" = "MultiPass0") (通常は "UniversalForward")
- 頂点シェーダの引数は自前で定義する (struct appdata)
- fixed は使用できないので half へ置き換える (ベクターも含めて)
- UnityObjectToClipPos() → TransformObjectToHClip() へ変更
- SRP Batcher を効かせたい場合、CBUFFER_START〜CBUFFER_ENDでテクスチャ以外の変数を囲む
- インクルードファイルの変更 (以下が主なファイル)
- Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl
- Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl
使用例
全て問題なく実装できたら以下のようにシェーダプロパティの Fur length を調整しながら、結果を確認できます。(他のシェーダプロパティの値もインスペクタ上で調整して確認してみてください)
最後に
この方法での実装は、唯一データ側 (マテリアル側) で層(シェル/パス) の数を指定できないという欠点がありますが、いかがだったでしょうか? 他の方法で実装するにしても、この記事がURP環境でのシェーダ作成の参考になればと思います。