20201227のUnityに関する記事は8件です。

Unity ShaderGraph レシピ #7 トゥーンシェーディング

トゥーンシェーディング

gif_animation_007.gif

作り方

ライトベクトルとメッシュの法線から頂点の明るさを算出し、その値をUVとしてトゥーン用のライトマップ(ToonMap)から最終的な明るさを決め、モデルに適用します。

1.ToonMapに使うUV値の計算

頂点の法線と、ライトの向きで内積を取ります。どちらも単位ベクトルの場合、結果は(-1〜1)の値に収まります。
法線がライトの向きと向き合っている場合(明るい場合)=1、ライトと同じ方向を向いている場合(暗い場合)=-1になります。
この値をUV値にしたい(0〜1の値)ので、まず0.5を掛けて(-0.5〜0.5)その結果に0.5を足すことで0〜1の範囲に収めることができます。

スクリーンショット 2020-12-27 17.58.09.png

2.UV値を使ってToonMapをマッピング

スクリーンショット 2020-12-27 22.51.16.png

3. ToonMapをメッシュのテクスチャと乗算してライティング

スクリーンショット 2020-12-27 23.50.11.png

完成!

スクリーンショット 2020-12-27 23.52.04.png

スクリーンショット 2020-12-27 23.50.43.png

ToonMapを変えることでシェーディングをカスタマイズできます

toon3.png
↑の場合は↓
(作ってる時には気づかなかったですが画像の右上にゴミがありますね…)
スクリーンショット 2020-12-27 23.52.04.png

toon2.png
↑の場合は↓
スクリーンショット 2020-12-27 23.52.16.png

toon1.png
↑の場合は↓
スクリーンショット 2020-12-27 23.52.24.png


その他のレシピはShaderGraphレシピ一覧にまとまっています

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity始めるときまずやること&トラシュ

まずやること

  • Playモード(▶)のときに背景色が変わるようにする
    • Playモードのときに編集したやつはPlayモードを解除すると全部もとに戻る。
    • Playモードだと気づかずに懸命に編集してしまったら後でメンタルが終わる
    • 方法
    • 編集>環境設定>色>Playmode tintで色を変える

トラブルシューティング

  • アップ/ダウングレードしたらPlayモードが実行できなくなった。「コンパイルエラー全部直さないと動かない」と出る。
    • 直し方
      1. Unityプロジェクト閉じる
      2. バックアップを取る
      3. Libraryフォルダを消す
      4. アップ/ダウングレード
      5. Package関連のバグが出たらPackageフォルダも消す
      6. アップ/ダウングレード
      7. 完治
  • textで日本語入力ができない
    • まだ直せないんだが
    • ググってもInputFieldに日本語打てない問題しか出てこないんだが
    • 別の場所で日本語打って、それをtextのとこにコピペすると一応入力できる。めんどい。
    • こちらの解決策は試した、ダメだった。
  • 日本語入力できたけど□□□みたいな表示になる
    • textmeshpro限定の解決策だがこちら
  • textで何か書いたのに画面に反映されない
    • Paragraph > 水平/垂直overflowをoverflowにする

所感

  • 初見殺し多くない?
  • 各ウィンドウの×ボタン押すの難しくない?
    • 幅変更の◀▶が表示されがち。判定シビア。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】 自分で作成した画像を2Dテクスチャとして組み込む

2Dゲームを作成する際に自分でイラレで書いた画像をUnityにテクスチャとして組み込む方法がわからなかったので自分用のメモとして記事を書きます。

illustrator

イラレ側でしておくことは後ほどの切り分けのことを考えて四角で囲えるようにそれぞれのパーツをわけておくこと& png形式で保存しておくこと。背景は別にしておく。
↓この形での保存が望ましいアートボード 2.png
↓このように背景をつけておくと後で切り分けた時に背景も合わせて切り取られてしまうのでよくない
アートボード 1.png

Unity

1.プロジェクト内にTextureフォルダを作成し、そこに先ほど作成したpng画像を入れる

スクリーンショット 2020-12-27 15.31.43.png

2.Inspector画面において、緑の丸の部分を設定し、赤丸の部分でApplyした後、青丸でSprite Editorを開く

(今回は一つのpngから複数のテクスチャを切り抜く場合を想定しているのでSpriteModeをMultipleに設定しているが、一つの画像をそのままテクスチャとして用いる場合はSingleで良い)



3.スプライトエディタにてテクスチャ分割をする

-形がオートで決まる場合

赤のSliceを押し、Sliceタブを出す。
TypeをAutomaticに設定し(青)、緑のSliceを押してテクスチャを分割。
この後は右上のApplyボタンを押すことで、分割されたシェーダーがフォルダに入る。

名前を変更したい場合はSlice後に各スプライトを選択後Nameを変更する。
フォルダ内ではRenameができないので注意。

-形を自分で設定したい時(背景透過で使いたいロゴなどの場合)

自分でドラッグしてサイズを決める。青色の四角で切り取った部分がShaderとなる。
青の線と緑の線がずれてしまった場合はBorderの部分のどれかの値が0で無くなっている場合だと思うので、0に直してあげればズレは直る。
サイズが決まったら右上のApplyを押してあげればそれぞれのShaderとして毎回保存される。
ひたすら上書きされてうまくいかない時は諦めてもう一回画像読み込みからやり直すと直った

4.できたシェーダーはそのままSceneビューにドラッグすれば使える!

まとめ

次回、自分で作った画像をボタンとして使えるようにするメモ!
まだまだ理解が甘いので間違ってるところなどあったら教えてください!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】 自分で作成した画像をUnity2Dにテクスチャとして組み込む

2Dゲームを作成する際に自分でイラレで書いた画像をUnityにテクスチャとして組み込む方法がわからなかったので自分用のメモとして記事を書きます。

illustrator

イラレ側でしておくことは後ほどの切り分けのことを考えて四角で囲えるようにそれぞれのパーツをわけておくこと& png形式で保存しておくこと。背景は別にしておく。
↓この形での保存が望ましいアートボード 2.png
↓このように背景をつけておくと後で切り分けた時に背景も合わせて切り取られてしまうのでよくない
アートボード 1.png

Unity

1.プロジェクト内にTextureフォルダを作成し、そこに先ほど作成したpng画像を入れる

スクリーンショット 2020-12-27 15.31.43.png

2.Inspector画面において、緑の丸の部分を設定し、赤丸の部分でApplyした後、青丸でSprite Editorを開く

(今回は一つのpngから複数のテクスチャを切り抜く場合を想定しているのでSpriteModeをMultipleに設定しているが、一つの画像をそのままテクスチャとして用いる場合はSingleで良い)



3.スプライトエディタにてテクスチャ分割をする

-形がオートで決まる場合

赤のSliceを押し、Sliceタブを出す。
TypeをAutomaticに設定し(青)、緑のSliceを押してテクスチャを分割。
この後は右上のApplyボタンを押すことで、分割されたシェーダーがフォルダに入る。

名前を変更したい場合はSlice後に各スプライトを選択後Nameを変更する。
フォルダ内ではRenameができないので注意。

-形を自分で設定したい時(背景透過で使いたいロゴなどの場合)

自分でドラッグしてサイズを決める。青色の四角で切り取った部分がShaderとなる。
青の線と緑の線がずれてしまった場合はBorderの部分のどれかの値が0で無くなっている場合だと思うので、0に直してあげればズレは直る。
サイズが決まったら右上のApplyを押してあげればそれぞれのShaderとして毎回保存される。
ひたすら上書きされてうまくいかない時は諦めてもう一回画像読み込みからやり直すと直った

4.できたシェーダーはそのままSceneビューにドラッグすれば使える!

まとめ

次回、自分で作った画像をボタンとして使えるようにするメモ!
まだまだ理解が甘いので間違ってるところなどあったら教えてください!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityで制限時間やN秒に一回行う処理をサクッと実装

はじめに

少し前に書いたコードがメモ帳にあったので、Qiitaを始めたついでに投稿してみる
カウントダウンやカウントアップができる簡単なタイマークラス KitchenTimer を作ってみた

「N秒に1回 〜する」 のような処理を簡単に試したい時に使う。

結論

完成形がこちら。使い方は下の方で説明。

KitchenTimer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

/// Unity で簡単に使えるタイマークラス
public class KitchenTimer: MonoBehaviour
{
    //-----------------------------------------------------    
    /// [CountUp] 0秒からカウントアップ, u秒毎にeveryUが呼ばれる
    public void CountUp(float u, Action<float> everyU)
    {
        isCountUp = true;
        this.currentTime = 0; // 正確に保持している秒
        StartCoroutine(this.IECountUp(u, everyU));
    }

    //-----------------------------------------------------
    /// [CountDown] c秒からカウントダウン, d秒毎にeveryDが呼ばれる
    public void CountDown(float c, float d, Action<float> everyD, Action timeOver)
    {
        isCountUp = false;
        this.currentTime = c; // 正確に保持している秒
        StartCoroutine(this.IECountDown(c, d, everyD, timeOver));
    }

    //-----------------------------------------------------
    /// [StopCount] カウントをストップ
    public float StopCount()
    {
        this.isTimeRunning = false;
        return this.currentTime;
    }

    //-----------------------------------------------------
    /// [RestartCount] ストップのあとで、もう一度カウント再開
    public void RestartCount()
    {
        this.isTimeRunning = true;
        if(isCountUp)
        {
            StartCoroutine(this.IECountUp(this.tempInterval, this.tempAction));
        }
        else
        {
            this.CountDown(this.currentTime, this.tempInterval, this.tempAction, this.tempTimeOver);
        }
    }

    //-----------------------------------------------------
    /// [Wait] W秒間待ったあとで実行
    public void Wait(float w, Action action)
    {
        this.action = action;
        Invoke("DoAction",w);
    }

    //-----------------------------------------------------
    /// [Repeat] 実行 -> r秒待つ -> 実行 を繰り返す
    public void Repeat(float r, Action action)
    {
        this.action = action;
        InvokeRepeating("DoAction",0f, r);
    }

    //-----------------------------------------------------
    /// [WaitAndRepeat] Wait と Repeat を両方する
    public void WaitAndRepeat(float waitTime, float intervalTime, Action action)
    {
        this.action = action;
        InvokeRepeating("DoAction", waitTime, intervalTime);
    }

    //-----------------------------------------------------
    /// [Dispose] 使い終わった タイマー を捨てる
    public void Dispose()
    {
        CancelInvoke();
        Destroy(this);
    }

    //*******************************************************************
    //                その他
    //*******************************************************************

    /// 時間を保持するプロパティ
    private float currentTime;

    /// カウントを進めて良いかどうか(時間が進んでいるか)
    private bool isTimeRunning = true;

    /// カウントアップ中かダウン中か
    private bool isCountUp;

    /// メソッドを保持するプロパティ
    private Action action;

    // 中断中のカウントの変数を一時保持する
    private float tempInterval;
    private Action<float> tempAction;
    private Action tempTimeOver;

    /// 保持したメソッドを実行する
    private void DoAction()
    {
        this.action();
    }

    /// カウントアップ用のIE
    private IEnumerator IECountUp(float u, Action<float> everyU)
    {
        // 中断用の一時変数を保存
        this.tempAction = everyU;
        this.tempInterval = u;

        var count = 0f; 
        while(currentTime < 600f) // 限界は600秒までとする
        {
            if (isTimeRunning)
            {
                this.currentTime += Time.deltaTime;
                count += Time.deltaTime;
            }
            else
            {
                break;
            }
            if(count > u)
            {
                everyU(this.currentTime);
                count -= u;
            }
            yield return null;
        }
        if (isTimeRunning) 
        { 
            // 最後に忘れず、一時変数をnullに
            this.tempAction = null;
        }
    }

    /// カウントダウン用のIE
    private IEnumerator IECountDown(float c, float d, Action<float> everyD, Action timeOver)
    {
        // 中断用の一時変数を保存
        this.tempInterval = d;
        this.tempAction = everyD;
        this.tempTimeOver = timeOver;

        var count = 0f; 
        while(currentTime > 0)
        {
            if (isTimeRunning)
            {
                this.currentTime -= Time.deltaTime;
                count += Time.deltaTime;
            }
            else
            {
                break;
            }
            if(count > d)
            {
                everyD(this.currentTime);
                count -= d;
            }
            yield return null;
        }
        if (isTimeRunning)
        {
            timeOver(); // 中断以外でここに来た場合はタイムオーバーということ

            // 最後に忘れず、一時変数をnullに
            this.tempAction = null;
            this.tempTimeOver = null;
        }
    }
}

使い方

1. KitchenTimer.cs をProjectウィンドウの中の好きなところに置く

2. timerインスタンスの生成

新しいタイマーを作る。
※このスクリプトを貼り付けたオブジェクトが消滅すると、タイマーもなくなるので注意。

    var timer = this.gameObject.AddComponent<KitchenTimer>(); 

カウントアップ(0, 1, 2 ...) timer.StartCountUp

1.0秒に1回、処理をする例
time には、その時の時間が少数まで入っている。 「1.13」 「2.04」
場合に合わせて四捨五入して使う

    timer.StartCountUp(1.0f, 
        (time) => 
        {
            // ここに1秒に1回やりたい処理を書く
            Debug.Log(time);
        }
    );

カウントダウン(30, 29, 28 ...) timer.StartCountDown

残り30秒からスタートして、1秒に1回処理をする例。

    timer.StartCountDown(30.0f, 1.0f, 
        (time) => 
        {
            // ここに1秒に1回やりたい処理を書く
            Debug.Log(time);
        }, 
        () => 
        {
            // 30.0秒のカウントダウンが終わったらここに来る
            Debug.Log("時間切れ");
        }
    );

一時ストップ timer.Stop

    timer.Stop(
        (time)=>
        {
            // time にはタイマーが止まった時の時間が入っている
            Debug.Log(time);
        }
    );

ストップ時点から再開 timer.StartFromStopPoint

    timer.StartFromStopPoint();

最初から測り直し timer.ResetAndReStart

    timer.ResetAndReStart();

タイマーを捨てる(メモリ開放を明示的にしたいとき) timer.Dispose

    timer.Dispose();

さいごに

時間の単位は全て 「秒」 です。変換クラスと上手いこと合わせて使ってやってください。
以上です!

それにしても「キッチンタイマー」て。。。変な命名にハマる時期ってありますよね。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Cluster用アイテム】プレイヤーが近づくと表示されるパネル&スライドショーっぽい説明パネル

【Cluster用アイテム】プレイヤーが近づくと表示されるパネル&スライドショーっぽい説明パネル
の解説です。

説明パネル (1).gif

以下、Boothの説明より。

Cluster用アイテム
「プレイヤーが近づくと表示されるパネル」です

アイテムの詳しい説明、NPCのセリフなど、
必要なんだけど、常時表示されるとイマイチなんだよな~
といったものに使うことを想定しています。
ダミーとメインアイテムは自由に入れ替えられます。

「スライドショーっぽい説明パネル」
も同様に、長々とした説明を1枚に詰め込むと
説明パネルがやたらとデカくなるのを
いい感じにしたくて作りました。

一緒にも別々にも使えるようにしてあります。

簡単な使い方解説(プレイヤーが近づくと表示されるパネル)

  • EreaSphere
    プレイヤーの接近を検知するエリアです。
    スケールを適当に変えたり、位置調整したり、BoxColliderに変えたりしていい感じにしてください。
    image.png

  • ダミー
    展示オブジェクトの縮小版とかアイコンとかをこれの下に入れてください。
    プレイヤーが近づくと消えます。
    image.png

  • メイン
    プレイヤーが近づくと出てくるやつを入れてください。
    プレイヤーが離れると消えます。
    音がついてるのは自分の好みなので、無くてもいいです。
    (個人的には、インタラクションには音をつける派です)
    にゅっと出るアニメーションとか追加するとかっこいいと思います。
    image.png

簡単な使い方解説(スライドショーっぽい説明パネル)

各Planeのマテリアルに、表示したい画像(のマテリアル)を入れてください。
表示時にはPlane1から順に表示されます
4まで行くと、1に戻ります。
image.png

4枚も要らぬ!というときは、
ロジックの赤丸のところの数字を適宜減らしてください。
image.png

増やしたいときは、Animaterと和解して(ページ遷移のアニメーションを追加して)
赤丸の数字を増やしてください。

着想

バーチャル特別展「アノニマス ー逸名の名画ー」|バーチャルSNS cluster(クラスター)
↑の展示で、近づくとショーケースから飛び出してくるのがすごくいいな!と思ったので、
こんな感じかな~と実装しました。

モノ自体はGameGAM 2020 Winter前にできてたので、
もっと早く公開しとけばよかったと思いました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Cluster用アイテム】プレイヤーが近づくと表示される詳細アイテム&スライドショーっぽい説明パネル

【Cluster用アイテム】プレイヤーが近づくと表示される詳細アイテム&スライドショーっぽい説明パネル - tsukinon*** - BOOTH
の取説です。

説明パネル (1).gif

これは何?

Cluster用アイテム
「プレイヤーが近づくと表示される詳細アイテム」

アイテムの解説パネル、NPCのセリフ、セーブポイントなど、
必要なんだけど、常時表示されると景観的にイマイチ(邪魔)なんだよな~
といったものに使うことを想定しています。
ダミーアイテムとメインアイテムは自由に入れ替えられます。
表示エリア自体はItemではないので、Itemも入れられます。

「スライドショーっぽい説明パネル」
も同様に、説明を1枚に詰め込むと
説明パネルがやたらとデカくなるのを
いい感じにしたくて作りました。

一緒にも別々にも使えるようにしてあります。
組み合わせると、目の前にくると詳しい説明が読める、といった動線ができます。

簡単な使い方解説(プレイヤーが近づくと表示される詳細アイテム)

  • EreaSphere
    プレイヤーの接近を検知するエリア(本体)です。
    スケールを適当に変えたり、位置調整したり、BoxColliderに変えたりしていい感じにしてください。
    image.png

  • ダミー
    展示オブジェクトの縮小版とかアイコンとかをこれの下に入れてください。
    プレイヤーが近づくと消えます。
    image.png

  • メイン
    プレイヤーが近づいたら見せたいものを入れてください。
    プレイヤーが離れると消えます。
    音がついてるのは自分の好みなので、無くてもいいです。
    (個人的には、インタラクションには音をつける派です)
    にゅっと出るアニメーションとか追加するとかっこいいと思います。
    デフォルトでは説明パネルを入れていますが、お好きな3Dモデルなど入れてください。
    ただのGameObjectなので、Itemも入れられます。
    image.png

簡単な使い方解説(スライドショーっぽい説明パネル)

各Planeのマテリアルに、表示したい画像(のマテリアル)を入れてください。
表示時にはPlane1から順に表示されます
4まで行くと、1に戻ります。
image.png

4枚も要らぬ!というときは、
ロジックの赤丸のところの数字を適宜減らしてください。
image.png

増やしたいときは、Animaterと和解して(ページ遷移のアニメーションを追加して)
赤丸の数字を増やしてください。

着想

バーチャル特別展「アノニマス ー逸名の名画ー」|バーチャルSNS cluster(クラスター)
↑の展示で、ショーケースに近づくと展示物が目の前に飛び出してくるのが、バーチャルならではでありながら、展示会体験としてすごくいいな!と思ったので、こんな感じかな~と実装しました。
結果的にだいぶ汎用性は高いと思います。
特別展は、トーハクの再現度も、展示空間の作りも、作品自体も、とにかく最高なのでおすすめです!!!

モノ自体はGameGAM 2020 Winter前にできてたので、
もっと早く公開しとけばよかったと思いました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Universal Render Pipeline環境でのシェーダー最適化対策

TL;DR

ShaderLabメモリ落ちに苦しんでいる方、アプリケーションのビルド時間が過度に長く、
なんとなくシェーダーが原因ではないかと考え始めた方はとにかく全部読んで損はありません。

概要

URPを使うプロジェクトでのメモリによるクラッシュの報告を受け、それの調査に取り掛かった際のやったことを書きます。
主にShaderコンパイルが関わるShaderLabのメモリ・ビルド時間に関わる話。

この記事はURP10.2.2を基準にして作成しています。

一般的なシェーダー最適化の話

まずこの辺が整理出来ていなかったのに気づいたのでURP以前にここから整理し始めました。

いつも#pragma multi_compileよりは先に#pragma shader_featureに出来ないか検討する。

  • どこにも含まれなかったvariant combinationは自動で外れることになります。(含まれる含まれない判定は、ビルド・もしくはAssetbundleなどの結果物に参照があるかどうかであります。)
  • 結果物から外れたvariantはそもそも含まれてないため、コード上でShader.EnableKeywordなどを用いて操作する場合は使えない

multi_compileよりmulti_compile_vertex or multi_compile_fragmentに出来ないか検討する。

Shader "Hoge/Fuga/Piyo"
{
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: vertex, keywords <no keywords>
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: vertex, keywords _HOGE
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: fragment, keywords <no keywords>
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: fragment, keywords _HOGE
  #pragma multi_compile _ _HOGE

  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: vertex, keywords <no keywords>
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: fragment, keywords <no keywords>
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: fragment, keywords _HOGE
  #pragma multi_compile_fragment _ _HOGE

  VertexOutput vert ( VertexInput v)
  {
    ...
  }

  half4 frag (VertexOutput IN) : SV_Target
  {
    ...

    #if _HOGE
     hogehoge
    #endif
  }
}

multi_compile/shader_featureのみだとvertex/fragment両方に含まれるので、このキーワードがvertexもしくはfragmentのみで使われることが自明であれば(特に自作するキーワードにつきましてはvertex/fragment両方全部使われることは体感上稀にしかなかったです)、余計な分を減らすことが出来ます。ちょこっと変えるだけで半分に減らすことが出来ると考えると積極的に使いたいですね。

シェーダーを二つに分けるという手もある

  • ShaderLabは現状ロードすべきのvariantだけではなく、全てのビルド上含まれている該当シェーダーのvariant combinationを全部メモリに載せます。(ただしシェーダーのコンパイル自体は該当するvariantのみが行われます)
  • なので、相互同マテリアルに存在しなければならない組み合わせでなければ、multi_compileの代わりにシェーダーを二つに分けることで管理のコストは上がるがhlslファイル分割・関数化・UsePassなりを用いてコードを重複を防ぎながら片方だけをShaderLabに載せるようにすることが出来ます。(やや手間があることではありますが)

その他

  • ProjectSettings/GraphicsのShaderStrippingを有効活用
  • Addressable/Assetbundleを使っているのであれば重複を防ぐ
  • IPreprocessShadersを有効活用 (後述) などなど・・

本題

URPでのシェーダープロファイル

  • Profilerを使う (それはそう)
  • Frame Debuggerを使う (それもそう)
  • Universal Render Pipeline Assetの設定を変える image.png

Shader Variant Log Levelを変えることで、ビルド時に下記のようにどれぐらいシェーダーが含まれ、どれが外れてどれが含まれたかをログに出してくれます。
ShaderPreprocessor.cs#L328
image.png
なのでビルド一番最後のログを見ると、全体にどれぐらいVariantCombinationがあり、ビルドにどれぐらい含まれたかが確認出来ます。

また、このオプションを有効にすることでアプリケーション上でシェーダーコンパイルが行われた際ログ上で確認が取れるようになります。

Compiled shader: Hidden/Universal Render Pipeline/FallbackError, pass: <unnamed>, stage: vertex, keywords <no keywords>
Compiled shader: Field/PBR/CloudTransparent, pass: Forward, stage: fragment, keywords FOG_EXP2 LIGHTMAP_ON _EmissionTypeNormal _MIXED_LIGHTING_SUBTRACTIVE

アプリケーションに含まれてしまったUniversal Render Pipelineのシェーダーを排除する

Universal Render PipelineのdefaultシェーダーはLit.shaderとなっており、これが例のStandardシェーダーみたいに、色んなライティング組み合わせが混ざったビルドに含まれるとそれなりにShaderLabのメモリを占有することになります。
昔これを排除するために参照されるマテリアルを頑張って消すなり色んな工夫をしていたのですが、

Unity2018.2からはIPreprocessShaders.OnProcessShaderという仕組みが用意されており、便利にビルドに含まれるシェーダーを制御することが出来るようになっています。
ビルド・Addressablesに参照がある対象のvariantについて、ここを通してユーザーが操作可能とする仕組みです。
ライト設定やフォグなどの環境については、同じく対象となっているシーンの環境の組み合わせと照らし合わせてくれます。

使用例はURPパッケージ内に結構良い感じのスクリプトがありますので、この辺を参考にすれば良いでしょう。
ShaderPreprocessor.cs

今回のプロジェクトでは基本URP用スクリプトはなく、全部自作のスクリプトを使っていたためこの辺は排除することにしました。

public class OptimizeShaderPreprocessor : IPreprocessShaders
{
    //IPreprocessShadersの呼び順を制御します。値が小さいほど先に呼ばれます。
    int IOrderedCallback.callbackOrder => default;

    void IPreprocessShaders.OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
    {
        if (shader.name.StartsWith("Universal Render Pipeline", System.StringComparison.OrdinalIgnoreCase))
        {
            //URP命名に引っかかったら対象から全て外す
            data.Clear();
        }
    }
}

それでもかなりShaderLabが大きい。URPシェーダーを書く際に必ずしも必要と思われるキーワード

URP環境上で新たにシェーダーを書く際に、なんとなくそのままLit.shaderなどからコピペーして持って来る部分があるでしょう。
自分は下記のところを何も考えず持ってきていました。

// Universal Pipeline keywords
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _SHADOWS_SOFT
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
#pragma multi_compile _ SHADOWS_SHADOWMASK
#pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE

さてと、これをこのまま使うとすると・・?

// Universal Pipeline keywords
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS // 1 * 2 = 2
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE // 2 * 2 = 4
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS // 4 * 3 = 12
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS // 12 * 2 = 24
#pragma multi_compile_fragment _ _SHADOWS_SOFT // 24 * 2 = 48
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION // 48 * 2 = 96
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING // 96 * 2 = 192
#pragma multi_compile _ SHADOWS_SHADOWMASK // 192 * 2 = 384
#pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE // 384 * 2 = 768

あの・・自分のキーワードを書く前に、もう768個のバリエーションが出来てしまいました。
ここで必要に応じて追加したのを足していくとどうなるでしょう。

#pragma multi_compile _ _A // 768 * 2 = 1536
#pragma multi_compile _ _B // 1536 * 2 = 3072
#pragma multi_compile _ _C // 3072 * 2 = 6144
#pragma multi_compile _ _D // 6144 * 2 = 12288
#pragma multi_compile _ _E // 12288 * 2 = 24576

32個で済ませるものが 24576個になってしまった。
ある程度はURP側でもこの辺を認識しているのか、これがまるごと全部載ることはなく、ShaderPreprocessor.cs
のところで理屈上ありえないパタンなどはある程度防ぐようになってますが、まだまだ不十分で

  • 無効の場合の最適化はあるが、有効のみの最適化が網羅出来ていない (後述)
  • そもそものURPで必要としているvariant combinationが多すぎて、IShaderPreprocessorで弾くのにも結構な時間を必要とするためビルド時間が伸びている
それぞれのパラメータの解説
  • Universal Render Pipeline Assetの設定ファイルに応じたキーワード

#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
メインライト(EnvironmentでSunとして設定されているライト。なければ一番明るいライト。Directionalに限る)の影を落とすかどうか
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
ShadowCascadeが設定されているかどうか
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
Main以外のライトを反映させるかどうか
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
Main以外のライトの影を落とすかどうか
#pragma multi_compile_fragment _ _SHADOWS_SOFT
SoftShadowにするかどうか
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
ライトマップで影を混合するかどうか
image.png
image.png

  • その他

#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
SSAOを使うかどうか (RenderFeatureにてSSAOを使わなければ要りません)
#pragma multi_compile _ SHADOWS_SHADOWMASK
#pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE
LIGHTMAP_SHADOW_MIXINGが設定されている前提で、BakedGIが設定され、かつSubtractive/Shadowmaskが設定されたシーンがある場合
image.png

この辺の設定はランタイム中に動的に切り替えたい場合は滅多にないと思われ、ここをmulti_compileとして設定する必要はないでしょう。
UniversalRenderPipelineAssetの設定に応じて#defineに書き換えました。

#if UNITY_HARDWARE_TIER1
    #define _MAIN_LIGHT_SHADOWS
#elif UNITY_HARDWARE_TIER2
    #define _MAIN_LIGHT_SHADOWS
    #define _ADDITIONAL_LIGHTS_VERTEX
    #define _ADDITIONAL_LIGHT_SHADOWS
    #define _SHADOWS_SOFT_ON //_SHADOWS_SOFTは_ONが必要だった
#elif UNITY_HARDWARE_TIER3
    #define _MAIN_LIGHT_SHADOWS
    #define _MAIN_LIGHT_SHADOWS_CASCADE
    #define _ADDITIONAL_LIGHTS
    #define _ADDITIONAL_LIGHT_SHADOWS
    #define _SHADOWS_SOFT_ON
#endif

結果

ランタイム中のShaderLabメモリは400mb -> 4mbになり、ビルド時間の7割を減らした

終わりに

UniversalRenderPipelineAssetの設定が変わると再度全シェーダーに対して調整する必要が確かにありますが、それにしても得られるメリットは非常に大きいので、しばらくはこういう感じで設定していこうと思います。
最近はパッケージに変更が激しいので、安定するまでのしばらくはリポジトリを注意して確認する必要があるかなーと

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む