inosyanのブログ

プログラム、家庭菜園、僕が興味を持ったことを書いていきます

Unityの通信コードを自動生成する「WWW Script Generator」

f:id:inosyan:20180202233332p:plain
3週間ほど前に申請したUnityのアセットパッケージ「WWW Script Generator」がやっとUnityアセットストアにリリースされました。このアセットパッケージは、サーバー通信に関するスクリプトを自動で書いてくれるツールです。

Unityでサーバー通信する際は標準で用意されているWWWクラスを使います。そのまま使ってもいいのですが、たいていの場合は共通処理があるので通信用のクラスを作り、その中でヘッダーに何かを入れたり、戻り値をパースしたりすると思います。
また、サーバー側で用意されているAPIやパラメーター、戻り値の構造をそのまま文字列で表現するのはタイプミスによりバグを引き起こします。なので、それらをクラス、メソッド、プロパティで表現することが必要になりますが、APIの数が多くなるとそれを書くのも大変です。
それに、たいていの場合はサーバーとUnityのエンジニアは別の人が担当すると思いますが、仕様についてのコミュニケーションが多く発生し、伝達ミスも起きる可能性があります。仕様が固まってない段階で開発が始まることも多く、頻繁に仕様変更が発生し、そのたびにUnityの開発者は変更になった箇所を把握しつつスクリプトを修正しなければなりません。さらに通信テストにも多くの工数を必要とします。
f:id:inosyan:20180202233559p:plain

このパッケージを使えば、仕様を設定ファイルにしてサーバー開発者と共有することで、仕様の伝達ミスをなくすことができます。
また、スクリプトの自動生成機能により、スクリプト作成にかかる時間を大幅に短縮できます。
仕様が変更になった場合でも、変更箇所を明確に把握しスクリプトを再生成することで簡単に対応できます。
そして、スクリプトと共に生成されるテストコードを使って、通信テストにかかる時間を大幅に短縮できます。
f:id:inosyan:20180202233624p:plain

自動生成されるスクリプトはサーバーAPIに特化しているので、他のロジックと明確に分離することができ、可読性が向上します。
通信につかうクラスはUnity標準の“WWW”で、このパッケージ特有の機能は使っていません。生成したスクリプトはパッケージのdllには依存しておらず、書き出されたクラスをカスタマイズすることで、ヘッダー情報や送信データのフォーマットなどを調整できるので、あらゆる通信に対応できます。
JSONをパースするためのモデルクラスも自動生成されるので、JSONをそのまま使うより安全に扱うことができます。
f:id:inosyan:20180202233658p:plain

さらに、Pro版ではTSVファイルの読み書きができるので、エクセルなどの外部ツールと連携でき、Unityをもっていないサーバー開発者との連携が可能になります。
f:id:inosyan:20180202233716p:plain

このパッケージは、私自身が開発時に経験した問題点を解決したいと思って作りました。みなさまの開発が少しでも楽になれば幸いです。

全ての機能が利用可能なProバージョンはこちら
機能を限定したFreeバージョンはこちら

チュートリアル(英語)
チュートリアル(日本語)

UnityのTimelineを使ったムービー制作 #2

f:id:inosyan:20180126081931j:plain

先日の記事で、アセットストア向けにアセットパッケージの制作をした際に、紹介ムービーをUnityで作成した際のことを書きましたが、今回はその続きです。

このムービーの冒頭の10秒間の部分がUnityで制作した部分です。


WWW Script Generator

ムービー開始後3~4秒付近の光る部分は点光源の光量をスクリプトで制御しています。 f:id:inosyan:20180126082039g:plain

シーンには点光源が置いてありますが、はじめは非アクティブで、タイムライン上で時間が来たらアクティブにしています。それと同時に点光源の光量を制御するスクリプトを開始しています。 はじめは光量0でだんだん明るくなり、一番明るくなったところからすこし暗くなって終わります。 f:id:inosyan:20180126082104p:plain

PointLightOnAsset.cs

[System.Serializable]
public class PointLightOnAsset : PlayableAsset
{
    public ExposedReference<Light> PointLight;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        var handle = ScriptPlayable<PointLightOn>.Create(graph);
        var beh = handle.GetBehaviour();
        beh.PointLight = PointLight.Resolve(graph.GetResolver());
        return handle;
    }
}

PointLightOn.cs

using UnityEngine;
using UnityEngine.Playables;

public class PointLightOn : PlayableBehaviour
{
    public Light PointLight;

    public override void OnGraphStart(Playable playable) {
    }

    public override void OnGraphStop(Playable playable) {
    }

    public override void OnBehaviourPlay(Playable playable, FrameData info) {
    }

    public override void OnBehaviourPause(Playable playable, FrameData info) {
        AdjustIntensity(1);
    }

    public override void PrepareFrame(Playable playable, FrameData info) {
        float rate = (float)(playable.GetTime() / playable.GetDuration());
        float d = 120 * rate / 180 * Mathf.PI;
        d = Mathf.Sin(d) / Mathf.Sin(120.0f / 180.0f * Mathf.PI);
        AdjustIntensity(d);
    }

    void AdjustIntensity(float intensity)
    {
        if (!PointLight) return;
        PointLight.intensity = intensity;
    }
}

PlayableAssetを継承したスクリプトをタイムラインに配置し、点光源のインスタンスを紐付けます。具体的な挙動はPlayableBehaviourを継承したスクリプトに記述します。

タイムラインのポジションが動くときに呼ばれるPrepareFrameの中で、

float rate = (float)(playable.GetTime() / playable.GetDuration());

タイムライン上のこのスクリプトの範囲を0~1で表したときのどの位置にいるのかを調べ、 それを0~120°の角度に換算し、

float d = 120 * rate / 180 * Mathf.PI;

後半明るくなってから少し暗くなるように、sin(120°)が最終的な光量になるように調整しています。

d = Mathf.Sin(d) / Mathf.Sin(120.0f / 180.0f * Mathf.PI);

sinの値は

sin(0°) = 0
sin(90°) = 1
sin(120°) = 0.87

なので、それぞれsin(120°)で割ると

sin(0°) / sin(120°) = 0
sin(90°) / sin(120°) = 1.15
sin(120°) / sin(120°) = 1

となり、一番明るいときの光量は1.15なので最後の1と較べてすこし明るいため、一瞬スパークしたように見えるのです。

ところで、このムービーで紹介しているアセットパッケージは申請してから2週間経とうとしますが、まだ審査中です。以前別のアセットパッケージを公開した際も2週間ほどかかったのでそろそろではないかとは思いますが、公開されたらあらためてご紹介したいと思います。

「HoloLens 日本上陸1周年記念!コミュニティのみなさまへの大感謝祭!」に行ってきました

昨日、HoloLensのイベント「HoloLens 日本上陸1周年記念!コミュニティのみなさまへの大感謝祭!」に行ってきました。一ヶ月前に申込んだときは応募開始後すぐに定員に達してしまいキャンセル待ちだったのですが、当日になって運良く行けることになりました。

HoloLensが日本に上陸して1年、早いものです。事例も増えてきているようです。 f:id:inosyan:20180119082447j:plain

パネルディスカッションで話題にのぼってましたが、認定パートナーになるのは大変そうです。まず日本で面接があり、それからシアトルに行って面接を受け、そこにはHoloLensアプリ開発用のちゃぶ台の置いてあるスペースがあり、そこで数日研修をうけるそうです。 f:id:inosyan:20180119082452j:plain

日本国内で発売されている、Windows Mixed Realityヘッドセットが展示されていました。HoloLens以外のデバイスはイマーシブデバイス(現実の視界が透けて見えない没入型のデバイス)ですが、HoloLensと比べると値段は安いです。 シェアリング機能(複数デバイスで空間を共有)はイマーシブデバイスでも可能らしいです。検証機としてあったほうがいいかもしれません。 f:id:inosyan:20180119082503j:plain

HoloLens アプリ開発コンテストが開かれるそうです。応募期間は4/1~4月末。これはぜひ参加しようと思います。 f:id:inosyan:20180119082458j:plain

情報を体験として共有することがあたりまえになる未来、それに備えて今できることをやっていきたいです。 f:id:inosyan:20180119082500j:plain

HoloLensは会社に買ってもらい、去年HoloLens向けのアプリWindow Breakerを作りました。そのアプリの紹介はまたこんど書きますが、今回のイベントに参加し、これからもっとたくさんアプリを作っていきたいと思いました。

UnityのTimelineを使ったムービー制作

f:id:inosyan:20180115081455j:plain 先日仕事で、Unityアセットストア向けにアセットパッケージの制作をしました。そちらはいま申請中ですので審査が通ったらまたお知らせしますが、申請に必要だった紹介ムービーをUnityを使って作成したので、今回は会社に許可をもらい、そのムービーをご紹介します。

このムービーの冒頭の10秒間の部分がUnityで制作した部分です。


WWW Script Generator

モデリングはmayaで行いました。冒頭で一部だけを光らせたいので、マテリアルを分けてあります。
f:id:inosyan:20180115081506j:plain

あとはUnityの作業です。 はじめに周りの3つの三角形が点滅し、次に中央が点滅しますが、この部分のマテリアルには、ビルドインシェーダーの Unity スタンダードシェーダー を使用しており、Emission の値を有効にしています。 f:id:inosyan:20180115081703p:plain

そして、点滅させるためにシェーダーのプロパティをスクリプトで制御しています。 そのためには PlayableAsset, PlayableBehaviour をそれぞれ継承した2つのクラスを用意し、PlayableAssetを継承したほうをTimelineに配置します。今回は点滅する箇所は、周りの三角形と中央の2箇所ありますが、共通のスクリプトを使ってenumでマテリアルを切り替えています。
f:id:inosyan:20180115081725p:plain

StartEffectAsset.cs

using UnityEngine;
using UnityEngine.Playables;

[System.Serializable]
public class StartEffectAsset : PlayableAsset
{
    public StartEffect.TargetEnum Target;
    public Material TopMaterial;
    public Material CenterMaterial;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject go) {
        var handle = ScriptPlayable<StartEffect>.Create(graph);
        var beh = handle.GetBehaviour();
        beh.Target = Target;
        beh.TopMaterial = TopMaterial;
        beh.CenterMaterial = CenterMaterial;
        return handle;
    }
}

StartEffect.cs

using UnityEngine;
using UnityEngine.Playables;

public class StartEffect : PlayableBehaviour
{
    public enum TargetEnum {
        Top,
        Center,
    }

    public TargetEnum Target;
    public Material TopMaterial;
    public Material CenterMaterial;

    Material TargetMat {
        get {
            switch(Target){
                case TargetEnum.Top: return TopMaterial;
                case TargetEnum.Center: return CenterMaterial;
                default: return TopMaterial;
            }
        }
    }

    void SetEmission(Color col)
    {
        if (TargetMat && TargetMat.HasProperty("_EmissionColor"))
            TargetMat.SetColor("_EmissionColor", col);
    }

    // Called when the owning graph starts playing
    public override void OnGraphStart(Playable playable) {
    }

    // Called when the owning graph stops playing
    public override void OnGraphStop(Playable playable) {
    }

    // Called when the state of the playable is set to Play
    public override void OnBehaviourPlay(Playable playable, FrameData info) {
        AdjustEmission(0);
    }

    // Called when the state of the playable is set to Paused
    public override void OnBehaviourPause(Playable playable, FrameData info) {
        AdjustEmission(1);
    }

    // Called each frame while the state is set to Play
    public override void PrepareFrame(Playable playable, FrameData info) {
        float rate = Mathf.Clamp01((float)(playable.GetTime() / playable.GetDuration()));
        AdjustEmission(rate);
    }

    void AdjustEmission(float rate)
    {
        var dark = new Color(0, 0, 0, 1);
        var light = new Color(0.362353f, 0.7f, 0.7f, 1);
        if (rate < 0.5) 
            SetEmission(dark);
        else
        {
            var d = rate * 270 / 180 * Mathf.PI;
            rate = Mathf.Abs(Mathf.Sin(d));
            SetEmission(rate < 0.5 ? dark : light);
        }
    }
}
}

PlayableBehaviourを継承したほうのスクリプトに、具体的な動きを記述します。
public override void PrepareFrame(Playable playable, FrameData info)
の中が、Timelineのスライダーが動くたびに呼ばれるところで、このスクリプトがアタッチされている箇所にスライダーが差しかかったときに、点滅の処理をします。

タイムライン上の範囲をrateという変数で表現します。rateは時間経過とともに0から1に変化します。このように、前半は光が消えている状態です。

if (rate < 0.5) 
    SetEmission(dark);

後半はrateを0~270°の角度に置き換え、sin式を使っています。

else
{
    var d = rate * 270 / 180 * Mathf.PI;
    rate = Mathf.Abs(Mathf.Sin(d));
    SetEmission(rate < 0.5 ? dark : light);
}

図にするとこんな感じです。
f:id:inosyan:20180115081802p:plain

再生するとこのようになります。
f:id:inosyan:20180115081823g:plain

プログラムでアニメーションを表現する際には、このようにsinをよくつかいます。
続きはまた後日書きたいと思います。

UnityのShaderによる映像制作の可能性

f:id:inosyan:20180107231719p:plain UnityShaderをさわっていたら、このような映像ができました。

f:id:inosyan:20180107232409p:plain f:id:inosyan:20180107233033p:plain


Light sphere

テクスチャを貼らないでShaderだけでどんな表現ができるのだろうと試していたら、このような映像ができました。
Unityのシーンは、球体にシェーダーを適用したマテリアルを貼り、その中をカメラが動いているシンプルなものです。
カメラのアニメーションにはTimelineを、ムービー用の連番画像の書き出しには Unityの Recorder を使いました。

f:id:inosyan:20180107234906p:plain

このシェーダーは、カメラからの距離によって模様や色が変わるようにしてあるので、カメラが動くことで万華鏡のような模様が現れます。

Shader "LightSphere" for Unity

自分で書いたShaderなら、テクスチャと標準のシェーダーとの組み合わせではできない面白いことができそうです。 しばらくShaderを色々試してみようと思います。