はじめに
今回は書いたことのなかったファーシェーダを試してみます。コードも書きやすいのでこれからはレガシーパイプラインではなく Universal Render Pipeline(URP)メインで書いていこうかな、と思っています(HDRP は直書きが大変すぎて趣味としては辛さが勝ったので余り触れない感じで...)。URP でのシェーダについては以下の記事をご参照ください。
デモ
以下の解説をします。
- 1 Pass(ジオメトリシェーダ利用)
- Lit シェーダ相当のライティング
- 法線の計算
- リムライト
ダウンロード
完成プロジェクトは以下にあげてあります。
ファーシェーダ
ファーシェーダを実現する手法は幾つかあり、シェル法とフィン法がよく使われるようです。
今回はシェル法をジオメトリシェーダを使ってやってみます。シェル法は法線方向に膨らませたポリゴンを何層も重ね、それぞれの層で毛生えを表現するテクスチャでクリッピングしたポイントを塗り重ねていくことで線っぽい見た目を表現する手法です。
層を作ってみる
ShaderLab
まずは Unlit でシンプルに作ってみます。頂点、ジオメトリ、フラグメントシェーダの実装は別ファイル(Fur.hlsl)に、ShaderLab の設定は Fur.shader に記述する形にします。まず、Fur.shader から見てみましょう。
Shader "Unlit/Fur" { Properties { _BaseMap("Base Map", 2D) = "white" {} _FurMap("Fur Map", 2D) = "white" {} [IntRange] _ShellAmount("Shell Amount", Range(1, 100)) = 16 _ShellStep("Shell Step", Range(0.0, 0.01)) = 0.001 _AlphaCutout("Alpha Cutout", Range(0.0, 1.0)) = 0.1 } SubShader { Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True" } LOD 100 ZWrite On Cull Back Pass { Name "Unlit" HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma multi_compile_fog #include "./Fur.hlsl" #pragma vertex vert #pragma require geometry #pragma geometry geom #pragma fragment frag ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode" = "DepthOnly" } ZWrite On ColorMask 0 HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma vertex DepthOnlyVertex #pragma fragment DepthOnlyFragment #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl" ENDHLSL } Pass { Name "ShadowCaster" Tags {"LightMode" = "ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma target 4.5 #pragma vertex ShadowPassVertex #pragma fragment ShadowPassFragment #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl" ENDHLSL } } }
影・デプス付き(Lit シェーダを参考に DepthOnly
および ShadowCaster
パスを追加)の Unlit シェーダとして Pass を作っておきます。影は今回はとりあえずファーは反映されず、もとのメッシュのものを出力するようにしています。Unlit パスでは Fur.hlsl を #include し、そちらに実装を記述するようにしているので、次はそちらを見てみましょう。
頂点・ジオメトリ・フラグメントシェーダ
#ifndef FUR_HLSL #define FUR_HLSL #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl" int _ShellAmount; float _ShellStep; float _AlphaCutout; TEXTURE2D(_FurMap); SAMPLER(sampler_FurMap); float4 _FurMap_ST; struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 uv : TEXCOORD0; }; struct Varyings { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float2 uv2 : TEXCOORD1; float fogCoord : TEXCOORD2; float layer : TEXCOORD3; }; Attributes vert(Attributes input) { return input; } void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index) { Varyings output = (Varyings)0; VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); float3 posWS = vertexInput.positionWS + normalInput.normalWS * (_ShellStep * index); float4 posCS = TransformWorldToHClip(posWS); output.vertex = posCS; output.uv = TRANSFORM_TEX(input.uv, _BaseMap); output.uv2 = TRANSFORM_TEX(input.uv, _FurMap); output.fogCoord = ComputeFogFactor(posCS.z); output.layer = (float)index / _ShellAmount; stream.Append(output); } [maxvertexcount(96)] void geom(triangle Attributes input[3], inout TriangleStream<Varyings> stream) { [loop] for (float i = 0; i < _ShellAmount; ++i) { [unroll] for (float j = 0; j < 3; ++j) { AppendShellVertex(stream, input[j], i); } stream.RestartStrip(); } } float4 frag(Varyings input) : SV_Target { float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv2); if (input.layer > 0.0 && furColor.r < _AlphaCutout) discard; float4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); float3 color = baseColor; color = MixFog(color, input.fogCoord); return float4(color, 1.0); } #endif
ジオメトリシェーダで、入力された三角ポリゴンを _ShellAmout
だけ法線方向に少し膨らませながら数を増やして層をつくっています。なお、このときレイヤも一緒に出力するようにしておきます。
フラグメントシェーダでは入力されたノイズテクスチャを _AlphaCutout
でクリッピングして黒い場所を捨て白い場所だけ残します。ただし、レイヤ 0 は通常のポリゴンとしてそのまま出力するようにこの処理を省いています。あとは通常通り色を出力するとノイズの部分だけが毛のように見えるようになります。
概念としてはこんな感じで、破線がノイズテクスチャを意味します。ステップを短くして層を増やすほど毛っぽさが出ます。
重ねられる層の数はジオメトリシェーダで指定する maxvertexcount
に依存します。数は出力構造体の大きさによって可変で、出力する数値の合計が 1024 以下になるようにする必要があります。ここでは Varyings
が 10 個の float を出力するため、最大数は 1024/10 = 102 が指定できる最大となり、一層でポリゴン分の 3 頂点を使うので、最大の層の数としては 34 層までとなります。1 枚のテクスチャ(例えば alpha チャンネルとか)に毛の生え具合もパックしてやれば 2 削減でき、128 頂点 42 層が最大になります。UV も今は _BaesMap
用と _FurMap
で分けてますが一緒にして _FurScale
みたいなものを用意すれば更に 2 削減できます。こうすれば 56 層まで作れますね。
結果
まだクオリティはアレですが層が重なってノイズによってイガイガが出来たのがわかります。
入力するノイズはとりあえずテスト用になにか与えましょう。ここでは CC0 の以下の画像をお借りしています。
見栄えを良くしてみる
フラグメントシェーダを少しいじってそれっぽく見えるように改良してみます。
float _Occlusion; float4 frag(Varyings input) : SV_Target { float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv2); // 先に行くほどノイズテクスチャが暗くなって細くなるようにする float alpha = furColor.r * (1.0 - input.layer); if (input.layer > 0.0 && alpha < _AlphaCutout) discard; float4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); // 根元の方を暗くする float occlusion = lerp(1.0 - _Occlusion, 1.0, input.layer); float3 color = baseColor * occlusion; color = MixFog(color, input.fogCoord); return float4(color, alpha); }
毛の視認性が上がりました。縁の方に行くにつれ層が見えやすくなってしまっていますね。これがシェル法の欠点なのでフィン法とハイブリッドにする方法もあるようです。
ノイズテクスチャを細かくすればより細かい毛も出来ます。
また、alpha
に適当なテクスチャを掛けてあげれば長さ制御も可能です。例えば _BaseMap
を使いまわして暗いところは毛が短くなるようにしてみます。
float4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv.xy); float alpha = furColor.r * (1.0 - input.layer) * baseColor.r; ...
毛を動かす
概要
ジオメトリシェーダで生成するそれぞれの層のポリゴンを少しずつズラしてあげると毛が風でなびいたり重力で垂れ下がったりする表現が出来ます。
コード
外から移動量を与え、毛先に行くほどズレが大きくなるようにコードを修正します。
// xyz: 移動方向 w: 曲がり具合 float4 _BaseMove; // xyz: 揺れの周期 float4 _WindFreq; // xyz: 揺れの大きさ w: ローカル座標による位相ズレ float4 _WindMove; void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index) { Varyings output = (Varyings)0; VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); // 先に行くほど曲がりを大きくする float moveFactor = pow((float)index / _ShellAmount, _BaseMove.w); // 定期的な揺れ表現 float3 posOS = input.positionOS; float3 windAngle = _Time.w * _WindFreq.xyz; float3 windMove = moveFactor * _WindMove.xyz * sin(windAngle + posOS * _WindMove.w); // 定常的な移動 float3 move = moveFactor * _BaseMove.xyz; // シェルの方向を変える(伸びないように正規化する) float3 shellDir = normalize(normalInput.normalWS + move + windMove); float3 posWS = vertexInput.positionWS + shellDir * (_ShellStep * index); float4 posCS = TransformWorldToHClip(posWS); output.vertex = posCS; output.uv = float4(TRANSFORM_TEX(input.uv, _BaseMap), TRANSFORM_TEX(input.uv, _FurMap)); output.fogCoord = ComputeFogFactor(posCS.z); output.layer = (float)index / _ShellAmount; stream.Append(output); }
結果
影も対応する
元の形はキープするのでそんなに違和感はないですが、パフォーマンスを犠牲にしてもきれいな影を出したい場合もあると思います。影はこれまでのコードをおおよそ流用することで対応できます。
ShaderLab
Shader "Unlit/Fur" { ... SubShader { ... Pass { Name "ShadowCaster" Tags { "LightMode" = "ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #include "./Fur.hlsl" #pragma vertex vert #pragma require geometry #pragma geometry geom #pragma fragment fragShadow ENDHLSL } } }
フラグメントシェーダ
void fragShadow( Varyings input, out float4 outColor : SV_Target, out float outDepth : SV_Depth) { float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv.zw); float alpha = furColor.r * (1.0 - input.layer); if (input.layer > 0.0 && alpha < _AlphaCutout) discard; outColor = outDepth = input.vertex.z / input.vertex.w; }
同じコードは DepthOnly
パスでも利用できます。これによってデプスプリパスが有効化されているケースやデプスを利用するポストエフェクトが必要なときに役に立ちます。
結果
ライティング対応
これまでは Unlit で見てきたので Lit シェーダをもとにしたシェーディングを行っていきましょう。基本は同じで、ライティングに必要なパラメタを追加でフラグメントシェーダに渡すようにし、UniversalFragmentPBR()
して上げれば良い形です。この Lit シェーダの仕組みは以前以下の記事で解説を行いました:
ShaderLab
Shader "Lit/Fur" { Properties { [MainColor] _BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1) _BaseMap("Base Map", 2D) = "white" {} [Gamma] _Metallic("Metallic", Range(0.0, 1.0)) = 0.5 _Smoothness("Smoothness", Range(0.0, 1.0)) = 0.5 _FurMap("Fur Map", 2D) = "white" {} [IntRange] _ShellAmount("Shell Amount", Range(1, 14)) = 14 _ShellStep("Shell Step", Range(0.0, 0.01)) = 0.001 _AlphaCutout("Alpha Cutout", Range(0.0, 1.0)) = 0.2 _Occlusion("Occlusion", Range(0.0, 1.0)) = 0.5 _BaseMove("Base Move", Vector) = (0.0, -0.0, 0.0, 3.0) _WindFreq("Wind Freq", Vector) = (0.5, 0.7, 0.9, 1.0) _WindMove("Wind Move", Vector) = (0.2, 0.3, 0.2, 1.0) } SubShader { Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "UniversalMaterialType" = "Lit" "IgnoreProjector" = "True" } LOD 100 ZWrite On Cull Back Pass { Name "ForwardLit" Tags { "LightMode" = "UniversalForward" } HLSLPROGRAM #pragma prefer_hlslcc gles #pragma exclude_renderers d3d11_9x #pragma target 2.0 // マテリアルのキーワード #pragma shader_feature _NORMALMAP #pragma shader_feature _ALPHATEST_ON #pragma shader_feature _ALPHAPREMULTIPLY_ON #pragma shader_feature _EMISSION #pragma shader_feature _METALLICSPECGLOSSMAP #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A #pragma shader_feature _OCCLUSIONMAP #pragma shader_feature _SPECULARHIGHLIGHTS_OFF #pragma shader_feature _ENVIRONMENTREFLECTIONS_OFF #pragma shader_feature _SPECULAR_SETUP #pragma shader_feature _RECEIVE_SHADOWS_OFF // URP のキーワード #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile _ _SHADOWS_SOFT #pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE // Unity のキーワード #pragma multi_compile _ DIRLIGHTMAP_COMBINED #pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile_fog #include "./FurLit.hlsl" #pragma vertex vert #pragma require geometry #pragma geometry geom #pragma fragment frag ENDHLSL } Pass { Name "DepthOnly" ... } Pass { Name "ShadowCaster" ... } } }
Lit シェーダの設定をそのまま持ってきます。Properties には _Metallic
と _Smoothness
を追加しておきます。シェーダ自体は FurLit.hlsl に書くので次はそちらを見てみましょう。
頂点・ジオメトリ・フラグメントシェーダ
ちょっと長いですが...、全文です:
#ifndef FUR_LIT_HLSL #define FUR_LIT_HLSL #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" int _ShellAmount; float _ShellStep; float _AlphaCutout; float _Occlusion; float4 _BaseMove; float4 _WindFreq; float4 _WindMove; TEXTURE2D(_FurMap); SAMPLER(sampler_FurMap); float4 _FurMap_ST; struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; float2 lightmapUV : TEXCOORD1; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float3 positionWS : TEXCOORD0; float3 normalWS : TEXCOORD1; float4 uv : TEXCOORD2; DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 3); float4 fogFactorAndVertexLight : TEXCOORD4; // x: fogFactor, yzw: vertex light float layer : TEXCOORD5; }; Attributes vert(Attributes input) { return input; } void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index) { Varyings output = (Varyings)0; VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); float moveFactor = pow(abs((float)index / _ShellAmount), _BaseMove.w); float3 posOS = input.positionOS.xyz; float3 windAngle = _Time.w * _WindFreq.xyz; float3 windMove = moveFactor * _WindMove.xyz * sin(windAngle + posOS * _WindMove.w); float3 move = moveFactor * _BaseMove.xyz; float3 shellDir = SafeNormalize(normalInput.normalWS + move + windMove); output.positionWS = vertexInput.positionWS + shellDir * (_ShellStep * index); output.positionCS = TransformWorldToHClip(output.positionWS); output.uv = float4(TRANSFORM_TEX(input.texcoord, _BaseMap), TRANSFORM_TEX(input.texcoord, _FurMap)); output.normalWS = TransformObjectToWorldNormal(input.normalOS); output.layer = (float)index / _ShellAmount; float3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS); float fogFactor = ComputeFogFactor(vertexInput.positionCS.z); output.fogFactorAndVertexLight = half4(fogFactor, vertexLight); OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV); OUTPUT_SH(output.normalWS.xyz, output.vertexSH); stream.Append(output); } [maxvertexcount(42)] void geom(triangle Attributes input[3], inout TriangleStream<Varyings> stream) { [loop] for (float i = 0; i < _ShellAmount; ++i) { [unroll] for (float j = 0; j < 3; ++j) { AppendShellVertex(stream, input[j], i); } stream.RestartStrip(); } } float3 TransformHClipToWorld(float4 positionCS) { return mul(UNITY_MATRIX_I_VP, positionCS).xyz; } float4 frag(Varyings input) : SV_Target { float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv.zw); float alpha = furColor.r * (1.0 - input.layer); if (input.layer > 0.0 && alpha < _AlphaCutout) discard; SurfaceData surfaceData = (SurfaceData)0; InitializeStandardLitSurfaceData(input.uv.xy, surfaceData); surfaceData.occlusion = lerp(1.0 - _Occlusion, 1.0, input.layer); InputData inputData = (InputData)0; inputData.positionWS = input.positionWS; inputData.normalWS = input.normalWS; inputData.viewDirectionWS = SafeNormalize(GetCameraPositionWS() - inputData.positionWS); #if defined(_MAIN_LIGHT_SHADOWS) && !defined(_RECEIVE_SHADOWS_OFF) inputData.shadowCoord = TransformWorldToShadowCoord(input.positionWS); #else inputData.shadowCoord = float4(0, 0, 0, 0); #endif inputData.fogCoord = input.fogFactorAndVertexLight.x; inputData.vertexLighting = input.fogFactorAndVertexLight.yzw; inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS); float4 color = UniversalFragmentPBR(inputData, surfaceData); color.rgb = MixFog(color.rgb, inputData.fogCoord); return color; } #endif
ジオメトリシェーダを見てみると、Varyings
にライティング用の法線や頂点ライティングといったパラメタが増えています。この関係で先に説明したように maxvertexcount
が減るので 1 Pass では 14 層が最大になってしまっています...。より多くシェルの層を行いたい場合は 2 Pass、3 Pass といったようにパスを増やすことを検討しないとなりません。
フラグメントシェーダでは InitializeStandardLitSurfaceData()
してプロパティで設定したパラメタを SurfaceData
に格納したあと、InputData
の各メンバに情報を詰め、UniversalFragmentPBR()
でライティングや影の計算が行われます。SurfaceData
の occlusion
に Unlit のときは直接カラーに掛けてた分を代入しておきます(albedo
にも乗算しても良いかもしれません)。
結果
結果を見てみると...、ちょっと根本が暗すぎます。これは影が過度に落ちてしまっているからです。これを修正するには ShadowCater
パスに手を入れます。
ShadowCaster
バイアスの調整
先程 Unlit で見た ShadowCaster
でのコードはバイアスを考慮していませんでした。これにより毛を生やさないときはシャドウアクネが生じているのが見えます。
なのでバイアスを掛けるようコードを修正します。
void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index) { ... // float4 posCS = TransformWorldToHClip(posWS); // バイアスを適用するように修正 float4 posCS = TransformWorldToHClip(ApplyShadowBias(posWS, normalInput.normalWS, _LightDirection)); #if UNITY_REVERSED_Z posCS.z = min(posCS.z, posCS.w * UNITY_NEAR_CLIP_VALUE); #else posCS.z = max(posCS.z, posCS.w * UNITY_NEAR_CLIP_VALUE); #endif ... }
過度に影が出なくなりました。
更にバイアスを調整
それでもちょっと影が気になる気もします。影はここでは出ないようにしたいな~という場合には更にこのバイアスの計算に手を入れます。具体的には _ShadowBias
としてグローバルに設定されているところにオブジェクト単位の追加値を加算できるようにします。
float _ShadowExtraBias; inline float3 CustomApplyShadowBias(float3 positionWS, float3 normalWS) { positionWS += _LightDirection * (_ShadowBias.x + _ShadowExtraBias); float invNdotL = 1.0 - saturate(dot(_LightDirection, normalWS)); float scale = invNdotL * _ShadowBias.y; positionWS += normalWS * scale.xxx; return positionWS; } inline float4 GetShadowPositionHClip(float3 positionWS, float3 normalWS) { positionWS = CustomApplyShadowBias(positionWS, normalWS); float4 positionCS = TransformWorldToHClip(positionWS); #if UNITY_REVERSED_Z positionCS.z = min(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE); #else positionCS.z = max(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE); #endif return positionCS; } void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index) { ... float4 posCS = GetShadowPositionHClip(posWS, normalInput.normalWS); ... }
_ShadowExtraBias
にマイナスの値を与えてあげると次のようになります。
良くなりました!この見た目だと occlusion
を albedo
に乗算するとより良くなる感じがします。
法線を考慮する
それでもまだリアリティが足りません。よくよく見ると毛の 1 本 1 本にライティングが施されていません。これは毛のライティングとして用いる法線はもともとのポリゴン(ここではスフィア)の法線が使われてしまっているからです。そこで法線を考慮したライティングをすることにします。
法線マップ
法線情報としては _FurMap
に対応した法線テクスチャを外から与えることにします。法線テクスチャは以下のサイトを利用すると簡単に作ることが出来ます:
シェーダの変更
まず、ShaderLab の Properties ブロックに法線テクスチャ用のスロットを足します:
Properties { ... _NormalMap("Normal", 2D) = "bump" {} _NormalScale("Normal Scale", Range(0.0, 2.0)) = 1.0 ... _FurScale("Fur Scale", Range(0.0, 10.0)) = 1.0 ... }
フラグメントシェーダで法線マップの読み込みを行うために、Varyings
に tangent
を追加します。このため、ジオメトリシェーダの出力数が更に減ってしまうのと、ファー用のノイズテクスチャと法線マップで同じ UV のスケールを使いたいので、UV で float4 使っていたところを float2 になるようにするため、_FurScale
を追加します。そして Varyings
は以下のようにします。
struct Varyings { float4 positionCS : SV_POSITION; float3 positionWS : TEXCOORD0; float3 normalWS : TEXCOORD1; float3 tangentWS : TEXCOORD2; float2 uv : TEXCOORD4; DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 5); // float3 float4 fogFactorAndVertexLight : TEXCOORD6; // x: fogFactor, yzw: vertex light float layer : TEXCOORD7; };
これで構造体のサイズとしては 23 なので、maxvertexcount
は 1024 / 23 ~ 44 で、最大の層数は 44 / 3 ~ 14 層です。10 層以上あるので数は十分確保できそうですね。
そしてジオメトリシェーダの中で tangentWS
を渡すように修正します。
void AppendShellVertex(inout TriangleStream<Varyings> stream, Attributes input, int index) { ... output.tangentWS = normalInput.tangentWS; ... }
フラグメントシェーダの中でこれを使って法線マップから法線情報を取り出します。
float _FurScale; TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); float4 _NormalMap_ST; float _NormalScale; float4 frag(Varyings input) : SV_Target { float2 furUv = input.uv / _BaseMap_ST.xy * _FurScale; float4 furColor = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, furUv); float alpha = furColor.r * (1.0 - input.layer); if (input.layer > 0.0 && alpha < _AlphaCutout) discard; float3 normalTS = UnpackNormalScale( SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, furUv), _NormalScale); float sgn = input.tangentWS.w; // viewDirWS.y float3 bitangent = sgn * cross(input.normalWS.xyz, input.tangentWS.xyz); float3 normalWS = TransformTangentToWorld( normalTS, float3x3(input.tangentWS.xyz, bitangent.xyz, input.normalWS.xyz)); SurfaceData surfaceData = (SurfaceData)0; ... inputData.normalWS = normalWS; inputData.viewDirectionWS = viewDirWS; ... }
結果
これで次のようになります。とてもライティングに馴染んだ見た目になりました!
床もモフモフにしてみました。奥の Unlit なファーと比べると大分印象が違いますね。
リムライトの追加
実際の毛は細く透ける感じがします。特に逆光時の縁は光が透けて見える感じがしますよね。次はそういったリムライト成分を追加してみたいと思います。
ShaderLab
Properties { ... _RimLightPower("Rim Light Power", Range(0.0, 20.0)) = 6.0 _RimLightIntensity("Rim Light Intensity", Range(0.0, 1.0)) = 0.5 ... }
リムライトの適用度合いは視線方向と法線方向やライト方向の内積で決めますが、この際どれくらいの範囲に適用するかコントロールするために pow()
の係数として _RimLightPower
を用意しておきます。
シェーダ
以下のように ApplyRimLight()
を PBR の計算の後ろに入れます。URP の真骨頂である GetMainLight()
や GetAdditionalPerObjectLight()
によって様々なライトの情報(色、向き、減衰具合、影)が簡単に取得できるので、これによって影や距離も考慮したリムライトが可能になります。
void ApplyRimLight(inout float3 color, float3 posWS, float3 viewDirWS, float3 normalWS) { float viewDotNormal = abs(dot(viewDirWS, normalWS)); float normalFactor = pow(abs(1.0 - viewDotNormal), _RimLightPower); Light light = GetMainLight(); float lightDirDotView = dot(light.direction, viewDirWS); float intensity = pow(max(-lightDirDotView, 0.0), _RimLightPower); intensity *= _RimLightIntensity * normalFactor; #ifdef _MAIN_LIGHT_SHADOWS float4 shadowCoord = TransformWorldToShadowCoord(posWS); intensity *= MainLightRealtimeShadow(shadowCoord); #endif color += intensity * light.color; #ifdef _ADDITIONAL_LIGHTS int additionalLightsCount = GetAdditionalLightsCount(); for (int i = 0; i < additionalLightsCount; ++i) { int index = GetPerObjectLightIndex(i); Light light = GetAdditionalPerObjectLight(index, posWS); float lightDirDotView = dot(light.direction, viewDirWS); float intensity = max(-lightDirDotView, 0.0); intensity *= _RimLightIntensity * normalFactor; intensity *= light.distanceAttenuation; #ifdef _MAIN_LIGHT_SHADOWS intensity *= AdditionalLightRealtimeShadow(index, posWS); #endif color += intensity * light.color; } #endif } float4 frag(Varyings input) : SV_Target { ... float4 color = UniversalFragmentPBR(inputData, surfaceData); ApplyRimLight(color.rgb, input.positionWS, viewDirWS, input.normalWS); ... return color; }
結果
ディレクショナルライトだけでなくスポットライトなど追加のライトも含めて、いい感じにリムライトが適用されました!
おまけ
Metallic と Smoothness の変更
ヒョウ柄
おわりに
久しぶりにシェーダで遊びましたがとても面白かったです。シェル法は実装がシンプルな割に見た目が大きく変わって楽しいですね。また、URP のライティング周りはとても書きやすくて好きです。
続き
フィン法も書きました。