2017年7月19日

Unity スプライトに影をつける ~その2~


スプライトの裏表に影をつけるために悪戦苦闘した記録をメモメモ…


※このページの内容の動作確認にはUnity5.6を使用しています。
前回はSprite Rendererのマテリアルを変更することで一応スプライトに影をつけることができました。ただ、スプライトの裏面が全く描画されなくなってしまったので、今回はその対策をしていきたいと思います。

反対側から見た惨状

通常、Standardシェーダーをはじめとした多くのシェーダーでは面の裏側は描画しないようになっています。どうせほとんどの場合メッシュの内側になって見えないので、描画してもムダだからです。おそらくスプライトは片面だけの四角の板(Quad)に画像を貼り付けて表示しているためStandardシェーダーだと裏側が消えちゃう…ということなんでしょう。

つまり、表裏両面ある板のモデルを用意する、というのがちゃんとした解決策だと思うのですが、今回は一応シェーダー側で対応してみたいと思います。

Cull Off を指定する

デフォルトではCull Back(裏面を省略=表面だけ描画)になっているので、シェーダー内でCull Off(省略なし=表裏どちらも描画)に変更します。

組み込みのStandardシェーダーのソースコードはこちらのページでダウンロードできる(現在使用しているUnityのバージョンの「ダウンロード」 > 「ビルトインシェーダー」を選択)ので、Standard.shaderだけを抜き取りましょう。中身を見ると、まずすごい数のプロパティが並んでいて、あとはひたすらPassが書かれています。実際の処理は別ファイル(.cginc)に丸投げしているんですね…

とりあえず最初のSubShaderの冒頭部分に"Cull Off"を追加してみます。

  1. [前略]
  2.  
  3. SubShader
  4. {
  5. Tags { "RenderType"="Opaque" "PerformanceChecks"="False" }
  6.  LOD 300
  7.  
  8. Cull Off
  9. // ------------------------------------------------------------------
  10. // Base forward pass (directional light, emission, lightmaps, ...)
  11. Pass
  12. {
  13.  
  14. [後略]

こんな感じ。自分の環境(バージョン5.6.0)の場合は59行目に追加しました。変更点としてはこれだけです。あとはスプライト用のマテリアルにこの修正したシェーダーを設定します。

これでどうなるかというと…


裏面が表示されました!やったね!

ただ、まだ若干おかしなところがあります。光源が左奥にあって、花や草の裏側(手前を向いている面)には直接光が当たっていないはずなのに、すごく明るくなってしまっているのです。これは裏面の影のつき方が表面そのままになっているからですね。紙や葉っぱみたいな光を通すほど薄い材質のものならこの表現でも特に違和感はありませんが、今回は表面と裏面で別々の影がつくよう修正してみます。

Cutoutシェーダーを作る

というわけでまたシェーダーに変更を加えるのですが、さすがに組み込みのStandard.Shaderをこれ以上編集するのは自分の手に負えない気がしてきました。だって別ファイルのコード量すげー多いんだもん!把握しきれん!

てなわけでもっと手軽に管理できるよう、Create > Shader > Standard Surface Shaderから新しくシェーダーを作成し、そこにCutoutの処理をつけてみることにしました。

  1. Shader "Custom/Cutout Double-sided" {
  2. Properties {
  3.   _Cutoff ("Cutoff", Range(0,1)) = 0.5
  4.   _Color ("Color", Color) = (1,1,1,1)
  5.   _MainTex ("Albedo (RGB)", 2D) = "white" {}
  6.   _Glossiness ("Smoothness", Range(0,1)) = 0.5
  7.   _Metallic ("Metallic", Range(0,1)) = 0.0
  8. }
  9. SubShader {
  10.   Tags { "Queue"="AlphaTest" "RenderType"="TransparentCutout" }
  11.   LOD 200
  12.   
  13.   Cull Off
  14.   
  15.   CGPROGRAM
  16.   #pragma surface surf Standard alphatest:_Cutoff addshadow fullforwardshadows
  17.   #pragma target 3.0
  18.   
  19.   sampler2D _MainTex;
  20.  
  21.   struct Input {
  22.    float2 uv_MainTex;
  23.   };
  24.  
  25.   half _Glossiness;
  26.   half _Metallic;
  27.   fixed4 _Color;
  28.  
  29.   void surf (Input IN, inout SurfaceOutputStandard o) {
  30.    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
  31.    o.Albedo = c.rgb;
  32.    o.Metallic = _Metallic;
  33.    o.Smoothness = _Glossiness;
  34.    o.Alpha = c.a;
  35.   }
  36.   ENDCG
  37. }
  38. FallBack "Diffuse"
  39. }

こんな感じです。基本的な変更点は4つだけで、あとはそのままです(GPUインスタンシングの設定行はよくわらないので消しちゃいました☆テヘペロ)。

  1. _Cutoff ("Cutoff", Range(0,1)) = 0.5

まず3行目に_Cutoffの変数を追加。16行目のalphatest:_Cutoffで使用します。

  1. Tags { "Queue"="AlphaTest" "RenderType"="TransparentCutout" }

10行目では透過シェーダーとして認識されるようタグを指定しています。
参考: ShaderLab:SubShader内のTags, Replaced Shadersでのレンダリング

  1. Cull Off

13行目のCull Offは先ほど設定したとおり、両面を描画する処理です。

  1. #pragma surface surf Standard alphatest:_Cutoff addshadow fullforwardshadows

そして最後に最も重要なのが16行目。alphatest:[変数名]というパラメータを指定すると、アルファ値が[変数名]より大きい箇所のみを描画し、小さい箇所は完全な透明にしてくれます。このシェーダーの肝ですね。ただ、それだけだと影のつき方がおかしくなる(透過を無視したつき方になる)ので、addshadowを使ってきちんと影がつくようにしています。しかしさらに問題があって、addshadowはDirectional Light(つまり「太陽」)では影がつくのですが、SpotlightやPoint Lightではうまく機能しません。そこでfullforwardshadowsを指定してSpotlightやPoint Lightでも影がちゃんと落ちるようにしているわけです。逆に言うと、Directional Lightしか使わないのであれば別に指定しなくても大丈夫だと思います(たぶん)。
参考: サーフェスシェーダーの記述

左が fullforwardshadows を指定しない場合 右が指定した場合

実際の計算処理は書かず、パラメータを指定しただけでできちゃいました。いやあ、サーフェスシェーダーってほんと便利~。ひとまずこれでStandardのCutoutとも遜色なく…とはいかないまでも、凝った処理じゃなければきちんと動くシェーダーになりました。正直、シェーダーに関してまだまだ初心者なので、「さしあたり問題なく動く」ようにするのが精一杯です。

裏側を描画しなおす

さて、Cutoutシェーダーを作成し、なんとなく馴染みのあるコードになったのはいいものの、このままでは状況は変わりません。目標は「裏面に陰が描画されるようにする」ことなのです!

しばらく考えていろいろ試した結果、次のようなシェーダーを書いてみました。

  1. Shader "Custom/Cutout Double-sided" {
  2. Properties {
  3.   _Cutoff ("Cutoff", Range(0,1)) = 0.5
  4.   _Color ("Color", Color) = (1,1,1,1)
  5.   _MainTex ("Albedo (RGB)", 2D) = "white" {}
  6.   _Glossiness ("Smoothness", Range(0,1)) = 0.5
  7.   _Metallic ("Metallic", Range(0,1)) = 0.0
  8. }
  9. SubShader {
  10.   Tags { "Queue"="AlphaTest" "RenderType"="TransparentCutout" }
  11.   LOD 200
  12.   
  13.   Cull Off
  14.   
  15.   CGPROGRAM
  16.   #pragma surface surf Standard alphatest:_Cutoff addshadow fullforwardshadows
  17.   #pragma target 3.0
  18.  
  19.   sampler2D _MainTex;
  20.   
  21.   struct Input {
  22.    float2 uv_MainTex;
  23.   };
  24.   
  25.   half _Glossiness;
  26.   half _Metallic;
  27.   fixed4 _Color;
  28.  
  29.   void surf (Input IN, inout SurfaceOutputStandard o) {
  30.    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
  31.    o.Albedo = c.rgb;
  32.    o.Metallic = _Metallic;
  33.    o.Smoothness = _Glossiness;
  34.    o.Alpha = c.a;
  35.   }
  36.   ENDCG
  37.  
  38.   // ここから追加
  39.   Cull Front
  40.   
  41.   CGPROGRAM
  42.  
  43.   #pragma surface surf Standard alphatest:_Cutoff fullforwardshadows vertex:vert
  44.   #pragma target 3.0
  45.  
  46.   sampler2D _MainTex;
  47.  
  48.   struct Input {
  49.    float2 uv_MainTex;
  50.   };
  51.  
  52.   // 法線を反転させて裏面の影の描写がきちんと行われるようにする
  53.   void vert (inout appdata_full v) {
  54.    v.normal.xyz = v.normal * -1;
  55.   }
  56.  
  57.   half _Glossiness;
  58.   half _Metallic;
  59.   fixed4 _Color;
  60.   
  61.   void surf (Input IN, inout SurfaceOutputStandard o) {
  62.    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
  63.    o.Albedo = c.rgb;
  64.    o.Metallic = _Metallic;
  65.    o.Smoothness = _Glossiness;
  66.    o.Alpha = c.a;
  67.   }
  68.   ENDCG
  69. }
  70. FallBack "Diffuse"
  71. }

追加したのは39行目から。

  1. Cull Front

Cull Frontで表面を省略=裏面だけ描画するように指定しています。

  1. v.normal.xyz = v.normal * -1;

頂点情報をいじるvert関数内では、法線(面の向き)に-1をかけて反転させています。

61行目以降のsurf関数を含め、後半部分はほぼすべて前半部分のコピペです。つまりこのシェーダーでは「法線を逆にして、もっぺん表面と同じように裏面も処理しろ!」と命令しているわけです。

単純すぎるだろ!とか、なんか冗長だし全然スマートじゃない!とか、パフォーマンス的にどうなんだ!とか、もっと効率的な方法があるだろ!とか、いろんな気持ちが湧いてきますが、そこはグッとこらえましょう。


兎にも角にも、これで光源の反対側にちゃんと影がつきました。


くるくる回転させてみると、裏表関係なく影/陰が描画されているのがわかります。

一応これで影をつけるという目的は果たしたのですが、Sprite Rendererのデフォルトのシェーダー(Sprites-Default.shaderやUnitySprites.cginc)を覗いてみると、インスタンス化して描画を最適化する処理が書かれています。GPUインスタンシングの処理はさっき「テヘペロ」とか言いつつ抹消してしまいましたが、また暇があればインスタンス化にも挑戦してみようと思います(無理そう)。

<次回> スプライトに影をつける ~その3~

0 件のコメント:

コメントを投稿