20191218のUnityに関する記事は19件です。

uGUIでオーバードローを減らそう

本記事は、サムザップ Advent Calendar 2019 #1 の12/22の記事です。

はじめに

ゲームのUIって画像が何枚も重なり合って動作が重くなってしまう現象が発生しがち。
そんなUIのオーバードローを減らしてゲームの動作を軽くしようというお話です。

サンプルUI

こんなUIがあったとしましょう
(無理やり重ねた感)
GameView-s.png

オーバードロー

このUIのオーバードローを見てみると…
Overdraw1-s.png
色が明るくなっているところが何枚も重なり合っているところです。
これはUnityのSceneViewで左上のプルダウンからOverdrawを選択すると表示されます。

描画範囲

斜めになっている四角い画像は色がついている部分だけじゃなく全体が描画されてしまいます。
(透明だから見えないだけ)
Sprite-View-s.png
透明で見えない部分まで描画されてしまうなんてもったいない。

透明部分を描画しない方法

実は簡単にできます。
画像とImageのInspectorを設定するだけ。

画像のInspectorはMeshTypeをTightにするだけ!
Inspector-Sprite.png
ImageのInspectorはUseSpriteMeshにチェックを入れるだけ!
Inspector-Image.png

そうすると描画されていない部分のオーバードローが消えました!
Overdraw2-s.png

効果

問題はこれをやることがどれほど効果があるのかってことですよね。
この程度のオーバードローだとあんまり変わらないのでもっと重ねてみたいと思います。

Use Sprite Mesh チェックなし

Test1.png
適当な三角形の画像を作成して画面内に3000枚表示してみました。
FPSも表示してみましたが約30fpsぐらいでした。

Use Sprite Mesh チェックあり

Test2.png
約50fpsでした。
※検証にはAndroid端末(GalaxyS9)を使用しました。

サンプルコード

Test.cs
public 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を見てみると
Test1-Status.png
3000枚の画像をUseSpriteMeshにチェックを入れず描画した方はVertsが12.0k(1万2千頂点)なのに対し
UseSpriteMeshにチェックを入れた方は117.0k(11万7千頂点)となっています。
Test2-Status.png

まとめ

頂点数が増えてもオーバードローが減れば動作は軽くなる!

それでは明日は@kotaroyさんの記事です。
お楽しみに!

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

Assembly Definitionの関係性を可視化するEditor拡張

はじめに

Unityのプロジェクトで使用されているAssembly Definitionの依存元/依存先を可視化するEditor拡張を開発しました。

image.png

開発するまでの経緯などを記事にまとめさせてもらいました。

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 がある
  • GraphViewNode が属する(AddElement()する)
  • Node にはinとoutの Port がある
  • PortEdge で繋がる

という関係性になっています。
EdgecontentContainer.Add() してやらないと表示されないということを知るまで結構かかりました。。

やりたいこと

とりあえず見るだけなら今の機能でもできますが、特定の .asmdef の関係性にフォーカスする機能は欲しいかなと思っています。ノードを選んでそれに関係するノードとエッジをハイライトする(もしくは無関係なノードを見えなくする)イメージです。

やらないこと

.asmdef のパスがわかるので、Jsonファイルとして処理すれば編集できなくもないですが、プロジェクトをぶっ壊しそうなのでやるつもりはありません。?
CompilationPipeline のAPIも get しか公開されていませんでした。

おわりに

Assembly Definition手軽で強力な機能なので、ぜひ試してみてください。
あとAPIとかEditor拡張用のクラスとか知らなかった便利な機能がUnityには色々あるんだなと思いました。?

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

uGUI上にParticleを表示させたい

最近見つけた便利なアセットを紹介します

uGUI上でパーティクルを表示するアセット

ParticleEffectForUGUI
https://github.com/mob-sakai/ParticleEffectForUGUI

通常、uGUIとParticleSystemを同じ画面で表示させると、uGUIの描画が上になってしまって、ParticleSystemが後ろになってしまいます

このパーティクルが・・・
コメント 2019-12-18 162038.jpg

uGUIより後ろになってしまう
コメント 2019-12-18 183712.jpg

Hierarchyの順番もParticleが上になっている
コメント 2019-12-18 183723.jpg

そこでこの 「ParticleEffectForUGUI」を使うと、uGUIの上にParticleSystemを表示させることが出来ます

使い方

https://github.com/mob-sakai/ParticleEffectForUGUI/releases

1.こちらからunitypackageを落としてProjectにインポートします

2.UI上に表示したいParticleSystemがついたGameObjectにUIParticleをつけます。以上
あとはUIParticleのScaleをいい感じに調整したら終わりです

分かりやすいように色変えました
コメント 2019-12-18 185229.jpg

簡単にuGUI上にParticleが表示できるので便利です

参考サイト
http://baba-s.hatenablog.com/entry/2018/08/20/090000
https://github.com/mob-sakai/ParticleEffectForUGUI

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

【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の画像が用意できていると良いです。
image.png

撮影時の設定にもよりますが、私が使用したオリジナル画像は 7680x7680 pixel でした。
Unity(OculusGo)で快適に動作するよう、Photoshop等の画像編集ソフトで 2048x1024 のサイズに上下を切り分けました
上半分が右眼、下半分が左眼です。

準備

実装を始めるにあたって、諸々必要な設定を行ってください。
UnityにおけるVR開発や、OculusGoのビルドに際しての設定は省略します。他の記事を参考にしてください。

必要なアセットのダウンロード

両眼用の天球の作成

まず、新しいMaterial Left Right を作ります。
それぞれのShaderを Unlit/Texture に変更し、先ほど作成した全天球立体視の分割画像をそれぞれアタッチします。
image.png

続いて、Sphere100をScene上に配置し、名前を LeftSphere 等にします。
MeshRendererのMaterialに先ほど作成した Left をアタッチします。するとこんな感じ。
image.png

そしてLeftSphereのLayerを Left にします。デフォルトではLeftレイヤーはないと思うので、Add Layer... から追加します。
このレイヤー設定をすることで、後のカメラ設定と合わせて両眼立体視で見られるようになるので、忘れないように!
image.png

RightSphereも同様に作成し、RightMaterialをアタッチ、Rightレイヤーを新しく作成して設定します。
LeftSphereとRightSphereは全く同じ位置に配置してください。
image.png

カメラの設定

続いてカメラの設定です。
まずOculus Integrationから OVRCameraRigをSceneに配置します。その他Oculus開発に関する説明は省略します。
OVRCameraRig > TrackingSpace 配下に Camera_LCamera_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

image.png
image.png

これで、実際にヘッドマウントディスプレイで見ると右眼と左眼で異なる画像が描画されるので、立体視として全天球画像が見られると思います。
同様の方法で、3Dの全天球動画も実装できます。Unityでの動画再生自体はまた別の設定が必要ですがそこはよしなに...

最後に

今回の仕組みは東大VRサークル UT-virtualの新歓VRのために実装したのですが、完成品では立体視全天球画像を単に置くだけでなく、Shaderをいじってオシャレなフェードインをさせるなど随分と面倒な仕組みを追加していました。
こんな感じのものも作れちゃうUnityすごいですね。

welcomeVR2019.gif

読んでくださりありがとうございました。

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

【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 を見てみると、以下のようになっていました!

image.png

項目がいっぱいありますね!この1つ1つが Action と呼ばれるものです!

それぞれの項目の説明は、以下の公式説明ドキュメントに記載があるのですが、一部説明がないものもありました。

VRChat / v2019.3.2 / Guides / Actions

説明がないものは、検索や質問したりして集めた情報を記載しようと思います。

アルファベット順です。

ActivateCustomTrigger

カスタムトリガーを発火します。

公式説明:https://docs.vrchat.com/docs/activatecustomtrigger

たとえば、

  • ボタンAを押したら扉が開く。
  • ボタンBを押しても扉は開く。

みたいなシーンを実現したいとします。

以下のように設定すれば一応実現できます。

  • ボタンAのActionに、扉が開くアニメーションを設定する。
  • ボタンBのActionに、扉が開くアニメーションを設定する。

image.png

しかし、カスタムトリガーを利用すれば、以下のように、共通化できます。

  • カスタムトリガーを作成し、そのActionに、扉が開くアニメーションを設定する。
  • ボタンAのActionに、カスタムトリガーを設定する。
  • ボタンBのActionに、カスタムトリガーを設定する。

image.png

これはいわゆる 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

指定した Animatorbool 変数の値を変更します。

公式説明:https://docs.vrchat.com/docs/animationbool

AnimationFloat

指定した Animatorfloat 変数の値を変更します。

公式説明:https://docs.vrchat.com/docs/animationfloat

AnimationInt

公式に説明がないため、根拠はありませんが、おそらく、指定した Animatorint 変数の値を変更するものだと思います。

AnimationIntAdd

指定した Animatorint 変数の値に、指定した数値を加算します。

公式説明:https://docs.vrchat.com/docs/vrchat-201811

AnimationIntDivide

指定した Animatorint 変数の値を、指定した数値で除算します。

公式説明:https://docs.vrchat.com/docs/vrchat-201811

AnimationIntMultiply

指定した Animatorint 変数の値に、指定した数値を乗算します。

公式説明:https://docs.vrchat.com/docs/vrchat-201811

AnimationIntSubtract

指定した Animatorint 変数の値から、指定した数値を減算します。

公式説明:https://docs.vrchat.com/docs/vrchat-201811

AnimationTrigger

Animatorのトリガーパラメーターを発火します。

公式説明:https://docs.vrchat.com/docs/animationtrigger

AudioTrigger

指定した Audio SourceAudio 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 Advent Calendar 2019 の19日目の記事でした。
明日は @rakurai5 さんによる Amplify Shader Editorを用いてメッシュを雪で覆うシェーダーを作る です!

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

ディゾルブシェーダー

溶ける

こんな感じで溶けます
2019-12-18-17-01-21_Trim.gif

Unityで使える、溶けるタイプのシェーダーです。
※ライトの影響やスペキュラなどは設定していないので、
必要であればいい感じに追加してください。m(_ _)m

コード

Dissolve.shader
Shader "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に設定してください。
nc190430.png

仕組み

DissolveTexに設定した白黒画像をしきい値(Threshold)以下の場合はピクセルを描画しないで、
しきい値を加算したり減算したりすることで見た目を変化させています。
なので、画像の具合に表現がかなり左右されます。
色々設定して試してみてください。

使い方

Thresholdの値を増やすと出現し、減らすと消えます。

Mode

Manual : 手動で溶かせます。アニメなどでThresholdを変化させてください。
Time : 時間経過で現れます。一回出現して終わりです。Speed設定で出てくる速度を設定してください。
PingPong :時間経過で出現したり溶け消えたりを繰り返します。 Speed設定で速度を設定してください。

<設定例>
dissolveキャプチャ.PNG

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

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弾実装してみるよ!

これを実行すると
ezgif.com-video-to-gif.gif

ちゃんと3-Way攻撃になりました!
今後は中心の弾がプレイヤーに向けて撃たれるようにしたいです。
結構調べたりしたのでめっちゃ時間かかった(泣)
次はボスだぁー

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

Unityカスタムパッケージ作成

最近のUnityが機能を分別してPackageManagerからほしいものだけをインストールする形になりました。自分もこういうパッケージをどうやって作るか検討しました。

パッケージ用プロジェクトのレイアウト

まずはパッケージになる新しいUnityプロジェクトを作成。プロジェクト内のフォルダーなどを下記のレイアウトと合わせる
参考:Unityマニュアル https://docs.unity3d.com/Manual/cus-layout.html

Unityマニュアル通りにしたら通常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.cs

package.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でテストが正常に動くか

Screen Shot 2019-12-17 at 12.36.03.png
* 後でちゃんとしたテストスクリプト作りましょう!

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)

おわりに

パッケージ分けるとちゃんとしたフレームワークができて各プロジェクト使い回すことが本当に便利!後から分別するのも大変かもしれないので早い段階でパッケージ化するのはおすすめ!

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

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オブジェクトよりも表示順が強くなります。
spine1_1.gif

生み出されたパーティクルがそれぞれ個別の表示順を獲得するには、ZWriteをOnにするのが手っ取り早いです。
しかし、その際に意図しない見た目になることが多いと思います。
Spineなどの2Dオブジェクトはたいていポリゴンの形と画像の形は一致していません。
ポリゴンの余白の部分があるのですが、通常は透明になっているので目には見えません。
ZWriteをOnにすると、パーティクルよりも先に余白の部分がレンダリングされてしまう事があるのです。
spine2_1.gif
↑足の部分で後ろに回り込んだパーティクルがレンダリングされていない

透明と不透明がはっきりしているデザインの場合、これを回避するのは比較的容易です。
シェーダーにカットオフ用のレンジスライダーを作り、その値に応じてフラグメントシェーダーでクリップを行えば
不要な部分がレンダリングされないので、意図した見た目に近くなります。
spine3_1.gif

参考までにこんな感じで追加してみました。
_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さんの記事です
それでは、良いお年を〜

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

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の実装方法

ローカライズに使えそうなサンプル実装をしてみました。

作ったもの

スクリーンショット 2019-12-18 12.59.10.png

ポップアップ形式で、仮想マスターデータからテキストを選ぶ形式にしてみました。
シリアライズしているのはマスターのキー(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行追加するだけだと、残念な感じになります。

スクリーンショット 2019-12-18 13.20.40.png

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さんの記事です。

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

ベクトルの内積によるサイコロの出目判定

PONOS Advent Calendar 2019の19日目の記事です。
昨日は@loveRiceさんの「[Unity] DOTweenを使ってみよう」でした。

はじめに

今日はUnityでサイコロを作ったお話をさせていただきます。
スクリーンショット.png
上の画像が完成形ですが、物理演算で転がり終えたサイコロの出目をテキストで表示しています。

とあるゲーム企画でサイコロを出してみたいという要望があり試作したものです。

要件

サイコロの要件は以下の通り。
① 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が青で色付けされています。

まず、基準となるワールド空間の座標は以下のようになっています。
スクリーンショット 2019-12-12 13.37.07.png
グレーの原点から上方向に伸びる緑色の線をアップベクトル(=Vector3.up)とし、基準にします。

一方、サイコロが転がり終えた後のベクトルはこんな状態です。
スクリーンショット 2019-12-12 13.32.01.png

サイコロの赤青黄の線のうち、上記ワールド空間のアップベクトルと方向が一致しているのは青い線ですね。青い線は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さんですー!

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

Unity臭さを消す方法10連発

Unity臭さとは

これ

image.png

非常にいい感じの画面ですね。こういう雰囲気の画面を見たとき、一目で「あーUnityだなー」と感じるような体験をしたことがある方は多いのではないでしょうか。今回はこの画面をどうにかしていきます。

数が多いうえに基本的なテクニックが多いので、この記事ではそれぞれの手段についてそこまで詳細に説明してません。キーワードをもとに適宜ググればいくらでも記事がヒットしますので、そっちに丸投げすることにします。

あらかじめことわっておきますが、別に「Unity臭い作品は悪」というわけではなく、私が「いい作品なのにUnity臭さが抜ければなー」と思う機会が多いので、ひと手間、手軽にクオリティアップができる手段として「Unity臭さを消す」方法をまとめておきたいな、という意図で書いています。ですのでプロトタイプやゲームの手触りを確かめるような目的のもとでは、今回上げるような方法は必要ないかもしれません。
あとこの記事に逆行しまくれば、Unity臭さを逆手に取ったチープな雰囲気が演出できるのでそれもアリだと思います。

1. ライティング編

1.1 SkyBoxを変える

image.png

Default-Skyboxも味わいがあって僕は好きですが、実はこいつが一番の臭いのもとです。
Asset Storeで探してきたやつにしてみます。

Wispy Skybox - Asset Store

image.png

これだけでかなりマシになりましたね。

1.2 マテリアルを変える

テクスチャのないプレーンなマテリアルは、わりとどうあがいても臭いを発してしまいがちです。
これもAssetStoreで適当に選んでみましょう。ノーマルマップがついているやつがベターです。

image.png

テクスチャのないマテリアルでも、設定次第である程度いい感じにすることはできます。公式のガイドがあるので、こちらも参考にしてみましょう。

マテリアルチャート - Unity マニュアル

1.3 影を焼く

いい感じにUnity臭さがなくなってきましたが、テクスチャを貼ったことにより、なんかむしろ一昔前のプリレンダCGみたいになってしまいました。モデルをStaticに設定して、Lighting ウィンドウから影をベイクしてみましょう。

image.png

間接光と、Emissiveに設定したマテリアルがいい味を出してます。

2. UI編

つぎにUIです。
image.png
ライティングはそこそこいい感じになってきましたが、UIを乗せるとこれまたくさいですね。

2.1 フォントを変える

さすがにデフォルトのArialフォントは見る人が見ればわかってしまいますし、WebGL環境で日本語が表示されない問題もあります。他環境でも日本語のフォールバックがわりと怪しい感じになってしまうので、変更は必至です。
適当なフリーフォントを探してみましょう。ゲーム内に埋め込んで使用する場合のライセンスに注意です。

個人的にはM+をよく使います。

image.png

2.2 UI画像を変える

ボタンの画像もデフォルトから変えてあげます。オススメは、角丸のフラットな四角形スプライトを用意することです。これひとつでボタンにも、ウィンドウにも使いまわせます。

Sprite EditorでSliceを設定してあげます。

image.png

image.png

最後にレイアウトをちょっと整えましょう。背景にそのまま文字をかぶせると読みにくいので、ウィンドウなどを適宜追加して……

image.png

良い感じになってきました。

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はライティングにメリハリを与えてくれます。

image.png

それでもまだ物足りなければ、VignetteとChromatic Aberration あたりを加えて、よりリッチな感じにしてみましょう。遠近感を効果的に使っている画面では、Depth of Fieldも有効です。

image.png

image.png

3.2 デフォルトのパーティクルをどうにかする

image.png

これもデカいです。
意識すべきポイントとしては、

  • サイズを小さくする
  • 数を増やす
  • 消えるときにフェード又は縮小して消えるようにする
  • いろいろランダムにして動きに幅を持たせる

以上に加えて、NoiseとColor over Lifetimeを設定してみました。

image.png

3.3 シーンロードで暗転する

これは地味ですが、「タイトルシーンでボタンを押したら少しのフリーズの後シーン遷移してプレイヤーキャラが空から降ってくるやつ」も、わりとUnity臭さがあります。

シーンロードの度に一度画面を真っ暗にフェードして、ロード後に戻してあげるのがいいです。

naichilab/Unity-FadeManager

4. ビルド編

最後、細かいポイントを仕上げていきます。

4.1 ビルド後のアイコンを変える

これだけ頑張っても、ビルド後のアプリのアイコンがUnityだと残念感があります(どうせPersonalライセンスではスプラッシュスクリーンが出るのでアレですが)。Player Settingsを開いて自作のアイコンに変更しましょう。

4.2 Display Resolution Dialogを消す

Standaloneビルドで起動時に出るあのダイアログです。将来バージョンでは消えるようですが、これもUnity臭さの一因なので、解像度設定、キーコンフィグなどをゲーム内実装して、消したいところです。

お疲れ様でした

どうでしょうか?今回紹介したテクニックは、定石すぎてむしろUnity臭いような感じもしますが、やらないよりもずっとよいはずです。つくったゲームの画面がなんかチープだな?と思ったときは、是非この記事を参照していただければと思います。

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

「東方Project×能」がテーマのホラーゲームを作っている話

この記事は個人開発 Advent Calendar 2019の23日目の記事です。

はじめに

こんにちは。T.Dと申します。
5年ほど前からUnityを使って東方Projectの二次創作ゲームを個人で制作しています。
今までは以下のゲームを作りました。(リンクは全てAndroid)

個人開発がテーマとの事でこの3作の話をしても良かったのですが、今回は現在開発中である
「東方Project×能」がテーマのホラーゲーム(名称未設定)
のお話をして行きます。
技術的な話よりも、ゲーム内容や実装したシステムの話が多いです。

ゲームについて

ゲームの動作を見てくれ

最初にゲームの動画を見て頂いた方がこの後の話も理解しやすくなると思います。
Twitterに上げた動画からピックアップします。

どんなゲームなんだよ

コンセプトとテーマはこんな感じ。

  • テーマ:「東方×能」
  • ジャンル:「ホラー×不思議のダンジョン」
  • 暗闇に包まれた迷宮を道具を駆使して探索し、突破せよ。
  • 迷宮には面を被った幻想少女が徘徊している。立ち向かおうなど決して思わず逃げ続けよ。
  • 面はプレイヤーにも力を与える。上手に使え。
  • 安心なんて何処にもない。恐怖は絶える間も無く有り続ける。

有り体に言えば探索型ホラーゲームです。
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秒ほど経過するとプレイヤーから薄暗い光が発生し、その後数分かけて光の範囲が拡大します。
しかし最初期はほぼ目の前だけ、最大範囲でもあまり遠くは見えないのであくまで救済処置です。

移動

移動の様子は上のツイートの動画を見てください。
プレイヤーの移動には3種類あり、それぞれの特徴は以下の通りです。

  • 通常移動
    • 普通の移動。原則この状態で移動する。
    • 足音が鳴るが一部を除き敵に気付かれることは無い。
  • 高速移動
    • スタミナを消費する速い移動。敵に追われている時はスタミナ消費が倍になる。
    • 音が鳴り、聞いた敵はその場所まで確認しに来る。
  • 低速移動
    • 遅い移動。特殊なケースで使用する。
    • 全く足音を出さず、低速移動時はアイテム使用の手助けとなる照準(未実装)が表示される。

移動状態や移動場所によって足音も変わるよう設定してます。
細かい音の変化ですが凝りました。

メニュー

メニューは徹底的に黒と白を基調としたシンプルなUIとしています。
こうする理由は下手に派手にしてホラー要素を損ないたくないから、黒と白が織りなすデザインの美しさをプレイヤーの皆さんに知って頂きたいからです。

アイテムメニュー

ItemMenu
現在所持している道具の一覧を表示します。
アイテム説明文は効果や使い方というより、雰囲気を出すためにアイテム設定を書いています。アイテム効果は使って理解しろ
この画面ではアイテムを置くなど、「使う」以外の補助的な動作を行います。

敵一覧

敵一覧
敵一覧メニューは敵に接近したり特定のアイテムを使う事で埋まります。
敵には原則4種類存在し、それぞれ以下の特徴を持っています。

  • 為手(シテ):マップに1、2体存在する主役級の敵。足止めは可能だが倒す事が出来ない。
  • 脇(ワキ):マップの補助的な敵。人数制限は特に無く、道具などを使えば倒せる。
  • 連(ツレ):一部の為手に付随する敵。道具などを使えば倒せるが一定時間後に復活する。
  • 脇連(ワキヅレ):一部の脇に付随する敵。道具などを使えば倒せる。

日本の伝統芸能である能楽に明るい人なら察しがつきました通り、敵分類は能楽の役者の分類に大変近い物となっています。
為手と脇が東方キャラクターで、連と脇連が一部のキャラクターに付随するキャラクターです。(アリスにおける上海人形お燐におけるゾンビフェアリーなど)
上の画像の連と脇連がダミーテキストなのはみんなには内緒だよ

ちなみにフランちゃんの名前の伸ばし棒はではなくを使っています。
この縦棒はの右側だそうです。

終わりに

今年の9月ごろから制作を開始しましたが、現時点で結構形になって来たと思います。
しかし未実装の部分の方が多いのも事実なので今後もこつこつと実装を続けて行きたいです。
来年の上旬にはテスト版を出したいですね。

(おまけ)実装予定のゲームシステムと仕様

最終的に以下の機能を全て実装したいと思ってます。

  • 迷宮が不思議のダンジョンとなっており、毎回形状が異なる。
  • 迷宮には敵が複数体存在し、シテとワキに分類される。
  • 敵の強さはEasy,Normal,Hard,Lunaticの4種類が存在する。
    • Easyは脇限定、Lunaticは為手限定。
    • 英語だと雰囲気に合わないので能に合わせて生成、般若、蛇、真蛇に変えるかもしれない。
  • クエスト(仮称)によってはアイテムが完全未識別。
    • どんな道具も使わないと分からないし、使ったところで名前が識別される訳でもない。
    • 道具の入手時に「やった〜強アイテムだ〜」などと言う安心感など与えさせない。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity] モバイルでも動く!風に揺れる布シェーダー(解析的な法線導出)

これは【unityプロ技②】 Advent Calendar 2019の18日目の記事です。


こんな感じの「風に揺れる布シェーダー」をUnityで実装したので、簡単に解説します。

cloth_loop_v4.gif

軽さの秘訣

このシェーダー、なんとモバイルでも余裕で動作するくらい軽量です!

その軽さの秘訣は、次の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性能を発揮したときは、さらに実行時間が減ると思われます。

スクリーンショット 2019-12-15 21.54.29.png

シェーダ全文

シェーダー全文とUnityプロジェクトはGitHubに公開しています。

GitHubおよび本記事に登場するソースコードはMIT Licenseです。

シェーダー解説

実装を踏まえながら、シェーダーを解説していきます。

Meshの準備

1x1の大きさのGridをつくります。分割数は 40x40 くらいが丁度いいです。UVも必要です。

Houdiniの場合は、GridノードとUV Flattenノードで作れます。

HoudiniでMesh作成

頂点シェーダーの解説

まず、頂点シェーダーから実装を解説していきます。

頂点シェーダー
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 = 0

xy平面の斜め方向を 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.5Half-Lambert の計算式と同値になります。

half halfLaumber = lerp(1.0, diffuse, _ShadowIntensity);

簡易なライティングですが、計算量と品質のバランスは取れており、モバイルなら必要十分だと思います。

さらに軽量化が必要な場合は、このライティング処理を頂点シェーダーで行うという方法もありますが、シェーダーの見通しの良さを重視して、今回はピクセルシェーダーで実装しました。

おわりに

いかがでしたでしょうか?

「短いシェーダーと簡単な数式だけでも、ちゃんと揺れる布を実装できるんだ!」
というのが伝われば幸いです。

レイマーチングでも、 陰関数を偏微分した勾配から法線を導出 するテクニックが有名ですが、それと同じ理論で法線を導出しています。

レイマーチングでは、距離関数を数値的に偏微分する必要があるため、距離関数を複数回(4~6回)評価する必要がありますが、
今回は波の式を単純化することによって、解析的に偏微分を行って計算量を減らしました。

数学の知識を応用すると、シェーダーをシンプルかつ軽量に実装ができます!

数学をつかって複雑な課題をシンプルに解決できたときの快感が、私は好きです。

関連リンク

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

数値を数字として表示するシェーダーを作ったおはなし

この記事はシェーダーアドベントカレンダー18日目の記事として書かれています。
https://qiita.com/advent-calendar/2019/shader-advent-calender-2019

はじめに

シェーダーの数値って色として見ることは多いのだけど、普通(?)のプログラムのように数字として見ることは少ないと思います。
VR空間でVJをするときに、パラメータの値を数字として目で見たかったので数値を数字として表示するシェーダーを作りました。
今回はその数字シェーダーを何を考えながらどういう風に作っていったか書きたいと思います。

完成したサンプル。機能として桁数指定、マイナス表示など。

-100から100まで表示してみた動画。

完成したコードとテクスチャです。
https://github.com/noriben327/DisplayNumber/blob/master/DisplayNumber.shader

既に数字シェーダーを作っている方々

同じようなことを思っている方はたくさんいて、シェーダーを配布している方もいます。

BUTADIENE WORKS
ブタジエンさん
https://twitter.com/butadiene121/status/1063451198194413568

Position Shader
オノッチさん
https://onotchi.booth.pm/items/996827

少しコードを読んでみたものの自分にはわからなかったので、1から考えながら作ることにしました。

基本の考え

数字を書いたテクスチャを用意して、例えば数値が1ならテクスチャの1が書いてある部分にスクロールして表示する。これだけです。

今回作った数字のテクスチャはこれです。
Y座標を10分割して数字を書いているので、uvfloat2(0, 0.1)1float2(0, 0.7)7のテクスチャにそのままスクロールできます。
numTex 1.png

説明用に2桁で、隠している部分も表示してみたもの。数値にあわせてUVスクロールしてるのがわかるんじゃないかな~と思います。0.0から2.0を表示させています。

数値の抽出

数値そのままではどうにも扱えないので、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.30.299999と表示されてしまうときがある場合のとりあえずの対策である:thinking::thinking::thinking:(本来は必要ない)

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)を可視化してみたりと結構実用的なシェーダーとして使っています。

全コード

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
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

数値を数字として表示するシェーダーを作ったおはなし[Unity]

この記事はシェーダーアドベントカレンダー18日目の記事として書かれています。
https://qiita.com/advent-calendar/2019/shader-advent-calender-2019

はじめに

シェーダーの数値って色として見ることは多いのだけど、普通(?)のプログラムのように数字として見ることは少ないと思います。
VR空間でVJをするときに、パラメータの値を数字として目で見たかったので数値を数字として表示するシェーダーを作りました。
今回はその数字シェーダーを何を考えながらどういう風に作っていったか書きたいと思います。

完成したサンプル。機能として桁数指定、マイナス表示など。

-100から100まで表示してみた動画。

完成したコードとテクスチャです。
https://github.com/noriben327/DisplayNumber/blob/master/DisplayNumber.shader

既に数字シェーダーを作っている方々

同じようなことを思っている方はたくさんいて、シェーダーを配布している方もいます。

BUTADIENE WORKS
ブタジエンさん
https://twitter.com/butadiene121/status/1063451198194413568

Position Shader
オノッチさん
https://onotchi.booth.pm/items/996827

少しコードを読んでみたものの自分にはわからなかったので、1から考えながら作ることにしました。

基本の考え

数字を書いたテクスチャを用意して、例えば数値が1ならテクスチャの1が書いてある部分にスクロールして表示する。これだけです。

今回作った数字のテクスチャはこれです。
Y座標を10分割して数字を書いているので、uvfloat2(0, 0.1)1float2(0, 0.7)7のテクスチャにそのままスクロールできます。
numTex 1.png

説明用に2桁で、隠している部分も表示してみたもの。数値にあわせてUVスクロールしてるのがわかるんじゃないかな~と思います。0.0から2.0を表示させています。

数値の抽出

数値そのままではどうにも扱えないので、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.30.299999と表示されてしまうときがある場合のとりあえずの対策である:thinking::thinking::thinking:(本来は必要ない)

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)を可視化してみたりと結構実用的なシェーダーとして使っています。

全コード

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
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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ぐらいはありそうです。
そこでいつも問題となるのが、ライブラリの配布方法です。主に

  1. git submoduleでの取り込み
  2. unitypackageでの配布
  3. 自社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.yml
name: 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.yml
name: 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.yml
  deploy:
    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のセルフホステッドランナーに寄せられないか検証を進めていいきたいと思います。


  1. 大手のクラウドサービスにはマネージドなCI/CDサービスがあるため、サーバー開発ではそれを利用することが多い。またUnity社もクラウドビルドサービスを提供しているが、大規模開発では使いづらい面がある。 

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

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に組み込んだスクリーンショットが以下になります.

image.png

環境

  • 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

shadergraph.png

恐らく,一般的なToonShaderの処理をなぞっている感じになっているかと思いますので,実装についてはToonShaderの解説記事や実際のシェーダをご覧ください.

実装に際して,主に以下を参考にしています.
https://github.com/andydbc/unity-shadergraph-sandbox
https://connect.unity.com/p/zelda-inspired-toon-shading-in-shadergraph

HDRPへの組み込み

準備

まず,Unity Hubの新規プロジェクト作成画面から,HDRPテンプレートを選択して新規プロジェクトを作成します.

image.png

サンプルシーンが最初に現れると思いますが,今回は見栄えを良く,実際の利用環境に近づけるため環境モデルのアセットを導入します.

この記事ではこちらの教室のアセットを導入します.(有料アセットですが,必須ではないのでご容赦ください)
https://assetstore.unity.com/packages/3d/environments/japanese-school-classroom-18392

アセットをImportし,Edit/Render Pipeline/Upgrade Selected Materials to High Definition Materialsでマテリアルを変換,その後手動でマテリアルやシーン,Post Processingを調整した結果以下のようになりました.

image.png

記事の簡略化のため,シーンのセットアップの説明は省略します.
多くの方が素晴らしい解説記事を書いてくださっているためそちらをご覧ください.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で利用できるようになります.
image.png

キャラクターのセットアップ

今回テスト用に戸森ひかげさんのシャーロちゃん(冬服)モデルを使用します.
https://tomori-hikage.booth.pm/items/1572472

キャラクターをシーンに入れた段階ではマゼンタ(シェーダエラー)になっていると思いますので,マテリアルをHDToonに入れ替えていきます.

image.png

マテリアルの設定項目はこのようになっています(Specularは使わないほうが良いです...).
image.png

  • Base~
    • 通常色の設定
  • Shadow~
    • 影色の設定
  • ShadingToony
    • 影の境界をはっきりさせる
  • ShadowThreshold
    • 影の閾値
  • Brightness
    • 明るさ
  • Rim~
    • リムライトの設定

簡易的な設定にまとまってるかと思います.
うまくシーンに馴染まない場合は,Brightnessを上下させてマスタの明るさを調節するのをおすすめします.

マテリアルの設定,テクスチャ等をすべてキャラクタに適用して調節するとHDRPシーンの中にToonShaderのかかったキャラクタが現れます.
image.png

Brightnessをかなり高くしてShadow~をベースカラー,Base~をハイカラーとして扱い強い光が当たっているような表現をすることもできます.

image.png

教室のシーンに組み込んでポーズや表情などつけてみると以下のようになります!かわいい!
シーンに馴染むようにHDToonのBrightnessや,Post Processingのカラーグレーディングなどを調整しています.

image.png

HDRPにはPlanar Reflection Probeが含まれているのでこのような鏡面反射をキャラクタに適用することもできます.(鏡面反射教室,なんか"良い"ですね)

image_0004.png

まだ適当にシーンに入れるだけで綺麗に馴染む汎用的なものには仕上がってませんが,映像用途,VTuber用途などですとある程度カット毎の調節をかけられるのでまだ使えるかなーとは思います.(ゲームでの利用もしてくださったら嬉しいです!)
使ってくださった方はご一報いただけると嬉しいです.

アウトライン付けてみる(おまけ)

ShaderGraphでアウトラインつけるのは厳しいのは前述しましたが,ポストプロセスでアウトライン付けてみても面白いかなと思ったので試してみました.

keijiroさん(神)が公開してくださっているHDRP用のポストプロセス集のKinoの中に輪郭線強調ができるものがありましたので,一旦そちらを利用させて頂きます.
https://github.com/keijiro/Kino

プロジェクトにKinoを導入(上記リンクを参考)して,シーンに配置されているVolumeのプロファイルに"Recolor"を追加します.
image.png

一番上の"Edge"が輪郭線を出すエフェクトなので,ColorのAlphaを255にしてその他パラメータもいい感じに調整します.

結果,まあまあなアウトラインが出せていることがわかります(ポストエフェクトなので,もちろんキャラクタ以外のものの輪郭線も出ます)
image.png

keijiro先生のRecolorはもちろんそれ用に作られたものではないので,トゥーン表現用のポストエフェクトなど作ると,HDRPで更に良い表現ができるかもしれません!

最後に

フォトリアル風景に2次元キャラクタが立っている絵,好きですよね?(僕は好きです)
みなさんもぜひHDRPに自慢のキャラクタを入れて遊んでみてください!

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

【Unity学习笔记】使用Cinemachine快速实现具有锁定敌人功能的第三人称相机方案

Unity官方的Cinemachine工具,功能非常强大并且方便使用,可以用其轻松实现各种相机的功能。本文介绍一种简单快速实现锁定敌人功能的第三人相机方案。效果如下图所示

1.gif

实现步骤介绍

1.准备Unity2018.3以上的版本,新建一个空白的3d项目,在Pakage Manager中下载安装Cinemachine(本文使用的是Unity2019.3.0f3 + Cinemachine 2.3.4)

コメント 2019-12-16 221943.png

2.新建一个第三人称虚拟相机Cinemachine FreeLook,一个名为Follow的空物体,一个名为LookAt的空物体。为使Hierarchy保持整洁,新建一个名为CameraRoot的空物体,将上述三者和Main Camera都纳入其下。

コメント 2019-12-16 222236.png コメント 2019-12-15 224719.png

3.设置CM FreeLook的基本属性

コメント 2019-12-15 231404.png
将CM FreeLook的Follow设置为Follow空物体,LookAt设置为LookAt空物体(这里为何不将Follow直接设置为玩家本身,不将LookAt直接设置为要锁定的敌人呢?放到后面具体实现的时候来说明)。其余项目可以保持默认值。

4.设置CM FreeLook两条轴的属性

コメント 2019-12-15 233053.png コメント 2019-12-15 233120.png コメント 2019-12-16 233542.png

  • 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的运动属性

コメント 2019-12-16 234942.png

  • TopRig中的Body属性下的XYZ Damping是在三个轴的方向上虚拟相机位置跟随Follow物体的缓冲值,可根据实际需求设置。0为不缓冲,保持相机旋转轴中心的位置坐标和Follow对象始终保持一致。本方案不需要位置跟随上的缓冲,因此均设置为0。
  • YawDamping是相机Yaw轴(可以简单理解成y轴方向)旋转值跟随Follow的旋转值的缓冲值,0表示不缓冲,可根据实际需求设置。本方案中将其设置为0.2。

コメント 2019-12-17 235925.png

  • Aim属性下的各项属性基本保持默认即可。本方案的设置如图所示。

6.建立一个Player物体和若干个Enemy物体。

コメント 2019-12-17 220520.png
物体的形状可以随意设定,本文为了方便就设置为胶囊形,并添加一个小球放在Z轴正方向表示物体的前方。
コメント 2019-12-17 221347.png

7.编写相机控制脚本

新建一个CameraController用于控制相机跟随。将其添加到CameraRoot上,并设置好各项属性。
コメント 2019-12-17 220325.png

CameraController.cs
using 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处于一条直线上

2.gif

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.要点总结

  1. 使用Cinemachine中的FreeLook相机,Follow和LookAt属性分别设置为Follow空物体和LookAt空物体。
  2. 设置Orbits下的Binding Mode为Lock To Target With World Up。
  3. 编写脚本,使Follow空物体位置跟随Player,旋转指向LookAt空物体。LookAt空物体位置和旋转都跟随要锁定的Enemy。

以上就是使用Cinemachine快速实现具有锁定敌人功能的第三人称相机方案。

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