Help us understand the problem. What is going on with this article?

[Unity] 邪道なDeferredの使い方で苦しんだ話(昼夜のドット絵で夕焼けアニメ:2)

はじめに

以前、「[Unity] 昼夜二枚のドット絵をもとにシームレスな夕焼けアニメーションをする」という記事を書きましたが、最後の補足でこう書きました。

シムシティのようなゲームを考えた場合、建物それぞれが夕焼け効果をレンダリングするのは無駄が多いと思われます。昼と夜のシーン画像をレンダリングして、ポストエフェクトなどでシーン全体で夕焼け効果を合成するのが理想的でしょう。いずれ機会を見つけてチャレンジしたいですね。

ということで、今回はシーン全体で夕焼けアニメーションするチャレンジしてみました。
ちなみに、このシーンは以前記事に書いたドット絵を3Dメッシュに正射影して作ったものを前提としています。
timelapse2.gif

※建物の絵は FreeTrain というオープンソースのゲームから、作者の了解を得て拝借しています。

その際、Deferredレンダリングを本来の目的と違う邪道な使い方で使ったのでタイトルです。なのでアンチパターンみたいな内容期待してた方、釣りタイトルですみません:bow:

それと、途中まではタイトルどおり、いかに苦しんでどう解決したかの話なので、結果だけが知りたいんじゃ!という方は一気に「最終的な実装」まで読み飛ばしちゃってください。

私が Deferred を選んだわけ

前の記事では「ポストエフェクトなどで」なんて書いておいて、なぜ Deferred なのか。

ポストエフェクトでやる場合、昼夜のシーンをそれぞれ別のカメラでテクスチャに描画することになりそうだな、と考えていました。でもそれって、頂点シェーダーやら深度バッファーやら全く同じなのに二度処理するなんて無駄だよなー。DX11 には MRT(Multi Render Target)なんて便利な機能があったけど、ああいうのが欲しいなー。
などと考えていた時、社内勉強会で Deferred レンダリングについて聞いたことを思い出しました。

ご存知の方には今更ですが、 Deferred レンダリングというのは、シーン全体のオブジェクトの色・材質や法線などをそれぞれ別のバッファーに書き出して、最後に合成する手法 ですよね(参考:UnityのDeferredでCommandBufferを利用してGBufferをいじってみる )。これの各バッファへの書き出しと最後の合成をカスタマイズできないかと思って調べていました。

Scriptable Rendering Pipeline (SRP)は使えないのか?

Deferred レンダリングについて調べていて、すぐに SRP という技術があることも知りました。最近出てきた物理ベースレンダリングの HDRP とやらも SRP の一形態らしいし、とにかく今までUnityがこっそりやってきた部分まで、スクリプトでカスタマイズできるらしい。これこそ求めていたやつ!?

ってことで HDRP のサンプルプロジェクトDLしてみたものの、ものすごく大量のファイルがあってどこから手を入れたらいいのやら。
ググってみても、HDRP の使い方はちらほらあるものの、私みたいに邪道な使い方のために HDRP を改造しようなんて例は当然見つからないし、SRP も「一から自分で作るのは大変」なんて書いてある始末。

こりゃあ、SRP どころか HDRP すら初心者の自分が手を出せる代物ではないな、と断念しました。

用途ごとの各バッファ(G-Buffer)に書き出す例(UnityのDeferredでCommandBufferを利用してGBufferをいじってみる)と 各バッファを合成するシェーダーの作り方(Unityで影だけを表示させたい)を見つけたことで、九割方できたと思いました。

後でそれは大きな間違いだったとわかるのですが・・・

Deferred レンダリングをカスタマイズ

Deferred レンダリングでは、下表のようにGBufferと呼ばれる複数のバッファーにそれぞれ異なる情報を書き出して、最後に合成するわけですが、これを右端の列のように変えたいと思います。

g-Buffer 本来の用途 カスタマイズ後の用途
RT0 Diffuse (RGB), occlusion (A) 昼用画像 (RGB)
RT1 Specular (RGB), roughness (A) IDカラー (ARGB)
RT2 World space normal (RGB) そのまま
RT3 Emission, lighting, lightmaps, reflection probes 夜用画像 (RGB)

参考:【公式】ディファードシェーディングレンダリングパス

最終パスでは、昼用画像(RT0)に必要に応じて夕焼けなどのリニア焼き込みカラー処理をして、夜用画像(RT1)と比較して明るい方を合成します。

問題1:Deferred は Orthographic カメラで使えない!?

手始めにカメラを Deferred 設定に切り替えたところこんな警告が出ました。
ortho-camera-deferred.jpg
いわく、「Deferred レンダリングは Orthographic カメラでは動かないので、Forward を使いますよ」

初っ端から躓きました。
しかし慌てることはありません。おそらくこれは、Deferred におけるライティングか何かの計算が(例えば0除算とかで)うまくできないということでしょう。
今回の目的は単に MRT ぽいことがやりたいだけで Deferred の本来のライティングは不要です(ドット絵には既に陰影がついている!)

ただ問題は、このままでは勝手に Deferred が無効化されてしまうことです。どうにかしてカメラを騙して、 Orthographic な行列を使いつつ、Orthographic になってないと思わせる必要があります。

幸いなことに、現在のシーンにはカメラの射影行列をカスタマイズするスクリプトが既に入っています。これをちょっと書き換えるだけで、カメラを騙すことができました。

 Assets/Assets/Scripts/QuaterView.cs | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/Assets/Assets/Scripts/QuaterView.cs b/Assets/Assets/Scripts/QuaterView.cs
index 91be993..6199b96 100644
--- a/Assets/Assets/Scripts/QuaterView.cs
+++ b/Assets/Assets/Scripts/QuaterView.cs
@@ -30,10 +30,11 @@ public class QuaterView : MonoBehaviour
         Awake();
     }

-    private float CalcOrthoSize()
+    private Vector2 CalcOrthoSize()
     {
         // 画面の高さの半分=等倍サイズ
-        return camera.pixelHeight / (zoom * 2f);
+        var hsize = camera.pixelHeight / (zoom * 2f);
+        return new Vector2((float)camera.pixelWidth / camera.pixelHeight * hsize, hsize);
     }

     private void AdjustCamera()
@@ -52,9 +53,11 @@ public class QuaterView : MonoBehaviour
             m20 =-0.5f, m21 = 1.0f, m22 =-0.5f, m23 = 0.0f - pos.z - distance,
             m30 = 0.0f, m31 = 0.0f, m32 = 0.0f, m33 = 1.0f            
         };
-
+        
+        var orthoSize = CalcOrthoSize();
+        var projMatrix = Matrix4x4.Ortho(orthoSize.x * -1, orthoSize.x, orthoSize.y * -1, orthoSize.y, 0, 1000);
+        camera.projectionMatrix = projMatrix;
         camera.worldToCameraMatrix = matrix;
-        camera.orthographicSize = CalcOrthoSize();
-       
+
     }
 }

ポイントは、 camera.orthographicSizeセットしないようにすることです。
代わりに orthographic な行列を生成して projectionMatrix にセットしました。

それと当然ですが、カメラ Component の方は Perspective に戻します。冒頭の警告は出ていません。
camera-projection.jpg

現段階では、まだドット絵のシェーダーは以前のままなので変化しません。代わりにただの白い立方体をデパートの上にのせてみました。

Deferred無効 カメラを騙して描かせたら
whitebox.jpg blackbox.jpg

左(修正前)は普通に立方体が描画されていますが、これは Deferred が無効になっていて普通に Forward でレンダリングされてるので当然です。右(修正後)は真っ黒になってしまいましたが、逆にこれは Deferred でレンダリングされるようになったと考えてよいでしょう。あんな警告が出るぐらいですから、まともに描画されるとは期待していません。想定内、想定内・・・・。

問題2:Deferred レンダリング結果が真っ黒になる

次に冒頭で紹介した参考記事に従って Deferred 用のカスタムシェーダーを適用してみました。

参考1:UnityのDeferredでCommandBufferを利用してGBufferをいじってみる
参考2:[teratail] Unityで影だけを表示させたい

ちなみに公式ドキュメントにあるビルトインシェーダーのリンクは404ですが、ググると下記の github のリポジトリが見つかります。
https://github.com/TwoTailsGames/Unity-Built-in-Shaders/tree/master/DefaultResourcesExtra

カスタマイズの要所だけ抜き出すと、まず各建物オブジェクトをG-Bufferに書き出すフラグメントシェーダーがこんな感じ。これは各ゲームオブジェクトのマテリアルに設定します。

DayNightPixelArtsGBuffer.shader
      void frag (in v2f i, out flagout o)
      {
        fixed4 colDay = tex2D(_DayTex, i.uv);
        // stop rendering if it is transpalent color.
        fixed3 diff = abs(colDay.rgb - _Transpalent.rgb);
        if(length(diff) < 0.0001) discard;

        // use 25% brightness of day texture, if night texture is disabled.
        fixed4 colNight = lerp(colDay * 0.25, tex2D(_NightTex, i.uv), _NightTexEnabled);

        o.gBuffer0 = colDay;
        o.gBuffer3 = colNight;
        o.gBuffer2 = float4(i.normal, 0) * 0.5 + float4(0.5, 0.5, 0.5, 0);
        o.gBuffer1 = _IDColor;
        o.depth = i.position.z;
      }

gBuffer2 はオリジナルのままの法線計算です。gBuffer1 の _IDColor って何だ?と思われるでしょうが、これは建物オブジェクト単位で違う色を割り当てて将来ヒットテストに使えないかと思っての布石です。どちらも今回は使いませんので、軽く流していただければ幸い。

そして、G-Bufferから最終的な画像を描画するため、ビルトインの Inner-DefferdShading.shader を置き換えたフラグメントシェーダーはこう。

DayNightPixelArtsLit.shader
#ifdef UNITY_HDR_ON
half4
#else
fixed4
#endif
frag (unity_v2f_deferred i) : SV_Target
{
    float2 uv = i.uv.xy / i.uv.w;
    half4 c =  tex2D (_CameraGBufferTexture0, uv);
    #ifdef UNITY_HDR_ON
    return c;
    #else
    return exp2(-c);
    #endif
}

このシェーダーはこんな風に Project Settings > Graphics > Built-in Shader Settings に設定しました。
custom-shader.jpg
ところが昼画像を書き出すようにしたつもりなのに真っ黒になります。
silhouette.jpg
そういえば、カメラを騙して無理やりかかせた立方体も真っ黒でしたね。

どうやらまだ、置き換え足りないところがあるようです。
とりあえず Project Settings を調べてみると、 Deferred Reflections ってシェーダーがありました。
projectsettings.jpg
名前からしてリフレクション(反射)だけど、今回の目的には必要ない(※ドット絵オブジェクトは一方向からしか見ない前提の作りなので、どのみち他の角度からの映像は綺麗に描けない)ので No Support にしてみたら、ドット絵が表示されました!!
blend-default.jpg

問題3:Deferred-Lighting パスで意図しない色が混ざる

しかし、よくよく見ると表示された絵がちょっとおかしい。今は一旦昼画像だけを描画するようにしたつもりなのに、夜画像が混じってる!しかもなんか全体的に白っぽくなってます。G-Buffer-3 が意図せず混じってるようです。

現実(その1) 理想(Photopia)
blend-default.jpg blend-photopea.jpg

なので、あらかじめ G-Buffer-0 から G-Buffer-3 の分を引いておいたらいい感じに混ざるんじゃないかと思ったんですが、そう簡単な話でもないようで・・・(赤い矢印に注目)

現実(その2): RT0-RT3 理想(Photopia)
blend-default-diff.jpg blend-photopea.jpg

当初は、Unity がまだ独自に処理してる部分があるんだと考えて、無効化あるいは置き換える方法を探していましたが、一向に見つかりません。

最早万策尽きたかと思いましたが、ふとシェーダーの Blend 指定に目がいき、One とか Zero とか弄ることで描画結果が変わることに気づきました。
恥ずかしながらここに至って、参考記事に書いてあった下記の文の意味がようやく腑に落ちました。gBuffer-3 はただの前処理用のサーフェスではなく、レンダリング結果の描きだし先でもあるんですね。

HDRのときは4つ目のGBufferのRT3にはカメラのターゲットが利用されるようです。

そこで、望み通りの合成結果が出るように、適切なブレンド設定を探してみました。
参考:[公式]ShaderLab構文:Blending

試行錯誤するうちに、BlendOp Max というのが目に留まりました。建物単位でシェーダーかけてた時は、夜と昼と明るい方のピクセルを採用するという処理がありました。その代わりに BlendOp Max を使えばシェーダーの処理を少し省略しつつ問題も解決できて一石二鳥ではないか!と。

早速試してみた結果がこれです。(コメントアウトしてある行はオリジナルの記述です。)

DayNightPixelArtsLit.shader
Pass {
    ZWrite Off
    //Blend [_SrcBlend] [_DstBlend]
    BlendOp Max
現実(最終結果) 理想(Photopia)
blend-op-max.jpg blend-photopea.jpg

完璧! :thumbsup:

Blend Zero One とか、もっと簡単なブレンドモードがあるのでは?

そんな難しいこと考えなくても Blend Zero One とかしたら、書き込み先の色の影響を受けずに書き込みできるのでは?
・・・私も最初そう考えました。
ところが、そうすると tex2D (_CameraGBufferTexture3, uv) した時点でもう真っ黒なんです。
他にも GBuffer3 のアルファ値を 0 にして Blend destAlpha One とかやってみたけど、 tex2D で取得したら RGB 値まで 0 になっちゃってました。

要するにDstのブレンドモードの結果はフラグメントシェーダーに入った時点で適用されちゃっているということなんでしょう。
XOR モードでもあれば、Dstカラーを完全に打ち消しつつSrcカラーを書き込めるんでしょうが、DX11 でしかサポートしてないようなので、今回は諦めました。

問題4:Built-In を置き換えたシェーダーのPropertyが書き換えられない

ここまで来たらもう安心、と思ったら最後の伏兵にやられました。
なんと、シェーダープロパティ( _BurnColor や _BurnRatio など)を更新する方法がわかりません。

ググって調べた内容(※1, ※2)を参考に、動的に作った Material にシェーダーをセットして変更したり、Shader.SetGlobalXxx() 系のメソッドを使ったりしてみましたが、エラーこそ出ないもののFrameDebuggerで見てもまったく変化なし。
それどころか、なんと初期値を書き換えても Unity を再起動するまで反映されない(ロジック書き換えれば即反映されるので、コードが更新されていないことはあり得ないです)。Unity が見えない場所で値をキャッシュしてるんでしょうか?

なんとか解決策はないかと、散々調べたんですが成果なし。いやもう、今度こそお手あげです。

しかし、ここまで来ておいて諦めるのは悔しすぎます。そこで代替案として LightColor に情報を持たせることを思いつきました。幸い LightColor は現シーン唯一のDirectionalLightの色を変わるとリアルタイムで反映されるようです。
案として rgbは _BurnColor、 a(アルファ)は _BurnRatio または _DayRatio として使います。 _BurnRatio と _DayRatio を別々に設定できないので自由度は失われますが、元々タイムラプスアニメーションでは _BrunRatio = 1 - _DayRatio が常に成り立つ使い方しかしていないので、なんとかなりそうです。

コードはざっくりこんな感じ。

DayNightPixelArtsLit.shader
half4 CalculateLight (unity_v2f_deferred i)
{
    float2 uv = i.uv.xy / i.uv.w;
    half4 colDay = tex2D (_CameraGBufferTexture0, uv);
    half4 colNight = tex2D (_CameraGBufferTexture3, uv);

    // apply color burn effect to the day color.
    fixed3 colBurn = (1 - _LightColor .rgb) * _LightColor.a;
    colDay = fixed4(colDay.rgb - colBurn,1);
    return lerp(colNight, colDay, 1 - _LightColor.a);
}
DayNightAnimator.cs
    private void Update()
    {
        float time = Mathf.Repeat(Time.fixedTime / 5f, 1f);
        if (SUNSET_BEGIN < time && time < SUNSET_END) // 夕焼けタイム
        {
            float rate = (time - SUNSET_BEGIN) / (SUNSET_END - SUNSET_BEGIN);
            light0.color = new Color(1f, 0.3f, 0f, rate);
        }
        if (SUNRISE_BEGIN < time && time < SUNRISE_END) // 夜明け前タイム
        {
            float rate = (time - SUNRISE_BEGIN) / (SUNRISE_END - SUNRISE_BEGIN);
            light0.color = new Color(0.2f, 0.6f, 1.0f, 1 - rate);
        }
    }

ここに至って、ようやく完成の目途が付きました。

[追記] Shader.SetGlobalXxx の正しい使い方

上記対策をしてからしばらくして、なぜ SetGlobalColor, SetGlobalFloat などがうまく行かなかったか理由がわかりました。

http://answers.unity.com/answers/502891/view.html

Shader.setGlobal functions would only work on variables that are not exposed as a property in the property block.

SetGlobalXxx 系メソッドを使う場合は、シェーダーの Properties ブロックに変数を公開してはいけないそうです。実際 DayNightPixelArtsLit.shader で試してみたところ、うまくいきました。
ただ、既に実装したライトのカラーで制御する方法が自分で気に入ったのと、たった一つのシェーダーのために全シェーダー共通のプロパティを定義するのは無駄が多い気がしたので、書きなおすことはしませんでした。

最終的な実装

今回作成した全ソースおよび関連ファイルはGitHubにあります。

カメラ設定

camera_prefab.jpg
Projection: Orthographic と Rendering Path: Deferred は両立できないので、 Projection は Perspective にします。
カメラを騙すために QuarterView.AdjustCamera を修正しました。

QuarterView.cs
    private Vector2 CalcOrthoSize()
    {
        // 画面の高さの半分=等倍サイズ
        var hsize = camera.pixelHeight / (zoom * 2f);
        return new Vector2((float)camera.pixelWidth / camera.pixelHeight * hsize, hsize);
    }

    private void AdjustCamera()
    {
        camera.transform.rotation = Quaternion.identity;
        camera.transform.position = Vector3.zero;
        camera.ResetProjectionMatrix();

        int depth = (lookAt.x + lookAt.z) / 2;
        Vector3Int pos = new Vector3Int(lookAt.x - lookAt.z, lookAt.y + depth, lookAt.y - depth);

        var matrix = new Matrix4x4()
        {
            m00 = 1.0f, m01 = 0.0f, m02 =-1.0f, m03 = 0.5f - pos.x,
            m10 = 0.5f, m11 = 1.0f, m12 = 0.5f, m13 = 0.0f - pos.y,
            m20 =-0.5f, m21 = 1.0f, m22 =-0.5f, m23 = 0.0f - pos.z - distance,
            m30 = 0.0f, m31 = 0.0f, m32 = 0.0f, m33 = 1.0f            
        };

        var orthoSize = CalcOrthoSize();
        var projMatrix = Matrix4x4.Ortho(orthoSize.x * -1, orthoSize.x, orthoSize.y * -1, orthoSize.y, 0, 1000);
        camera.projectionMatrix = projMatrix;
        camera.worldToCameraMatrix = matrix;

    }

ファイル全体

このクラスが何をやっているかは、こちらの記事に詳しく書いておりますのでご参考までに。
【Unity】 クォータービューのドット絵に深度バッファを適用する(重なり順解決)

Built-in Deferred Shader の書き換え

こちらからオリジナルコードを取得して、以下のように書き換えました。

DayNightPixelArtsLit.shader
Shader "Hidden/DayNightPixelArtsLit"
{
SubShader {
// Pass 1: Lighting pass
//  LDR case - Lighting encoded into a subtractive ARGB8 buffer
//  HDR case - Lighting additively blended into floating point buffer
Pass {
    ZWrite Off
    //Blend [_SrcBlend] [_DstBlend]
    BlendOp Max

CGPROGRAM
// ..中略.. //

sampler2D _CameraGBufferTexture0;
sampler2D _CameraGBufferTexture1;
sampler2D _CameraGBufferTexture2;
sampler2D _CameraGBufferTexture3;
sampler2D _DepthTexture;

half4 CalculateLight (unity_v2f_deferred i)
{
    float2 uv = i.uv.xy / i.uv.w;
    half4 colDay = tex2D (_CameraGBufferTexture0, uv);
    half4 normal = tex2D (_CameraGBufferTexture1, uv);
    half4 idCol = tex2D (_CameraGBufferTexture2, uv);
    half4 colNight = tex2D (_CameraGBufferTexture3, uv);

    half4 dpt = tex2D (_DepthTexture, i.uv);



    // apply color burn effect to the day color.
    fixed3 colBurn = (1 - _LightColor .rgb) * _LightColor.a;
    colDay = fixed4(colDay.rgb - colBurn,1);
    return lerp(colNight, colDay, 1 - _LightColor.a);
}

// ..後略.. //

ファイル全体

Project Settings > Graphics > Built-in Shader Settings を開いて下図のように作成したファイルを Deferred シェーダーにセットします。また Deferred Reflections は NoSupport に設定します。
projectsettings.jpg

建物用マテリアルのシェーダー書き換え

こんな感じのフラグメントシェーダーを書きます。

DayNightPixelArtsGBuffer.shader
      struct flagout
      {
        float4 gBuffer0 : SV_TARGET0;
        float4 gBuffer1 : SV_TARGET1;
        float4 gBuffer2 : SV_TARGET2;
        float4 gBuffer3 : SV_TARGET3;
        float depth: SV_DEPTH;
      };

      sampler2D _DayTex;
      float4 _DayTex_ST;
      sampler2D _NightTex;
      float4 _NightTex_ST;
      float4 _IDColor;

      float _NightTexEnabled;
      fixed3 _Transpalent;

      void frag (in v2f i, out flagout o)
      {
        fixed4 colDay = tex2D(_DayTex, i.uv);
        // stop rendering if it is transpalent color.
        fixed3 diff = abs(colDay.rgb - _Transpalent.rgb);
        if(length(diff) < 0.0001) discard;

        // use 25% brightness of day texture, if night texture is disabled.
        fixed4 colNight = lerp(colDay * 0.25, tex2D(_NightTex, i.uv), _NightTexEnabled);

        o.gBuffer0 = colDay;
        o.gBuffer3 = colNight;
        o.gBuffer2 = float4(i.normal, 0) * 0.5 + float4(0.5, 0.5, 0.5, 0);
        o.gBuffer1 = _IDColor;
        o.depth = i.position.z;
      }

ファイル全体

なお、小さい三角屋根の家みたいに色相置換する場合はこちらのシェーダーを使います。
huetransHouse.jpg

HueTransDNPixelArtsGBuffer.shader
      fixed3 _RedTransfar;
      fixed3 _GreenTransfar;
      fixed3 _BlueTransfar;

      inline fixed4 transferColor(in fixed4 srcCol) {
        float lowest = min(srcCol.r, min(srcCol.g, srcCol.b));
        fixed3 hueCol = srcCol.rgb - lowest;
        fixed3 hilight = fixed3(lowest,lowest,lowest);

        // test srcCol if the two of r,g,b are 0.
        float d1 = (1 - hueCol.r) * (1 - hueCol.g) * (1 - hueCol.b);
        float d2 = hueCol.r + hueCol.g + hueCol.b;
        float flag = step(d1 + d2, 1);

        fixed3 rgb = lerp(
        srcCol.rgb,
        _RedTransfar * hueCol.r + _GreenTransfar * hueCol.g + _BlueTransfar * hueCol.b + hilight,
        flag );


        return fixed4(rgb,1);
      }

      void frag (in v2f i, out flagout o)
      {
        fixed4 colDay = tex2D(_DayTex, i.uv);
        colDay = transferColor(colDay);
        // stop rendering if it is transpalent color.
        fixed3 diff = abs(colDay.rgb - _Transpalent.rgb);
        if(length(diff) < 0.0001) discard;

        fixed4 colNight = tex2D(_NightTex, i.uv);
        colNight = transferColor(colNight);

        // use 25% brightness of day texture, if night texture is disabled.
        colNight = lerp(colDay * 0.25, colNight, _NightTexEnabled);

        o.gBuffer0 = colDay;
        o.gBuffer3 = colNight;
        o.gBuffer2 = float4(i.normal, 0) * 0.5 + float4(0.5, 0.5, 0.5, 0);
        o.gBuffer1 = _IDColor;
        o.depth = i.position.z;
      }

ファイル全体

シェーダーコードの説明はこちらの記事に詳しく書いておりますので、ご参考までにどうぞ。
[Unity] 昼夜二枚のドット絵をもとにシームレスな夕焼けアニメーションをする
[Unity] RGB三チャネル同時色相変換シェーダー(明暗グラデーション対応)

以上のシェーダーをセットしたマテリアルを作成して、建物のゲームオブジェクトに適用します。
building_material.jpg

building_prefab.jpg

地面(おまけ)

仮の地面として plane (組み込みの3Dオブジェクト)を設置してます。
scene_view.jpg
建物が一部歪んで見えるのは、ドット絵を包含するように余白を加味した直方体に投影しているためです

マテリアルとして下図のようなものをセットすると、ドット絵と同じように夕焼けアニメーションができます。
plane_material.jpg
テクスチャはセットしてませんが、シェーダーではデフォルトの白テクスチャが使われます。

タイムラプスアニメーション

light_prefab.jpg
下記のようなスクリプトを作ってメインライトに Add Component します。

DayNightAnimator.cs
    const float SUNSET_BEGIN = 0.1f;
    const float SUNSET_END = 0.4f;
    const float SUNRISE_BEGIN = 0.6f;
    const float SUNRISE_END = 0.9f;
    const float LIGHT_BEGIN = 0.2f;
    const float LIGHT_END = 0.7f;
    private Light light0 = null;

    void Awake()
    {
        if (light0 == null)
        {
            light0 = GetComponent<Light>();
        }
    }

    private void Update()
    {
        float time = Mathf.Repeat(Time.fixedTime / 5f, 1f);
        if (SUNSET_BEGIN < time && time < SUNSET_END) // 夕焼けタイム
        {
            float rate = (time - SUNSET_BEGIN) / (SUNSET_END - SUNSET_BEGIN);
            light0.color = new Color(1f, 0.3f, 0f, rate);
        }
        bool nightTex = LIGHT_BEGIN < time && time < LIGHT_END;
        Shader.SetGlobalFloat("_NightTexEnabled", nightTex ? 1f : 0f);
        if (SUNRISE_BEGIN < time && time < SUNRISE_END) // 夜明け前タイム
        {
            float rate = (time - SUNRISE_BEGIN) / (SUNRISE_END - SUNRISE_BEGIN);
            light0.color = new Color(0.2f, 0.6f, 1.0f, 1 - rate);
        }
    }

ファイル全体

結果

冒頭に上げたようなシーンが再生されます
timelapse2.gif

書き換えられたレンダリングパス

フレームデバッガーで見るとこんな感じ
framdebugger.jpg
左のツリーで RenderDeferred.Lighting を選択すると右に詳細がでますが、ちゃんと置き換えたシェーダーファイルが使われているのがわかります。ブレンドモードやプロパティ値などもわかりますね。

ツリーから RenderDeferred.GBuffer を選択して、右のヘッダー部分のRT-0の部分を切り替えると各g-Bufferの描画状態をゲームビューで見ることができます。
gbuffer_select.jpg

各g-Buffer(レンダーターゲット)の内容

Gbuffer-0: 昼画像

gbuffer-rt0.jpg

GBuffer-1: IDカラーマップ

gbuffer-rt1.jpg
これはオブジェクト毎に異なる色を割り振って塗ったもので、いずれHitTestとかに使えないかと思って準備してます。

GBuffer-2: 法線マップ(実装不備)

gbuffer-rt2.jpg
Deferred パス本来の使い方ですが、建物を描画してるメッシュに適切な法線を設定してないのでのっぺりしてます。そのうち法線マップ対応のドット絵とか使いたくなったらこのバッファーでやりくりすることになるでしょう。

GBuffer-3: 夜画像

gbuffer-rt3.jpg

Depth: 深度バッファー

gbuffer-dpth.jpg
今更ですが、ドット絵にちゃんと深度が適用されているのがわかりますね!

まとめ・感想

昼夜二枚の画像を合成して夕焼けアニメーションをシーン全体に適用するため、レンダリングパイプラインをカスタマイズする方法を検討しました。

  • SRPも調べたが難しすぎて断念した
  • 幾つか問題はあったが、最終的に Deferred レンダリングをカスタマイズして目的達成できた
    • Built-in のいくつかのシェーダーは設定で独自のものに置き換えることができる
    • Deferred では Orthographic な設定は使えないが、カメラを騙して Orthographic な射影行列のまま描画することはできた
    • 前項の理由は反射(Reflection)に問題があるためと思われるが、設定で Reflection シェーダーを無効にすることで対処できた
    • g-Buffer3 は最終的なレンダリング先として描画結果にも影響するので、一時的なバッファに利用するのは難しいが、Blend 方法を工夫することで有効活用できた
    • Built-in を置き換えたシェーダーのプロパティーを変更する方法が見つからなかったが、LightColorを代用することで対処できた
    • SetGlobal系メソッドを使う手もあるが、その場合は変数をプロパティー公開してはいけない
  • フレームデバッガーで見る限り、余計なドローコールが減ったので、シーン全体でアニメーションすことはパフォーマンスに効果がありそう
  • 加えて昼夜以外のレイヤーも使えるので、将来色々な拡張の期待が持てる
    • 深度バッファでエッジ検出&輪郭強調
    • 法線マップ適用して本格的なライティング計算
    • IDカラーマップでピクセル単位の精確なクリック判定

以上のように、予想より困難が多かったですが、それを乗り越えてなんとか目的を達成できたのは達成感もあり、Unityのコアな部分について今までより詳しく知ることが出来て良かったです。
ただ、やはりある機能を本来と違う目的に使うのは大変だということ、今後Unityがバージョンアップしていった時にいつまで通用するか、という反省や心配もありますね。
ですがここまでやったからには、もう少しこのまま改良を進めていくつもりです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away