【教えてください】頂点シェーダで非表示を決定→フラグメントシェーダーでdiscardする適切な方法
Unityの頂点シェーダで非表示を決定→フラグメントシェーダーでdiscardするときの、適切&エレガントな方法を教えてくださいm(__)m
なんでそんなことをしたくなったか:前半
こちらは我らがImagineGirlsのR1N4 v2さんにUTS 2.0.5Test版を適用したものである。設定は割と単純で、眉毛貫通とかもしていない。
これに、R1N4さんの顔のUVを参考にしつつ作ったOutline_Samplerを適用するとこんな感じになる。
口元にあった変な線が消えた。
ついでに顔の輪郭線も強弱ができた。
(カラダは別マテリアルなのでそのまま)
なんでそんなことをしたくなったか:後半
ところがここに、自作のアレなBlendShapeを適用するとまた変な線がでてきてしまう。
(口の下のところ)
恐らく原因はBlenderでBlendShapeを作ったとき、ポリゴンの配置がヘンになってしまったからだ。
しかしモデリング技術がアレなのはどうにもならないし、トゥーン系の表現ならこの線さえ消えてくれればどうにか妥協できないこともない。
ちなみにこの線、Outline由来なのは間違いない(Outline_Colorの色を変えるとそれに従って変わる)。
だが、Outline_Widthを0にしてもこのように残ってしまう。
画像を見てもわかる通り、顔の他の輪郭線は消えたのに口の周りだけ線が残る。
ムリヤリどうにかしてみる
そこで、「UCTS_Outline.cginc」をのぞいてみる(以前のバージョンでは「MyUCTS_Outline.cginc」だった模様)。
散々試行錯誤した結果、以下のようにしたらうまくいった。
(うまくいった結果を先に貼っておく)
(以下の改造は自己責任でお願いします。また、この改造の内容についてUTS公式さんなどに問い合わせないでくださいm(__)m )
VertexOutputに、1つ値を追加する。
struct VertexOutput { float4 pos : SV_POSITION; float2 uv0 : TEXCOORD0; float3 normalDir : TEXCOORD1; float3 tangentDir : TEXCOORD2; float3 bitangentDir : TEXCOORD3; fixed4 disableFlag : Color; //これを加える };
vert関数の後半をこのように変える。
#ifdef _OUTLINE_NML //v.2.0.4.3 baked Normal Texture for Outline if (Set_Outline_Width <= 0) { //ここから o.disableFlag.r = 1; return o; } o.disableFlag.r = 0; //ここまでが追加された内容 o.pos = UnityObjectToClipPos(lerp(float4(v.vertex.xyz + v.normal*Set_Outline_Width,1), float4(v.vertex.xyz + _BakedNormalDir*Set_Outline_Width,1),_Is_BakedNormal)); #elif _OUTLINE_POS
frag関数の冒頭のこのように変える。
float4 frag(VertexOutput i) : SV_Target{ if (i.disableFlag.r == 1) {//ここから discard; } //ここまで
これで、Set_Outline_Widthが0以下のときは頂点シェーダでdisableFlag.rに1が入る。
そしてフラグメントシェーダの冒頭でdisableFlag.rが1ならdiscardで表示が破棄される。
(Set_Outline_Widthはアレコレ計算した結果の主線の太さらしい)
(before→afterで再掲)
その結果、↑のようにヘンな線が消えてくれる。
もうちょっと綺麗な適切な書き方はないのか
しかし色とナンの関係もないのにColorセマンティクスをムリヤリ加え、それをフラグとして使って値を放り込むというのは気持ちが悪い。
また、if文はシェーダーを遅くする原因というのも聞いたことがある。
frag関数のほうはともかく、vert関数までifを使っていいものかという疑問が残る。
そもそも頂点シェーダで「このケースは描画破棄」とできれば一番ラクなのだが……
というわけで、シェーダに詳しい方、こういうケースでもっとエレガントな書き方があれば教えてくださいm(__)m
プロジェクトタブからAnimationClipをAnimatorに加えるEditor拡張
アニメーション編集でVeryAnimationなどを使っているとき、操作できるAnimationClipはAnimator(AnimationController)に加えられているものだけだ。
プロジェクトウィンドウにあるClipをAnimatorタブにドラッグアンドドロップすればいいのだが、地味に面倒くさい。
そこで、少し汚い方法だがEditor拡張を書いてしまう。
AnimationClipを追加したいキャラの名前を決め打ちで書いているのがアレだが、実用上はそこまで困らない。
スクリプトの内容
using UnityEditor; using UnityEditor.Animations; using UnityEngine; public class AnimConMenu { [MenuItem("Assets/Add to anim-controller")] static void AddToController() { if (Selection.activeObject is AnimationClip) { var key = "R1N4_V2_mine01 (1)"; //ここに対象にしたいキャラの名前を入れる AnimationClip clip = (AnimationClip)Selection.activeObject; var animator = GameObject.Find(key).GetComponent<Animator>(); if (animator == null) return; var controller = animator.runtimeAnimatorController as AnimatorController; ; if (controller == null) return; Motion motion = (Motion)clip as Motion; controller.AddMotion(motion); } } }
var key = のところを、自分が使っているキャラの名前に変更するのを忘れないこと!
このようなスクリプトを「AnimConMenu.cs」といった名前でEditorフォルダに保存する。
実行手順
こんなAnimatorに、
この「mikarup_st006.anim」を加えたいとする。
(繰り返しになるが、一回きりならドラッグアンドドロップすればいいだけである。ただAnimatorタブを開きに行くなどの手順が面倒くさいので……)
さきほど書いたスクリプトにより、Add to anim-controllerというメニュー項目ができている。
実行すると、たしかに「mikarup_st006」が加わった。
VeryAnimationでも……
VeryAnimationでもこの通り、選択できるクリップのリストに「mikarup_st006」が加わっている。
選択すれば、先程のハゲ頭がやっていたポーズと同じポーズになっている。
あとはVeryAnimationの「Tools - Create New Clip」の「Duplicate」でも使えば、元のクリップを残したまま新しいAnimationClipを編集することも可能だ。
(逆に最初の段階でProjectタブからCTRL+Dで複製し、それをAdd to anim-controllerするのでももちろん良い)
メリットは
プロジェクトタブで「よし、このClipを元に新しいClipを作ろう」と思ったときに
- Animatorタブを選択して
- 元々選んでいたClipを改めて選択して長距離移動のドラッグアンドドロップをし
- Animatorタブをまたウラに戻す(常時開いていてもだいたい邪魔なだけでメリットがない。普通はSceneやGameタブを大きく開いているはず)
とかやるのは案外面倒である。
ちょっとした時間の短縮だが、VeryAnimationを使っている場合はけっこう作業しやすさが増すと思う。
【教えてください】UnityでMonoBehaviorでないSingletonはどうやる?
Singletonでいこう
世間ではUniRxやらZenjectでSingleton祭りを脱却しよう、みたいな記事がたくさん出ている。
だが、小規模アマチュア僕たちはSingleton祭りでいいんじゃないかなという気がしてしまう。
どうせゲームのTestは書きづらいということもあるし。
MonoBehaviorのSingletonは既に色々説明記事がある
qiita.com
tsubakit1.hateblo.jp
このようにMonoBehaviorのSingletonは既に色々説明記事がある。
が、「別にMonoBehaviorの機能いらないよ」という場合はコレで実装するのがどうも気が引ける。
MonoBehaviorに生えているプロパティやらメソッドの数は結構なものであるし、インテリセンスで余計なものがサジェストに大量に出てくるのも微妙だ。
MonoBehaviorでないSingletonを書く
MonoBehaviorでないSingleton実装方法としては、普通はこんな感じだろうか。
(非常にSingletonにされやすい音関係をイメージしてクラス名はSoundManagerとした)
public class SoundManager { private static SoundManager mInstance = null; public static SoundManager Instance { get { if (mInstance == null) { mInstance = new SoundManager(); } return mInstance; } } //privateなので var s = new SoundManager(); とかはできない private SoundManager() { } public void Play(string key) { ... }
これで、
SoundManager.Instance.Play("bang");
とか
var SoundManager soundManager = SoundManager.Instance;
soundManager.Play("bong");
とかすればフツーに使える。
newもできないのでたしかにSingletonとして機能している。
うまくいかない、SingletonBaseを継承するパターン
が、毎度毎度mInstanceのどうこうを書くのは非常にめんどい。
SingletonBase
public class SoundManager : SingletonBase<SoundManager> { private SoundManager() { } public void Play(string key) { ... }
とかできれば非常にラクで見やすい。
ところが、
public abstract class SingletonBase<T> where T : SingletonBase<T>, new() { private static T mInstance = new T(); public static T Instance { get { return mInstance; } } }
のように書いて継承するとエラーが起きてしまう。
(パブリックでないとダメだそうだ……)
SoundManagerコンストラクタをpublicにすればエラーはでないが……
その場合、
普通にnewできてしまう。
これではSingletonではない。
中心がズレた小道具をAdjust Pivotで直す
こちらはImagineGirlsのVienneV2のfbxをBlenderに読み込み、メガネ以外を全部削除して再度fbxとして書きだしたもの。
メガネだけをよそで転用したい、みたいな感じである。
それをUnityに読み込んだ。
中心がズレた小道具の問題
見ての通り、中心点(Pivot)とメガネの中心位置が激しく違う。
このまま回転させると……
この通りである。これでは位置合わせがやりづらくてしょうがない。
Adust Pivot(無料)の登場
そこでAdust Pivot(無料)を活用する。
assetstore.unity.com
プロジェクトにインポートしたら、[Window]-[Adjust Pivot]ができるので早速起動。
Adjust Pivotのウィンドウが出てきた。
位置調整オブジェクトの追加
今回中心を調整したいglassesの子に、位置調整用の空オブジェクトを作る。
名前はGame Objectのままで特に問題ナシ。
位置調整オブジェクトの位置合わせ
普通にUnityの移動ツールでGame Objectをメガネの中心になるように動かしていく。
カメラの角度によって中心だと思っていた位置が全然違っていたりするので、カメラを回転させたりしながら確認していこう。
そしてGame Objectをメガネの中心に持ってきた後で、Adust Pivotの「Move ~~ pivot here」を押すと……
移動の中心がメガネの中心になった
このように、「移動の中心=メガネの中心」になった。
最初と違って、回転させてもとんでもない動き方をしない。
これなら位置調整もしやすい。
ちなみに、一度こうなったら位置調整用のGame Objectは削除してしまって構わない。
あとはPrefabにでもして保存、Unity Packageとして書き出したりすれば使い回しもしやすいだろう。
位置調整を目分量でやらない
ところで、上のやり方はGameObjectを手動で動かしていることになる。
カメラの角度によって見え方がまるで違ったりするし、結構大変だ。
で、メッシュの中心はGetComponent
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を実行する。
自動で作られたCenterTargetというオブジェクト。
勝手に選択状態になる。
位置調整も自動で終わっているので、そのまま「Adjust Pivot」を実行。
さっき手動でやったのと同じように、回転の位置がちょうどよくなった。
もし微妙に中心をズラしたい場合は、「Adjust Pivot」実行前に手動で調整すればよい。
また、小道具によっては自動計算された中心と見た目の中心がまるで違うこともある。
その場合は手動でガンバルしかないだろう。
Blenderからの書きだしのときの多少の注意点?
メガネだけになったVienne。
記憶があいまいだが、メガネは最初非表示だったかもしれない。
その場合、Unityに持ってきたときMeshRendererがオフになっているので手動でメガネのMeshRendererをInspectorからオンにすること。
書き出しの際はオプションの「エンプティ」「カメラ」「ランプ」「アーマチュア」「メッシュ」「その他」と並んでいるところから「メッシュ」だけ選んだ。日本語化していないと表記が違うかもしれない
ImagineGirlsのライセンス表記
なお、今回使ったVienneV2は無料素材ではなく有料素材なのでそこは注意。
と言っても使っているのはメガネだけだが……
www.dlsite.com
(初代Vienneなどは無料だし、V2にもフリー版はあるもよう)
imaginegirls.com
Unityで静止画像のアレコレ&背景透過でキャプチャしたいのに色がついてしまう場合
なぜわざわざUnityで静止画を
最近のUnityは3Dキャラをレンダリングして出力するツールとしても悪くない。もちろん普通のことをするだけなら既存のDCCツールのほうが優れているのだろうが、
あたりが魅力的な場合は十分選択肢に入る。
特にトゥーンシェーダーの情報が充実してきているのが大きく、これで静止画像を生成するためにUnityを始めても惜しくない状況まで来ているように思う。
静止画は透明が欲しい
で、動画を撮る場合は背景までしっかりレンダリングしたものをRecorderあたりのアセットを使って録画することになるだろう。
一方、静止画の場合は背景が透明なほうが使いやすいことも多い。
どうせUnityで作るなら動画にすればいいのでは? なんでわざわざ2Dの静止画を? という意見もあるだろうが、
- 2D画像にしてしまえば従来の吉里吉里などのゲームエンジンでも活用可能
- Unity2Dなどで比較的軽めのゲームを作るときに活用することもできる
- Unityのシェーダーでは綺麗に出しにくいクッキリした落ち影などを後で描き加えてしまうことも可能
なんてメリットがある。
qiita.com
で、透明でもスクショを撮れる、というとこうしたコードを参考にさせて頂くのが良い感じである。
こちらはMacやiOSでおかしくなることの問題を説明されているのだが、とりあえずWindowsでやる限りは最初のほうのコードでも問題がなかった。
保存するファイル名をtest.pngではなく、System.DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".png"とかにすれば使い勝手も良い。
背景が透明にならない
ところが背景が透明にならないという別の問題が生じてしまった。
これはPostProcessingStackを付けたままだったからで、PostProcessingStackを切り、カメラのClearFlagsをSolidColorの「0,0,0,0(完全透明)」にすれば上手くいった。
Unityでレンダリングした二次画像をテキトーな背景と合成したもの。PostProcessingは切ってある。縮小した以外は特に加工なし
PostProcessingStackの代わり
PostProcessingStackが掛からないぶんについては、PhotoShopなどで処理すればいいだろう。
dic.pixiv.net
一番使うであろうブルームについては、ディフュージョンとかとかでググれば色々出てくる。
アンチエイリアスは、大きめにレンダリングして最後にPhotoShopから書き出すとき小さめに縮小すれば割と問題なくなる。
AO影がないのは寂しいが、Unity上でキャラの動きの完全に動きを切れば、PostProcessingStackのDebugViewsを使ってAO影だけぬきだし、PhotoShop上で合成などということも可能だ。
(※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メニューの「ファイル」→「スクリプト」に保存した名前のスクリプトができているはずである。
実際に書いてやってみる
こんな感じのPhotoShopデータを使う。
ファイア 3 炎を放つ 3 アイス 4 氷で攻撃する 2 ヒール 2 HPを30前後回復する 4 マグマ 18 溶岩で攻撃する上級魔法 0 シールド 5 防御力を50%あげる 1
で、上記のようなことが書かれたテキストを読み込み、スクリプトでボタンを作成する。
(テキストファイルの名前は、psdファイルと同じにする)
最終結果はこんな感じになる。
では以下のスクリプトをコピペして、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("終了");
psdファイルを開いた状態でスクリプトを実行した結果を再掲。
psdと同じフォルダに、連番で5つのPNGファイルができている。
実際は出力先を違うフォルダにしたほうが扱いやすいだろう
(例えばUnityのアセットフォルダの中に出力先を作り、そのフォルダからアトラスが作られるようにUnityで設定する)。
慣れてきたら
慣れてきたらスクリプトとpsdの構造を見比べてみよう。
- 「group」の中に「text」「subtext」「infotext」、それから別に「fill」というレイヤーが必要な構造である。
- それより下のほうのレイヤーはどんな名前でもいい。
- スクリプトと見比べながら、自分の作りたいように改造してみると良い
- (スクリプトが良く分からなくても、最悪使わないとこは文字を入れなければいい)
- データについてはExcelで作り、テキストファイルにコピペするのが手軽だ。
一応データもこちらからダウンロード可能
http://cbaku.com/hatena/sample000/ui_magic.zip
いちおう参考までにデータも(スクリプトは入っていない)
- 源真ゴシックPのHeavyが入っていないとフォントが変わってしまう。
- 古いPhotoShopでは開かないかもしれない。