20191224のUnityに関する記事は12件です。

AmplifyShaderEditorのPostProcessであそぶ

はじめに

 ある日「カメラエフェクト作りたいな・・・」と思いAmplifyShaderEditor(以下ASE)でPostProcessが作れると知り、ASE自体ぺろぺろした程度しか知識はありませんが、調べたことをメモしていきます。

動作環境

Windows10
Unity 2019.2.17f1
AmplifyShaderEditor 1.7.5

準備

①新規でシェーダーを作る

 ASEポスプロ用シェーダーはCreate→AmplifyShader→Legacy→PostProcessの順で新規作成できます。ここでは仮に「pp_01」と名付けました
2101fe13784550616066d9a1a1b341cbd0.png

 あと、マテリアルも作成しシェーダーをpp_01に設定しておきましょう。
image.png

②Cameraにマテリアルをつけたい!

44a8e44c127c17ac209501ba42c57ef7_600.jpg
 とりあえずカメラにシェーダーをかけないとプレビューできないので、ASEが丁寧に用意してくれたPostProcessExampleをカメラにAddComponentします。
2101fe13784550616066d9a1a1b34cbd0.png
 そして先ほど作ったマテリアルをPostProcessMatに設定しましょう。スクリプトをアクティブにして準備完了です!
image.png

シェーダーを作る

 今は画面が真っ白なので、ここから画面を映していきます。
image.png
ダランディさんが制作したキョンシー娘の女の子を使わせていただきました!ダランディさんありがとうございます!!!!

とりあえず画面を映す

 Template parameterを用意しましょう、そこからTextureSampleをつなぎ描画させます。
2101fe13784550616066d9a1a1b3411cbd0.png
image.png

あそぶ

PixelateUVでモザイクっぽさをだす

Texture CoordinatesからPixelateUVをつなぎ、モザイク化させてみました、PixelateUVのPixel XとYにfloatノードをつなげば解像度を変えれます。
にしても、これはなかなか遊べそう・・・。
image.png

階調化させる

TextureSampleからPosterizeをつなぎ階調化させました、PowerにFloatを繋ぎ階調化数を変えれます。
image.png

ここまできたらアレしかない

PosterizeからGlayScale(白黒化)ノードを繋ぎ、HSV to RGBで「Hue」「Saturation」をいじり緑色にします、ゲームボーイっぽさが出ました。
image.png

まわす

まわしました(?)TextureCoordinatesからRotator(回転)を繋いでRotatorノードのAnchorにVector2を繋ぎ中心点をX0.5、Y0.5に設定します。
TimeParameterとFloatをMultipyして速度を調節します。
7bd2a8c0dd44732ee71c06234e52de04.gif

おわりに

ぎこちない文章で申し訳なかったのですが、参考にしてもらえればと思います。
みんなもポスプロで遊ぼう!

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

Unity2019の2D Animationでいらすとやを走らせた

はじめに

前回の記事ではUnity2018の2D Animationでいらすとやの素材を使ってスプライトアニメーションを作りました。

1年経ってUnity2019.3が出て、2D Animationもとうとう preview が外れたらしいので改めてまとめました。

今回は前回より少し頑張って走るアニメーションです。
(色々荒いのが見えてますが、これが私の限界です...)

irasutoya_run.gif

なお環境は

  • Unity 2019.3
  • 2D Animation 3.0.8

です。

作り方

2D Animationを[Window] > [Package Manager]からimportしてください。

動かしたい画像をUnityのプロジェクトにドラッグ&ドロップなどで追加します。
その際、inspectorから画像のTexture Typeが Sprite (2D and UI) になるようにします。

inspectorの[Sprite Editor]を押すと編集windowが開きます。
window左上のプルダウンから Skinning Editor を選択します。

image.png

[Create Bones] から設定したいようにボーンを追加していきます。
また、ボーンを消したい場合は ⌘+[delete] で消せます。(windowsなら ctrl+deleteかな?)

image.png

画像の輪郭線を Geometry から設定します。[Auto Geometry] を選択し、3つのパラメータをいじって[Generate For Selected]を押すと、輪郭が設定できます。
Weights にチェックを入れておくと同時に設定してくれるようです。

image.png

値をいじって再生成して合わせるなり、[Edit Geometry]から自分で編集するなりして下さい。

image.png

ちなみにですが、この時点でボーンを動かしてみることが出来ます。

arm.gif

また、編集も⌘+[z]でUndoできるなど、機能のパワーアップが見られます。

ここから先はあまり変わっておらず、空のGameObjectをシーンに追加して、先ほど編集した画像を空のGameObjectにドラッグします。

そして、inspectorの [Add Component] から Sprite Skin を追加して、[Create Bones] すればOKです。

image.png

しかし、

鞄を持った腕を動かすとあられもない姿になります。

スクリーンショット 2019-12-24 17.34.20.png

一枚絵にボーンを設定しているので、肩のボーンを動かすと顔にも影響してしまいます。おまけに鞄が体にめり込んでしまっています。

流石にこの辺りが一枚絵で行う限界のようです。走るアニメーションのためパーツ分けを行いました。
(2D Animationの説明はここでほとんど終わりになります)

クオリティアップのために

パーツ分け

パーツ分けには PhotoShop を使いました。ほぼ初めて使うツールでしたが、なんとかなりました。
無料でやるなら GIMP などで作業することになると思います。

パーツ分けは Live2Dのページを参考にしました。

基本的には腕や顔など切り分けたい箇所が含むように四角で選択。選択部分を別レイヤにコピーして、腕など目的の部分以外を消しゴムツールで削いでいく。そして全パーツできたら1枚の画像に合わせるという手順で作りました。(もっと確立された方法があると思いますが)

スクリーンショット 2019-12-24 18.39.18.png

今回は諦めましたが、さらにクオリティを高めるなら、

  • 腕や胴の接合部は丸くする
    • 角ばってると動かした時に不自然見える
  • 消し残しを徹底的に消す
    • あとでUnityでSpriteを分ける時に、ゴミのピクセルも拾ってしまう

必要もあるかと思いました。

Unityにインポート

PhotoShopで出力した画像をUnityのプロジェクトに持ってきます。
そして1枚の画像を複数のSpriteに分けるのですが、Sprite Modeを Multiple にしないとSprite Editorの[Slice]が選択できないので注意してください。

スクリーンショット 2019-12-24 17.40.33.png

前述のゴミピクセルがあるとここのSpriteが必要以上に増えます。
(不要な領域は⌘+[delete]で消せます)

ボーンの設定もこの時行いますが、一枚絵では無くなったので設定するボーンも減ります。基本的には

  • 頭など関節の無いSprite

    • ボーンを1本だけ設定する
  • 腕など関節を設定したいSprite

    • ボーンを設定する
    • GeometryとWeightも設定する

ことになると思います。関節のないSpriteでも、ボーンを設定しないと回転中心が画像の真ん中になってしまうので、ボーンを設定した方が意図通りに回転させやすいです。
顔だとボーンを設定すれば首中心に回転させられますが、無ければ鼻のあたりが回転の中心になってしうといった具合です。

組み立てる

前回同様、空のGameObjectに画像を設定します。しかし今回はSpriteが分かれているので、Spriteの数だけ追加してやる必要があります。 Sprite Skin の設定とボーンの作成もこの時にやっておきましょう。

スクリーンショット 2019-12-24 19.27.26.png

この時は人の形をしていません。自力で人の形に直す必要があります。この時ボーンの親子関係も考慮した階層にした方があとで楽かもしれません。

スクリーンショット 2019-12-24 19.29.14.png

この時点で Prefab 化しておくのが無難でしょう。

この時少し混乱したのが、先ほど Sprite Skin を設定したGameObjectを回転させてもボーンの回転にはならないということです。ボーンの末端を軸に動かしたいのであれば、子の Transform を回転させてください。

これで肩を回しても顔などに影響しなくなりました。

Sorting Order

しかし、腕が体にめり込む問題が依然としてあります。
Sprite Skin には Order in Layer の設定があり、Sprite同士の前後関係を定義できます。整数で設定しますが、数が大きいほど前に来ます。負の値も設定可能です。

デフォルトでは default のLayerにSpriteが属しているので、そのLayerの中での前後関係になります。別途Layerも追加定義すれば、レイヤ同士の前後関係も定義できるようです。

胴を基準に 0 として以下のように設定しました。

  • 右手と右足:-1
  • 胴:0
  • 顔と左足:1
  • 左手と鞄:2

あとはUnityでキーフレームアニメーションを作れば最初のアニメーションの完成です。

感想

最近のスマホゲームのアニメーションを作ってる人たちは凄まじいなと思いました。

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

パスに沿ってなめらかに一定速度でオブジェクトを移動させる実装の解説

my.gif

まえがき

問題

ゲームだと、パスに沿ってキャラを移動させたい時があります。
ただ、単純に実装すると速度がバラバラになってしまいます。

目的

今回は曲線でもだいたい一定の速度で動かせるようにする実装を解説します。
UnityならDoTweenのDoPathやCinemachineのCinemachinePathを使用すれば実装しなくても可能ですが、ベジェ曲線のみで、他の曲線を使うことはできません。
自分で実装すれば、好きな曲線を使えます。
ただ、今回は解説のため、ベジェ曲線を使います。

https://ja.wikipedia.org/wiki/ベジェ曲線

Vector3 Bezier3(float t, Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4)
{
    var d = 1 - t;
    return      d * d * d * p1 +
            3 * d * d * t * p2 +
            3 * d * t * t * p3 +
                t * t * t * p4;
}

参考

この記事はCinemachineのCinemachinePathBaseを参考に作っています。
github

解説

単純な実装

first.gif

これはベジェ曲線の引数tに$Time.t$を渡すだけの実装です。
明らかに速度がおかしいです。
0-0.1と0.5-0.6の移動距離が違うのと、
スタート位置から1つ目のパスまで1秒、そこからゴールまで1秒かけているのが原因です。

void Update()

{
    var t = Time.time;
    var indexA = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, t));
    var indexB = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, indexA+1));

    if (indexA == indexB) return;

    transform.position = CalcPos(t);
}

Vector3 CalcPos(float t)
{
    var indexA = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, t));
    var indexB = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, indexA+1));
    return Bezier3(
        t-indexA,
        Paths[indexA].Pos,
        Paths[indexA].Pos + Paths[indexA].Tangent,
        Paths[indexB].Pos - Paths[indexB].Tangent,
        Paths[indexB].Pos);
}

改善した物

my.gif

この問題はtを0-ベジェ曲線の長さで扱えれば速度をだいたい一定にすることができます。

やっていることは、tを少しずつ動かして、進んだ距離を測る。
進んだ距離からtを返すテーブルを作るという事です。

以上です。
こうすると、距離からtに変換する関数を作成できるので、だいたい一定の速度で移動できるようになります。

コード
[SerializeField] int Segment;
[SerializeField] PathContainer Paths;

float PathLength;

//セグメントの総数
int NumKeys;
float[] DistanceToTArray;
float DistanceStepSize;

void Start()
{
    Build();
}

void Build()
{
    PathLength = 0;
    NumKeys = (Paths.Length-1) * Segment+1;

    var tToDistance = CalcTToDistance();
    DistanceToTArray = CalcDistanceToT(tToDistance);
}

void Update()
{
    transform.position = CalcPos(DistanceToT(Time.time*PathLength/2));
}

//距離からtに変換
float DistanceToT(float distance)
{
    float d = distance / DistanceStepSize;
    int index = Mathf.FloorToInt(d);
    if(index>=DistanceToTArray.Length-1)return DistanceToTArray[DistanceToTArray.Length-1];
    float t = d - index;
    return Mathf.Lerp(DistanceToTArray[index], DistanceToTArray[index+1], t);
}

//tをSegmentに分割して進んだ距離を配列に入れて返す
float[] CalcTToDistance()
{
    var tToDistance = new float[NumKeys];

    var pp = Paths[0].Pos;
    float t = 0;
    for (int n = 1; n < NumKeys; n++)
    {
        t += 1f / Segment;
        Vector3 p = CalcPos(t);
        float d = Vector3.Distance(pp, p);
        PathLength += d;
        pp = p;
        tToDistance[n] = PathLength;
    }

    return tToDistance;
}

//距離をSegmentに分割してその位置のtを配列に入れて返す
float[] CalcDistanceToT(float[] tToDistance)
{
    var distanceToT = new float[NumKeys];
    distanceToT[0] = 0;
    DistanceStepSize = PathLength/(NumKeys-1);
    float distance = 0;
    int tIndex=1;
    for (int i = 1; i < NumKeys; i++)
    {
        distance += DistanceStepSize;
        var d = tToDistance[tIndex];
        while (d < distance && tIndex < NumKeys - 1)
        {
            tIndex++;
            d = tToDistance[tIndex];
        }

        var prevD = tToDistance[tIndex - 1];
        float delta = d - prevD;
        float t = (distance - prevD) / delta;
        distanceToT[i] = (1f/Segment)*(t + tIndex - 1);
    }

    return distanceToT;
}

Vector3 CalcPos(float t)
{
    var indexA = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, t));
    var indexB = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, indexA+1));
    return Bezier3(
        t-indexA,
        Paths[indexA].Pos,
        Paths[indexA].Pos + Paths[indexA].Tangent,
        Paths[indexB].Pos - Paths[indexB].Tangent,
        Paths[indexB].Pos);
}


あとがき

以上です。
動作するコードはこちらです。
https://github.com/nakajimakotaro/PathSmoothMove

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

AnimationCurveを使わずにVRMをC#で走らせてみる【Unity】

これは Unity Advent Calendar 2019 の25日目の記事です。
昨日は @youri_ssk さんによる 2.5Dキャラクターアニメーション - Mirror Animation Playable でした。

Animation Curveを使わずにC#だけでVRMを走らせてみる

Animation Curveを使えば、便利なGUIで3Dアニメーションを作ることができます!

そう!普通の人ならAnimation Curveを使いましょう!!(もちろん、BlenderとかUnity外のツールでもいいです)

しかし!Qiita読者の皆さんは プログラマー なんです!

プログラマーだったらプログラミングで3Dアニメーションを作りたいですよね!

というわけで、VRMアバターをC#で動かしてみます!

最終的にはこんな感じのアニメーションが作れました。

201912241856a.gif

動作環境

以下の環境でやりました。

  • Windows 10 64bit
  • Unity 2019.2.11f1
  • UniVRM 0.54.0

初期状態

とりあえずVRMファイルをシーンに読み込んでみます。

今回は 今里尚吾 くんにご協力いただきます!(VRoid Studioで作りました!)

image.png

Hierarchyを見てみたらこんな感じでした!

image.png

股関節を30度に曲げてみる

まずは、股関節を曲げてみましょう。

以下のスクリプトを作成します。

using System;
using UnityEngine;

public class PendulumRunning : MonoBehaviour
{
    // transformを保管する変数
    private Transform cHips;
    private Transform lUpperLeg;
    private Transform rUpperLeg;

    void Start()
    {
        // 腰のtransformを取得
        cHips = transform.Find("Root")
                        .Find("J_Bip_C_Hips");

        // 股関節のtransformを取得
        lUpperLeg = cHips.Find("J_Bip_L_UpperLeg");
        rUpperLeg = cHips.Find("J_Bip_R_UpperLeg");
    }

    void FixedUpdate()
    {
        // 脚を30°傾ける
        lUpperLeg.rotation = Quaternion.AngleAxis(-30.0f, Vector3.right);
        rUpperLeg.rotation = Quaternion.AngleAxis(30.0f, Vector3.right);
    }
}

作成できたら、シーン内のVRMにアタッチします。

image.png

脚が前後に開きました!

image.png

image.png

振り子のように足を揺らしてみる

せっかくなので、アニメーションさせたいですよね。

ということで、振り子のように揺らしてみます!

    void FixedUpdate()
    {
        // 1秒周期の振り子を用意する
        float pendulum = (float)Math.Sin(Time.time * Math.PI);

        // 股関節を右軸(x軸)を中心に±60°幅で揺らす
        lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum, Vector3.right);
        rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum, Vector3.right);
    }

201912241743a.gif

いいかんじ!

膝を曲げてみる

膝も曲げてみたくなったので、 LowerLeg を追加してみました。あと係数とかを微調整したのがこちらです。

public class PendulumRunning : MonoBehaviour
{
    private Transform cHips;
    private Transform lUpperLeg;
    private Transform rUpperLeg;
    private Transform lLowerLeg;
    private Transform rLowerLeg;

    void Start()
    {
        cHips = transform.Find("Root")
                        .Find("J_Bip_C_Hips");
        lUpperLeg = cHips.Find("J_Bip_L_UpperLeg");
        rUpperLeg = cHips.Find("J_Bip_R_UpperLeg");

        // 膝のtransformを取得
        lLowerLeg = lUpperLeg.Find("J_Bip_L_LowerLeg");
        rLowerLeg = rUpperLeg.Find("J_Bip_R_LowerLeg");
    }

    void FixedUpdate()
    {
        float pendulum = (float)Math.Sin(Time.time * Math.PI);

        // 股関節の動きを少し変更
        lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 20.0f, Vector3.right);
        rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum - 20.0f, Vector3.right);

        // 膝を揺らす
        lLowerLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 60.0f, Vector3.right);
        rLowerLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum + 60.0f, Vector3.right);
    }
}

201912241818a.gif

膝も曲がりました。

全身動かしてみる

同じ要領で、全身の関節の動かしてみます。色々調整したら、以下のようなコードになりました。

public class PendulumRunning : MonoBehaviour
{
    private Transform cHips;
    private Transform lUpperLeg;
    private Transform lLowerLeg;
    private Transform rUpperLeg;
    private Transform rLowerLeg;
    private Transform cSpine;
    private Transform cChest;
    private Transform cUpperChest;
    private Transform lShoulder;
    private Transform lUpperArm;
    private Transform lLowerArm;
    private Transform rShoulder;
    private Transform rUpperArm;
    private Transform rLowerArm;


    void Start()
    {
        // 全身の関節のtransformを取得
        cHips = transform.Find("Root")
                        .Find("J_Bip_C_Hips");
        lUpperLeg = cHips.Find("J_Bip_L_UpperLeg");
        lLowerLeg = lUpperLeg.Find("J_Bip_L_LowerLeg");
        rUpperLeg = cHips.Find("J_Bip_R_UpperLeg");
        rLowerLeg = rUpperLeg.Find("J_Bip_R_LowerLeg");
        cSpine = cHips.Find("J_Bip_C_Spine");
        cChest = cSpine.Find("J_Bip_C_Chest");
        cUpperChest = cChest.Find("J_Bip_C_UpperChest");
        lShoulder = cUpperChest.Find("J_Bip_L_Shoulder");
        lUpperArm = lShoulder.Find("J_Bip_L_UpperArm");
        lLowerArm = lUpperArm.Find("J_Bip_L_LowerArm");
        rShoulder = cUpperChest.Find("J_Bip_R_Shoulder");
        rUpperArm = rShoulder.Find("J_Bip_R_UpperArm");
        rLowerArm = rUpperArm.Find("J_Bip_R_LowerArm");
    }

    void FixedUpdate()
    {
        // 速度を3倍に変更
        float pendulum = (float)Math.Sin(Time.time * Math.PI * 3.0f);

        // 脚を揺らす        
        lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 20.0f, Vector3.right);
        rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum - 20.0f, Vector3.right);
        lLowerLeg.localRotation = Quaternion.AngleAxis(-30.0f * pendulum + 60.0f, Vector3.right);
        rLowerLeg.localRotation = Quaternion.AngleAxis(30.0f * pendulum + 60.0f, Vector3.right);

        // 腰にひねりを加える
        cHips.localRotation = Quaternion.AngleAxis(10.0f * pendulum, Vector3.up) * Quaternion.AngleAxis(10.0f, Vector3.right);

        // 胸は腰と反対にひねる
        cChest.localRotation = Quaternion.AngleAxis(-10.0f * pendulum, Vector3.up);
        cUpperChest.localRotation = Quaternion.AngleAxis(-20.0f * pendulum, Vector3.up);

        // 腕を揺らす
        lUpperArm.localRotation = Quaternion.AngleAxis(60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(70.0f, Vector3.forward);
        rUpperArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(-70.0f, Vector3.forward);
        lLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 60.0f, Vector3.up);
        rLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 60.0f, Vector3.up);
    }
}

201912241847a.gif

だんだんそれっぽくなってきました。

ただ、重心が上下しないのはちょっと違和感がありますね。

重心を上下させてみた

重心も上下させてみます。

public class PendulumRunning : MonoBehaviour
{
    private Transform cHips;
    private Transform lUpperLeg;
    private Transform lLowerLeg;
    private Transform rUpperLeg;
    private Transform rLowerLeg;
    private Transform cSpine;
    private Transform cChest;
    private Transform cUpperChest;
    private Transform lShoulder;
    private Transform lUpperArm;
    private Transform lLowerArm;
    private Transform rShoulder;
    private Transform rUpperArm;
    private Transform rLowerArm;

    // 腰の初期位置を保管する変数
    private Vector3 firstHipsPosition;


    void Start()
    {
        cHips = transform.Find("Root")
                        .Find("J_Bip_C_Hips");
        lUpperLeg = cHips.Find("J_Bip_L_UpperLeg");
        lLowerLeg = lUpperLeg.Find("J_Bip_L_LowerLeg");
        rUpperLeg = cHips.Find("J_Bip_R_UpperLeg");
        rLowerLeg = rUpperLeg.Find("J_Bip_R_LowerLeg");
        cSpine = cHips.Find("J_Bip_C_Spine");
        cChest = cSpine.Find("J_Bip_C_Chest");
        cUpperChest = cChest.Find("J_Bip_C_UpperChest");
        lShoulder = cUpperChest.Find("J_Bip_L_Shoulder");
        lUpperArm = lShoulder.Find("J_Bip_L_UpperArm");
        lLowerArm = lUpperArm.Find("J_Bip_L_LowerArm");
        rShoulder = cUpperChest.Find("J_Bip_R_Shoulder");
        rUpperArm = rShoulder.Find("J_Bip_R_UpperArm");
        rLowerArm = rUpperArm.Find("J_Bip_R_LowerArm");

        // 腰の初期値を取得する
        firstHipsPosition = cHips.localPosition;
    }

    void FixedUpdate()
    {
        float pendulum = (float)Math.Sin(Time.time * Math.PI * 3.0f);
        lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 20.0f, Vector3.right);
        rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum - 20.0f, Vector3.right);
        lLowerLeg.localRotation = Quaternion.AngleAxis(-30.0f * pendulum + 60.0f, Vector3.right);
        rLowerLeg.localRotation = Quaternion.AngleAxis(30.0f * pendulum + 60.0f, Vector3.right);
        cHips.localRotation = Quaternion.AngleAxis(10.0f * pendulum, Vector3.up) * Quaternion.AngleAxis(10.0f, Vector3.right);
        cChest.localRotation = Quaternion.AngleAxis(-10.0f * pendulum, Vector3.up);
        cUpperChest.localRotation = Quaternion.AngleAxis(-20.0f * pendulum, Vector3.up);
        lUpperArm.localRotation = Quaternion.AngleAxis(60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(70.0f, Vector3.forward);
        rUpperArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(-70.0f, Vector3.forward);
        lLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 60.0f, Vector3.up);
        rLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 60.0f, Vector3.up);

        // 周期が半分の振り子を用意する
        float halfPendulum = (float)Math.Sin(Time.time * Math.PI * 3.0f * 2.0f);

        // 腰の位置を上下させる
        cHips.localPosition = firstHipsPosition + new Vector3(0.0f, 0.04f * halfPendulum, 0.0f);
    }
}

201912241856a.gif

ちょ、ちょっとだけ、マシになったかな?

ゲシュタルト崩壊してきたので、このあたりで止めておきます。

一応、AnimationCurveは使わずにC#だけでアニメーションを作成することができました!

AnimationClipに保存してみる

せっかくだからAnimationClipに保存してみます。

Unityの GameObjectRecorder というAPIを使えば、シーン実行中のアニメーションをAnimationClipに保存できます。

以下のスクリプトを作成し、シーン内のVRMにアタッチしてください。

using UnityEngine;
using UnityEditor.Animations;

public class RecordTransformHierarchy : MonoBehaviour
{
    public AnimationClip clip;

    private GameObjectRecorder m_Recorder;

    void Start()
    {
        m_Recorder = new GameObjectRecorder(gameObject);
        m_Recorder.BindComponentsOfType<Transform>(gameObject, true);
    }

    void LateUpdate()
    {
        if (clip == null){
            return;
        }
        m_Recorder.TakeSnapshot(Time.deltaTime);
    }

    void OnDisable()
    {
        if (clip == null){
            return;
        }

        if (m_Recorder.isRecording)
        {
            m_Recorder.SaveToClip(clip);
        }
    }
}

適当なフォルダーに空のAnimationClipファイルを作成します。

image.png

ここでは仮に pendulum-running というファイル名にします。

image.png

これを RecordTransformHierarchyClip に割り当てます。

image.png

これでシーンを実行すれば、AnimationClipに動きが保管されます!

FBX Exporter等を使えば、FBXに変換することもできますね!

さいごに

C#でもAnimationClipを作成することができました!

そのことに、果たして意味があるかはわかりませんが、個人的には Quaternion の勉強をできたのが収穫です。

本記事作成にあたり、以下の記事を参考にさせていただきました。ありがとうございました。

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

【Unity】ParticleSytemのCustomDataを使うときの処理をcgincにまとめる

開発環境 : Unity2018.4.3f1

こちらの記事ではUnityのShaderでParticleSystemのCustomDataを利用するときの処理を共通化についてになります。
具体的にはテクスチャのUVスクロールをCustomDataで実現する場合になります。

cgincで共通のコードを定義

UnityのShaderを書く際に重複コードになりそうな処理をcgincにまとめておくことができます。
今回は次のようなコードを定義しました。

#ifndef My_PARTICLE_INCLUDED
#define My_PARTICLE_INCLUDED

// CustomDataのプロパティ設定
#define SET_CUSTOM_DATA_PROPERTY(name) uniform int name##U; \
        uniform int name##USwizzle; \
        uniform int name##V; \
        uniform int name##VSwizzle;

// 該当のCustomDataを取得    
#define GET_CUSTOM_DATA(name) half2(customData[name##U][name##USwizzle],customData[name##V][name##VSwizzle])

#endif

SET_CUSTOM_DATA_PROPERTY は4つプロパティを定義している箇所をまとめています。
GET_CUSTOM_DATAはプロパティで設定した値を使ってCustomDataから必要なパラメータを取得しています。

Shaderで利用する

プロパティ定義する

[HideInInspector]_MainU ("_mainucoord", int) = 2
[HideInInspector]_MainUSwizzle ("_mainuswizzle", int) = 0
[HideInInspector]_MainV ("_mainvcoord", int) = 2
[HideInInspector]_MainVSwizzle ("_mainvswizzle", int) = 0       

cgincをSubShaderのPassに定義します。

#include "my_Particle.cginc"

Passにプロパティを定義します。
ここで SET_CUSTOM_DATA_PROPERTYを使います。

SET_CUSTOM_DATA_PROPERTY(_Main);

UVスクロール

バーテックスShaderでの処理を次のようになります。
customData1とcustomData2はCustomDataのストリームです。

half4x4 customData = { v.customCoords1, v.customCoords2, half4(0,0,0,0), half4(0,0,0,0)};
half2 mainCustomCoord = GET_CUSTOM_DATA(_Main);
o.uv.xy = TRANSFORM_TEX(v.texcoords.xy,_MainTex) + mainCustomCoord;

最後に

cgincに定義をしておけば複数のShaderで使い回すことができますので、共通化して利用できそうな処理があればまとめておくと良いです。

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

AzureKinectの赤外線画像をUnityで見る

概要

小ネタです。
AzureKinectから得られた赤外線画像をUnityで見れるようにしようとしたけど、
UnityのTextureに変換する所に一瞬つまづいたので、その解決策の忘備録も含めています。

結果

以下のようにUnityで赤外線画像が見れるようになります。

AzureKinectから赤外線画像を取得してUnityで見れる形にする

https://github.com/microsoft/Azure-Kinect-Sensor-SDK/issues/490
IRデータの取り扱い方は上のリンク先の投稿を参考にしました。

IR画像のサイズはdepthのwidth,heightと同じで、データ的には16bitでIRの輝度が書かれています。
このデータをUnityのRenderTextureに変換するにはデータを変換する必要があります。
この変換はshader内でやってしまうのが一番速度が速いです。

以下に、Unity上でKinectからIRデータを取得してUnityのRenderTextureに変換するコードを乗っけます。

また、あくまでサンプルなので簡潔に書いているため実用的なソースではありません。本来はAzureKinect側からデータを受け取る側を別スレッドにすべきだし、またKinectは最高30FPSなのでUpdateでデータの処理をするのは無駄なのでそこら辺の考慮もすべきです。

using UnityEngine;
using UnityEngine.Experimental.Rendering;
using Device = Microsoft.Azure.Kinect.Sensor.Device;

/// <summary>
/// IR画像取得サンプル
/// </summary>
public class TestIR : MonoBehaviour
{
    /// <summary>
    /// ColorFormatをR8_UNormに設定したRenderTexture(width,heightはDepth画像に合わせる)
    /// </summary>
    [SerializeField] private RenderTexture _irTexture = null;
    [SerializeField] private Shader _unprojectIRshader = null;
    [SerializeField] private float _maxIRValue = 10000;
    [SerializeField] private float _minIRVaue = 0;

    private Material _material;
    private Texture2D _tempIR;
    private Device _kinectDevice;


    // Start is called before the first frame update
    void Start()
    {
        //GraphicsFormat.R8_UNorm
        _material = new Material(_unprojectIRshader);
        _tempIR = new Texture2D(_irTexture.width, _irTexture.height, GraphicsFormat.R16_UNorm, 0);
        _kinectDevice = Device.Open();
    }

    // Update is called once per frame
    void Update()
    {
        //irのデータ配列を取得
        var capture = _kinectDevice.GetCapture();
        var irData = capture.IR.Memory;

        //シェーダーに放り込むソース用のテクスチャにロード
        _tempIR.LoadRawTextureData(irData.ToArray());
        _tempIR.Apply();

        capture.Dispose();

        //変換元のソースのテクスチャをセット
        _material.SetTexture("_IrTexture", _tempIR);

        //IR輝度の最大値(画像の見え方の調整)
        _material.SetFloat("_MaxValue", _maxIRValue);

        //IR輝度の最小値(画像の見え方の調整)
        _material.SetFloat("_MinValue", _minIRVaue);

        var prevRT = RenderTexture.active;
        //変換先のテクスチャをセット(ColorFormatをR8_UNorm)
        Graphics.SetRenderTarget(new RenderBuffer[1]{ _irTexture.colorBuffer }, _irTexture.depthBuffer);
        Graphics.Blit(null, _material, 0);

        RenderTexture.active = prevRT;
    }

    private void OnDestroy()
    {
        if (_material != null) Destroy(_material);
        if (_tempIR != null) Destroy(_tempIR);
    }
}

以下はshader側の処理です。

Shader "UnprojectIR"
{
    CGINCLUDE
    #include "UnityCG.cginc"

    texture2D _IrTexture;

    float _MaxValue;
    float _MinValue;

    //16bitをR8_Normフォーマットに変換(0~1.0)
    inline float GetIR(texture2D source, int tx, int ty, int vmax, int vmin)
    {
        int v = source[uint2(tx, ty)] * 65535;
        float colorValue = 0.0f;
        if (v <= vmin)
        {
            colorValue = 0.0f;
        }
        else if (v >= vmax)
        {
            colorValue = 1.0f;
        }
        else
        {
            colorValue = (float)(v - vmin) / (float)(vmax - vmin);
        }
        return colorValue;
    }


    void Vertex(
        float4 position : POSITION,
        out float4 positionOut : SV_Position,
        inout float2 texCoord : TEXCOORD0
    )
    {
        positionOut = UnityObjectToClipPos(position);
    }

    void Fragment(
        float4 position : SV_Position,
        float2 texCoord : TEXCOORD0,
        out float colorOut : SV_Target0
    )
    {
        uint w, h;
        _IrTexture.GetDimensions(w, h);

        // Texture index
        uint tx = texCoord.x * w;
        uint ty = texCoord.y * h;

        float ir = GetIR(_IrTexture, tx, ty, _MaxValue, _MinValue);
        //output write
        colorOut = ir;
    }

    ENDCG

    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        Pass
        {
            CGPROGRAM
            #pragma vertex Vertex
            #pragma fragment Fragment
            ENDCG
        }
    }
}

まとめ

UnityでAzureKinectの赤外線を見る方法を調べた。これを利用して以前の別記事でやったような再帰性反射材をつかったトラッキングをやってみたい。

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

EPSONスマートグラスMOVERIOにアプリをビルドしてみた

Unityのアセットを使ってMOVERIOにAPPビルドしてみたメモ
MOVERIOはAndroid5.1のようなのでSDKも同じバージョンを用意するようにしました

BT-350を体験してみた感想

BT-350を使ってみた感じは、中身はAndoridタブレットのUIでした
トラックパッドなコントローラーあるですが使いづらい感じでした。。
表示は輝度も高く見やすいですが、歩きながらだと、注視点が画面になるので、ぶつかりそうで危ない感じですた
映画に出てくるような、スマートグラス感は無いです

Android5.1のビルド環境を準備

Unity HubなどでAndroid書き出し環境のモジュールをインストール

NoName_2019-12-24_11-48-56_No-00.png

AndroidStudioでSDKをインストール

Tools > SDK Manager > AndroidSDK でAndroid 5.1 (Lollipop)をインストール
NoName_2019-12-24_11-50-33_No-00.png

Unityでサンプルプロジェクトをビルド

NoName_2019-12-24_11-53-9_No-00.png

参考サイト

スマートグラス・スマートヘッドセット
https://www.epson.jp/products/smartglasses/

Moverio BT-300 Unity Plugin
https://assetstore.unity.com/packages/tools/integration/moverio-bt-300-unity-plugin-83627

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

WebGLでビルドされたUnityとコンソールアプリケーション間で通信したい

WebGLのUnityとコンソールアプリを通信したい

ブラウザ上に表示されるWebGLビルドされたUnityのアプリケーションと,ローカルで起動したコンソールアプリケーションを通信できるようにしたいときってありますよね.

スタンドアロンなUnityアプリケーションとコンソールアプリケーションを通信するだけであれば,TCPソケットはもちろん,Websocketでも接続できます.それから推察するに,WebGLビルドされたUnityのアプリケーションとコンソールアプリケーションもWebsocketを使えば通信できそうですが,なぜかそうはいかないようです(参考).

当エントリーでは,WebGLビルドされたUnityのアプリケーションとコンソールアプリケーションを通信できるように頑張った結果を報告します.もしかしたらよりエレガントな解決策があるかもしれません.

以下環境設定.
- Unity: 2019.1.8f1
- chrome: 79.0
- Windows: 10 pro 1903

解決策

*.jslibにWebsocketで通信するjavascriptを書き,Unityのスクリプト(C#)ではjavascriptが受け取ったメッセージをUnityの空間に持ち上げるようにする.

具体的には?

1. ファイルの用意

まずUnityのAssetsディレクトリ以下に,ファイルを二つ作ります.ファイル名はなんでもいいのですが,便宜上以下のものに決めます.階層構造は一応順守で.
- Assets/Plugins/Connector.jslib
- Assets/Scripts/Main.cs

2. Connector.jslibの編集

WebGLビルドされたUnityはこのファイルを通じてjavascriptの空間にアクセスできるようです.詳細は以下をURLを.
- https://qiita.com/gtk2k/items/1c7aa7a202d5f96ebdbf
- https://docs.unity3d.com/jp/540/Manual/webgl-interactingwithbrowserscripting.html

このファイルを以下のように編集し,まずはjavascript上でWebsocketのクライアントを作成します.

Connector.jslib
var ws;
var messages;

mergeInto(LibraryManager.library, {

  Initialize: function (server) {
    ws = new WebSocket( Pointer_stringify(server) );

    messages = new Array();

    ws.onmessage = function (evt) {
        var message = evt.data;

        messages.push( message );
    };

    ws.onopen = function () {
        console.log("system::open")
    };

    ws.onclose = function () {
        console.log("system::close")
    };
  },

  GetMessage: function () {
    var msg = "";

    if( messages.length > 0 ) {
        msg = messages.shift();
    }

    var buffer = _malloc( lengthBytesUTF8(msg) + 1 );
    writeStringToMemory(msg, buffer);
    return buffer;
  },

  SendMessage: function (message) {
    if (ws) {
        ws.send( Pointer_stringify(message) );
    }
  },

  Close: function () {
    if (ws) {
        ws.close();
    }
  }

});

3. Main.csの編集

次にMain.csを以下のように編集します.

Main.cs
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine;

public class Main : MonoBehaviour
{
  private GameObject logger;
  private Queue<string> ReceivedMessages = null;
  private static string server = @"ws://localhost:2333/ConnectionTest";

  [DllImport( "__Internal" )]
  private static extern void Initialize( string server );
  [DllImport( "__Internal" )]
  private static extern string GetMessage();
  [DllImport( "__Internal" )]
  private static extern void Send( string message );
  [DllImport( "__Internal" )]
  private static extern void Close();


  private void OnDestroy() {
    Close();
  }

  void Start() {
    logger = GameObject.Find( "Logger" );

    ReceivedMessages = new Queue<string>();

    Initialize( server );
  }


  private static string Formatter( string message ) => $"message from server:\n{message}";

  void Update() {

    string message = GetMessage();

    if( message != "" ) {
      ReceivedMessages.Enqueue( message );

      Debug.Log( $"Received -> {message}" );
    }

    if( ReceivedMessages.Any() ) {

      string line = ReceivedMessages.Dequeue();

      logger.GetComponent<TextMesh>().text = Formatter( line );
    }


  }
}

UnityのScene上にテキストを表示できるGameObjectを配置し,受け取ったメッセージを表示するようにしておきました.下図参照.
unity.png

ちなみにこのスクリプトは,javascript上に定義された関数をインポートして呼び出しているので,WebGLビルド以外ではそれら関数が呼び出せずエラーを吐くはずです.つまり,WebGLビルドしないと動作確認できません.

変数serverには,Websocketサーバーが起動しているPCのIPアドレスを入力します.今はlocalhostとしていますが,LAN内の別PC (192.168.11.*)を指定しても接続できていました.

4. UnityをWebGLビルドし実行,接続確認

ソースコードの編集が終わったのち,UnityをWebGLビルドします.方法は適当に調べてください.

作成されたindex.htmlをchromeで開くと,下図のようにUnityのプレーヤーの初期化時にエラーが生じて実行できない(Firefoxでは実行できるとかなんとか)ようです.エラーメッセージ見ると,ウェブサーバーにアップロードしろ的な感じなので,ビルドにより生成されたすべてのファイルをウェブサーバーにアップロードします.
error.png

そして,Websocketサーバーとしての機能を持つコンソールアプリケーションをあらかじめ作成しておき,起動します.Websocketサーバーはnode.jsで作る人が多いようですが,筆者はC#信者なのでC#で作りました.ライブラリとしてwebsocket-sharpにお世話になりました.

ローカルでWebsocketサーバーを立ち上げたのち,ブラウザから先ほどのindex.htmlにアクセスします.その後ローカルのサーバーからメッセージを送信すると,ブラウザ上のUnityにメッセージが表示されました(下GIF参照).

setuzoku_dekita.gif

このGIFの上半分はindex.htmlにアクセスしているブラウザの様子で,下半分はローカルに起動しているWebsocketサーバーの様子です.Websocketサーバーのコンソールに文字を入力し,エンターキーを入力すると,それまでの文字列がブラウザ上のUnityに送信されていることが確認できます.ブラウザ上のUnity上に表示されている文字が多少見切れていますが,全画面表示にするといい感じに表示されています.

おわりに

エレガントな方法かどうかはわかりませんが,ブラウザのUnityアプリケーションとローカルのコンソールアプリケーションが通信できるようになりました.

そういえば,ここによればjavascriptの空間からC#のスクリプト上に定義されたメソッドを呼ぶこともできるようなので,うまくやればメッセージがあるかどうかを無意味にビジーウェイトしなくてもよくなりそうです.

後日暇を見つけて,作成したUnityのスクリプトなどをGithub上にアップロードしてようと思います.Unityのエディター上でも動作確認できるような工夫もしていたり,Websocketのサーバーアプリケーションもアップロードしておこうかなあとも思っていたり.

これでなんか面白いアプリケーション作れないかな...

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

[Unity] Box Projectionで屋内シーンの反射による映り込みの品質を向上する

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


Unity標準の ReflectionProbe のオプションにある Box Projection を設定することで、屋内シーンの反射の破綻を軽減して、映り込みの品質を向上する方法を紹介します。

百聞は一見に如かずということで、 Box ProjectionをON/OFF した動画を用意しました。

※画像が読み込まれるまでお待ちください Box Projection

  • Box Projection: OFF では、カメラの位置が無視されて、家具などの映り込みの位置がずれてしまっています。
  • Box Projection: ON では、カメラの位置が正しく考慮されて、家具などの映り込みの位置が正しくなっています!

環境

  • 2018.4.13f1 (LTS)
  • Build-in Rendering Pipeline および LWRP(Light Weight Render Pipeline)

Box Projectionとは、どんな技術なのか?

公式マニュアルにもBox Projectionの解説はあるのですが、ちょっと初心者には理解しずらかったので、簡単に解説をします。

端的に言うと、Cubemap(ReflectionProbe)のテクスチャをサンプリングするための反射ベクトルの向きを、カメラの位置と部屋の広さを定義するBoundingboxを考慮して補正する手法です。

普通のCubemapとの違いは、反射ベクトルの計算の違いだけなので、GPUの処理負荷もそれほど高くはありません。

反射はしていますが、レイトレをするわけではないので、RTXも不要です!

Parallax-Corrected Cubemapの解説


※Box Projectionの理論に興味がない人は、この節はスキップして本題から読んでください。
Unity標準のStandardShaderでBox Projectionは実装済みなので、理論は理解しなくても使うことはできます。


UnityではBox Projectionという名称ですが、Parallax-Corrected Cubemap とも呼ばれる手法です。

Parallax-Corrected Cubemapの具体的な理論と実装については、このスライドに非常に詳しく解説してあります。

この図は上記のスライドから引用しました。

image.png

この図を用いて簡単にParallax-Corrected Cubemapの解説を行います。基本的な考え方はそれほど難しくはありません。

まず各記号の説明をします。

  • $\vec{V}$: カメラのビューベクトル
  • $C$: ReflectionProbeの位置
  • $\vec{N}$: 法線
  • $\vec{R}$: 反射ベクトル(補正前)
  • $\vec{R'}$: 反射ベクトル(補正後)
  • $P$: $\vec{R}$と壁の交差点

$\vec{R}$ 方向をサンプリングすれば、 普通のCubemap ですが、
$\vec{R'}$ 方向をサンプリングすると視差が考慮された Parallax-Corrected Cubemap になります。

そして、図が示すように $\vec{R}$ を補正した $\vec{R'}$ は

\vec{R'} = P - C

によって計算できます。

$C$ は既知なので、 $\vec{R}$と壁の交差点である $P$ を計算すれば、$\vec{R'}$ が求まります。

ベクトルとAABBの交差判定のアルゴリズムを用いれば、$P$ は計算ができます。

HLSLによる Parallax-Corrected Cubemap の反射ベクトル $\vec{R'}$ を求める実装例です(MITライセンス)。

// Parallax-Corrected Cubemap
// https://seblagarde.files.wordpress.com/2012/08/parallax_corrected_cubemap-siggraph2012.pdf
float3 calcParallaxCorrectedCubemapReflect(float3 worldPos, float3 worldReflect, float3 cubemapPos, float3 boxMin, float3 boxMax)
{
    // AABB と 反射ベクトル の交差点 worldIntersectPos を計算します
    float3 firstPlaneIntersect  = (boxMax - worldPos) / worldReflect;
    float3 secondPlaneIntersect = (boxMin - worldPos) / worldReflect;
    float3 furthestPlane = max(firstPlaneIntersect, secondPlaneIntersect);
    float dist = min(min(furthestPlane.x, furthestPlane.y), furthestPlane.z);
    float3 worldIntersectPos = worldPos + worldReflect * dist;

    // Cubemapから交差点に向かうベクトルが ParallaxCorrected された反射ベクトルになります
    return worldIntersectPos - cubemapPos;
}

壁の近似はAABBに限定しなくても交差判定ができれば何でも良いのですが、計算コストと品質のバランスを考慮してAABBが一般的に用いられるようです。

Box Projectionに対応した自作シェーダーの紹介

当初は、Parallax-Corrected Cubemapに対応したUnityシェーダーを実装するという記事を執筆する予定だったのですが、Unity標準のStandardShaderで実装済みであることを記事用のシェーダーの実装が完成してから知りました :sob:

供養のために、Box ProjectionとLightmapに対応した自作シェーダーのリンクを紹介します。

[本題] Box Projectionの設定

それでは、ここからが本題です!

Box Projectionの設定をして、最初の動画のようなシーンを作る方法をチュートリアル形式で紹介していきます!

0: 屋内のアセットを用意する

まずは屋内シーンのアセットを用意します。

今回はUnityアセットストアから Pack Gesta Furniture #1 をダウンロードして使わせていただきました :bow:

1-1: ReflectionProbeを配置する

まずは ReflectionProbe を配置します。

普通は部屋の中心あたりに適当に配置すればOKです。

今回は映り込みの品質を向上するために、ちょっとカメラの位置に寄せました。

image.png

1-2: ReflectionProbe の BoxSize と BoxOffset を設定する

部屋の広さにぴったり合うように、BoxSize と BoxOffset を設定します。

Wireframeモードにしたほうが、作業がしやすいかもしれません。

また、BoxSize と BoxOffset は ReflectionProbe の影響範囲の指定も兼ねているので、床のMeshに少しオーバーラップするようにサイズを調整する必要があります。

オーバーラップしないと、ReflectionProbe の影響範囲外となってしまいます。

image.png

image.png

1-3: Box Projection等の設定とBake

ReflectionProbeの設定を変更します。

  • Box Projection: ON
  • Type: Baked
  • その他はお好みでOK

設定が完了したら、 [Bake] ボタンを押して、Cubemapのテクスチャを生成します。

image.png

2: 床のマテリアルの設定

床のマテリアルを反射が見えやすく調整します。

  • StandardShader を設定(ReflectionProbeに対応していれば何でも良い)
  • Metalic: お好みですが、フローリングの床感を出すなら 0.2 前後あたりがいい感じです
  • Smoothness: 1.0 に近づくほど反射がハッキリ見えます
  • Normal Map: なしにするか、影響度を 0.01 など小さな値にするほど反射がハッキリ見えます

image.png

以上でいい感じに反射するようになると思います。

まるで年末の大掃除でピカピカに磨き上げられた綺麗なフローリング床みたいですね!

image.png

トラブルシューティング

記事の執筆中に遭遇したトラブルをまとめます。

ReflectionProbe に映らないオブジェクトがある

  • 対象の GameObject は static になっていますか?
  • ReflectionProbe の CullingMask は Everything になっていますか?

再生したら反射が消えた

staticバッチングが原因の可能性があります。床のMeshだけ static を OFF にしましょう。

Box Projection が有効にならない

Project Settings > Graphics > Reflection Probes Box Projection が ON になっていることを確認してください。

image.png

まとめ

Unity標準ReflectionProbeのBox Projectionと標準シェーダーだけで、屋内シーンの反射による映り込みを大きく改善できました。

映り込みが綺麗にできると、光沢感やリッチ感を表現できますし、設定もそこまで手間がかからないので、コストパフォーマンスが高いと思います!

みなさんも是非 Box Projection を使いこなしてみてください!

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

3Dマークアップ言語とIoTを組み合わせてみる

目的

ちょっと実用的な例として、WifiにつながるArduino系マイコンボードから、温度湿度などの環境情報をARで空間に提示する仕組みを作ってみます。

道具立て

使うマイコンボードは、ESPr® One
Wifiがついているので、こういう時に、使いやすくて便利な奴です。
APモードで動かし、Hololensからつなぎにいきます。
センサーには、BME280を使います。これは、Amazonで安く買いました。
適当にブレッドボードを使うなどして、SPIでつないで動くようにします。

仕組み

マイコンで取得した温度湿度気圧を、マークアップにしてHTTPサーバーとして出力。Wifi経由でHoloLensからアクセスしてレンダリングします。

プログラム

以下のプログラムを、ArduinoIDEで書き込みました。直近5個の値を保存してグラフにします。HoloLensの方は、5秒間隔で再アクセスするようにUnity側のプログラムを作っています。

server.ino
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#define WIFI_SSID "orthoverseAP"
#define WIFI_PW "test"

#define BME_SCK 14
#define BME_MISO 12
#define BME_MOSI 13
#define BME_CS 16

#define SEALEVELPRESSURE_HPA (1013.25)

Adafruit_BME280 bme(BME_CS); // hardware SPI

unsigned long delaytimeBegin;

ESP8266WebServer server(80);
IPAddress ip(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);

float temp[5] = {0, 0, 0, 0, 0};
float press[5] = {0, 0, 0, 0, 0};
float hum[5] = {0, 0, 0, 0, 0};
unsigned long timeBegin = 0;

void setup() {
  Serial.begin(9600);
  Serial.println("BME280 test");

  bool status;

  status = bme.begin();
  if (!status) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  } else {
    Serial.println("BME280 found");
  }

  Serial.println("-- Default Test --");
  Serial.println("WiFi initializing");
  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(ip, ip, subnet);
  bool apst = WiFi.softAP(WIFI_SSID, WIFI_PW);

  if (apst) {
    Serial.println("Ready WiFi");
  } else {
    Serial.println("Something wrong WiFi");
  }

  server.on("/", []() {
    Serial.println("Access");
    String html = "<homl><head><title>3D Graph of Environment Sensor</title></head><body><a-scene wx=0.5 wy=0.001 wz=0.4>";

    for (int i = 0; i < 5; i++) {
      html += "<a-cylinder height=" + String((temp[i] - 20) / 30) + " r=0.05 x=" + String((i - 2.0) / 10.0) + " y=" + String((temp[i] - 20) / 30) + " z=-0.1 color=red />";
      html += "<a-cylinder height=" + String((press[i] - 1000) / 400000) + " r=0.05 x=" + String((i - 2.0) / 10.0) + " y=" + String((press[i] - 1000) / 400000) + " z=0 color=blue />";
      html += "<a-cylinder height=" + String((hum[i]) / 200) + " r=0.05 x=" + String((i - 2.0) / 10.0) + " y=" + String((hum[i]) / 200) + " z=0.1 color=green />";
    }
    html += "<a-text size=0.1 x=0.25 y=0.1 z=-0.1 color=red>"+ String(temp[4]) +"C</a-text>";
    html += "<a-text size=0.1 x=0.25 y=0.1 z=0    color=blue>"+ String(press[4]/100.0F) +"hPa</a-text>";
    html += "<a-text size=0.1 x=0.25 y=0.1 z=0.1  color=green>"+ String(hum[4]) +"%</a-text>";
    html += "</a-scene></body></homl>";

    server.send(
      200,
      "text/html",
      html);
  });
  Serial.println("WebServer setup done");
  server.begin();
  Serial.println("webserver begin");

  timeBegin = millis();
}


void loop() {
  server.handleClient();

  if (timeBegin < millis() - 5000) {
    for (int i = 0; i < 4; i++) {
      temp[i] = temp[i + 1];
      press[i] = press[i + 1];
      hum[i] = hum[i + 1];
    }
    temp[4] = bme.readTemperature();
    press[4] = bme.readPressure();
    hum[4] = bme.readHumidity();

    timeBegin = millis();
  }
}


void printValues() {
  Serial.print("Temperature = ");
  Serial.print(bme.readTemperature());
  Serial.println(" *C");

  Serial.print("Pressure = ");

  Serial.print(bme.readPressure() / 100.0F);
  Serial.println(" hPa");

  Serial.print("Approx. Altitude = ");
  Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
  Serial.println(" m");

  Serial.print("Humidity = ");
  Serial.print(bme.readHumidity());
  Serial.println(" %");

  Serial.println();
}

結果

image.png

ちょっと値の振れ幅が少なくて、面白みのないグラフになっていますが、IoT機器の情報をHoloLensで表示することができました。
あともっと表示のかっこよさを考えないといけないですね。ちなみに、写真は2018年のMakerFaireTokyoで会社のブースで展示していた時のものです。

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

【Unity(C#)】UniRxを使ってスキップ機能付きのテキストを1文字ずつ表示させる実装を試したけど微妙だった

UniRx

Unityで利用できるReactiveExtensionらしいです。
この説明だと私自身も意味がわからないので、参考リンク1のお言葉を頂戴して説明します。

直訳すると「反応性拡張」で、イベントに対する反応を拡張するためのライブラリ

何かしらのイベント(ボタンを押した、プレーヤーが移動したなど何でも)に対する反応を
簡単に書くことができるっぽいです。

私自身、ソースコードを見に行って、
"はいはいなるほどね"と言えるレベルまで理解できていません。

Rxに採用されているデザインパターンで、Observerパターンというのがあるのですが、
それに関しても人に説明できるところまで理解でき次第、まとめようと思っています。

今回は、使いながら覚えていきましょう 
という意図で調べながら作ったらなかなか残念な仕上がりだったのでそれをメモに残します。

テキストを1文字ずつ表示

GIFで見たまんまの意味です。
RxText.gif

既に何年も前に実装されている先駆者様がいらっしゃいました。
【参考リンク】: UniRXでuGUIのテキストをアニメーションさせる

文字を1文字ずつ表示する機能自体は参考リンクで完結しているので、
今回はスキップ機能(文字をいっきに最後まで表示させる機能)をUniRxで実装してみます。

コード

テキストコンポーネントを持つオブジェクトにアタッチ
using UniRx;
using System;
using UnityEngine;
using UniRx.Triggers;
using UnityEngine.UI;

public class TextPerOneWrite : MonoBehaviour
{
    [SerializeField]
    float m_textInterval = 0.2f;

    [SerializeField]
    KeyCode m_keyCode = KeyCode.Space;

    Text m_windowText;

    IDisposable m_textDispose;
    IDisposable m_updateDispose;

    void Start()
    {
        m_windowText = this.gameObject.GetComponent<Text>();
        m_windowText.text = "";

        //実行サンプル 文字を1文字ずつ出す
        ShowPerOne("ウホウホバナナヨコセ");
        ShowPerOne("ウホウホバナナヨコセ(早口)", 0.1f);

    }

    void ShowPerOne(string commentText)
    {
        m_windowText.text = "";

        if (m_textDispose != null)
        {
            m_textDispose.Dispose();
            m_updateDispose.Dispose();
        }

        m_textDispose = Observable.Interval(TimeSpan.FromSeconds(m_textInterval))
            .Take(commentText.Length)
            .Select(_ => 1)
            .Scan((accumulation, newValue) => accumulation + newValue)
            .DoOnCompleted(() => m_updateDispose.Dispose())
            .SubscribeToText(m_windowText, length => commentText.Substring(0, length))
            .AddTo(this);

        //特定のキー入力で文字を1文字ずつ出す機能を止める
        m_updateDispose =  this.UpdateAsObservable()
            .FirstOrDefault(_ => Input.GetKeyDown(m_keyCode))
            .Subscribe(_ =>
            {
                m_textDispose.Dispose();
                m_windowText.text = commentText;
            });
    }

    void ShowPerOne(string commentText, double textInterval)
    {
        m_windowText.text = "";

        if (m_textDispose != null)
        {
            m_textDispose.Dispose();
            m_updateDispose.Dispose();
        }

        m_textDispose = Observable.Interval(TimeSpan.FromSeconds(textInterval))
            .Take(commentText.Length)
            .Select(_ => 1)
            .Scan((accumulation, newValue) => accumulation + newValue)
            .DoOnCompleted(()=>m_updateDispose.Dispose())
            .SubscribeToText(m_windowText, length => commentText.Substring(0, length))
            .AddTo(this);

        //特定のキー入力で文字を1文字ずつ出す機能を止める
        m_updateDispose =  this.UpdateAsObservable()
           .FirstOrDefault(_ => Input.GetKeyDown(m_keyCode))
           .Subscribe(_ =>
           {
               m_textDispose.Dispose();
               m_windowText.text = commentText;
           });
    }
}

特定のキーを押すと1文字ずつ流れる文字を一気にスキップして表示することができます。

Observable.Interval

引数に時間を指定してあげるとその指定した間隔で値を流す(処理を実行)ことができます。

ちなみにTimeSpan.FromSecondsというのはどうやらC#の機能のようで
時間を指定するときによく使うみたいです。

【参考リンク】:TimeSpan.FromSeconds(Double) メソッド

Scan

Scanというオペレーターは前回発行された値と今発行された値の畳み込みを行うことができます。
平たく言うと重ねて足し合わせるイメージです。

.Scan((accumulation, newValue) => accumulation + newValue)

accumulation蓄積、累算などの意味を持つので、言葉の意味で覚えるとわかり易いです。
累算した値に、受け取った値(newValue)を足しているというわけですね。

【参考リンク】:UniRxを使ってみる

ストリームの寿命管理

uniRxは非常に便利ですが、
使う上で気を付けることの1つにストリームの寿命管理があります。

ストリームというのは

メッセージが伝達される経路、仕組み、機構のこと

らしいです。(またの名をObservableというらしい)

【引用元】:ObserverパターンからはじめるUniRx

このストリームというのが役目を終了した(もう必要でなくなった)段階で
購読を終了してあげる必要があります。

そうしてあげないと、パフォーマンスが低下したり、
もう存在していないGameObjectを参照してエラーが起きたりします。

【参考リンク】:UniRx入門 その2 - メッセージの種類/ストリームの寿命


Dispose

ストリームの購読終了を任意のタイミングで行うことが可能です。
一回変数に入れて、好きなタイミングで呼び出したらいいんじゃないでしょうか。


AddTo

AddToというメソッドを利用して、
先述したもう存在していないGameObjectの参照を未然に防ぐことができます。

引数に与えたGameObjectが削除された際に、自動的にDisposeを呼び出してくれます。


OnCompleted

このメッセージが発行されSubscribeまで到達すると購読が終了するらしいです。

今回どこにもOnCompletedを書いてませんが、
どうやらTakeによって指定回数分メッセージが通った際に発行され、
最後のSubscribeに到達しているようです。
(違うかもしれないんで、使ってておかしいと思ったらまた書き直します)

ストリームいっぱいできちゃう問題

前回のストリームが実行中であっても次に作成したストリームを同じ変数に突っ込めば
前回のストリームを止めた上で次のストリームを実行できそう!

ダメでした。なので、変数に既に何かしらが格納されているかチェックして
もし入っていたらストリームを止めてます。

    if (m_textDispose != null)
    {
        m_textDispose.Dispose();
        m_updateDispose.Dispose();
    }

m_textDispose = Observable.Intervalのように1つの変数に格納したからといって、
ストリームが1つしか作成されるわけではない、前回のストリームは止まらない...というのがわかりました。

ストリームがストリームを監視するのはあまりよくない

強い人に見て頂いた際にご指摘頂きましたが、けっこうごちゃごちゃしてしまっています。

自分で作っていても感じたことなのですが、

・ストリームの処理を条件分岐したい
・ストリームの挙動を途中で動的に変えたい

というような要望をUniRx使用時に盛り込むと、
”ストリームB”で”ストリームA”を監視するような状態になるので
条件分岐や変えたい挙動の数だけストリームが増えてしまいます。

なので、今回のように
Observable.IntervalUpdateAsObservableから任意のタイミングで止めるというのは
本来便利なRxを使っているにもかかわらずややこしくなってしまっています。

初心者あるあるらしいので、次回からはその辺りも意識してみようと思います。

参考リンク

UniRx入門シリーズ 目次
UniRx オペレータ逆引き
UniRxを紐解く「Take(1)とFirst()の違い」
【Unity】【UniRx】Observable.DoXxx()系のメソッドの挙動まとめ
【Unity】UniRx入門リンク集

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

Unityでクリスマスを飾ろう!(UnityLight2Dの話)

image.png

Unity #3 Advent Calendar 2019 12/24・・・僕の番だ!シャンシャンシャンシャン:confetti_ball:

今日はクリスマスイヴですね。
去年はクリスマスの時期にUnityを使ってクリスマスカードを作ろうの話を投稿しましたが今年もしますよ:snowflake:

Unityでクリスマスカードを作ろう

作りましょう。

  • 使ってみたかったUnityの機能があるので試す
    • Unity Light2Dとドット絵で作ろう
  • WebGLでかきだしてUnityRoomにあげる
  • UnityRoomのURLをペッと送ればOK

Unity Light2D

このLight2DはUnity2019.2から使用が可能です。なんとなくクリスマスのイルミネーションピカピカさせっかーと思い使おうとしました。

UnityのLight2Dを使う準備

Window->PackageManagerからLightweight RPをインストールします。
image.png

Projectウインドウ上でPipeline Assetと2D RendererをCreateします。
image.png

Pipeline AssetのRenderer TypeをCustomに変更し、Dataに先程作成した2D Rendererを設定します。
image.png

その後、projectSettingのGraphicsにあるScriptable Renderer Pipeline Assetに先ほど作成したPipeline Assetを設定します。
image.png

これで、Hierarch上でLight2Dが作成できるようになりました!
image.png

Unity Light2Dでできる表現

image.png
こちらに動画を上げました。
https://www.youtube.com/watch?v=2knzJoysZ1s

法線マップを使ってちょっとだけ立体表現してみる

そもそも法線とは?

面に垂直なベクトルのことです。下の図で言うと、黄色の面から垂直に出てる青い矢印が法線です。
image.png

(画像はwikipediaより)

法線マップとは?

オブジェクトの法線ベクトルのX, Y, Z座標に対応したRGB画像のことです。ノーマルマップともいいます。
左の図の法線マップを立方体に張り付けると、立方体が法線マップに応じて凸凹しているのが分かります。
image.png

(画像は http://cpetry.github.io/NormalMap-Online/ より)

簡単に法線マップを作ろう

NormalMap-Onlineを使います。
面白いので、サイトにいって遊んでみてください。
image.png

実際に作ってみた

image.png

こいつを立体的にしてみましょう。
ここに影が入るかな~と思ったところを暗くしたHeightMapとやらをつくって変換してみました。

image.png

SpriteにNormalMapを設定

Sprite EditorのSecondary Textureから設定します。
image.png

Nameの部分は、ラベルの右にある▼から選びます。
※手入力でいれるとうまくいきませんでした(これに気づくのに30分くらいかかった人)
image.png

Light2Dの設定を行う

こちらがノーマルマップ適用前
image.png

Light2Dの設定にUse Normal Mapにチェックをいれるだけで・・・
image.png

にじいろカワウソが立体っぽくなりました!
image.png

こちらに動画をあげています。
https://www.youtube.com/watch?v=C_MHtLP36zc

ってことでUnityRoomにクリスマスカードをうpしました。

とある場所をクリックすることで開封(ライトアップ)します。
icon.png
https://unityroom.com/games/xmascard2019

みなさん、良きクリスマスを!

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