- 投稿日:2019-12-18T20:53:27+09:00
uGUIでオーバードローを減らそう
本記事は、サムザップ Advent Calendar 2019 #1 の12/22の記事です。
はじめに
ゲームのUIって画像が何枚も重なり合って動作が重くなってしまう現象が発生しがち。
そんなUIのオーバードローを減らしてゲームの動作を軽くしようというお話です。サンプルUI
オーバードロー
このUIのオーバードローを見てみると…
色が明るくなっているところが何枚も重なり合っているところです。
これはUnityのSceneViewで左上のプルダウンからOverdrawを選択すると表示されます。描画範囲
斜めになっている四角い画像は色がついている部分だけじゃなく全体が描画されてしまいます。
(透明だから見えないだけ)
透明で見えない部分まで描画されてしまうなんてもったいない。透明部分を描画しない方法
実は簡単にできます。
画像とImageのInspectorを設定するだけ。画像のInspectorはMeshTypeをTightにするだけ!
ImageのInspectorはUseSpriteMeshにチェックを入れるだけ!
そうすると描画されていない部分のオーバードローが消えました!
効果
問題はこれをやることがどれほど効果があるのかってことですよね。
この程度のオーバードローだとあんまり変わらないのでもっと重ねてみたいと思います。Use Sprite Mesh チェックなし
適当な三角形の画像を作成して画面内に3000枚表示してみました。
FPSも表示してみましたが約30fpsぐらいでした。Use Sprite Mesh チェックあり
約50fpsでした。
※検証にはAndroid端末(GalaxyS9)を使用しました。サンプルコード
Test.cspublic class Test : MonoBehaviour { [SerializeField] RectTransform rectTransform = null; [SerializeField] GameObject goImage = null; [SerializeField] Text txtFps = null; int frames; float prevTime; // Start is called before the first frame update void Start() { float width = rectTransform.rect.width; float height = rectTransform.rect.height; for( int i = 0; i < 3000; i++ ) { GameObject go = Instantiate( goImage, rectTransform ); go.SetActive( true ); go.transform.localPosition = new Vector2( Random.Range( -width * 0.5f, width * 0.5f ), Random.Range( -height * 0.5f, height * 0.5f ) ); } frames = 0; prevTime = Time.realtimeSinceStartup; } // Update is called once per frame void Update() { ++frames; float time = Time.realtimeSinceStartup - prevTime; if( time >= 0.5f ) { txtFps.text = string.Format( "FPS:{0:f1}", frames / time ); frames = 0; prevTime = Time.realtimeSinceStartup; } } }仕組みの解説
普通に描画すると四角形で描画されてしまうのにどうやって色の付いてる部分だけを描画しているのかを解説します。
といっても解説するほどのことはなく画像をMeshとして切り出しています。
その証拠にGameViewでStatusを見てみると
3000枚の画像をUseSpriteMeshにチェックを入れず描画した方はVertsが12.0k(1万2千頂点)なのに対し
UseSpriteMeshにチェックを入れた方は117.0k(11万7千頂点)となっています。
まとめ
頂点数が増えてもオーバードローが減れば動作は軽くなる!
それでは明日は@kotaroyさんの記事です。
お楽しみに!
- 投稿日:2019-12-18T20:41:58+09:00
Assembly Definitionの関係性を可視化するEditor拡張
はじめに
Unityのプロジェクトで使用されているAssembly Definitionの依存元/依存先を可視化するEditor拡張を開発しました。
開発するまでの経緯などを記事にまとめさせてもらいました。
Assembly Definition
Unityの機能にディレクトリ単位でアセンブリを分割できるAssembly Definitionがあります。
使い方や利点などの解説はこちらの記事が詳しいです。関心の分離やビルド時間の短縮に効果があり、開発効率を上げるために使いこなしたい機能の1つです。
数が増えがち
こちらの記事ではClean Architectureの原則に基づいてAssembly Definitionを設定する試みを行いました。
この時、 11個の.asmdef
ファイルが作られることになりました。1プロジェクトに数個くらいであれば特にAssembly Definitionの関係性を気にする事なく使えると思いますが、真面目に
.asmdef
を切っていくと数が増えて管理が大変になり、意図しない参照の定義が入ってしまい最悪破綻する可能性も出てきます。(聞く話によると、3桁の.asmdef
が存在する規模のプロジェクトもあるらいしいです)Assembly Definitionの依存関係を可視化できることができると、管理の助けになると思い上記のEditor拡張を開発しました。
AsmdefGraph
https://github.com/naninunenoy/AsmdefGraph
できること
プロジェクト内のAssembly Definitionの関係を取得し、
GraphView
のノードエディタベースで表示します。
あくまで表示するだけで、.asmdef
の編集/追加/削除には対応していません。また、全てのノードを同じ初期位置に生成するため、見やすいように人間が移動させたり、デフォルトで入ってるプロジェクトに関係ないノードを削除するなどして、自力で見やすくしてもらう必要があります。 ?
やったこと
CompilationPipeline
最初はプロジェクトのルートディレクトリから
GetFiles("*.asmdef", SearchOption.AllDirectories)
みたいに取得して、Jsonを読み込んでましたが、専用のAPIがあることを知り、そっちに切り替えました。
CompilationPipeline.GetAssemblies()
でプロジェクトに関係する.asmdef
の情報をAssembly
クラスの一覧として取得できます。その中に、assemblyReferences
のフィールドがあるので、参照している.asmdef
のフルパスが取得できます。
これによって.asmdef
の関係を網羅したマップを作成できます。GraphView
作成したマップをもとに可視化する訳ですが、
UnityEditor.Experimental.GraphView
を用いました。こちらの記事のおかげで完成させることができました。?
EditorWindow
の中にGraphView
があるGraphView
にNode
が属する(AddElement()
する)Node
にはinとoutのPort
があるPort
はEdge
で繋がるという関係性になっています。
Edge
はcontentContainer.Add()
してやらないと表示されないということを知るまで結構かかりました。。やりたいこと
とりあえず見るだけなら今の機能でもできますが、特定の
.asmdef
の関係性にフォーカスする機能は欲しいかなと思っています。ノードを選んでそれに関係するノードとエッジをハイライトする(もしくは無関係なノードを見えなくする)イメージです。やらないこと
.asmdef
のパスがわかるので、Jsonファイルとして処理すれば編集できなくもないですが、プロジェクトをぶっ壊しそうなのでやるつもりはありません。?
CompilationPipeline
のAPIもget
しか公開されていませんでした。おわりに
Assembly Definition手軽で強力な機能なので、ぜひ試してみてください。
あとAPIとかEditor拡張用のクラスとか知らなかった便利な機能がUnityには色々あるんだなと思いました。?
- 投稿日:2019-12-18T19:03:24+09:00
uGUI上にParticleを表示させたい
最近見つけた便利なアセットを紹介します
uGUI上でパーティクルを表示するアセット
ParticleEffectForUGUI
https://github.com/mob-sakai/ParticleEffectForUGUI通常、uGUIとParticleSystemを同じ画面で表示させると、uGUIの描画が上になってしまって、ParticleSystemが後ろになってしまいます
そこでこの 「ParticleEffectForUGUI」を使うと、uGUIの上にParticleSystemを表示させることが出来ます
使い方
https://github.com/mob-sakai/ParticleEffectForUGUI/releases
1.こちらからunitypackageを落としてProjectにインポートします
2.UI上に表示したいParticleSystemがついたGameObjectにUIParticleをつけます。以上
あとはUIParticleのScaleをいい感じに調整したら終わりです簡単にuGUI上にParticleが表示できるので便利です
参考サイト
http://baba-s.hatenablog.com/entry/2018/08/20/090000
https://github.com/mob-sakai/ParticleEffectForUGUI
- 投稿日:2019-12-18T19:01:30+09:00
【Unity】VRで立体視360画像を見る
はじめに
こんにちは。UT-virtual Advent Calendar 2019 、19日目担当のwappaboyです。
Insta360Proなどの全天球カメラで撮影した立体視360静止画をVRで見られるようにUnityで実装する方法を紹介します。2019年4月に、サークルの新歓活動のために開発した「新歓VR」のプロジェクトを掘り起こして説明します。
使用したもの
- Unity 2018.3.6f1
- OculusGo
- Insta360 Pro で撮影した両眼立体視の全天球静止画
こんな感じの、上下で右眼用と左眼用に分かれている1:1の画像が用意できていると良いです。
撮影時の設定にもよりますが、私が使用したオリジナル画像は 7680x7680 pixel でした。
Unity(OculusGo)で快適に動作するよう、Photoshop等の画像編集ソフトで 2048x1024 のサイズに上下を切り分けました。
上半分が右眼、下半分が左眼です。準備
実装を始めるにあたって、諸々必要な設定を行ってください。
UnityにおけるVR開発や、OculusGoのビルドに際しての設定は省略します。他の記事を参考にしてください。必要なアセットのダウンロード
- Oculus Integration (https://assetstore.unity.com/packages/tools/integration/oculus-integration-82022)
- 全天球画像を貼り付けるためのSphere
- warapuriさんのSphere100を使用すると安定します。こちらの記事からありがたく頂戴します。記事内の「Sphere100.fbx」をダウンロードしてUnityプロジェクト内に入れてください。
両眼用の天球の作成
まず、新しいMaterial
Left
Right
を作ります。
それぞれのShaderをUnlit/Texture
に変更し、先ほど作成した全天球立体視の分割画像をそれぞれアタッチします。
続いて、Sphere100をScene上に配置し、名前を
LeftSphere
等にします。
MeshRendererのMaterialに先ほど作成したLeft
をアタッチします。するとこんな感じ。
そしてLeftSphereのLayerを
Left
にします。デフォルトではLeftレイヤーはないと思うので、Add Layer... から追加します。
このレイヤー設定をすることで、後のカメラ設定と合わせて両眼立体視で見られるようになるので、忘れないように!
RightSphereも同様に作成し、
Right
Materialをアタッチ、Rightレイヤーを新しく作成して設定します。
LeftSphereとRightSphereは全く同じ位置に配置してください。
カメラの設定
続いてカメラの設定です。
まずOculus IntegrationからOVRCameraRig
をSceneに配置します。その他Oculus開発に関する説明は省略します。
OVRCameraRig
>TrackingSpace
配下にCamera_L
とCamera_R
を作成しましょう。
そしてそれぞれについて以下の設定をします。
Camera_L
- Clear Flagsを
Depth only
に- Culling Maskを
Left
のみにチェック- Target Eyeを
Left
にCamera_R
- Clear Flagsを
Depth only
に- Culling Maskを
Right
のみにチェック- Target Eyeを
Right
にこれで、実際にヘッドマウントディスプレイで見ると右眼と左眼で異なる画像が描画されるので、立体視として全天球画像が見られると思います。
同様の方法で、3Dの全天球動画も実装できます。Unityでの動画再生自体はまた別の設定が必要ですがそこはよしなに...最後に
今回の仕組みは東大VRサークル UT-virtualの新歓VRのために実装したのですが、完成品では立体視全天球画像を単に置くだけでなく、Shaderをいじってオシャレなフェードインをさせるなど随分と面倒な仕組みを追加していました。
こんな感じのものも作れちゃうUnityすごいですね。読んでくださりありがとうございました。
- 投稿日:2019-12-18T18:30:58+09:00
【VRChat】VRC_TriggerのActions一覧
これは、 VRChat Advent Calendar 2019 の19日目の記事です。
昨日は yukonkon3 さんによる アタマワルイになろう!(海外パリピを学ぶ) でした。VRC_TriggerのActions一覧
VRChatのワールド内にボタンとかを設置したい場合、
VRC_Trigger
というものが必要らしく、先週くらいから触り始めました。トリガーで何が起こせるの?
っていうのをググってみると、少し情報が古かったりで、意外と一覧でまとまっているページが見当たりませんでしたので、作ることにしました。
Actions一覧
- Unity: 2017.4.28f1
- VRChat SDK: 2019.09.18.12.05
上記の環境で
Actions
を見てみると、以下のようになっていました!項目がいっぱいありますね!この1つ1つが
Action
と呼ばれるものです!それぞれの項目の説明は、以下の公式説明ドキュメントに記載があるのですが、一部説明がないものもありました。
VRChat / v2019.3.2 / Guides / Actions
説明がないものは、検索や質問したりして集めた情報を記載しようと思います。
アルファベット順です。
ActivateCustomTrigger
カスタムトリガーを発火します。
公式説明:https://docs.vrchat.com/docs/activatecustomtrigger
たとえば、
- ボタンAを押したら扉が開く。
- ボタンBを押しても扉は開く。
みたいなシーンを実現したいとします。
以下のように設定すれば一応実現できます。
- ボタンAのActionに、扉が開くアニメーションを設定する。
- ボタンBのActionに、扉が開くアニメーションを設定する。
しかし、カスタムトリガーを利用すれば、以下のように、共通化できます。
- カスタムトリガーを作成し、そのActionに、扉が開くアニメーションを設定する。
- ボタンAのActionに、カスタムトリガーを設定する。
- ボタンBのActionに、カスタムトリガーを設定する。
これはいわゆる
UIとロジックの分離
というやつです。(プログラミング界隈でよく聞くやつ)
扉を開ける
というUIと、カスタムトリガーを発火する
というロジックを分離することによって、今後、改良がしやすくなるという利点があります。たとえば、ボタンC、D、Eみたいのが増えたとしても、それぞれのActionにカスタムトリガーを設定するだけで済みます。
さらに、
扉が開いたら風のエフェクトを発生させる
みたいな仕様変更があったら、ボタンA~Eすべてを変更するのは大変です!カスタムトリガーがあれば、カスタムトリガーののActionだけを変更すればよいから楽ですよね!
AddAngularVelocity
公式に説明がないため、根拠はありませんが、おそらく、
Rigidbody
に角速度を与えるものだと思います。AddDamage
GameObject
にダメージを与えます。公式説明:https://docs.vrchat.com/docs/adddamage
AddForce
Rigidbody
に力を加えます。公式説明:https://docs.vrchat.com/docs/vrchat-201821
AddHealth
GameObject
にヘルスを与えます。公式説明:https://docs.vrchat.com/docs/addhealth
おそらく、ダメージで減った体力が回復するのだと思います。
AddVelocity
公式に説明がないため、根拠はありませんが、おそらく、
Rigidbody
に速度を与えるものだと思います。AnimationBool
指定した
Animator
のbool
変数の値を変更します。公式説明:https://docs.vrchat.com/docs/animationbool
AnimationFloat
指定した
Animator
のfloat
変数の値を変更します。公式説明:https://docs.vrchat.com/docs/animationfloat
AnimationInt
公式に説明がないため、根拠はありませんが、おそらく、指定した
Animator
のint
変数の値を変更するものだと思います。AnimationIntAdd
指定した
Animator
のint
変数の値に、指定した数値を加算します。公式説明:https://docs.vrchat.com/docs/vrchat-201811
AnimationIntDivide
指定した
Animator
のint
変数の値を、指定した数値で除算します。公式説明:https://docs.vrchat.com/docs/vrchat-201811
AnimationIntMultiply
指定した
Animator
のint
変数の値に、指定した数値を乗算します。公式説明:https://docs.vrchat.com/docs/vrchat-201811
AnimationIntSubtract
指定した
Animator
のint
変数の値から、指定した数値を減算します。公式説明:https://docs.vrchat.com/docs/vrchat-201811
AnimationTrigger
Animatorのトリガーパラメーターを発火します。
公式説明:https://docs.vrchat.com/docs/animationtrigger
AudioTrigger
指定した
Audio Source
のAudio Clip
を再生します。公式説明:https://docs.vrchat.com/docs/audiotrigger
DestroyObject
GameObject
を破棄します。公式説明:https://docs.vrchat.com/docs/destroyobject
PlayAnimation
旧
Animation
コンポーネントのアニメーションを再生します。公式説明:https://docs.vrchat.com/docs/playanimation
SendRPC
スクリプトから関数を発火します。
Advanced Mode
の時のみ利用できます。公式説明:https://docs.vrchat.com/docs/sendrpc
SetAngularVelocity
指定した値に、
Rigidbody
の角速度を変更します。公式説明:https://docs.vrchat.com/docs/vrchat-201821
SetComponentActive
Component
のアクティブ状態を変更します。公式説明:https://docs.vrchat.com/docs/setcomponentactive
SetGameObjectActive
GameObject
のアクティブ状態を切り替えます。公式説明:https://docs.vrchat.com/docs/setgameobjectactive
例えば、ボタンを押したらパーティクルライブの
Timeline
を開始するみたいなことができます。
Timeline
を設定したGameObject
を用意して、このSetGameObjectActive
でそのGameObject
をアクティブにするという流れです。(パーティクルライブのDiscordで教えてもらいました。)SetLayer
指定したレイヤーに、選択した
GameObject
のレイヤーを変更します。公式説明:https://docs.vrchat.com/docs/setlayer
SetMaterial
指定したマテリアルに、選択した
GameObject
のマテリアルを変更します。公式説明:https://docs.vrchat.com/docs/setmaterial
SetParticlePlaying
パーティクルシステムの放出のアクティブ状態を切り替えます。
公式説明:https://docs.vrchat.com/docs/setparticleplaying
SetUIText
指定した値に、
UIText
コンポーネントのテキストを変更します。公式説明:https://docs.vrchat.com/docs/vrchat-201821
SetVelocity
指定した値に、
Rigidbody
の速度を変更します。公式説明:https://docs.vrchat.com/docs/vrchat-201821
SetWebPanelURI
指定したURIをウェブパネルにセットします。現在無効化されているようです。
公式説明:https://docs.vrchat.com/docs/setwebpaneluri
SetWebPanelVolume
ウェブパネルの音量を調整します。現在無効化されているようです。
公式説明:https://docs.vrchat.com/docs/setwebpanelvolume
SpawnObject
指定した
Prefab
をスポーンさせます。公式説明:https://docs.vrchat.com/docs/spawnobject
SetGameObjectActive
との違いは、新たなGameObject
を生成するかどうかです。
Unity C#でいうInstantiate
、PlaymakerでいうCreate Object
に相当するものかなぁと思います。TeleportPlayer
設定した場所にプレイヤーを移動させます。
公式説明:https://docs.vrchat.com/docs/teleportplayer
さいごに
本記事作成にあたり、以下の記事を参考にさせていただきました。ありがとうございました。
- VRChat / v2019.3.2 / Guides / Actions
- VRChat / v2019.3.2 / Guides / VRChat 2018.1.1
- VRChat Document 日本語訳 Wiki / VRC_Trigger / アクション
- [VRChat]初学者向けAnimatorの使い方
- Programming in VRChat / Custom Trigger
これは、 VRChat Advent Calendar 2019 の19日目の記事でした。
明日は @rakurai5 さんによる Amplify Shader Editorを用いてメッシュを雪で覆うシェーダーを作る です!
- 投稿日:2019-12-18T17:32:32+09:00
ディゾルブシェーダー
溶ける
Unityで使える、溶けるタイプのシェーダーです。
※ライトの影響やスペキュラなどは設定していないので、
必要であればいい感じに追加してください。m(_ _)mコード
Dissolve.shaderShader "Custom/Dissolve" { Properties { _MainTex ("Texture", 2D) = "white" {} _DissolveTex ("DissolveTex", 2D) = "white" {} [KeywordEnum(Manual, Time, PingPong)] _Mode ("Mode", Int) = 0 _Threshold("Threshold", Range(0, 1)) = 0 _Speed("Speed", Range(0, 5)) = 0 _PatternSize("Pattern Size", Range(0, 5)) = 1 } SubShader { Tags { "RenderType" = "Transparent"} Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #pragma multi_compile _MODE_MANUAL _MODE_TIME _MODE_PINGPONG struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; fixed4 wPos : TEXCOORD1; }; sampler2D _MainTex; sampler2D _DissolveTex; float _Threshold; float _Speed; float _PatternSize; v2f vert (appdata v) { v2f o; o.wPos = v.vertex; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { float4 sPos = ComputeScreenPos(i.wPos); float2 uv = sPos.xy / sPos.w; uv *= -1; float4 disolveVar = tex2Dlod(_DissolveTex, float4(uv / _PatternSize, 0, 0)); //視界に合わせて動く // float4 disolveVar = tex2D(_DissolveTex, i.vertex.xy / _PatternSize); float gray = (disolveVar.x + disolveVar.y + disolveVar.z) / 3; float threshold = 0; #ifdef _MODE_MANUAL threshold = _Threshold; #elif _MODE_TIME threshold = _Time.x * (1 + _Speed); #elif _MODE_PINGPONG threshold = 0.1 + (_SinTime.y * (1 + _Speed)); #endif if( gray < 1 - threshold ){ discard; } fixed4 col = tex2D(_MainTex, i.uv); return col; } ENDCG } } }必要なもの
こういういい感じの白黒画像を用意して、
DissolveTexに設定してください。
仕組み
DissolveTexに設定した白黒画像をしきい値(Threshold)以下の場合はピクセルを描画しないで、
しきい値を加算したり減算したりすることで見た目を変化させています。
なので、画像の具合に表現がかなり左右されます。
色々設定して試してみてください。使い方
Thresholdの値を増やすと出現し、減らすと消えます。
Mode
Manual : 手動で溶かせます。アニメなどでThresholdを変化させてください。
Time : 時間経過で現れます。一回出現して終わりです。Speed設定で出てくる速度を設定してください。
PingPong :時間経過で出現したり溶け消えたりを繰り返します。 Speed設定で速度を設定してください。
- 投稿日:2019-12-18T17:09:00+09:00
Unityでシューティングゲームを作る(2)
ここまでの進捗
- 背景がループするようにした。
- 普通の敵の動作を作成し、その敵が3秒ごとに生成される。
- 瞬間移動する敵の動作を作成し、5秒ごとに生成する。
- プレイヤーが画面の範囲外に行かないようにした。
- 敵とプレイヤーが衝突したらプレイヤーが消滅する。
今後やること
- オープニングシーンとエンディングシーンを追加する。
- ボスキャラの動作を実装する。
- 分散攻撃の敵を実装する。
- エフェクトとBGMを追加する。
- 様々な敵の出現方法を考える。
この記事で書くのは赤文字の部分
分散攻撃の敵を実装する
敵キャラの親クラスに分散攻撃をするための関数を以下のように作った
public void NwayShot(Transform enemy,float angle) { Instantiate(EnemyProjectilePrefab, enemy.position, Quaternion.Euler(new Vector3(0.0f,0.0f,angle))); }これのEnemyProjectilePrefabは敵が撃つ弾のPrefabで、enemy.positionは敵の位置、Quaternion.Eulerで弾の向きを設定している。
弾の向きの計算は以下のようにしたpublic int NwayCount = 3;//何方向に攻撃するか public int NwayAngle = 10;//弾の角度 . . . for(int i=1; i <= NwayCount; i++) { float angle = -(NwayCount + 1) * NwayAngle / 2 + i * NwayAngle; base.NwayShot(gameObject.transform, angle); }この計算は以下のサイトを参考にさせていただきました。
計算が苦手だったので非常に助かりました。
参考:【第2回】n-way弾実装してみるよ!ちゃんと3-Way攻撃になりました!
今後は中心の弾がプレイヤーに向けて撃たれるようにしたいです。
結構調べたりしたのでめっちゃ時間かかった(泣)
次はボスだぁー
- 投稿日:2019-12-18T16:01:20+09:00
Unityカスタムパッケージ作成
最近のUnityが機能を分別してPackageManagerからほしいものだけをインストールする形になりました。自分もこういうパッケージをどうやって作るか検討しました。
パッケージ用プロジェクトのレイアウト
まずはパッケージになる新しいUnityプロジェクトを作成。プロジェクト内のフォルダーなどを下記のレイアウトと合わせる
参考:Unityマニュアル https://docs.unity3d.com/Manual/cus-layout.htmlUnityマニュアル通りにしたら通常Unityプロジェクトではないので作る時は別のプロジェクトにインポートしないといけない。これは使いにくいのかなと思って通常のプロジェクトの中に突っ込みました。
<root> └── Assets ├── PackageContents │ ├── package.json │ ├── README.md │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── Editor │ │ ├── [YourPackageName].Editor.asmdef │ │ └── EditorScript.cs │ ├── Runtime │ │ ├── [YourPackageName].asmdef │ │ └── RuntimeScript.cs │ └── Documentation~ │ │ └── Tests ├── Editor │ ├── [YourPackageName].EditorTests.asmdef │ └── EditorTests.cs └── Runtime ├── [YourPackageName].RuntimeTests.asmdef └── RuntimeTests.cspackage.jsonを作成
まずはパッケージの情報をpackage.jsonっていうファイルに書き込もう
参考:https://docs.unity3d.com/Manual/upm-manifestPkg.html例
{ "name": "com.company.package", "displayName": "Package Name", "version": "1.0.0", "unity": "2019.2", "description": "A Test Package", "keywords": [ "package" ], "category": "Utility" }もっと設定あるけど基本的にこのくらいあると大丈夫かな?
name = reverse url形式のパッケージ名
displayName = 人間が読みやすい名前
version = パッケージのバージョン(major.minor.patch)
unity = unityの必要バージョン
description = 説明文
keywords = 検索用言葉
category = パッケージグループ名C#スクリプトを作成
これはもちろん自由なところです。作りたいパッケージを実装する。例えばAssetBundleの作成とロード周りのシステムとか?
Editor用ならAssets/PackageContents/Editorのフォルダーを利用してゲーム用のスクリプトならAssets/PackageContents/Runtime。Assembly Definitionを作成
Assembly Definitionを追加するとパッケージのDLLを作成してくれます。これはメインプロジェクトのコンパイル時間が早くなったり、誰かにDLLだけ渡したり、便利なものです!追加するだけで終わるので入れましょう。
EditorとRuntimeフォルダーに別々に追加してEditorのdefinitionでは参照するdllのところにruntime用dllを付けるTestファイルを作成
必要かどうか個人判断だけど、せっかくいい機能があるのでテスト用スクリプト追加してもいいよね。
- Testフォルダーの中にEditor用とランタイム用分かれてる
- テスト用スクリプトなどがまだ作られてない場合、Testフォルダーに右クリック→Create→Testing→Tests Assembly Folderを選択
- 作られたフォルダーをEditorかRuntimeの名前に変更して中にあるassembly definitionの名前を好きに変更
- そのフォルダーにいって右クリック→Create→Testing→C# Test Scriptを選択してスクリプトを追加
- 出来上がったテストスクリプトはだいたいこの感じになります
using UnityEngine; using UnityEngine.TestTools; using NUnit.Framework; using System.Collections; public class EditorTests { [Test] public void RunTests() { Asset.Pass(); } [UnityTest] public IEnumerator RunTestsWithEnumerator() { yield return null; } }
- 確認方法: UnityのメニューからWindow→General→Test Runnerにいって出てくるwindowでテストが正常に動くか
Verdaccioっていうnpm repositoryにパブリッシュ
これから作るパッケージがどこかに管理したいのでどこかに入れましょう。 UnityのPackage Managerがnpm利用してるので自分が作ったものをPackage Managerに出したいので合わせる必要がある。Unityさんが推奨したのはVerdaccioっていうもの。(多分インストールと準備が楽だから、unityが内部的に違うもの使ってるらしい)
ここからダウンロードとインストール:https://github.com/verdaccio/verdaccioインストール後command lineでpackage.jsonがあるフォルダーにいって
npm publish --registry http://localhost:4873を叩くと登録されます!(もちろん別のマシンにnpmホストしてるならurlをあわせる必要ある)
本番プロジェクトに利用
まずは私達作ったrepositoryはどこにあるかUnityに教えないといけない。
Packagesっていうフォルダーの中にあるmanifest.jsonを開いてscopedRegistriesっていうjsonオブジェクトを追加する。
単純にurl、名前、とパッケージの逆urlを追加する{ "scopedRegistries": [ { "name": "My Packages", "url": "http://localhost:4873", "scopes": [ "com.company" ] } ], "dependencies": { "com.unity.ads": "2.0.8", "com.unity.analytics": "2.0.16", ..hogehoge.. } }後は"dependencies"のところに自分のパブリッシュされたパッケージ名とバージョンを定義するだけ
"dependencies": { "com.unity.ads": "2.0.8", "com.unity.analytics": "2.0.16", ..hogehoge.., "com.company.package": "1.0.0" }Unityに戻ったらパッケージがダウンロードされて使えるようになります!
さらに便利に
まずはコード管理するにはgit repositoryがもちろんやったほうがいいよね。さらにGitLabのCI機能を利用してテストクラスを自動的に走らせてOKが出ればnpmにpublishするような流れるを作る!
・Git repoを作る
・git runnerを作る(やり方がCIのところに書いてる)
・git runner用のスクリプトを追加 (ルートフォルダーに.gitlab-ci.ymlを追加)自分が作ったスクリプトはこんな感じになりました
stages: - runTests - publish variables: version: "awk '$$1==\"m_EditorVersion:\"{print $$2}' ./ProjectSettings/ProjectVersion.txt" editor-tests: script: - "echo 'Running Editor Tests...'" - "unityVersion=$(eval $version)" - "unity='/Applications/Unity/Hub/Editor/'$unityVersion'/Unity.app/Contents/MacOS/Unity'" - $unity -batchmode -runTests -projectPath . -testResults ./editmodeResults.xml -testPlatform editmode stage: runTests tags: - unity only: - master runtime-tests: script: - "echo 'Running Runtime Tests...'" - "unityVersion=$(eval $version)" - "unity='/Applications/Unity/Hub/Editor/'$unityVersion'/Unity.app/Contents/MacOS/Unity'" - $unity -batchmode -runTests -projectPath . -testResults ./playmodeResults.xml -testPlatform playmode stage: runTests tags: - unity only: - master publish-npm: script: - "cd ./Assets/PackageContents" - "npm publish --registry http://localhost:4873/" stage: publish tags: - unity only: - master・2つのステージに分ける(テストとパブリッシュ)
・editor用とランタイム用のテストスクリプトを実行する(起動するunityバージョンをProjectVersionsのテキストから引っ張る)
・テストがOKならnpm publishかける
・メインブランチしか動かない(only: master)おわりに
パッケージ分けるとちゃんとしたフレームワークができて各プロジェクト使い回すことが本当に便利!後から分別するのも大変かもしれないので早い段階でパッケージ化するのはおすすめ!
- 投稿日:2019-12-18T14:24:33+09:00
Spine・・というかSortingOrderがあるものに対して、パーティクルを回り込ませる
本記事は,サムザップ Advent Calendar 2019 #2 の12/24の記事です
はじめに
みなさま、はじめまして
サムザップの中山です。今年は、Unite Tokyo 2019にてAddressable Assets Systemについて
お話させていただく機会がありました。
此度のアドベントカレンダーでもその話の続きを・・・と思いましたが
今回は、本業であるインゲーム開発に関わる話をしようと思います。2Dゲームの表示順について
SortingLayerとSortingOrderを組み合わせて、階層構造で表示順を決めていくのが一般的です。
ある意味web制作的でシンプルなので、大抵の場合問題なく開発を進められると思います。
しかし、表示されるオブジェクトの順番が整数で決まるので、中途半端なかぶりを許してくれません。
2Dで取り扱われる想定のオブジェクトには、"RenderType = Transparent"、"ZWrite = off"が記述されたシェーダーが刺さっています
これはSpineの基本的なシェーダーでもそうですし、Unityのビルトインシェーダーでも同じです。
半透明かつZwriteを無視することで、いわゆる2D表現を行っています。パーティクルとの組み合わせ
とはいえ、ゲーム上で扱うには表示順が曖昧であったほうがいいときもあります。
パーティクルと組み合わせるときが代表的な例です。
Spineのオブジェクトとパーティクルを重ねたらどうなるでしょうか?
Spineのサンプルデータで見てみます。SortingOrderとSortingLayerが同じだった場合
最終的な表示順は、Z軸でどちらが前かで決まります。
パーティクルの場合エミッターが基準となります。
つまりエミッターがSpineより少しでもカメラに近ければ
そこから発生されるすべてのパーティクルがSpineオブジェクトよりも表示順が強くなります。
生み出されたパーティクルがそれぞれ個別の表示順を獲得するには、ZWriteをOnにするのが手っ取り早いです。
しかし、その際に意図しない見た目になることが多いと思います。
Spineなどの2Dオブジェクトはたいていポリゴンの形と画像の形は一致していません。
ポリゴンの余白の部分があるのですが、通常は透明になっているので目には見えません。
ZWriteをOnにすると、パーティクルよりも先に余白の部分がレンダリングされてしまう事があるのです。
↑足の部分で後ろに回り込んだパーティクルがレンダリングされていない透明と不透明がはっきりしているデザインの場合、これを回避するのは比較的容易です。
シェーダーにカットオフ用のレンジスライダーを作り、その値に応じてフラグメントシェーダーでクリップを行えば
不要な部分がレンダリングされないので、意図した見た目に近くなります。
参考までにこんな感じで追加してみました。
_Cutoff ("Shadow alpha cutoff", Range(0,1)) = 0.1
//中略
clip(texcol.a - _Cutoff);
その他
ZWriteを追加したことによって、Z軸から表示順を決めることができるようになりました。
しかし、同時にSortingLayerとSortingOrderも生きている状態です。
Z軸での判定が効くのは、ZWriteをOnにしたオブジェクトに対してOffの状態のオブジェクトのSortingLayerが同等か高いときです。
そうでない場合はSortingLayerとSortingOrderに準じた表示順になります。まとめ
お手軽ですが、なかなか反動のあるやり方でもあるので
使えない場面も、まぁあるかなと思いますが
一つの手段として覚えておくといいかもしれません。明日は@shirahama_manabuさんの記事です
それでは、良いお年を〜
- 投稿日:2019-12-18T13:38:58+09:00
PropertyDrawerが思ったより便利だった
サムザップ #2 Advent Calendar 2019 の12/18の記事です。
株式会社サムザップでUnityエンジニアやってる二宮です。
最近、Editor拡張を教えてもらって、今更ながらナニコレスゴクツカエルって思ったので紹介します。PropertyDrawer
今回紹介したいのはこちらっ!
PropertyDrawerこれを使うと、型をシリアライズした時にカスタマイズしたViewで表示されるようになります。
これが、思ったよりもだいぶ強力なのです。
素敵な使用例
マスターデータを参照して情報を表示したい!みたいな場合に使えます。
そう例えばローカライズ!UIにテキストデータを書き込みたいところだけれど、テキストデータは言語設定によって変わるため、直シリアライズはNG,,,.
そうすると、「マスターのキーをシリアライズして、Runtimeでテキストデータを言語設定に応じて取得して表示」となるのですが、これは設定が結構辛くなる。シリアライズ時のキーがあっているかどうかがぱっと見わかりづらい。
こうなると、うっかりキーを打ち間違えたり、Previewが面倒だったりします。じゃあちゃんとカスタマイズして、選択式にするぞ、、、!とかって考えてはみるものの、
Editor拡張で頑張ろうとしても表示するUIが変わると、それごとに拡張書かなければならなくなってしまって辛くなってきます。
こちとらマスターデータからテキストをとって表示したいだけなのに!そんな時に!プロパティドロワー!
こんな感じにいつも通りシリアライズするだけで、どのスクリプトでも同じ拡張が表示されます。
Textでもポップアップでも、トグルのラベルでもボタンでも! なんと素敵な、、、![RequireComponent(typeof(Text))] public class TextView : MonoBehaviour { [SerializeField] private LocalizedText _text; private void Awake() { GetComponent<Text>().text = _text.Value; } } /// ローカライズされたテキストデータ [Serializable] public class LocalizedText { // マスターから読み込むのに使うキー [SerializeField] private string _textKey; // Viewで使うテキストデータ public string Value => TextMaster.Instance.GetText(_textKey); } /// 仮想テキストマスター。言語設定に合わせたテキストのマスターデータを読み込んで使う public class TextMaster { private static TextMaster _instance = null; public static TextMaster Instance => _instance ?? (_instance = new TextMaster()); public readonly Dictionary<string, string> Data = new Dictionary<string, string>(); public TextMaster() { // TODO: 言語設定に合わせたテキストマスターを取得する for (int i = 0; i < 10; i++) { Data[$"key{i}"] = $"テキストデータ {i}"; } } /// マスターからテキストデータを取得する public string GetText(string key) => Data[key]; }作るのが一つで良いから、選択式にしたりとか便利さを追求したくなっちゃいますよね!
どうでしょう?使えそうって思ったんじゃないですか??よさが十分に伝わったところで、簡単に実装例を出しておきますね。
PropertyDrawerの実装方法
ローカライズに使えそうなサンプル実装をしてみました。
作ったもの
ポップアップ形式で、仮想マスターデータからテキストを選ぶ形式にしてみました。
シリアライズしているのはマスターのキー(LocalizedText._textKey)のはずですが、テキストデータが表示されるようにしています。PropertyDrawerの実装
[CustomPropertyDrawer(typeof(LocalizedText))] public class PropertyDrawerLocalizedText : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { // 現在格納されているキーデータをもとに、ポップアップに必要なデータを取得 var keys = TextMaster.Instance.Data.Keys.ToArray(); var textKeyProperty = property.FindPropertyRelative("_textKey"); // カスタムしているプロパティがもつ"_textKey"変数のSerializedPropertyを取得する var previousIndex = Array.IndexOf(keys, textKeyProperty.stringValue); // ポップアップを表示 var selectedIndex = EditorGUI.Popup(position, previousIndex, TextMaster.Instance.Data.Values.ToArray()); // ポップアップで値を変えていれば、キーを取得してシリアライズ情報を更新 if (selectedIndex != previousIndex) { previousIndex = selectedIndex; textKeyProperty.stringValue = keys[previousIndex]; // 修正があったらシリアライズしてあるオブジェクトに変更を反映 textKeyProperty.serializedObject.ApplyModifiedProperties(); // これがないとデータが何も変更されない } } }Editor拡張全般に言える気がしますが、割とごにょっとします。
でもまぁこれでこの型をシリアライズしたら毎回ポップアップが表示されてくれると思えば全然良いですよね。注意点
- EditorGUILayoutが使えなくて、EditorGUIで頑張る必要がある
- 高さが変わる場合、高さを指定する必要がある
Layout系が使えないので、Rectをきちんと指定する必要があって、不慣れだと辛いです。辛かったです。
PropertyDrawerでは、高さの変更を記述してあげる必要があります。
これを怠って、こんな感じに1行追加するだけだと、残念な感じになります。public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { var keys = TextMaster.Instance.Data.Keys.ToArray(); EditorGUI.LabelField(position, "テキストキー"); position.y += EditorGUIUtility.singleLineHeight; var textKeyProperty = property.FindPropertyRelative("_textKey"); var previousIndex = Array.IndexOf(keys, textKeyProperty.stringValue);EditorGUI初心者だったのでしっかりハマりました。はい。
こうやって、高さを1行増やしたことを通知してあげる必要があるんですね。
/// プロパティの高さを取得する。カスタムによって高さが変わるなら必須 public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return base.GetPropertyHeight(property, label) + EditorGUIUtility.singleLineHeight; }というわけで、実装にちょいと癖はあるので気をつけつつ、本当に便利なので是非使ってみてください〜
明日は@sato_tatsukiさんの記事です。
- 投稿日:2019-12-18T13:09:35+09:00
ベクトルの内積によるサイコロの出目判定
PONOS Advent Calendar 2019の19日目の記事です。
昨日は@loveRiceさんの「[Unity] DOTweenを使ってみよう」でした。はじめに
今日はUnityでサイコロを作ったお話をさせていただきます。
上の画像が完成形ですが、物理演算で転がり終えたサイコロの出目をテキストで表示しています。とあるゲーム企画でサイコロを出してみたいという要望があり試作したものです。
要件
サイコロの要件は以下の通り。
① 3D空間上にサイコロを出現させたい
② 物理演算でリアルに転がしたい
③ 転がり終わったときの出目を判定したい技術的に可能かどうか聞かれ、もちろん実装できますよ!と即答したものの...
Unityなら①と②はお茶の子さいさいなのですが、さて③の出目判定はどうしたものか...?ここはエンジニアの腕の見せ所!と、ちょっぴり本気出して考えてみました。
運や偶然
いきなりですが、ちょっと脱線します。
サイコロやルーレットなど、運や偶然の要素は楽しさを演出しプレイヤーをワクワクさせてくれます。
ただ、そういったゲームの殆どは、実は表示される前から結果が決まっているもので、
私が知る限り、大半は「予め結果を確定させた上でビジュアルをその結果に合わせる」処理をしています。いかにも今この場でリアルタイムに確定したように見えても、実はそう思わされているだけなのです。
例えば、サイコロをふるボタンを押して3秒後に出目が確定するゲームの場合
1.サイコロをふるボタンが押された
2.乱数により「5」が選ばれる(※この時点で確定)
3.サイコロのアニメーション再生開始(再生開始ポイントは5が出る3秒前に設定)
4.出目が5の状態でサイコロのアニメーションが停止する
5.プレイヤーが結果が5であることを認識するこういう話をすると、そんなのインチキだと暴れ出す人もいるかもしれませんが、これはユーザーの当選権利を守ったり、確率を適正にコントロールするための仕様だったりします。
(少なくとも自分がこれまで関わったゲームについてはインチキしていませんので。^^;)理由はそれぞれとしても、大概のゲームはルーレットが回り始める前に結果を確定させています。
なので、今回やろうとしている出目判定というのは、サイコロが登場するゲームの中でも特殊と言えます。
いくつかの案
さて、どうやってサイコロの出目をリアルタイムに判定するか?に戻しますが、
軽く考えを巡らせてみただけでもアイデアはいくつか出てきます。・6面それぞれにCollider(当たり判定)を配置し、地面と衝突している面の対面を出目とする。
・6面それぞれの面の中心座標を比較し、最も高い位置にある面を出目とする。
・立方体モデルの角の8頂点のうち高さの上位4頂点の組み合わせから出目を求める。
・立方体モデルの向き(回転角度)から出目を求める。
などなど今回は「立方体モデルの向きから出目を求める」方法をチョイスしました。
回転角度から出目を求める
オブジェクトの回転角度ですが、UnityのGameObjectならtransform.rotationで取得できます。
ただ、角度をオイラー角で判定するというのは経験上ろくなことがなかったので(0度や360度を跨いだ時の処理とか、ジンバルロックとか...)、迷わず「ベクトルの内積」を使って判定することにしました。測定したことはないですが、おそらく計算処理も軽いはずです。
ベクトルの内積
ベクトルの内積を使うと以下のことがわかります。
・2つのベクトルの射影
・2つのベクトルが平行かどうかの判定
・2つのベクトルが垂直かどうかの判定
・2つのベクトルの角度差UnityにはVector3.Dot()という便利なものが用意されています。
具合案
最初に考えた案は、サイコロの6面の法線ベクトル(面に垂直なベクトル)の中で、ワールド空間上の上向きベクトル(アップベクトル)に最も近いものを探す、というものでした。
でも面と裏面の法線ベクトルは符号の違いでしかないので、6面分の法線ベクトルでなくても3面分で良いことに途中で気付きました。更に突き詰めると法線も必要なく、transformの3つの回転ベクトル成分で十分ということになりました。
整理すると...
ワールド空間のアップベクトルとサイコロの回転ベクトルXYZを比較して、最もアップベクトルに近いベクトルをみつけ、且つそのベクトルがプラス方向かマイナス方向なのかで出目を判定します。実装してみた
UnityエディタのGizmoでは、Xが赤、Yが緑、Zが青で色付けされています。
まず、基準となるワールド空間の座標は以下のようになっています。
グレーの原点から上方向に伸びる緑色の線をアップベクトル(=Vector3.up)とし、基準にします。サイコロの赤青黄の線のうち、上記ワールド空間のアップベクトルと方向が一致しているのは青い線ですね。青い線はZ方向ベクトル(.foward)、且つ正方向なので、出目は1だと判定できます。
スクリプトは以下の通り。
(Unity c# MonoBehaviour) // 出目チェック int GetNumber (Transform diceTransform) { int result = 0; float innerProductX = Vector3.Dot (diceTransform.right, Vector3.up); float innerProductY = Vector3.Dot (diceTransform.up, Vector3.up); float innerProductZ = Vector3.Dot (diceTransform.forward, Vector3.up); if ((Mathf.Abs (innerProductX) > Mathf.Abs (innerProductY)) && (Mathf.Abs (innerProductX) > Mathf.Abs (innerProductZ))) { // X軸が一番近い if (innerProductX > 0f) { result = 4; } else { result = 3; } } else if ((Mathf.Abs (innerProductY) > Mathf.Abs (innerProductX)) && (Mathf.Abs (innerProductY) > Mathf.Abs (innerProductZ))) { // Y軸が一番近い if (innerProductY > 0f) { result = 5; } else { result = 2; } } else { // Z軸が一番近い if (innerProductZ > 0f) { result = 1; } else { result = 6; } } return result; }少しだけ解説すると、以下がベクトルの内積を求めている部分であり、サイコロのX方向ベクトル(.right)とワールド空間のアップベクトル(.up)の内積、つまり角度差を求めています。
float innerProductX = Vector3.Dot (diceTransform.right, Vector3.up);XYZのベクトルのうち最もワールド空間のアップベクトルと角度差が小さいものを判定し、更に正方向か否か場合分けして出てきた答えをresultとして返します。
実際動かしてみると思った通りの判定結果を得ることができました!
補足
サイコロの目の配置は世界標準で決まっているそうです。
裏表の目を足して7になるというルールは有名かと思いますが、それ以外の配置も決まっています。
「天一地六東五西二南三北四」
1は天の方向、6は地の方向、4は北の方向...という意味で、その配置が世界基準です。しかしながらあまり知られていないので、そもそもサイコロ3Dモデルがそのルールに法って作られているか確認も必要です。上記サンプルのサイコロ3DモデルはZ方向が「1」となっていたので、スクリプトもそれに合わせています。
おわりに
今回の判定方法はあくまでもひとつの手段に過ぎません。
もっとスマートな方法もあるでしょうし、どの方法が最適かは要件によっても変わってくるでしょう。結果ではなく、自分が試行錯誤した過程をみていただければと思い記事にしました。
そもそもサイコロ作らなきゃいけない人ってそんなに居ないでしょうしw明日は@e73ryoさんですー!
- 投稿日:2019-12-18T12:50:11+09:00
Unity臭さを消す方法10連発
Unity臭さとは
これ
非常にいい感じの画面ですね。こういう雰囲気の画面を見たとき、一目で「あーUnityだなー」と感じるような体験をしたことがある方は多いのではないでしょうか。今回はこの画面をどうにかしていきます。
数が多いうえに基本的なテクニックが多いので、この記事ではそれぞれの手段についてそこまで詳細に説明してません。キーワードをもとに適宜ググればいくらでも記事がヒットしますので、
そっちに丸投げすることにします。あらかじめことわっておきますが、別に「Unity臭い作品は悪」というわけではなく、私が「いい作品なのにUnity臭さが抜ければなー」と思う機会が多いので、ひと手間、手軽にクオリティアップができる手段として「Unity臭さを消す」方法をまとめておきたいな、という意図で書いています。ですのでプロトタイプやゲームの手触りを確かめるような目的のもとでは、今回上げるような方法は必要ないかもしれません。
あとこの記事に逆行しまくれば、Unity臭さを逆手に取ったチープな雰囲気が演出できるのでそれもアリだと思います。1. ライティング編
1.1 SkyBoxを変える
Default-Skyboxも味わいがあって僕は好きですが、実はこいつが一番の臭いのもとです。
Asset Storeで探してきたやつにしてみます。これだけでかなりマシになりましたね。
1.2 マテリアルを変える
テクスチャのないプレーンなマテリアルは、わりとどうあがいても臭いを発してしまいがちです。
これもAssetStoreで適当に選んでみましょう。ノーマルマップがついているやつがベターです。テクスチャのないマテリアルでも、設定次第である程度いい感じにすることはできます。公式のガイドがあるので、こちらも参考にしてみましょう。
1.3 影を焼く
いい感じにUnity臭さがなくなってきましたが、テクスチャを貼ったことにより、なんかむしろ一昔前のプリレンダCGみたいになってしまいました。モデルをStaticに設定して、Lighting ウィンドウから影をベイクしてみましょう。
間接光と、Emissiveに設定したマテリアルがいい味を出してます。
2. UI編
つぎにUIです。
ライティングはそこそこいい感じになってきましたが、UIを乗せるとこれまたくさいですね。2.1 フォントを変える
さすがにデフォルトのArialフォントは見る人が見ればわかってしまいますし、WebGL環境で日本語が表示されない問題もあります。他環境でも日本語のフォールバックがわりと怪しい感じになってしまうので、変更は必至です。
適当なフリーフォントを探してみましょう。ゲーム内に埋め込んで使用する場合のライセンスに注意です。個人的にはM+をよく使います。
2.2 UI画像を変える
ボタンの画像もデフォルトから変えてあげます。オススメは、角丸のフラットな四角形スプライトを用意することです。これひとつでボタンにも、ウィンドウにも使いまわせます。
Sprite EditorでSliceを設定してあげます。
最後にレイアウトをちょっと整えましょう。背景にそのまま文字をかぶせると読みにくいので、ウィンドウなどを適宜追加して……
良い感じになってきました。
3. 演出編
演出です。演出はごり押しで割とどうにかなる(最低)ので、もりもりやっていきましょう。
3.1 ポストプロセスをつける
画面全体にポストエフェクトをかけていい感じにしましょう。
これもかなり強力な一手です。Package ManagerからPost Processing Stack v2をダウンロードして、セットアップします。【Unity】Post Processing Stack Version 2.x を使用する - Qiita
PostProcessing Stack v2を使う - tanaka's Programming Memo必須なのはBloomとAmbient Occlusionです。Bloomは光ってるところをさらにいい感じにしてくれます。Ambient Occlusionはライティングにメリハリを与えてくれます。
それでもまだ物足りなければ、VignetteとChromatic Aberration あたりを加えて、よりリッチな感じにしてみましょう。遠近感を効果的に使っている画面では、Depth of Fieldも有効です。
3.2 デフォルトのパーティクルをどうにかする
これもデカいです。
意識すべきポイントとしては、
- サイズを小さくする
- 数を増やす
- 消えるときにフェード又は縮小して消えるようにする
- いろいろランダムにして動きに幅を持たせる
以上に加えて、NoiseとColor over Lifetimeを設定してみました。
3.3 シーンロードで暗転する
これは地味ですが、「タイトルシーンでボタンを押したら少しのフリーズの後シーン遷移して
プレイヤーキャラが空から降ってくるやつ」も、わりとUnity臭さがあります。シーンロードの度に一度画面を真っ暗にフェードして、ロード後に戻してあげるのがいいです。
4. ビルド編
最後、細かいポイントを仕上げていきます。
4.1 ビルド後のアイコンを変える
これだけ頑張っても、ビルド後のアプリのアイコンがUnityだと残念感があります(どうせPersonalライセンスではスプラッシュスクリーンが出るのでアレですが)。Player Settingsを開いて自作のアイコンに変更しましょう。
4.2 Display Resolution Dialogを消す
Standaloneビルドで起動時に出るあのダイアログです。将来バージョンでは消えるようですが、これもUnity臭さの一因なので、解像度設定、キーコンフィグなどをゲーム内実装して、消したいところです。
お疲れ様でした
どうでしょうか?今回紹介したテクニックは、定石すぎてむしろUnity臭いような感じもしますが、やらないよりもずっとよいはずです。つくったゲームの画面がなんかチープだな?と思ったときは、是非この記事を参照していただければと思います。
- 投稿日:2019-12-18T10:39:27+09:00
「東方Project×能」がテーマのホラーゲームを作っている話
この記事は個人開発 Advent Calendar 2019の23日目の記事です。
はじめに
こんにちは。T.Dと申します。
5年ほど前からUnityを使って東方Projectの二次創作ゲームを個人で制作しています。
今までは以下のゲームを作りました。(リンクは全てAndroid)個人開発がテーマとの事でこの3作の話をしても良かったのですが、今回は現在開発中である
「東方Project×能」がテーマのホラーゲーム(名称未設定)
のお話をして行きます。
技術的な話よりも、ゲーム内容や実装したシステムの話が多いです。ゲームについて
ゲームの動作を見てくれ
最初にゲームの動画を見て頂いた方がこの後の話も理解しやすくなると思います。
Twitterに上げた動画からピックアップします。最近のホラゲーの進捗
— T.D (@TD12734) December 3, 2019
遂に「道具」の実装を開始し、まずは「札」カテゴリのアイテムを実装した
札は貼った場所を照らし、探索の道標となる
「札」は特別な効果は無いが「光霊の札」は貼った札を剥して再利用可能
警鐘と誘蛾の効果はまだ秘密 pic.twitter.com/CW1ARWCNwIどんなゲームなんだよ
コンセプトとテーマはこんな感じ。
- テーマ:「東方×能」
- ジャンル:「ホラー×不思議のダンジョン」
- 暗闇に包まれた迷宮を道具を駆使して探索し、突破せよ。
- 迷宮には面を被った幻想少女が徘徊している。立ち向かおうなど決して思わず逃げ続けよ。
- 面はプレイヤーにも力を与える。上手に使え。
- 安心なんて何処にもない。恐怖は絶える間も無く有り続ける。
有り体に言えば
探索型ホラーゲーム
です。
2Dのダンジョンを敵から逃げながら探索し、出口を目指したり一定時間生き残り続けます。もしかして普通のホラゲー?
とんでもない!
今作は以下の2つの要素によりオリジナリティ、ユニークさ、絶妙な難易度を作り出します。1:探索マップが不思議のダンジョン
風来のシレンやトルネコの大冒険みたいにマップが完全ランダム生成です。
探索ホラゲーは溢れていますが、不思議のダンジョンあるいはランダムマップとなるとその数は極端に減少します。
攻略する度に地形、道具、敵の配置が変化するのでホラゲー特有の「覚えれば怖くないし楽勝」現象は絶対に起きません。
攻略難易度は高まりますが、1000回遊べるホラゲーになるでしょう。(願望)2:東方と能楽の融合
東方には神子やこころちゃん、隠岐奈みたいに能楽が大きく絡む子がちらほら居ますが、能に重点をおいたゲームは東方二次創作ゲーム多しと言えど現状皆無です。
そこで、東方も能も大好きな私がこの2つを融合させたゲームを作らなければと言う使命感に駆られ、制作を行うことにしました。
融合と言う以上、ゲームには東方要素と能要素がそれぞれ存在します。
東方要素
- 登場人物は東方の幻想少女
- 一部BGMは東方原曲のアレンジ
- 敵として登場する幻想少女の特徴は、原作での能力を再現した物になっている
能楽要素
- 敵は能面を被っており、プレイヤーも能面を被るとパワーアップする
シテ
、ワキ
、離見の見
など能由来のシステムが存在- 雰囲気が静かで幽玄
できたもの
ダンジョン生成
地形生成のアルゴリズムの根幹はできました。
ざっくり言うとこんな感じにダンジョンを生成します。ダンジョンマップを複数個のセクター(区画)に分ける。
セクターには固定マップ部分とランダムマップ部分があり、ランダムマップ部分では壁、通路、部屋を自動で作成する。ランダムマップ生成はややこしいので割愛。
現段階では以下のようなダンジョンが自動生成されます。
通路や襖を自動生成するプログラムと合わせると上の画像のようなダンジョンが出来上がります。
現在のセクターは中央大部屋、中央9部屋、バラバラ9部屋(テストマップ)の3パターンのみですが最終的には20~30パターンは実装する予定です。壁や床はSpriteRendererで表示し、衝突判定はBoxCollider2Dで実装しています。
今後Navigation 2Dを使う事を考慮してTilemapに置き換えたいですね。光源
光源は
LWRP
のLight2DのPoint Lightを使ってます。
LWRPは登場してから日が浅いですが、光の色や明るさや境界のぼんやり具合を楽に調整できます。
using UnityEngine.Experimental.Rendering.LWRP;
することでスクリプト上からも使用可能になります。
現時点で以下の4つの光源を実装出来ました。1:プレイヤーの光源
ホラーでおなじみの懐中電灯…ではなく龕灯
(がんどう)という、中に蝋燭を入れて前方を照らす江戸時代の道具です。
基本的にこいつで前方を明るくして探索を進めます。
しかし蝋燭の炎は有限です。
左上の炎の画像が上から下に無くなっていき、全て無くなった時は蝋燭が燃え尽きてしまいます。2:ダンジョンにある燭台などのオブジェクトの光源
ホラーには欠かせない蝋燭の光源です。
ぼんやり赤くして、炎っぽさと周囲の暗さを強調しています。3:周囲を明るくするアイテム「札」の光源
目印として使える札の光源です。
貼られている札の向きを貼った時のプレイヤーの向きと合わせるなど、細かいところを凝っています。4:暗順応
全ての和蝋燭を使い果たし、札も持っていない場合は視覚的に詰むので暗順応
システムを実装しました。
龕灯の火を消してから10秒ほど経過するとプレイヤーから薄暗い光が発生し、その後数分かけて光の範囲が拡大します。
しかし最初期はほぼ目の前だけ、最大範囲でもあまり遠くは見えないのであくまで救済処置です。移動
ホラゲーの移動の仕様
— T.D (@TD12734) December 16, 2019
プレイヤーの以下の移動状態を持っている
・通常移動(歩く)
・高速移動(走る)
・低速移動(忍び足)
場所や状態に応じて足音が変わる
高速移動はスタミナを消費して走る
速いが音が鳴り、聞いた敵はその場所まで確認しに来る
低速移動時は音が鳴らず(将来的に)照準が表示される pic.twitter.com/uvJ6eic04N移動の様子は上のツイートの動画を見てください。
プレイヤーの移動には3種類あり、それぞれの特徴は以下の通りです。
- 通常移動
- 普通の移動。原則この状態で移動する。
- 足音が鳴るが一部を除き敵に気付かれることは無い。
- 高速移動
- スタミナを消費する速い移動。敵に追われている時はスタミナ消費が倍になる。
- 音が鳴り、聞いた敵はその場所まで確認しに来る。
- 低速移動
- 遅い移動。特殊なケースで使用する。
- 全く足音を出さず、低速移動時はアイテム使用の手助けとなる照準(未実装)が表示される。
移動状態や移動場所によって足音も変わるよう設定してます。
細かい音の変化ですが凝りました。メニュー
メニューは徹底的に黒と白を基調としたシンプルなUIとしています。
こうする理由は下手に派手にしてホラー要素を損ないたくないから、黒と白が織りなすデザインの美しさをプレイヤーの皆さんに知って頂きたいからです。アイテムメニュー
現在所持している道具の一覧を表示します。
アイテム説明文は効果や使い方というより、雰囲気を出すためにアイテム設定を書いています。アイテム効果は使って理解しろ
この画面ではアイテムを置くなど、「使う」以外の補助的な動作を行います。敵一覧
敵一覧メニューは敵に接近したり特定のアイテムを使う事で埋まります。
敵には原則4種類存在し、それぞれ以下の特徴を持っています。
- 為手(シテ):マップに1、2体存在する主役級の敵。足止めは可能だが倒す事が出来ない。
- 脇(ワキ):マップの補助的な敵。人数制限は特に無く、道具などを使えば倒せる。
- 連(ツレ):一部の為手に付随する敵。道具などを使えば倒せるが一定時間後に復活する。
- 脇連(ワキヅレ):一部の脇に付随する敵。道具などを使えば倒せる。
日本の伝統芸能である能楽に明るい人なら察しがつきました通り、敵分類は能楽の役者の分類に大変近い物となっています。
為手と脇が東方キャラクターで、連と脇連が一部のキャラクターに付随するキャラクターです。(アリスにおける上海人形、お燐におけるゾンビフェアリーなど)
上の画像の連と脇連がダミーテキストなのはみんなには内緒だよちなみにフランちゃんの名前の伸ばし棒は
ー
ではなく丨
を使っています。
この縦棒は引
の右側だそうです。終わりに
今年の9月ごろから制作を開始しましたが、現時点で結構形になって来たと思います。
しかし未実装の部分の方が多いのも事実なので今後もこつこつと実装を続けて行きたいです。
来年の上旬にはテスト版を出したいですね。(おまけ)実装予定のゲームシステムと仕様
最終的に以下の機能を全て実装したいと思ってます。
- 迷宮が不思議のダンジョンとなっており、毎回形状が異なる。
- 迷宮には敵が複数体存在し、シテとワキに分類される。
- 敵の強さはEasy,Normal,Hard,Lunaticの4種類が存在する。
- Easyは脇限定、Lunaticは為手限定。
- 英語だと雰囲気に合わないので能に合わせて生成、般若、蛇、真蛇に変えるかもしれない。
- クエスト(仮称)によってはアイテムが完全未識別。
- どんな道具も使わないと分からないし、使ったところで名前が識別される訳でもない。
- 道具の入手時に「やった〜強アイテムだ〜」などと言う安心感など与えさせない。
- 投稿日:2019-12-18T08:16:58+09:00
[Unity] モバイルでも動く!風に揺れる布シェーダー(解析的な法線導出)
これは【unityプロ技②】 Advent Calendar 2019の18日目の記事です。
こんな感じの「風に揺れる布シェーダー」をUnityで実装したので、簡単に解説します。
頂点シェーダーで揺らめく布を実装しました。
— がむ (@gam0022) October 30, 2019
元のMeshは適度に分割したGridで、縦方向に折りたたむことも可能です。
解析的に法線を導出しているので、モバイルでも余裕で動作するくらい負荷が軽量です。#Unity3D #CreativeCoding #Shader #HLSL pic.twitter.com/79XgCc44q6軽さの秘訣
このシェーダー、なんとモバイルでも余裕で動作するくらい軽量です!
その軽さの秘訣は、次の3点です!
- 布の動きを頂点シェーダーで計算
- スキニングの計算やクロスの物理シミュレーションが不要
- 解析的に法線を導出
- 数値解を求めるよりも計算量を削減
- 計算をなるべく頂点シェーダーで行う
- (Meshによるが)ピクセル数よりも頂点数の方が少ない
- 頂点シェーダーで動きや法線を計算することで、GPUの計算量を削減
環境
- 2018.4.13f1 (LTS)
- Build-in Rendering Pipeline
GPU負荷の実機計測
iPhone8 と Xcode11.3 でGPU負荷を計測したところ、布シェーダーの実行時間は 0.21 ms でした。
これはUnity標準のSkyboxの半分以下のGPU負荷です。
また、iPhoneは前のフレームのGPU負荷によってGPU性能が可変のようなので、最大のGPU性能を発揮したときは、さらに実行時間が減ると思われます。
シェーダ全文
シェーダー全文とUnityプロジェクトはGitHubに公開しています。
GitHubおよび本記事に登場するソースコードはMIT Licenseです。
シェーダー解説
実装を踏まえながら、シェーダーを解説していきます。
Meshの準備
1x1の大きさのGridをつくります。分割数は 40x40 くらいが丁度いいです。UVも必要です。
Houdiniの場合は、GridノードとUV Flattenノードで作れます。
頂点シェーダーの解説
まず、頂点シェーダーから実装を解説していきます。
頂点シェーダーv2f vert(appdata v) { v2f o; // UVの斜め方向のパラメータを t と定義します float t = v.uv.x + v.uv.y; // 周波数とスクロール速度から t1 を決定します float t1 = _WaveFreq1 * t + _WaveSpeed1 * _TIME; // 波の高さ wave1 を計算します float wave1 = _WaveAmplitude1 * sin(t1); // wave1 を t1 で偏微分した dWave1 を計算します float dWave1 = _WaveFreq1 * _WaveAmplitude1 * cos(t1); // wave1 と同様にして wave2 を計算します float t2 = _WaveFreq2 * t + _WaveSpeed2 * _TIME; float wave2 = _WaveAmplitude2 * sin(t2); float dWave2 = _WaveFreq2 * _WaveAmplitude2 * cos(t2); // 上部を固定するための値を計算します float fixTopScale = (1.0f - v.uv.y); // 2つの波を合成して、頂点座標に反映します float wave = fixTopScale * (wave1 + wave2); v.vertex += wave; // 波(位置)を偏微分した勾配から、法線を計算します float dWave = fixTopScale * (dWave1 + dWave2); float3 objNormal = normalize(float3(dWave, dWave, -1.0f)); o.normal = mul((float3x3)unity_ObjectToWorld, objNormal); o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; }波の動きの計算とMeshの変形
まず最初に波の動きを計算します。
今回は単純に2つの sin 波(wave1 と wave2)の重ね合わせて波を作り出しました。
// 1つ目の波 float t1 = _WaveFreq1 * t + _WaveSpeed1 * _TIME; float wave1 = _WaveAmplitude1 * sin(t1); // 2つ目の波 float t2 = _WaveFreq2 * t + _WaveSpeed2 * _TIME; float wave2 = _WaveAmplitude2 * sin(t2); // 2つの波を合成して、頂点座標に反映します float wave = fixTopScale * (wave1 + wave2); v.vertex += wave;波が1つだけだと動きが非常に単調になってしまうので、振幅と周波数が違う複数の波を重ねることで、より自然な波の動きにしています。
これは 非整数ブラウン運動やfBm と呼ばれる有名なシェーダーのテクニックです。
また、波の振幅と周波数はプロパティ化して、インスペクタで微調整できるようにすると便利です。最後に、波の高さを頂点座標に加算することで、波の動きに合わせてMeshを変形します。
解析的な法線の導出
いよいよ 最重要ポイントである解析的な法線の導出 の解説です。
一言で説明すると、陰関数を偏微分した勾配から法線を計算しています。
波の関数は z 方向の高さマップなので、次の式で表されますが、
z = f(x, y)変形によって陰関数となります。
g(x, y, z) = f(x, y) - z = 0xy平面の斜め方向を t と定義します。
こうすることで、xとyの偏微分の結果が同じになるので、計算量を少しだけ減らせます。t = x + y\\ g(t, z) = f(t) - z = 0// UVの斜め方向のパラメータを t と定義します float t = v.uv.x + v.uv.y;また、t に周波数と時間によるスピードの影響を加えて、
t1 = _WaveFreq1 * t + _WaveSpeed1 * _TIME
と定義します。// 周波数とスクロール速度から t1 を決定します float t1 = _WaveFreq1 * t + _WaveSpeed1 * _TIME;ここで、
a = _WaveFreq1
b = _WaveAmplitude1
c = _WaveSpeed1 * _TIME
- tに依存しない定数とみなせるので、まとめて変数にします
と変数をおくと、
wave1 = _WaveAmplitude1 * sin(_WaveFreq1 * t + _WaveSpeed1 * _TIME)
は
wave1 = b * sin(a * t + c)
となるので、wave1 を t で偏微分します。
\frac{\partial}{\partial t} g(t,z ) = \frac{\partial}{\partial t} (b \sin(a t + c) - z) = \frac{\partial}{\partial t} b \sin(a t + c) = a b \cos(a t + c)以上により、 wave1 を偏微分した dWave1 の解析解が求まり、HLSLで実装すると以下のようになります。
位置を偏微分// 波の高さ wave1 を計算します float wave1 = _WaveAmplitude1 * sin(t1); // wave1 を t1 で偏微分した dWave1 を計算します float dWave1 = _WaveFreq1 * _WaveAmplitude1 * cos(t1);dWave2 も同様に計算ができて、wave1 と wave2 はそれぞれ独立しているので、それぞれ微分してから足し合わせても同じ結果になります。
また、g を z で偏微分をすると -1 の定数になります。
\frac{\partial}{\partial z} g(t, z) = \frac{\partial}{\partial z} f(t) - z = -1以上により、法線 $n$(勾配)を計算するための、波の関数の偏微分が求まりました。
n = (\frac{\partial}{\partial t} g(t, z), \frac{\partial}{\partial t} g(t, z), \frac{\partial}{\partial z} g(t, z)) = (a b \cos(a t), a b \cos(a t), -1)HLSLにすると、こうなります。
// 波(位置)を偏微分した勾配から、法線を計算します float dWave = fixTopScale * (dWave1 + dWave2); float3 objNormal = normalize(float3(dWave, dWave, -1.0f)); o.normal = mul((float3x3)unity_ObjectToWorld, objNormal);フラグメントシェーダーの解説
フラグメントシェーダーでは、頂点シェーダーで求めた法線とDirectionalLightからライティング計算をして、最終的なピクセルの値を決定します。
フラグメントシェーダーhalf4 frag(v2f i) : SV_Target { half4 col = tex2D(_MainTex, i.uv); col *= _TintColor * _LightColor0; // DirectionalLight によってライティングします half diffuse = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz)); // 影の強さを _ShadowIntensity で調整します // _ShadowIntensity = 0.5 で Half-Lambert と同じ効果が得られます half halfLambert = lerp(1.0, diffuse, _ShadowIntensity); col.rgb *= halfLambert; return col; }カスタムシェーダーからDirectionalLightを利用
DirectionalLightのパラメータは次のから取得できます。
- ワールド空間の方向:
_WorldSpaceLightPos0.xyz
から- ライトのカラー:
_LightColor0
これらのパラメータをシェーダーから参照するためには、Tagsに
"LightMode" = "ForwardBase"
を指定する必要がありました。Tagsに"LightMode"を指定Tags{ "Queue" = "Geometry" "RenderType" = "Opaque" + "LightMode" = "ForwardBase" "IgnoreProjector" = "True" }
少し一般化した Half-Lambert
ライティングには少し一般化した Half-Lambertを用いました。
まずは法線とライトの内積から完全拡散反射を計算します。
half diffuse = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz));このままだと、陰影がハッキリしすぎて不自然なので、
_ShadowIntensity
というパラメータで陰影の強さを調整できるようにしました。
_ShadowIntensity = 0.5
で Half-Lambert の計算式と同値になります。half halfLaumber = lerp(1.0, diffuse, _ShadowIntensity);簡易なライティングですが、計算量と品質のバランスは取れており、モバイルなら必要十分だと思います。
さらに軽量化が必要な場合は、このライティング処理を頂点シェーダーで行うという方法もありますが、シェーダーの見通しの良さを重視して、今回はピクセルシェーダーで実装しました。
おわりに
いかがでしたでしょうか?
「短いシェーダーと簡単な数式だけでも、ちゃんと揺れる布を実装できるんだ!」
というのが伝われば幸いです。レイマーチングでも、 陰関数を偏微分した勾配から法線を導出 するテクニックが有名ですが、それと同じ理論で法線を導出しています。
レイマーチングでは、距離関数を数値的に偏微分する必要があるため、距離関数を複数回(4~6回)評価する必要がありますが、
今回は波の式を単純化することによって、解析的に偏微分を行って計算量を減らしました。数学の知識を応用すると、シェーダーをシンプルかつ軽量に実装ができます!
数学をつかって複雑な課題をシンプルに解決できたときの快感が、私は好きです。
関連リンク
- 投稿日:2019-12-18T07:11:42+09:00
数値を数字として表示するシェーダーを作ったおはなし
この記事はシェーダーアドベントカレンダー18日目の記事として書かれています。
https://qiita.com/advent-calendar/2019/shader-advent-calender-2019はじめに
シェーダーの数値って色として見ることは多いのだけど、普通(?)のプログラムのように数字として見ることは少ないと思います。
VR空間でVJをするときに、パラメータの値を数字として目で見たかったので数値を数字として表示するシェーダーを作りました。
今回はその数字シェーダーを何を考えながらどういう風に作っていったか書きたいと思います。完成したサンプル。機能として桁数指定、マイナス表示など。
パラメータの数値を数字として表示するシェーダーを書いた~。小数点以下の深いところの動きは怪しいけど。シェーダーではじめてまともにfor文使ったかもしれない。#Unity #Shader pic.twitter.com/Sw9PncoCzU
— noriben? (@noriben327) October 23, 2019-100から100まで表示してみた動画。
数字シェーダー完成図(アドベントカレンダー記事用)
— noriben? (@noriben327) December 17, 2019
-100から100まで表示してみる。あと桁数を増やしてみたり。#Unity #Shader pic.twitter.com/MjZa0UUsaP完成したコードとテクスチャです。
https://github.com/noriben327/DisplayNumber/blob/master/DisplayNumber.shader既に数字シェーダーを作っている方々
同じようなことを思っている方はたくさんいて、シェーダーを配布している方もいます。
BUTADIENE WORKS
ブタジエンさん
https://twitter.com/butadiene121/status/1063451198194413568シェーダ内の任意の数値を任意の桁数を指定して表示するシェーダ(というか関数とテクスチャの組み合わせ)できた~ pic.twitter.com/IvxuqsFs4Z
— ブタジエン (@butadiene121) October 27, 2018Position Shader
オノッチさん
https://onotchi.booth.pm/items/996827座標シェーダー進捗
— オノッチ (@onotchi_) August 29, 2018
・背景画像を任意で設定可能に
・全体の透過率を設定可能に
もちろん数字テクスチャも好きなのが使えます。
基本機能はこれで十分かな。特に問題が見つからなかったら、今晩でもリリースします。 #VRChat pic.twitter.com/B66tj1KZTM少しコードを読んでみたものの自分にはわからなかったので、1から考えながら作ることにしました。
基本の考え
数字を書いたテクスチャを用意して、例えば数値が1ならテクスチャの1が書いてある部分にスクロールして表示する。これだけです。
今回作った数字のテクスチャはこれです。
Y座標を10分割して数字を書いているので、uv
がfloat2(0, 0.1)
で1
、float2(0, 0.7)
で7
のテクスチャにそのままスクロールできます。
説明用に2桁で、隠している部分も表示してみたもの。数値にあわせてUVスクロールしてるのがわかるんじゃないかな~と思います。0.0から2.0を表示させています。
数字シェーダー説明図(アドベントカレンダー記事用)
— noriben? (@noriben327) December 17, 2019
2桁だけで表示。説明用に隠していた部分も表示。数値にあわせてUVスクロールしてる。#Unity #Shader pic.twitter.com/kJRdqSdaFp数値の抽出
数値そのままではどうにも扱えないので、1桁ずつ切り取って0~9の値にして、1/10にすることで0.0~1.0のUV値として使える値にする。それを全桁に実行することで最終的に数字として表示する。
今回は整数部と小数部を分けてfor
文にして処理した。整数部の抽出
まず1の位だけやってみる。
数値は仮に123.141592とする。
1/10する。
- 12.3141592
frac
で小数点のみにする。
- 0.3141592
10倍する。
- 3.141592
floor
で小数点以下を切り捨てる。
- 3
これで1の位の数値である3のみを取り出せる。
あとは1/10して0.3とすれば0~1の範囲になるのでそのままUVの値として使えるようになる。実際のコード。
for(j = 1; j < _IntDigits + 1; j++) { multi = pow(10, j); val = numVal; com = val * 1 / multi; val = frac(com) * 10; val = floor(val) * 0.1; uv = i.uv; uv.y += val; //数字移動 uv.x += -0.04 + 0.05 * j; //桁移動 numCol = numCol + tex2D(_NumTex, uv); }小数部の抽出
次に小数点第1位だけ取り出してみる。
数値は仮に123.141592とする。
frac
で小数点のみにする
- 0.141592
10倍する
- 1.141592
floor
で小数点以下を切り捨てる
- 1
これで小数点第1位の1という数字を取り出せる。
1/10すれば0.1とそのままUVの値として使える。実際のコード。
おまじないとして元の値に0.00001
を足しているが、これは例えば0.3
が0.299999
と表示されてしまうときがある場合のとりあえずの対策である(本来は必要ない)
for(j = 0; j < _DecimalDigits; j++) { multi = pow(10, j); val = numVal + 0.00001;//おまじない com = val * multi; val = frac(com) * 10; val = floor(val) * 0.1; uv = i.uv; uv.y += val; //数字移動 uv.x -= 0.06 + 0.05 * j; //桁移動 numCol = numCol + tex2D(_NumTex, uv); }負号
if
でマイナス以外のときは負号を消すようにした。
桁数の増加にあわせて常に先頭に表示するようUV.x値を移動している。(動画参照)
(_IntDigits(整数部の桁数)の1つ上の桁に負号を表示)float2 muv = i.uv; muv.x += -0.005 + 0.05 * (_IntDigits + 1);見た目を整えて完成
カンマを描画したり、見せたくない部分をマスクするなど。
あとがき
プログラムの知識も経験も少ないのですが、それでもシェーダーを書くのは楽しいです。このコードも楽しく書けたのでせっかくなので解説の記事を書こうかな~と思ってアドベントカレンダーに参加しました。
決め打ちの数字が多かったり、数字の表示部分を関数にして他のコードで使いやすくすべきだよなーと反省点はあるものの、コードの一部の数値を確認したり、オブジェクトの座標を表示してみたり、Unityの時間の流れ(_Time.y)を可視化してみたりと結構実用的なシェーダーとして使っています。
これは可視化されたUnityの時の流れ(_Time.y) pic.twitter.com/lj2lTEWuZM
— noriben? (@noriben327)
October 26, 2019全コード
RenderTextureの値を取得するコードが一部入っていますが、今回は
_Test
の値を表示するようにしています。Shader "Noriben/DisplayNumber" { Properties { _NumTex ("NumTex", 2D) = "white" {} _RenderTex ("RenderTex", 2D) = "white" {} _Index ("Index", float) = 0 _IntDigits("Int Digits", int) = 2 _DecimalDigits("Decimal Digits", int) = 3 _Test("Test", Range(-100,100)) = 0 [Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull", float) = 0 } SubShader { Tags { "RenderType"="TransparentCutout" "Queue" = "AlphaTest" "DisableBatching" = "True"} LOD 100 Pass { Cull [_Cull] CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _NumTex; float4 _NumTex_ST; sampler2D _RenderTex; float _Test; float _Index; int _IntDigits; int _DecimalDigits; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _NumTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { //数値取得 float4 renderTex = tex2D(_RenderTex, float2((_Index + 0.5) * 0.1, 0.5)); //表示位置をセンターにオフセット i.uv.x += -0.37; //カンマの描画 float left = step(0.076, i.uv.x); float right = 1 - step(0.085, i.uv.x); float bottom = step(0.0234, i.uv.y); float top = 1 - step(0.033, i.uv.y); float comma = left * right * top * bottom; //数字表示 //float4 objPos = mul ( unity_ObjectToWorld, float4(0, 0, 0, 1)); //float minusCheck = renderTex.x; float minusCheck = _Test; //float minusCheck = -_Time.y; //表示する数値 float numVal = abs(minusCheck); fixed4 numCol = fixed4(0,0,0,0); float multi, val, com; float2 uv = i.uv; //1未満 int j = 0; for(j = 0; j < _DecimalDigits; j++) { multi = pow(10, j); val = numVal + 0.00001;//おまじない com = val * multi; val = frac(com) * 10; val = floor(val) * 0.1; uv = i.uv; uv.y += val; //数字移動 uv.x -= 0.06 + 0.05 * j; //桁移動 numCol = numCol + tex2D(_NumTex, uv); } //1以上 for(j = 1; j < _IntDigits + 1; j++) { multi = pow(10, j); val = numVal; com = val * 1 / multi; val = frac(com) * 10; val = floor(val) * 0.1; uv = i.uv; uv.y += val; uv.x += -0.04 + 0.05 * j; numCol = numCol + tex2D(_NumTex, uv); } //負号 float2 muv = i.uv; muv.x += -0.005 + 0.05 * (_IntDigits + 1); float mleft = step(0.076, muv.x); float mright = 1 - step(0.1, muv.x); float mbottom = step(0.044, muv.y); float mtop = 1 - step(0.054, muv.y); float mcol = mleft * mright * mbottom * mtop; //0以上のときは負号消す if(minusCheck >= 0) { mcol = 0; } //mix fixed4 col = numCol; col += float4(comma, comma, comma, 1); col += float4(mcol, mcol, mcol, 1); col = clamp(col, 0, 0.8); //明るさちょっとさげる //使わない部分黒塗り top = 1 - step(0.1, i.uv.y); float3 black = float3(top, top, top); col *= float4(black.xyz, 1); //黒部分透明化 clip(col.x - 0.5); return col; } ENDCG } } }
- 投稿日:2019-12-18T07:11:42+09:00
数値を数字として表示するシェーダーを作ったおはなし[Unity]
この記事はシェーダーアドベントカレンダー18日目の記事として書かれています。
https://qiita.com/advent-calendar/2019/shader-advent-calender-2019はじめに
シェーダーの数値って色として見ることは多いのだけど、普通(?)のプログラムのように数字として見ることは少ないと思います。
VR空間でVJをするときに、パラメータの値を数字として目で見たかったので数値を数字として表示するシェーダーを作りました。
今回はその数字シェーダーを何を考えながらどういう風に作っていったか書きたいと思います。完成したサンプル。機能として桁数指定、マイナス表示など。
パラメータの数値を数字として表示するシェーダーを書いた~。小数点以下の深いところの動きは怪しいけど。シェーダーではじめてまともにfor文使ったかもしれない。#Unity #Shader pic.twitter.com/Sw9PncoCzU
— noriben? (@noriben327) October 23, 2019-100から100まで表示してみた動画。
数字シェーダー完成図(アドベントカレンダー記事用)
— noriben? (@noriben327) December 17, 2019
-100から100まで表示してみる。あと桁数を増やしてみたり。#Unity #Shader pic.twitter.com/MjZa0UUsaP完成したコードとテクスチャです。
https://github.com/noriben327/DisplayNumber/blob/master/DisplayNumber.shader既に数字シェーダーを作っている方々
同じようなことを思っている方はたくさんいて、シェーダーを配布している方もいます。
BUTADIENE WORKS
ブタジエンさん
https://twitter.com/butadiene121/status/1063451198194413568シェーダ内の任意の数値を任意の桁数を指定して表示するシェーダ(というか関数とテクスチャの組み合わせ)できた~ pic.twitter.com/IvxuqsFs4Z
— ブタジエン (@butadiene121) October 27, 2018Position Shader
オノッチさん
https://onotchi.booth.pm/items/996827座標シェーダー進捗
— オノッチ (@onotchi_) August 29, 2018
・背景画像を任意で設定可能に
・全体の透過率を設定可能に
もちろん数字テクスチャも好きなのが使えます。
基本機能はこれで十分かな。特に問題が見つからなかったら、今晩でもリリースします。 #VRChat pic.twitter.com/B66tj1KZTM少しコードを読んでみたものの自分にはわからなかったので、1から考えながら作ることにしました。
基本の考え
数字を書いたテクスチャを用意して、例えば数値が1ならテクスチャの1が書いてある部分にスクロールして表示する。これだけです。
今回作った数字のテクスチャはこれです。
Y座標を10分割して数字を書いているので、uv
がfloat2(0, 0.1)
で1
、float2(0, 0.7)
で7
のテクスチャにそのままスクロールできます。
説明用に2桁で、隠している部分も表示してみたもの。数値にあわせてUVスクロールしてるのがわかるんじゃないかな~と思います。0.0から2.0を表示させています。
数字シェーダー説明図(アドベントカレンダー記事用)
— noriben? (@noriben327) December 17, 2019
2桁だけで表示。説明用に隠していた部分も表示。数値にあわせてUVスクロールしてる。#Unity #Shader pic.twitter.com/kJRdqSdaFp数値の抽出
数値そのままではどうにも扱えないので、1桁ずつ切り取って0~9の値にして、1/10にすることで0.0~1.0のUV値として使える値にする。それを全桁に実行することで最終的に数字として表示する。
今回は整数部と小数部を分けてfor
文にして処理した。整数部の抽出
まず1の位だけやってみる。
数値は仮に123.141592とする。
1/10する。
- 12.3141592
frac
で小数点のみにする。
- 0.3141592
10倍する。
- 3.141592
floor
で小数点以下を切り捨てる。
- 3
これで1の位の数値である3のみを取り出せる。
あとは1/10して0.3とすれば0~1の範囲になるのでそのままUVの値として使えるようになる。実際のコード。
for(j = 1; j < _IntDigits + 1; j++) { multi = pow(10, j); val = numVal; com = val * 1 / multi; val = frac(com) * 10; val = floor(val) * 0.1; uv = i.uv; uv.y += val; //数字移動 uv.x += -0.04 + 0.05 * j; //桁移動 numCol = numCol + tex2D(_NumTex, uv); }小数部の抽出
次に小数点第1位だけ取り出してみる。
数値は仮に123.141592とする。
frac
で小数点のみにする
- 0.141592
10倍する
- 1.141592
floor
で小数点以下を切り捨てる
- 1
これで小数点第1位の1という数字を取り出せる。
1/10すれば0.1とそのままUVの値として使える。実際のコード。
おまじないとして元の値に0.00001
を足しているが、これは例えば0.3
が0.299999
と表示されてしまうときがある場合のとりあえずの対策である(本来は必要ない)
for(j = 0; j < _DecimalDigits; j++) { multi = pow(10, j); val = numVal + 0.00001;//おまじない com = val * multi; val = frac(com) * 10; val = floor(val) * 0.1; uv = i.uv; uv.y += val; //数字移動 uv.x -= 0.06 + 0.05 * j; //桁移動 numCol = numCol + tex2D(_NumTex, uv); }負号
if
でマイナス以外のときは負号を消すようにした。
桁数の増加にあわせて常に先頭に表示するようUV.x値を移動している。(動画参照)
(_IntDigits(整数部の桁数)の1つ上の桁に負号を表示)float2 muv = i.uv; muv.x += -0.005 + 0.05 * (_IntDigits + 1);見た目を整えて完成
カンマを描画したり、見せたくない部分をマスクするなど。
あとがき
プログラムの知識も経験も少ないのですが、それでもシェーダーを書くのは楽しいです。このコードも楽しく書けたのでせっかくなので解説の記事を書こうかな~と思ってアドベントカレンダーに参加しました。
決め打ちの数字が多かったり、数字の表示部分を関数にして他のコードで使いやすくすべきだよなーと反省点はあるものの、コードの一部の数値を確認したり、オブジェクトの座標を表示してみたり、Unityの時間の流れ(_Time.y)を可視化してみたりと結構実用的なシェーダーとして使っています。
これは可視化されたUnityの時の流れ(_Time.y) pic.twitter.com/lj2lTEWuZM
— noriben? (@noriben327)
October 26, 2019全コード
RenderTextureの値を取得するコードが一部入っていますが、今回は
_Test
の値を表示するようにしています。Shader "Noriben/DisplayNumber" { Properties { _NumTex ("NumTex", 2D) = "white" {} _RenderTex ("RenderTex", 2D) = "white" {} _Index ("Index", float) = 0 _IntDigits("Int Digits", int) = 2 _DecimalDigits("Decimal Digits", int) = 3 _Test("Test", Range(-100,100)) = 0 [Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull", float) = 0 } SubShader { Tags { "RenderType"="TransparentCutout" "Queue" = "AlphaTest" "DisableBatching" = "True"} LOD 100 Pass { Cull [_Cull] CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _NumTex; float4 _NumTex_ST; sampler2D _RenderTex; float _Test; float _Index; int _IntDigits; int _DecimalDigits; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _NumTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { //数値取得 float4 renderTex = tex2D(_RenderTex, float2((_Index + 0.5) * 0.1, 0.5)); //表示位置をセンターにオフセット i.uv.x += -0.37; //カンマの描画 float left = step(0.076, i.uv.x); float right = 1 - step(0.085, i.uv.x); float bottom = step(0.0234, i.uv.y); float top = 1 - step(0.033, i.uv.y); float comma = left * right * top * bottom; //数字表示 //float4 objPos = mul ( unity_ObjectToWorld, float4(0, 0, 0, 1)); //float minusCheck = renderTex.x; float minusCheck = _Test; //float minusCheck = -_Time.y; //表示する数値 float numVal = abs(minusCheck); fixed4 numCol = fixed4(0,0,0,0); float multi, val, com; float2 uv = i.uv; //1未満 int j = 0; for(j = 0; j < _DecimalDigits; j++) { multi = pow(10, j); val = numVal + 0.00001;//おまじない com = val * multi; val = frac(com) * 10; val = floor(val) * 0.1; uv = i.uv; uv.y += val; //数字移動 uv.x -= 0.06 + 0.05 * j; //桁移動 numCol = numCol + tex2D(_NumTex, uv); } //1以上 for(j = 1; j < _IntDigits + 1; j++) { multi = pow(10, j); val = numVal; com = val * 1 / multi; val = frac(com) * 10; val = floor(val) * 0.1; uv = i.uv; uv.y += val; uv.x += -0.04 + 0.05 * j; numCol = numCol + tex2D(_NumTex, uv); } //負号 float2 muv = i.uv; muv.x += -0.005 + 0.05 * (_IntDigits + 1); float mleft = step(0.076, muv.x); float mright = 1 - step(0.1, muv.x); float mbottom = step(0.044, muv.y); float mtop = 1 - step(0.054, muv.y); float mcol = mleft * mright * mbottom * mtop; //0以上のときは負号消す if(minusCheck >= 0) { mcol = 0; } //mix fixed4 col = numCol; col += float4(comma, comma, comma, 1); col += float4(mcol, mcol, mcol, 1); col = clamp(col, 0, 0.8); //明るさちょっとさげる //使わない部分黒塗り top = 1 - step(0.1, i.uv.y); float3 black = float3(top, top, top); col *= float4(black.xyz, 1); //黒部分透明化 clip(col.x - 0.5); return col; } ENDCG } } }
- 投稿日:2019-12-18T02:40:41+09:00
GitHub ActionsでUnityのライブラリのリリースが捗る話
QualiArtsでUnity用のライブラリ開発をしつつ、その他Unity用の開発環境の整備を行っているUnityエンジニアの @asuuma です。
この記事はQualiArts Advent Calendar 2019の18日目の記事です。TL;DR
- Gitのタグ付けをトリガーに、GitHub ActionsでReleaseを作成
- npm publishで自社のUnity Package Registryにアップロード
- unitypackageも生成してReleaseの成果物に
- unitypackageの構造を解析して、pythonで生成することでUnityレスを実現
GitHub Actionsとは
GitHub上で直接動作するCI/CDをサポートするためのワークフローを自動化するための機能です。
https://github.com/features/actions
2019年の11月にGAになったばかりです。
その特徴としては、以下のようなことが挙げられます。
- インターフェイスがGitHub上に完全に統合されている
- 無料枠が設定されている
- マーケットプレイスを通して、ワークフローを構築するための部品が世界中で開発されている
- 通常はGitHubが用意するVM上で実行されるが、オンプレなどに自分用の実行環境を用意して実行させることができる
Unity開発におけるGitHub Actionsへの期待
Unity開発などのクライアントアプリの開発は、CI/CD環境を自分たちで用意する必要があります。 1
特にUnityを用いたiOS/Android向けのゲームを制作する際は、自前で用意したMacマシンにJenkinsをインストールして、
CI/CD環境を構築していることが多いかと思います。これは以下の事情に依るところが大きいです。
- Unityエディタがインストールされている環境がマネージドサービスに少ないため
- iOSアプリのビルドのためにXcodeがインストールされたMacマシンが必要
- Unityアプリのビルドはハイスペックなマシンでも30-60分ほどかかる
ところがJenkins環境を維持することは簡単なことではありません。
マシンのメンテナンスなどUnityエンジニアの本来の仕事とは遠いインフラ的な業務が中心になるからです。
またJenkinsの設定や、プラグインの管理なども求められてきます。
多くの場合仮想化されていない環境での構築になるため、一度壊れると復旧するのに大変な時間を要し、その間のプロジェクトの開発は止まってしまいます。
そんな環境に身を委ねていては夜も眠れなくなってしまいます。そこで登場したのがGitHub Actionsです。
個人的には以前から脱Jenkinsを目指していたので、GitHub標準のCI/CDはまさに待望のサービスでした。Unityのライブラリ開発をGitHub Actionsで自動化
QualiArtsでのライブラリ開発について
QualiArtsではライブラリ開発が盛んに行われています。
小規模な物を含めるとざっと20ぐらいはありそうです。
そこでいつも問題となるのが、ライブラリの配布方法です。主に
- git submoduleでの取り込み
- unitypackageでの配布
- 自社Unity Package Registry経由での配布
のパターンがあります。どれもメリデメがあるのでケースバイケースで使い分けています。
ライブラリのリリースフロー
主にGitHubのRelease機能を通してリリースします。
git-flowなどの開発を通して開発していき、リリースするタイミングになったらgitのタグを打ってプッシュします。
そしてGitHubのReleaseを作成し、changelogとライブラリの成果物と共にリリースします。
また最近はUnity2019以降で開発しているプロジェクト用にUnity Package Registryも社内に立て、そこにもnpm publishで成果物をアップロードします。
リリース先が2箇所あるのと、リリースのたびにchangelogなどを整形して、成果物をビルドして、リリースするのが地味に大変でした。
絶対的な工数はそこまでかからないものの、細かい作業も多くリリースする頻度が多くなると時間を奪われ、開発モチベーションも低下してしまいます。
なのでGitHub Actionsでなるべく自動化を頑張ってみました。リリース作業の自動化
早速GtiHub Actuonsで上記のリリースフローを自動化していきます。
タグが打たれたらGitHub Releaseを作る
これはGitHubの基本的な機能を利用することで簡単に実現できます。
main.ymlname: Release on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} draft: false prerelease: false - name: Upload Release Asset uses: actions/upload-release-asset@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: your.unitypackage asset_name: your.unitypackage asset_content_type: application/gzipここではいったんyour.unitypackageがあるものとしてて成果物としてアップロードしています。
次のステップで、unitypackageを作ります。Unityエディタレスでunitypackageを生成する
通常unitypackageを作る場合、Unityエディタが必要になります。
ただしUnityエディタを利用するためには、ライセンス認証が必要だったりととても手間がかかるのと、
そもそもUnityエディタをCI/CD環境に用意しないといけません。
そこで何とかUnityエディタ無しでunitypackageを生成する方法を模索してみます。unitypackageの構造
unitypackage自体はtar.gz形式です。
実際に解凍してみるとわかりますが、中のファイル構造は比較的シンプルです。
ある1つのアセットをunitypackageにすると、以下のような形で圧縮されます。-rw-r--r-- 1 a13440 CATK\Domain Users 284 4 23 2018 ./e3cd09c8238fd4842b0c87ef7c1ee257/asset.meta -rw-r--r-- 1 a13440 CATK\Domain Users 4220 11 16 2018 ./e3cd09c8238fd4842b0c87ef7c1ee257/asset -rw-r--r-- 1 a13440 CATK\Domain Users 59 12 12 16:09 ./e3cd09c8238fd4842b0c87ef7c1ee257/pathnameフォルダ名はアセットのguidそのものです。
assetはアセット本体、asset.metaはそのmetaファイル、pathnameはAssets/で始まる相対パスがテキストで書かれたファイルです。
これがアセット数分繰り返されるだけなので、これを再現してあげればunitypackageとして認識されます。unitypackage生成するためのpythonスクリプト
create_unitypackage.py#!/usr/bin/env python3 import sys import os import io import argparse import os.path import tarfile import yaml import glob parser = argparse.ArgumentParser(description='Create unitypackage without Unity') parser.add_argument('-r', '--recursive', action='store_true') parser.add_argument('targets', nargs='*', help='Target directory or file to pack') parser.add_argument('-o', '--output', required=True, help='Output unitypackage path') args = parser.parse_args() print('Targets:', args.targets) print('Output unitypackage:', args.output) print('Is recursive', args.recursive) for target in args.targets: if not os.path.exists(target): print("Target doesn't exist: " + target) sys.exit(1) def filter_tarinfo(tarinfo): tarinfo.uid = tarinfo.gid = 0 tarinfo.uname = tarinfo.gname = "root" return tarinfo def add_file(tar, metapath): filepath = metapath[0:-5] print(filepath) with open(metapath, 'r') as f: try: guid = yaml.safe_load(f)['guid'] except yaml.YAMLError as exc: print(exc) return # dir tarinfo = tarfile.TarInfo(guid) tarinfo.type = tarfile.DIRTYPE tar.addfile(tarinfo=tarinfo) if os.path.isfile(filepath): tar.add(filepath, arcname=os.path.join(guid, 'asset'), filter=filter_tarinfo) tar.add(metapath, arcname=os.path.join(guid, 'asset.meta'), filter=filter_tarinfo) # path: {guid}/pathname # text: path of asset tarinfo = tarfile.TarInfo(os.path.join(guid, 'pathname')) tarinfo.size= len(filepath) tar.addfile(tarinfo=tarinfo, fileobj=io.BytesIO(filepath.encode('utf8'))) with tarfile.open(args.output, 'w:gz') as tar: for target in args.targets: add_file(tar, target + '.meta') if args.recursive: for meta in glob.glob(os.path.join(target, '*.meta')): add_file(tar, meta) for meta in glob.glob(os.path.join(target, '**/*.meta'), recursive=True): add_file(tar, meta)pythonを使ってアセットを探し、tar.gzに固めていくスクリプトです。
引数は3種類ありますが、そんなに難しくないと思うので読めばなんとなくわかるかなと思います。
これでunitypackageをUnityを使わずに作れるようになりました。unitypackageのビルドとアップロード
先程のmain.ymlを少し改造して、unitypackageのビルドも同時にやるようにしたのが以下です。
main.ymlname: Release on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up python uses: actions/setup-python@master with: python-version: '3.x' - uses: actions/cache@v1 if: startsWith(runner.os, 'Linux') with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - name : Pip install run: pip install PyYAML - name: Create unitypackage run: python3 create_unitypackage.py -r -o you.unitypackage Assets/Path/To/Library - uses: actions/upload-artifact@v1 with: name: unitypackages path: your.unitypackage github_release: needs: [build] runs-on: [ubuntu-latest] steps: - uses: actions/download-artifact@master with: name: unitypackages - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} draft: false prerelease: false - name: Upload Release Asset uses: actions/upload-release-asset@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: unitypackages/your.unitypackage asset_name: your.unitypackage asset_content_type: application/gzip具体的に増えたstepは以下です。
- python3環境のセットアップ
- pipのインストールと高速化のためのキャッシュ設定
- unitypackageのビルド
- GitHub Actionsのartifact機能を利用した成果物のアップロード・ダウンロード
自社Unity Package Registryへのアップロード
最後にUnity Package Registryへのアップロード部分のstep抜粋です。
main.ymldeploy: needs: [build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Node.js uses: actions/setup-node@master with: node-version: '12.x' registry-url: 'http://your.registry.example.com' - name: Copy README.md run: cp README.md Assets/Path/To/Library - name: Publish if version has been updated run: npm publish Assets/Path/To/Library env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} # You need to set this in your repo settingsポイントは、npmの認証用のトークンを事前にsecretsの中に設定しておくのと、
もしレジストリをIP制限などかけている場合はGitHub Actionsのソースレンジを許可しておく必要があります。
ただし現状のGitHub Actionsのソースレンジはドキュメント記載の通り、
AzureのEast US 2リージョン丸ごとであり、かつ随時変わる可能性があるためIP制限はおすすめできません。
どうしてもIP制限が必要な場合は、セルフホステッドランナーを利用することをオススメします。GitHub Actionsで自動化を実現出来なかったこと
実は更に自動化することも検討していました。
例えば各種バージョン表記の更新、CHANGELOG.mdの自動生成などです。
前者はスクリプトまでは書いたのですが、 GitHub Actionsの何をトリガーにそれを実行するかを決められずに手動で実行することになりました。
後者を実現するGitHub Actionsのstepがマーケットプレイスにもあるのですが、commitメッセージをルール通りに書く必要があるなど、導入ハードルが高いため断念しました。まとめ
Jenkinsの利用を段階的に減らしていくために、GitHub Actions化をいろいろと頑張りました。
まずはライブラリ開発の地味に大変だったところを自動化しました。
今後は実際のゲームのビルドなど、頻繁に走るビルドをGitHub Actionsのセルフホステッドランナーに寄せられないか検証を進めていいきたいと思います。
大手のクラウドサービスにはマネージドなCI/CDサービスがあるため、サーバー開発ではそれを利用することが多い。またUnity社もクラウドビルドサービスを提供しているが、大規模開発では使いづらい面がある。 ↩
- 投稿日:2019-12-18T01:05:21+09:00
HDRPによる高品質なPBRとトゥーン表現の両立を試みる【Unity】
本記事はVTuber Tech #1 Advent Calendar 2019の18日目の記事です.
前日の記事
https://qiita.com/gon0515/items/a7841f964b358a24dbccはじめに
Unity HDRP(High Definition Rendering Pipeline)は一般的にPBR表現に特化しており,ToonShaderなどのNPR表現はターゲットとされていません.
しかし,HDRPに含まれている高皮質なVolumetric FogやPlanar Reflection Probe,Post Processingなどは魅力的であり,ぜひトゥーン表現と両立したい!そこでこの記事では,HDRPで辛うじて動作するToonShaderの実装と実際のシーンへの組み込みを行います.
制作したシェーダと実際にHDRPに組み込んだスクリーンショットが以下になります.
HDRP環境でトゥーン表現をするためのシェーダを組みました(注. HDRPはNPR非推奨)https://t.co/cLTdxMIs5N pic.twitter.com/msFIEI3e8t
— とぐち (@togucchi) December 13, 2019環境
- Unity 2019.3.0f3
- HDRP v7.1.6
HDRPについて
Unity 2018.1 で、スクリプタブルレンダーパイプライン(SRP)という新しいシステムが登場しました。これにより、プロジェクトのニーズに応じた独自のレンダリングパイプラインの作成が可能になりました。SRP には「ライトウェイトレンダーパイプライン(LWRP)」および「HD レンダーパイプライン(HDRP)」という既成の 2 つのパイプラインが含まれます。HDRP は忠実度の高いビジュアルに重点を置いており、PC や据え置き型プラットフォームに適しています。
HD レンダーパイプライン:アーティストのためのクイックスタートガイドUnity社がアーティスト向けのわかりやすいチュートリアルを出してくれています.
最近previewも外れたはずです!ToonShaderの実装
厳しいところ
HDRPをはじめSRPでのシェーダ実装は主にShaderGraphで行うことが推奨されていると思いますが,HDRPのShaderGraphではライト情報を受け取ることができなかったり,ToonShaderを実装する上での弊害があります.
また,ShaderGraph上ではアウトラインパスによるアウトラインの描画も行えないため,一般的なToonShaderに存在するアウトラインの実装も難しいことになります.
余談
Unity HDRPをカスタマイズしてアウトラインパスを追加する
Litシェーダ,レンダリングパイプラインの改造によりアウトラインを実装されている方もいらっしゃいます.
特にLitシェーダを元にできるとLightProbeの情報なども扱えそうですので,今後挑戦したいと思っています.(が,レンダリングパイプラインを改造する性質上配布する形に落とし込むのは厳しいかもしれません)解決(妥協)案
アウトラインの描画はレンダリングパイプラインの改造が必要であると考えられるため,今回はシェーダを配布できる形にするため捨てます.
さらに,ライト情報の受け渡しはDirectional Lightの方向,色,光量のみC#スクリプトからシェーダのパラメータに渡す簡易的な実装で行います.
ShaderGraphでの実装
実際にShaderGraphで実装したシェーダがこちらです.Specularは実験的実装で,恐らく正しく動作しません)
以下,HDToonと呼称します.https://github.com/togucchi/HDToon
恐らく,一般的なToonShaderの処理をなぞっている感じになっているかと思いますので,実装についてはToonShaderの解説記事や実際のシェーダをご覧ください.
実装に際して,主に以下を参考にしています.
https://github.com/andydbc/unity-shadergraph-sandbox
https://connect.unity.com/p/zelda-inspired-toon-shading-in-shadergraphHDRPへの組み込み
準備
まず,Unity Hubの新規プロジェクト作成画面から,HDRPテンプレートを選択して新規プロジェクトを作成します.
サンプルシーンが最初に現れると思いますが,今回は見栄えを良く,実際の利用環境に近づけるため環境モデルのアセットを導入します.
この記事ではこちらの教室のアセットを導入します.(有料アセットですが,必須ではないのでご容赦ください)
https://assetstore.unity.com/packages/3d/environments/japanese-school-classroom-18392アセットをImportし,Edit/Render Pipeline/Upgrade Selected Materials to High Definition Materialsでマテリアルを変換,その後手動でマテリアルやシーン,Post Processingを調整した結果以下のようになりました.
記事の簡略化のため,シーンのセットアップの説明は省略します.
多くの方が素晴らしい解説記事を書いてくださっているためそちらをご覧ください.HDRPは頻繁にバージョン更新されているため情報が古くなっている可能性があることに注意してください.一部貼っておきます.
http://sayachang-bot.hateblo.jp/entry/2018/09/23/203758
https://www.kemomimi.dev/2019/09/16/lightwayonhdrp/HDToonでライト情報を利用するため,メインとなるDirectional Lightに"ToonShaderLightSettings.cs"をアタッチしておきます.
これでこのライトの方向,色,光量をHDToonで利用できるようになります.
キャラクターのセットアップ
今回テスト用に戸森ひかげさんのシャーロちゃん(冬服)モデルを使用します.
https://tomori-hikage.booth.pm/items/1572472キャラクターをシーンに入れた段階ではマゼンタ(シェーダエラー)になっていると思いますので,マテリアルをHDToonに入れ替えていきます.
マテリアルの設定項目はこのようになっています(Specularは使わないほうが良いです...).
- Base~
- 通常色の設定
- Shadow~
- 影色の設定
- ShadingToony
- 影の境界をはっきりさせる
- ShadowThreshold
- 影の閾値
- Brightness
- 明るさ
- Rim~
- リムライトの設定
簡易的な設定にまとまってるかと思います.
うまくシーンに馴染まない場合は,Brightnessを上下させてマスタの明るさを調節するのをおすすめします.マテリアルの設定,テクスチャ等をすべてキャラクタに適用して調節するとHDRPシーンの中にToonShaderのかかったキャラクタが現れます.
Brightnessをかなり高くしてShadow~をベースカラー,Base~をハイカラーとして扱い強い光が当たっているような表現をすることもできます.
教室のシーンに組み込んでポーズや表情などつけてみると以下のようになります!かわいい!
シーンに馴染むようにHDToonのBrightnessや,Post Processingのカラーグレーディングなどを調整しています.HDRPにはPlanar Reflection Probeが含まれているのでこのような鏡面反射をキャラクタに適用することもできます.(鏡面反射教室,なんか"良い"ですね)
まだ適当にシーンに入れるだけで綺麗に馴染む汎用的なものには仕上がってませんが,映像用途,VTuber用途などですとある程度カット毎の調節をかけられるのでまだ使えるかなーとは思います.(ゲームでの利用もしてくださったら嬉しいです!)
使ってくださった方はご一報いただけると嬉しいです.アウトライン付けてみる(おまけ)
ShaderGraphでアウトラインつけるのは厳しいのは前述しましたが,ポストプロセスでアウトライン付けてみても面白いかなと思ったので試してみました.
keijiroさん(神)が公開してくださっているHDRP用のポストプロセス集のKinoの中に輪郭線強調ができるものがありましたので,一旦そちらを利用させて頂きます.
https://github.com/keijiro/KinoプロジェクトにKinoを導入(上記リンクを参考)して,シーンに配置されているVolumeのプロファイルに"Recolor"を追加します.
一番上の"Edge"が輪郭線を出すエフェクトなので,ColorのAlphaを255にしてその他パラメータもいい感じに調整します.
結果,まあまあなアウトラインが出せていることがわかります(ポストエフェクトなので,もちろんキャラクタ以外のものの輪郭線も出ます)
keijiro先生のRecolorはもちろんそれ用に作られたものではないので,トゥーン表現用のポストエフェクトなど作ると,HDRPで更に良い表現ができるかもしれません!
最後に
フォトリアル風景に2次元キャラクタが立っている絵,好きですよね?(僕は好きです)
みなさんもぜひHDRPに自慢のキャラクタを入れて遊んでみてください!
- 投稿日:2019-12-18T00:11:33+09:00
【Unity学习笔记】使用Cinemachine快速实现具有锁定敌人功能的第三人称相机方案
Unity官方的Cinemachine工具,功能非常强大并且方便使用,可以用其轻松实现各种相机的功能。本文介绍一种简单快速实现锁定敌人功能的第三人相机方案。效果如下图所示
实现步骤介绍
1.准备Unity2018.3以上的版本,新建一个空白的3d项目,在Pakage Manager中下载安装Cinemachine(本文使用的是Unity2019.3.0f3 + Cinemachine 2.3.4)
2.新建一个第三人称虚拟相机Cinemachine FreeLook,一个名为Follow的空物体,一个名为LookAt的空物体。为使Hierarchy保持整洁,新建一个名为CameraRoot的空物体,将上述三者和Main Camera都纳入其下。
3.设置CM FreeLook的基本属性
将CM FreeLook的Follow设置为Follow空物体,LookAt设置为LookAt空物体(这里为何不将Follow直接设置为玩家本身,不将LookAt直接设置为要锁定的敌人呢?放到后面具体实现的时候来说明)。其余项目可以保持默认值。4.设置CM FreeLook两条轴的属性
- Y Aixs的Value设置成1,并且始终保持不变。本方案始终使用TopRig的设置,不会用到MiddleRig和BottomRig。
- 将X Aixs和Y Aixs的InputAxisName中的MouseX和MouseY去掉。本方案不使用鼠标手动操作XY轴的运动。
- 将Orbits下的Binding Mode设置为Lock To Target With World Up。无论Follow对象如何旋转,围绕Follow的旋转轴始终朝向世界坐标的Up方向,即旋转轨迹面始终平行于世界坐标的水平平面。
5.设置CM FreeLook的运动属性
- TopRig中的Body属性下的XYZ Damping是在三个轴的方向上虚拟相机位置跟随Follow物体的缓冲值,可根据实际需求设置。0为不缓冲,保持相机旋转轴中心的位置坐标和Follow对象始终保持一致。本方案不需要位置跟随上的缓冲,因此均设置为0。
- YawDamping是相机Yaw轴(可以简单理解成y轴方向)旋转值跟随Follow的旋转值的缓冲值,0表示不缓冲,可根据实际需求设置。本方案中将其设置为0.2。
- Aim属性下的各项属性基本保持默认即可。本方案的设置如图所示。
6.建立一个Player物体和若干个Enemy物体。
物体的形状可以随意设定,本文为了方便就设置为胶囊形,并添加一个小球放在Z轴正方向表示物体的前方。
7.编写相机控制脚本
新建一个CameraController用于控制相机跟随。将其添加到CameraRoot上,并设置好各项属性。
CameraController.csusing UnityEngine; using Cinemachine; public class CameraController : MonoBehaviour { [SerializeField] private CinemachineFreeLook CmFreeLook; [SerializeField] private Transform follow; [SerializeField] private Transform lookAt; [SerializeField] private Transform player; [SerializeField] private Transform[] enemies; [SerializeField] private AimMark aimMark; // 当前锁定的敌人的Index private int lockedEnemyIndex; private void Start() { // 给FreeLook虚拟相机设置Follow和LookAt的物体 CmFreeLook.Follow = follow; CmFreeLook.LookAt = lookAt; // 设置锁定标记的跟随对象 aimMark.SetAimTarget(lookAt); } private void Update() { // 按下空格时切换锁定的敌人 if(Input.GetKeyDown(KeyCode.Space)) { lockedEnemyIndex = (lockedEnemyIndex + 1) % enemies.Length; aimMark.PlayAimAnimation(); } // 使lookAt的位置和旋转都和锁定的目标保持一致 lookAt.position = enemies[lockedEnemyIndex].position; lookAt.rotation = enemies[lockedEnemyIndex].rotation; // 使follow的位置和玩家一致 follow.position = player.position; // 使follow的z轴正方向指向lookAt follow.LookAt(lookAt); } }其中的
AimMark
是锁定的时候出现的UI,和相机功能无关,这里可以先不看。这里将LookAt空物体的位置和旋转始终保持和锁定中的敌人一致,Follow则是位置保持和Player一致同时使Z轴正方向指向LookAt。
上文提到,为何不直接将CM FreeLook的Follow和LookAt直接设置为Player和锁定中Enemy,而是需要用两个间接物体呢?建立一个Follow空物体,位置跟随Player,并看向LookAt物体的目的是为了使相机,Player,Enemy处于一条直线上
Orbits中的Binding Mode设置为Lock To Tartget后,相机的旋转轴的位置跟随Follow对象的位置,旋转值也会Follow对象的旋转值。
而Lock To Target的三种变体,相机的旋转轴的位置都是跟随Follow对象的位置,但旋转值的跟随方式则有所不同Lock To Tartget: 旋转值跟随Follow对象的旋转值 Lock To Target On Assign: Start时或Follow对象更变时,旋转值设置为Follow对象的旋转值一次,之后则不再变化。 Lock To Target With World Up: 只有Yaw轴的旋转值跟随Follow对象的旋转值。 Lock To Target No Roll: Pitch轴和Yaw轴跟随Follow对象的旋转值,Roll轴不跟随建立一个LookAt空物体的目的,是为了让切换锁定敌人时,镜头能平滑转向。
直接更换CM FreeLook的LookAt对象的话,镜头会瞬间指向对象,不会对其进行补间。而让LookAt对象始终为一个空物体,切换锁定时让空物体的位置更变为锁定敌人的位置,相机的转向就会进行补间。
8.要点总结
- 使用Cinemachine中的FreeLook相机,Follow和LookAt属性分别设置为Follow空物体和LookAt空物体。
- 设置Orbits下的Binding Mode为Lock To Target With World Up。
- 编写脚本,使Follow空物体位置跟随Player,旋转指向LookAt空物体。LookAt空物体位置和旋转都跟随要锁定的Enemy。
以上就是使用Cinemachine快速实现具有锁定敌人功能的第三人称相机方案。