サークル獏の佐藤敏 Unityとか備忘録

サークル獏の佐藤敏がUnityとかで知ったTipsを書いておく備忘録です。

中心がズレた小道具をAdjust Pivotで直す

f:id:VinSatoo:20180917074718p:plain
こちらはImagineGirlsのVienneV2のfbxをBlenderに読み込み、メガネ以外を全部削除して再度fbxとして書きだしたもの。
メガネだけをよそで転用したい、みたいな感じである。
それをUnityに読み込んだ。

中心がズレた小道具の問題

見ての通り、中心点(Pivot)とメガネの中心位置が激しく違う。
このまま回転させると……
f:id:VinSatoo:20180917074904p:plain
この通りである。これでは位置合わせがやりづらくてしょうがない。

Adust Pivot(無料)の登場

そこでAdust Pivot(無料)を活用する。
assetstore.unity.com
プロジェクトにインポートしたら、[Window]-[Adjust Pivot]ができるので早速起動。
f:id:VinSatoo:20180917075252p:plain
Adjust Pivotのウィンドウが出てきた。
f:id:VinSatoo:20180917075322p:plain

位置調整オブジェクトの追加

今回中心を調整したいglassesの子に、位置調整用の空オブジェクトを作る。
f:id:VinSatoo:20180917075447p:plain
名前はGame Objectのままで特に問題ナシ。
f:id:VinSatoo:20180917075516p:plain

位置調整オブジェクトの位置合わせ

普通にUnityの移動ツールでGame Objectをメガネの中心になるように動かしていく。
カメラの角度によって中心だと思っていた位置が全然違っていたりするので、カメラを回転させたりしながら確認していこう。
f:id:VinSatoo:20180917080314p:plain
f:id:VinSatoo:20180917075611p:plain
そしてGame Objectをメガネの中心に持ってきた後で、Adust Pivotの「Move ~~ pivot here」を押すと……
f:id:VinSatoo:20180917075730p:plain

移動の中心がメガネの中心になった

このように、「移動の中心=メガネの中心」になった。
最初と違って、回転させてもとんでもない動き方をしない。
これなら位置調整もしやすい。
f:id:VinSatoo:20180917080037p:plain
f:id:VinSatoo:20180917075949p:plain

ちなみに、一度こうなったら位置調整用のGame Objectは削除してしまって構わない。
あとはPrefabにでもして保存、Unity Packageとして書き出したりすれば使い回しもしやすいだろう。
 
 

位置調整を目分量でやらない

ところで、上のやり方はGameObjectを手動で動かしていることになる。
カメラの角度によって見え方がまるで違ったりするし、結構大変だ。

で、メッシュの中心はGetComponent().bounds.center;で得られるらしい。
thethird.hatenablog.com

なので、自動化することを考えた。
(Adjust Pivotにはこの機能は入っていないように見える……タブン。)

using UnityEngine;
using UnityEditor;

public class MeshCenter 
{

    // 自前のメニューと項目を作成
    [MenuItem("Window/MeshCenter", false, 1000)]
    static void MeshCenterProc() {
        if (Selection.activeGameObject == null)
            return;

        GameObject centerTarget = new GameObject("CenterTarget");

        var vec3 = Selection.activeGameObject.GetComponent<Renderer>().bounds.center;
        Selection.activeGameObject.transform.position = -vec3;
        centerTarget.transform.SetParent(Selection.activeGameObject.transform);

        Selection.activeGameObject = centerTarget;
    }
}

このようなC#スクリプトを書き、MeshCenter.csなどの名前でプロジェクト内の「Editor」フォルダに保存する。

MeshCenterの実行

Glassesを選択。
MeshCenterを実行する。
f:id:VinSatoo:20180917081353p:plain
自動で作られたCenterTargetというオブジェクト。
勝手に選択状態になる。
f:id:VinSatoo:20180917083005p:plain
 
位置調整も自動で終わっているので、そのまま「Adjust Pivot」を実行。
f:id:VinSatoo:20180917081427p:plain
さっき手動でやったのと同じように、回転の位置がちょうどよくなった。
もし微妙に中心をズラしたい場合は、「Adjust Pivot」実行前に手動で調整すればよい。
f:id:VinSatoo:20180917081435p:plain
また、小道具によっては自動計算された中心と見た目の中心がまるで違うこともある。
その場合は手動でガンバルしかないだろう。
 
 

Blenderからの書きだしのときの多少の注意点?

f:id:VinSatoo:20180917080854p:plain
メガネだけになったVienne。
記憶があいまいだが、メガネは最初非表示だったかもしれない。
その場合、Unityに持ってきたときMeshRendererがオフになっているので手動でメガネのMeshRendererをInspectorからオンにすること。

f:id:VinSatoo:20180917080617p:plain
書き出しの際はオプションの「エンプティ」「カメラ」「ランプ」「アーマチュア」「メッシュ」「その他」と並んでいるところから「メッシュ」だけ選んだ。日本語化していないと表記が違うかもしれない

ImagineGirlsのライセンス表記

f:id:VinSatoo:20180917081654p:plain
なお、今回使ったVienneV2は無料素材ではなく有料素材なのでそこは注意。
と言っても使っているのはメガネだけだが……
www.dlsite.com

(初代Vienneなどは無料だし、V2にもフリー版はあるもよう)
imaginegirls.com

Unityで静止画像のアレコレ&背景透過でキャプチャしたいのに色がついてしまう場合

なぜわざわざUnityで静止画を

最近のUnityは3Dキャラをレンダリングして出力するツールとしても悪くない。もちろん普通のことをするだけなら既存のDCCツールのほうが優れているのだろうが、

  • 無料
  • 情報が多い
  • アセットが多い
  • トゥーンシェーダーが充実してきている
  • 元々ゲームエンジンなのでスクリプトで複雑な動作をさせるのがラク
  • ゲーム用に作った動きのコードを流用できる

あたりが魅力的な場合は十分選択肢に入る。
特にトゥーンシェーダーの情報が充実してきているのが大きく、これで静止画像を生成するためにUnityを始めても惜しくない状況まで来ているように思う。
 
 

静止画は透明が欲しい

で、動画を撮る場合は背景までしっかりレンダリングしたものをRecorderあたりのアセットを使って録画することになるだろう。
一方、静止画の場合は背景が透明なほうが使いやすいことも多い。
どうせUnityで作るなら動画にすればいいのでは? なんでわざわざ2Dの静止画を? という意見もあるだろうが、

  • 2D画像にしてしまえば従来の吉里吉里などのゲームエンジンでも活用可能
  • Unity2Dなどで比較的軽めのゲームを作るときに活用することもできる
  • Unityのシェーダーでは綺麗に出しにくいクッキリした落ち影などを後で描き加えてしまうことも可能

なんてメリットがある。
 
qiita.com
で、透明でもスクショを撮れる、というとこうしたコードを参考にさせて頂くのが良い感じである。
こちらはMaciOSでおかしくなることの問題を説明されているのだが、とりあえずWindowsでやる限りは最初のほうのコードでも問題がなかった。
保存するファイル名をtest.pngではなく、System.DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".png"とかにすれば使い勝手も良い。
 
 

背景が透明にならない

ところが背景が透明にならないという別の問題が生じてしまった。
これはPostProcessingStackを付けたままだったからで、PostProcessingStackを切り、カメラのClearFlagsをSolidColorの「0,0,0,0(完全透明)」にすれば上手くいった。

f:id:VinSatoo:20180812200626p:plain
Unityでレンダリングした二次画像をテキトーな背景と合成したもの。PostProcessingは切ってある。縮小した以外は特に加工なし
 

PostProcessingStackの代わり

PostProcessingStackが掛からないぶんについては、PhotoShopなどで処理すればいいだろう。

dic.pixiv.net
一番使うであろうブルームについては、ディフュージョンとかとかでググれば色々出てくる。
アンチエイリアスは、大きめにレンダリングして最後にPhotoShopから書き出すとき小さめに縮小すれば割と問題なくなる。

AO影がないのは寂しいが、Unity上でキャラの動きの完全に動きを切れば、PostProcessingStackのDebugViewsを使ってAO影だけぬきだし、PhotoShop上で合成などということも可能だ。
f:id:VinSatoo:20180812195754p:plain
(※PostProcessingStack v2だとどうなるか分からない。。。)

Unityの過去のバージョンを落とすとき

(※下記の情報は2018年8月現在)

Unity Hubは各メジャーバージョンの最新版とかしか落とせず、Unity 2018.2.3が最新だけど2018.2.2を落としたいよ みたいなときには使えない。
で、過去のバージョンがどこにあるかというと公式サイトから結構見つけづらい気がしたので書いておく。あまり古いバージョンを使って欲しくないというのがあるのだろうが……
unity3d.com

読み込んだテキストを使い、PhotoShopのスクリプトでボタン作成

スクリプトの導入や基本はよそ様まかせ

PhotoShopスクリプトの導入方法などについては
helpx.adobe.com
www.ochiaimitsuo.com

PhotoShopスクリプトの基本については
Adobe Photoshop CS6使い方辞典

といったサイトが詳しく掲載してくださっているので割愛する。
まあ基本的に、

  • PhotoShopがインストールされたフォルダの「Presets¥Scripts」にjsという拡張子でテキストを保存すればよい。
  • PhotoShopが起動中なら再起動を忘れずに。
  • PhotoShopメニューの「ファイル」→「スクリプトに保存した名前のスクリプトができているはずである。

 
 

実際に書いてやってみる

f:id:VinSatoo:20180810141310p:plain:w300
こんな感じのPhotoShopデータを使う。

ファイア	3	炎を放つ	3
アイス	4	氷で攻撃する	2
ヒール	2	HPを30前後回復する	4
マグマ	18	溶岩で攻撃する上級魔法	0
シールド	5	防御力を50%あげる	1

で、上記のようなことが書かれたテキストを読み込み、スクリプトでボタンを作成する。
(テキストファイルの名前は、psdファイルと同じにする)

f:id:VinSatoo:20180810142936p:plain
終結果はこんな感じになる。
 
 
では以下のスクリプトをコピペして、js形式で保存しよう。ちょっと長いのはご勘弁。

//とりあえずpsd形式で保存されているファイル→PNGで出力のみ対応
var headTxt = app.activeDocument.name.replace('.psd', ''); 

//テキストデータ置き場を変えたいときはここをいじる
//フルパス決め打ちで "C:\\gamedata\\txt\\"とかでもいい
var sourceTextPath = app.activeDocument.fullName.parent.fsName+"\\"; 
//出力先も同様
var targetPath = app.activeDocument.fullName.parent.fsName+"\\"; 

var loadedArray = [];
var lineArray; //一時格納用

var textLayer;
var stextLayer;
var itextLayer;
var fillLayer;

var colors = []; //色データ

//PNGでセーブする用のデータ
var pngSaveOpt = new PNGSaveOptions();
pngSaveOpt.interlaced = false;


function MainProc() {
	if(LoadText(sourceTextPath + headTxt + ".txt") == false)
		return;
	FindLayers();
	MakeColorList();
	ExportAllProc();
}

function FindLayers() {

	textLayer = activeDocument.layerSets["group"].artLayers["text"];
	stextLayer = activeDocument.layerSets["group"].artLayers["subtext"];
	itextLayer = activeDocument.layerSets["group"].artLayers["infotext"];
	fillLayer = activeDocument.artLayers["fill"];
}


function MakeColorList() {
	colors.push(MakeColor(0,0,0)); //灰色0
	colors.push(MakeColor(235,97,0)); //オレンジ系1
	colors.push(MakeColor(0,100,238)); //青色2
	colors.push(MakeColor(255,10,2)); //赤色3
	colors.push(MakeColor(18,232,32)); //緑色4
}
function MakeColor(r, g, b) {
	var rgbColor = new RGBColor();

	rgbColor.model = ColorModel.RGB; 
	rgbColor.red = r;
	rgbColor.green = g;
	rgbColor.blue = b;

	return rgbColor;
}

function ExportAllProc() {
	var chosenColor;

	for (i = 0; i < loadedArray.length; i++) {
		textLayer.textItem.contents = loadedArray[i][0];
		stextLayer.textItem.contents = loadedArray[i][1];
		itextLayer.textItem.contents = loadedArray[i][2];

		chosenColor = loadedArray[i][3];
		if(chosenColor < 0) {
			chosenColor = 0;
		} else if(chosenColor >= colors.length) {
			chosenColor = colors.length-1;
		}
		activeDocument.activeLayer = fillLayer;
		activeDocument.selection.selectAll();
		activeDocument.selection.clear();
		activeDocument.selection.fill(colors[chosenColor], ColorBlendMode.NORMAL, 100, false);

		DoExportPNG(targetPath + headTxt + "_"+('0000' + i).slice(-4) + ".png"); //連番PNGで保存
	}
}

function LoadText(filename) {
	var loadedText;
	var splittedArray;

	fileObj = new File(filename);
	flag = fileObj.open("r");
	if (flag == true) {
		while (!fileObj.eof) {
			loadedText = fileObj.readln();
			splittedArray = loadedText.split("\t");

			 //空行は無視
			if (splittedArray.length == 0) {
				continue;
			}
			
			lineArray = ["","","",0]; //初期値を入れる

			//1~3列目は文字列 4列目数字というパターン
			//自分で使うならこれくらい決め打ちのコードでもいい
			if (splittedArray.length >= 1) {
				lineArray[0] = splittedArray[0];
			}
			if (splittedArray.length >= 2) {
				lineArray[1] = splittedArray[1];
			}
			if (splittedArray.length >= 3) {
				lineArray[2] = splittedArray[2];
			}
			if (splittedArray.length >= 4) {
				lineArray[3] = parseInt(splittedArray[3]);
			}
			loadedArray.push(lineArray);
		}
		fileObj.close();
	} else {
		alert("ファイルが開けませんでした");
		return false;
	}

	return true;
}

function DoExportPNG(saveFileName) {
	saveFileObj = new File(saveFileName);

	//保存する
	try {
		activeDocument.saveAs(
			saveFileObj,
			pngSaveOpt,
			true,
			Extension.LOWERCASE);
	} catch (msg) {
		alert(msg + " 保存できませんでした");
	}


}

MainProc();
alert("終了");


f:id:VinSatoo:20180810142936p:plain
psdファイルを開いた状態でスクリプトを実行した結果を再掲。
psdと同じフォルダに、連番で5つのPNGファイルができている。
実際は出力先を違うフォルダにしたほうが扱いやすいだろう
(例えばUnityのアセットフォルダの中に出力先を作り、そのフォルダからアトラスが作られるようにUnityで設定する)。

慣れてきたら

慣れてきたらスクリプトとpsdの構造を見比べてみよう。

  • 「group」の中に「text」「subtext」「infotext」、それから別に「fill」というレイヤーが必要な構造である。
  • それより下のほうのレイヤーはどんな名前でもいい。
  • スクリプトと見比べながら、自分の作りたいように改造してみると良い
  • スクリプトが良く分からなくても、最悪使わないとこは文字を入れなければいい)
  • データについてはExcelで作り、テキストファイルにコピペするのが手軽だ。

 
 

一応データもこちらからダウンロード可能

http://cbaku.com/hatena/sample000/ui_magic.zip
いちおう参考までにデータも(スクリプトは入っていない)

  • 源真ゴシックPのHeavyが入っていないとフォントが変わってしまう。
  • 古いPhotoShopでは開かないかもしれない。

乱暴に始めるUnityEditor拡張

乱暴に、レイアウトも雑に始めてしまえ

UnityEditor拡張は沼だとか言われるし、入門ページでも結構導入とかレイアウト操作を丁寧に書いているので難しそうに見える。
なので、乱暴ながら一応使い道がありそうな奴を。

f:id:VinSatoo:20180809133334p:plain:w300
Cubeを置いただけのUnityプロジェクト。

  • Editor拡張は「Editor」という名前のフォルダの中に作らないといけないので、フォルダ作成。
  • スペルミスしないように注意

 
 

スクリプトを書く

f:id:VinSatoo:20180809133639p:plain:w300
Editorフォルダの中に適当な名前でC#スクリプトを作る。
んで、そのファイルの中に以下をコピペして保存。

using UnityEngine;
using UnityEditor; //Editor拡張には必要

public class TestEditor : EditorWindow
{
    [MenuItem("Window/PosMove")]


    public static void ShowWindow() {
        EditorWindow.GetWindow(typeof(TestEditor));
    }

    void OnGUI() {
        if(GUI.Button(new Rect(20.0f, 20.0f, 120.0f, 20.0f), "X+")) { //ボタンの位置と名前を決める
            var obj = Selection.activeGameObject; //選択中オブジェクト
            if (obj != null) {
                var pos = obj.transform.position;
                pos.x += 0.1f;
                obj.transform.position = pos; //Xを0.1プラスした
            }
        } else if (GUI.Button(new Rect(20.0f, 50.0f, 120.0f, 20.0f), "X-")) { //ボタンの位置と名前を決める Y位置だけ1つめのボタンより下にずらした
            var obj = Selection.activeGameObject; //選択中オブジェクト
            if (obj != null) {
                var pos = obj.transform.position;
                pos.x -= 0.1f;
                obj.transform.position = pos;  //Xを0.1マイナスした
            }
        }
    }
}

 
 
f:id:VinSatoo:20180809141204p:plain:w300
Unityに戻りコンパイルが行われる。すると、Windowメニューにプログラムの6行目に書かれた名前の「PosMove」という項目ができている。

f:id:VinSatoo:20180809141104p:plain:w300
で、それを選ぶとこのように「TestEditor」という新しいウィンドウが現れる(変な場所に出ることもあるのでよく探して)。
X+を押すと選択中の物体のXが0.1ずつプラスされていく。X-はその逆。 
 
 

何が嬉しいのか?

この程度ならCubeを操作パネルからマウスで動かしても同じだ。

  • しかし、0.1ずつ微調整したいなんて時には使い道がある。
  • そして最大の違いは、SceneウィンドウでもGameウィンドウでも(つまりゲーム進行中でもSceneに戻らずに)使えるという点だ。
  • しかもスクリプトで書けるので複雑なこともできる。
  • 例えば「他のオブジェクトと合わせる」「Yだけ0にリセット(≒床に下ろす)」とか。

 
 
よくある「Gameという空オブジェクトにGameManagerコンポーネントがくっついていて、そこがゲーム進行の司令塔」なんて場合ならEditor拡張のボタンを押したときの処理に

var game = GameObject.Find("Game").GetComponent<GameManager>()
game.PlayBGM("エンディング");
game.ChangeScene("エンディングシーン");

とかすることでデバッグを便利にすることもできる。
 
 

乱暴に始めた後はレベルアップを

もちろん今回のはEditor拡張の初歩の初歩で、本当は細かいレイアウト機能や、ユーザーに数字を入力させてボタンを押したときに活用する機能など色々なものがある。
その辺はすぐれた入門ページがたくさんあるので、そちらで勉強を。
anchan828.github.io
caitsithware.com
qiita.com


ただ、乱暴に「ボタンを置いて、それを押したらなんか処理してくれる」だけでもEditor拡張は普通に便利だからどんどん活用していこう。
どうせ使うのは自分だ。

派生クラスのインスタンスを全部生成してDictionaryに入れるとか

どーんと派生クラスの一覧を得る便利なコード

esprog.hatenablog.com
@es_programさんのこういった記事がある。
 
 

自分用の使い道を考える

で、使い方として考えたのが、例えば「色々なボイス反応を返すクラスのインスタンスを全部自動生成して、辞書からアクセスできるようにする」っていう形。

あまり良い使い方でもないかもしれないが、調教SLGとかだと「パラメータで色々分岐するのでExcelの表みたいなので完全管理は難しい」ケースがあるのでこんな感じが割と使い勝手良かったりする。
(本来ならLuaとかのスクリプトに担当させて、いちいちコンパイルが走らないようにするのがカシコイのかもしれない)

以下のスクリプトのTestScriptをUnityで適当なempty objectとかにアタッチして実行すると、こんな実行結果が得られる。
f:id:VinSatoo:20180808133902p:plain

もっといいやり方があってもご愛敬。
 
 

スクリプト部分

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;

abstract public class VoiceResItem //抽象クラス
{
    public string name { get; set; }
    public virtual string GetResponse(int obey, bool damaged) {
        return "";
    }
}

public class VoiceHateResItem : VoiceResItem //派生クラス
{
    public VoiceHateResItem() {
        name = "憎悪";
    }

    public override string GetResponse(int obey,bool damaged) {
        if (damaged)
            return "うっ……くっ……!";
        switch (obey) {
            case 0:
                return "大嫌い!";
            case 1:
                return "きらい。";
            default:
                return "そういうことやめてよ";
        }
    }
}

public class VoiceLoveResItem : VoiceResItem //派生クラス
{
    public VoiceLoveResItem() {
        name = "好き";
    }

    public override string GetResponse(int obey, bool damaged) {
        if (damaged)
            return "私は……っ";
        switch (obey) {
            case 0:
                return "何よ…";
            case 1:
                return "別にイヤじゃないっていうか";
            default:
                return "好き。";
        }
    }
}

public class VoiceResManager //それらを管理
{
    Dictionary<string, VoiceResItem> dic;

    public VoiceResManager() {
        dic = new Dictionary<string, VoiceResItem>();

        var classesList = Assembly
.GetAssembly(typeof(VoiceResItem))
.GetTypes()
.Where(t => {
    return t.IsSubclassOf(typeof(VoiceResItem)) && !t.IsAbstract;
});

        foreach (Type type in classesList) {
            MakeDic((VoiceResItem)Activator.CreateInstance(type));
        }

    }

    void MakeDic(VoiceResItem resItem) {
        dic[resItem.name] = resItem;
    }

    public string GetResponse(string key, int obey, bool damaged) {
        if (dic.ContainsKey(key)) {
            return dic[key].GetResponse(obey, damaged);
        }

        Debug.Log("エラー");
        return "";
    }
}

public class TestScript : MonoBehaviour
//Unityで実際に使ってみる用 適当なEmpty Objectにでもアタッチして実行
{
    VoiceResManager voiceResManager;
    void Start() {
        voiceResManager = new VoiceResManager();
        for (int i = 0; i < 3; i++) {
            Debug.Log(voiceResManager.GetResponse("憎悪", i,false));
            Debug.Log(voiceResManager.GetResponse("好き", i, false));
        }
        Debug.Log(voiceResManager.GetResponse("憎悪", 0,true));
        Debug.Log(voiceResManager.GetResponse("好き", 2, true));
        Debug.Log(voiceResManager.GetResponse("普通", 1,true));
    }
}

Unityでカメラが近づくと角度によりモデル(の一部)が突然消える

カメラが近づくとモデルが突然消えた……

f:id:VinSatoo:20180813150726p:plain:w150
こうだったのが突然……
 
f:id:VinSatoo:20180813150522p:plain:w150
ギャー
 

どうすればモデルが消えなくなるか

Unityでカメラが近づきすぎるとモデルが消えるパターンとしてはまずカメラに設定してある「NearClip(CameraのClipping PlanesのNear)」が大きすぎるというのがある。特にVRなどでは顔を近づけてもギリギリまで描画して欲しいので、NearClipは小さめにする。

が、NearClipを小さめにしていても、モデル(の一部)が突然消える現象がある。
カメラが遠い場合は発生しないが、ある程度近づくとカメラの角度により突然消える。しかも再登場するときはDynamicBoneなどがボヨンボヨンするので、表示が消えていただけでなく動作自体が止まっていることがうかがえる。

これは、以下のブログの記述にある通り、「skinnedMeshRenderer」の「update whenoffscreen」をONにすればよい。

testaa.ryorika.com
 
 

より適切な解決方法

「update whenoffscreen」をONにするのは簡単でよいのだが、名前の通り「見えないときも処理を行う」ことになってしまうのでキャラがたくさん出てくるゲームなどでは問題になる。
で、より適切な解決方法としては「skinnedMeshRenderer」の「Bounds」の「Extent」を大きくすれば良いとのこと。



(onotchiさん、izmさん、説明頂きありがとうございますm(__)m )
 
 

なんでそんなことになるのか

じゃあなんで「Bounds」の値が適切じゃない=カメラの中にいるはずなのに非表示になるような値になっちゃってるのかというと、こういうことらしい。


(さすが@hecomiさん)

docs.unity3d.com

オブジェクトのビジビリティはメッシュの Bounds によって決定されます (言い換えると、全体のバウンディングボリュームはあらゆる有効なカメラの視界角外にある必要があります)。ただし、アニメーションしているメッシュの実際のバウンディングボリュームは、アニメーションの再生によって変化します (例えば、キャラクターが手を頭上に伸ばすと、ボリュームの高さが増えます)。Unity は、すべてのアタッチされたアニメーションが最大のバウンディングボリュームになるときを考慮しますが、すべての使用可能な状況を予想してバウンディングボリュームを計算することは、不可能な場合があります。

以下のような状況では、ボーンや頂点が事前計算されたバウンディングボリュームの外に押し出されてしまい、問題が生じます。

ランタイム時にアニメーションを追加する場合

要するに、動きのあるモデルの場合は最初に想定したモデルのサイズよりも大きい形になってしまう。
だからカメラの中にいるのに外にいると判断されることがあると。
 
 

「Update When Offscreen」にも使い所あり?

Extentを編集するのもモデルが増えてくると大変だし、どれくらいの数字が適切なのかも難しいし、

これらの例では、2 つの解決方法があります。

Bounds (境界) を修正してメッシュの可能なバウンディングボリュームに適合するように修正します。
Update When Offscreen をスキンに対して有効にし、常にスキンメッシュをレンダリングします。
通常は、パフォーマンスに与える影響も少ないため最初の方法を選択します。ただし、パフォーマンスを重視する必要がない場合や、バウンディングボリュームを予測できない場合 (例えばラグドール物理を使用する場合) に、2 番目の方法を選択します。

とUnity公式さんも言っているので、Unityで動画を撮るだけみたいなケースでは「Update When Offscreen」を使うのも良さそうだ。