UnityでVRコスプレするときに詰まったところの覚え書

100%自分用に書いたので記事内容のブラッシュアップをしていません。

MMD4Mecanim

MMD用モデルをUnityで扱えるように変換してくるアセット(?)

問題

  1. 変換されたモデルデータはMMDモデルと同じ場所に置かないとテクスチャが読み込まれない
    (同じ場所というかMMD4Mecanimで指定されたデフォルトフォルダ)

UnityCam

Unity内のCameraにアタッチするとその映像をWebカメラのように扱える

問題

  1. なぜかサンプルが動作しない
    →昔レジストリを弄りすぎて正常に登録できなかった?OS再インストールで解決

使い方(普通のプロジェクト)

  1. RunMe First をやる
  2. UnityCam-master\UnitySample をプロジェクトのAssetsフォルダにぶち込む
  3. UnitySample\Assets\UnityCam\Scripts にある UnityCam.cs を任意のCameraにアタッチ
  4. プロジェクトをRunしてOBSなどで映像を取得する

使い方(VRプロジェクト)

  1. Canvasを配置して子に UI -> Raw Image を置く
  2. これを無効化してGameウィンドウに表示されないようにする
  3. Assetsフォルダ以下にRender Textureを作成してSizeを指定する
  4. Raw ImageにRender Textureをアタッチ

使い方(ゲーム内で見る方法)

  1. Planeを作成する
  2. マテリアルのところにRaw Textureをアタッチ

ココを参考にしました

注意

  1. SteamVR -> CameraRig -> Camera(head) の子としてUnityCamをアタッチしたCameraを配置するとプレイヤー視点の映像が得られる。ただしフレームレートが落ちてVR酔いする。
    →素直にGameウィンドウをキャプチャすることでこの問題を回避

できたもの

WebCamTexture

Webカメラなどの外部から入力された映像をテクスチャとして貼り付けられる
HDMIキャプチャからゲームの映像を取得したい

ココとか参考にすればいいんじゃないかな
コッチのほうが詳しい

問題

  1. Unity内で映像を確認できたけど、ライティングの影響で影があると見えない
    →シェーダーの設定を変えることで解決
    Unlitはアンライティング、つまり光源に影響されないシェーダーということ。
    スクリーンショット (75).png

できたもの

Microphone

音声入力デバイスをUnity内で鳴らす
ココの最初を参考にしてゲーム音声をUnity内で流したい

問題

  1. なぜかデバイスの指定ができない(ちなUnity2017.3.0f3)
    Microphone.devicesの[0]番目しか指定できない。デバイス名による指定(Microphone.devices[1]とか)をしても反映されない。nullでもデフォルトデバイスに指定されない。
    →デバイス名を変更することで暫定的な解決

    スクリーンショット (61).png
    配列に入るデバイスは名前順というわけでもない。デバイス名の先頭に「あ」と入れておけばとりあえず先頭に来る。
    配列の先頭はデフォルトデバイス。それ以降はマジで謎な並び。 素直に諦めてキャプチャ音を直で聞いたほうがよさそう
    2018/2/2追記:UnityのIssue Trackerを見てみたらバグ報告されてました。のでUnity側の修正待ちです。暫定的にバージョンを落としてもいいかも。
    →バージョンを2017.1.3f1に下げることで暫定的な対応

できたもの

ないです

AssetStore

古いUnityバージョンで作られたアセットは動作しない可能性が高い
特にUnity5.0以前は注意?

Final IK(VRIK)

買う 買いました(2018/1/31)
UnityでIKを扱うのにメッチャ便利なアセット
基本的にはココと同じことをすれば問題ない

Solverにある各値の説明

長くなりすぎたので別記事に移動しました。

問題

  1. MMDモデルに対してVRIKをアタッチしてもReferencesを自動で設定してくれない
    →モデルのAnimation TypeをHumanoidにすることで解決
    スクリーンショット (62).png
    スクリーンショット (63).png
    参考:そのいち

  2. ワープしたときにワープ前の座標に足座標を置いてくる
    vlcsnap-2018-01-31-17h58m27s527.jpg
    こんな感じ
    →ワープしたときのイベントで足座標を移動してあげればよさそう(まだやってない)
    ココに解決策載ってた

    →VRIKの設定がおかしいのが原因だった
    各足の Position Weight を0にしないと原点に足が持ってかれる
    (ただし追加のトラッカーなどで足の座標を取得できるなら1にする)
    スクリーンショット (70).png

  3. モデルと自分の体格が合わない
    →モデルのScaleを少しだけ大きくすることで対応。Scaleを変えたら必ずVRIKの設定を見直すこと。参考
    VRIKの設定を弄ることで大きく改善することがあるため、面倒でもこの設定は拘ろう。
    また各種ターゲットの位置を微調整することでも改善する可能性がある。
    スクリーンショット (66).png
    スクリーンショット (67).png
    スクリーンショット (68).png

リップシンクしたい

VOICEROIDの音声に反応してリアルタイムで口が動いたら喋ってる感あっていいよね
OVRLipSyncを使いたい、のだけど名前が変わった? 今(2018/2/1)は Oculus LipSync Unity というらしい。
でもプロジェクトにインポートしたらOVRLipSyncと表示された。お前の名前どっちだよ。

コココレを参考に進めれば問題ない。
ちなみに公式の結月ゆかりMMDモデルのモーフはこんな感じ
スクリーンショット (71).png

Runするごとに音声入力デバイスを指定するのは面倒なので、デバイス名を固定にしてプログラム側で対象を探すようにちょっと改造しました。

OVRLipSyncMicInput.cs
void Start() 
{
    audioSource.loop = true;    // Set the AudioClip to loop
    audioSource.mute = false;   

    if(Microphone.devices.Length!= 0)
    {
        //selectedDevice = Microphone.devices[0].ToString();
        foreach (string device in Microphone.devices)
        {
            //"Yamaha" から始まるデバイスを指定する
            if (device.StartsWith("Yamaha"))
            {
                selectedDevice = device;
            }
        }

        micSelected = true;
        GetMicCaps();
    }
}

問題

  1. Microphoneのデバイスが指定できないからVOICEROIDのリップシンクができない
    →調べたらUnityのバグでした。修正待ち。詳しくはIssue Trackerへ。
    →バージョンを2017.1.3f1に下げることで暫定的な対応

AssetStoreで買ったカメラのモデル

UnityCamの位置を示す用に使う

問題

  1. コントローラでつかめない
    →Colliderが入ってなかった。Meshはあるので各パーツにMesh Colliderをアタッチして対応するメッシュを指定すればよい。
    →モノによってはStaticがオンになっている。オフにしたら動かせる。

コントローラをUnityCamに映したくない

ココを参考にコントローラのモデルのレイヤーを「Controller」と設定し、UnityCamのCullingMaskでControllerを除外すればOK。

できたもの

CM3D2のメニューボタンのように、押したらコントローラの役割が変わるようにしたい

具体的には、テレポートモード、手変化モード、表情変化モードの3つを実装したい
また役割が変わったらわかりやすいようにコントローラのちょっと上あたりに通知を出したい

役割を切り替えるのは、対象がコンポーネントならコッチ、オブジェクトならコッチ
通知を出す方法はココが参考になる

問題

  1. コントローライベントなどを追加したオブジェクトを取得できない
    VRTKというGameObject直下にLeftCとRightCというコントローライベントなどをアタッチしたオブジェクトがあるのだが、これをスクリプトで外部からFindとかGetComponentできなかった。
    123.png
    なぜかというと、Runした状態だとVRTKによりLeftCとRightCが[Camera Rig]の各Controller配下に移動していたから。
    124.png
    見る場所を間違えていたため取得できていなかった。

  2. 左手の指が逆パカされてしまう
    右手の指の回転軸で左手の指も回転していたため、回転方向がおかしくなってしまった。マイナスをかけることで修正。

できたもの

重い処理をしているわけではないのにフレームレートがガタ落ちしている

腕やオブジェクトを動かすと、滑らかに移動しないで若干ワープしながら移動する。
視点を揺らすと瞬間移動しているのがよくわかる。

ココを参考にFixedUpdateのFixed Timestepを0.0001に設定
2018/2/3追記:
これ効果あるかわかんねぇな
ついでにRigitbodyのInterpolateも設定

HDMIキャプチャ画面(WebCamTexture)が表示されていない時だけメチャクチャ動作が軽かったので、もしかしたらそれが原因かも。
→WebCamTextureを張り付けたPlaneを無効化したら動作が軽くなったので、コイツが原因と断定。
多分キャプチャ時のフレームレートに足を引っ張られてた。
別の方法でキャプチャ画面を表示する方法を考える。

2018/2/6追記:
「RedererTextureがくそ重いからUnityCam使わずに直接ウィンドウキャプチャした方が(多分)パフォーマンス出る」という情報をいただきました。
https://twitter.com/toRisouP/status/960569019681062912
https://twitter.com/toRisouP/status/960569087691653121

ゆかりさんとスプラトゥーン2 その1

2/2にニコ生で配信しました

【VR配信】ゆかりさんとスプラトゥーン2【あーかいぶ】

浮かび上がった問題点

実装・解決したものは打消し線を入れてます。

  1. キャプチャ映像と音がズレている
  2. カメラを持つとめっちゃブレる
  3. 生放送の管理ができない
  4. デスクトップの解像度が高すぎて文字が読めない
  5. 「ピース」って言ったらピースしたい
  6. 瞬きしたい
  7. モザイクで遊びたい
  8. なんか移動がおかしい
  9. 髪の毛をフワフワさせたい
  10. 特定のコメントを受信したら色々なモデルを出現させたい
    例えば「ゾウ」と打ったらUnity内にゾウが出現するとか
  11. ボタンを置いていろいろしたい
    カメラの切り替えとかオブジェクトの出現とか再配置とかゲーム画面をデスクトップ画面と置き換えるとか
  12. HTC VIVE と Nintendo Switch のコントローラを同時に持つことが難しい
    腕にマジックテープなどでVIVEのコントローラを固定すれば何とかできるかも?
  13. ユニティちゃん Candy Rock Star ライブステージ!にあるスピーカー実装したい

瞬きしたい

ココを参考にして瞬きしたい
AutoBlinkはGithubにある

SetBlendShapeWeightメソッド呼び出しの第1引数は、上に張ったモーフ一覧から瞬きモーフのインデックスを指定する。ゆかりさんなら0。
SkinnedMeshRendererはU_Char_1。

問題

  1. 瞬きの回数が少ない気がする
    →ソースコードに瞬きのインターバルを指定する変数があったのでそこを見てみる
AutoBlink.cs
public float threshold = 0.3f;   // ランダム判定の閾値
public float interval = 3.0f;    // ランダム判定のインターバル

publicな変数なのでUnityのインスペクタから変更できますね。
スクリーンショット (76).png
Thresholdは、瞬きする確率の閾値。この値を0にすると必ず瞬きする。1にすると瞬きしなくなる。初期は0.3なので、瞬きを しない:する = 4:7 ぐらいの確率。
Intervalは、瞬きする判定を何秒ごとに行うか。初期は3秒ごと。

できたもの

カメラの手ブレ補正

小数点第2位くらいを四捨五入すればいいんじゃない?
→小数点第4位を四捨五入したらいい感じになりました(なってない)

ShakeCorrection.cs
using System;
using UnityEngine;

public class ShakeCorrection : MonoBehaviour
{
    //位置
    //補正をするか
    public bool posEnable = true;
    //どの桁以上を出力するか
    public int posThreshold = 1;

    //角度
    //補正をするか
    public bool rotEnable = true;
    //どの桁以上を出力するか
    public int rotThreshold = 1;

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        //位置を取得
        Vector3 pos = transform.position;
        //角度を取得
        Vector3 rot = transform.eulerAngles;

        //小数点第4位を四捨五入して、第3位までを出力する
        //位置
        if (posEnable)
        {
            pos.x = (float)Math.Round(pos.x, posThreshold, MidpointRounding.AwayFromZero);
            pos.y = (float)Math.Round(pos.y, posThreshold, MidpointRounding.AwayFromZero);
            pos.z = (float)Math.Round(pos.z, posThreshold, MidpointRounding.AwayFromZero);

            //四捨五入した結果を入力
            transform.position = pos;
        }


        //角度
        if (rotEnable)
        {
            rot.x = (float)Math.Round(rot.x, rotThreshold, MidpointRounding.AwayFromZero);
            rot.y = (float)Math.Round(rot.y, rotThreshold, MidpointRounding.AwayFromZero);
            rot.z = (float)Math.Round(rot.z, rotThreshold, MidpointRounding.AwayFromZero);

            //四捨五入した結果を入力
            transform.eulerAngles = rot;
        }
    }
}

角度の補正はこれでも十分なのですが(元々大きい値のため)、位置の補正は満足いく形にはなりませんでした。
閾値を2にすると移動がカクつくし、かといって3にするとあまり補正してくれないし。
なのでより精度を高めるべく、四捨五入ではなく二捨三入まで拡張することにしました。

ShakeCorrection.cs
using System;
using UnityEngine;

public class ShakeCorrection : MonoBehaviour
{
    //位置
    //補正をするか
    public bool posEnable = true;
    //二捨三入にするか
    public bool posDetailedAccuracy = false;
    //どの桁以上を出力するか
    public int posThreshold = 1;

    //角度
    //補正をするか
    public bool rotEnable = true;
    //二捨三入にするか
    public bool rotDetailedAccuracy = false;
    //どの桁以上を出力するか
    public int rotThreshold = 1;

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        //位置を取得
        Vector3 pos = transform.position;
        //角度を取得
        Vector3 rot = transform.eulerAngles;

        //位置
        if (posEnable)
        {
            //二捨三入の準備
            if (posDetailedAccuracy)
            {
                pos *= 2;
            }

            //四捨五入
            pos.x = (float)Math.Round(pos.x, posThreshold, MidpointRounding.AwayFromZero);
            pos.y = (float)Math.Round(pos.y, posThreshold, MidpointRounding.AwayFromZero);
            pos.z = (float)Math.Round(pos.z, posThreshold, MidpointRounding.AwayFromZero);

            //二捨三入へ変換
            if (posDetailedAccuracy)
            {
                pos /= 2;
            }

            //結果を入力
            transform.position = pos;
        }


        //角度
        if (rotEnable)
        {
            //二捨三入の準備
            if (rotDetailedAccuracy)
            {
                rot *= 2;
            }

            //四捨五入
            rot.x = (float)Math.Round(rot.x, posThreshold, MidpointRounding.AwayFromZero);
            rot.y = (float)Math.Round(rot.y, posThreshold, MidpointRounding.AwayFromZero);
            rot.z = (float)Math.Round(rot.z, posThreshold, MidpointRounding.AwayFromZero);

            //二捨三入へ変換
            if (rotDetailedAccuracy)
            {
                rot /= 2;
            }

            //結果を入力
            transform.eulerAngles = rot;
        }
    }
}

できたもの

位置補正のみ、四捨五入バージョン

位置・角度補正あり、二捨三入バージョン

あまり効果がないような気がする。ここで悩んでても仕方ないので次行きます。

髪の毛をフワフワさせる

ココココを参考にゆかりさんのもみあげとアホ毛をフワフワさせたい。ついでにスカート?ワンピース?やパーカーのうさみみフードも。
SpringManagerはGithubにある。
SpringBoneはGithubにある。
ってかめんどくさいからunitychanのデータ全部DLしよう。

上手なSpringColliderを付けるコツ

  1. 体のSpringColliderの大きさと位置を決める
    体のSpringColliderは大体3個くらいに分けて、ピラミッド型(もしくは若干のボンキュッボン型)になるように大きさを調整する。体に対して若干小さめにしたほうが自然な感じになる。
    ただし頭は少しだけ大きくすることで髪の毛のフワフワ感が出る。
    スクリーンショット (79).png
    ここでモデルの状態を確認しても二度手間になるため調整してはいけない。後でまとめてやろう。

  2. フワフワ対象のSpringBoneのRadiusを決める
    SpringBoneを付けるときは体の半分だけ付けて、調整が終わってから全身に貼り付けよう。
    髪の毛など細いSpringBoneはRadiusを小さくして、うさみみなどのある程度の大きさを持ったものは初期値のまま、など対象のIKの大きさによってRadiusを調整するととても自然な感じになってハッピーになれる。
    スクリーンショット (81).png
    スクリーンショット (83).png
    あほ毛とうさみみの比較。あほ毛はRadiusを小さくしてうさみみは初期値のままにしてある。

  3. 不自然な個所の微調整
    ここで初めて大きさや位置の調整を行う。
    長いもみあげを持つキャラクター(結月ゆかりなど)は、肩のSpringColliderを作ってサイズを小さめに設定するといい感じになる。
    ある程度調整を行ったがどうしても不自然な個所がある場合は、元々のIKに回転をかけることも考えること。
    スクリーンショット (80).png
    ゆかりさんのもみあげは少しだけ前に回転させている。

  4. 必要ならSpringBoneのStiffness Forceの調整
    Stiffness Forceは小さくすると無重力空間にいる状態みたくなる。大きくするとフサフサ感がなくなる。

正直この調整は妥協点を見つけないと無限に時間を持っていかれる。どこかで区切りをつけた方がよい。

問題

  1. サンプルは動くけどVRコスプレのプロジェクトに適用しても動かない
    1つめの原因はココを見て解決。
    2つめの原因は謎。なんかSpringManagerとかSpringBoneとか設定メチャクチャ試したけど動かなかった。全部1回削除して再度適用しなおしたら動いた。謎。

できたもの

特定のコメントを受信したら色々なモデルを出現させたい

コメントをオブジェクトとして出現させる

コレを使ってやります!アンコちゃんからUnityへコメントを引っ張ってきて、コメントを3DモデルとしてUnity内に出現させたい。つまりみゅみゅさんのパクリ。
Unityから直接ニコ生のAPIを叩く方法もある。
文字の3DモデルはFlyingText3Dというアセットを使って生成する。FlyingText3Dについてはココを参考に作業すれば問題ない。

  1. フォントファイルがある場所を探す
    とりあえずメイリオを使いたいので、Windows標準のフォント格納場所である C:\Windows\Fonts\ にアクセス。メイリオの名前は "Meiryo UI"。

  2. .ttcファイルを分解して.ttfファイルにする
    ココとか参考にすればいいんじゃないかな

  3. 適当なGameObjectにFlyingTextをアタッチ

  4. 同じオブジェクトに以下のようなスクリプトをアタッチ

CreateText.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CreateText : MonoBehaviour
{
    int timer = 0;

    // Use this for initialization
    void Start()
    {

        //文字の指定
        var objectParent = FlyingText.GetObjects(timer.ToString()).transform;

        //位置の指定
        var pos = objectParent.position;
        pos.y = 1;
        pos.x = -10;
        objectParent.position = pos;


        //Rigidbodyの指定
        Rigidbody[] rigidBodies = objectParent.GetComponentsInChildren<Rigidbody>();

        foreach(Rigidbody rb in rigidBodies)
        {
            rb.mass = (float)0.001;
            rb.drag = 10;
        }

    }

    // Update is called once per frame
    void Update()
    {
        if (timer >= 100)
        {
            Start();
            timer = 0;
        }

        timer++;
    }
}

これで100フレームごとに3Dオブジェクトが出現する。
RigidbodyのMASSとかDRAGの値はこのくらいだといい感じにフワーっと落ちてくれる
Colliderの付け方は、インスペクタから "Add Rigidbody" の上にある "Collider Type" で適当なのを選べばいい。

これで動的にテキストの3Dオブジェクトを生成することができるようになりました。
NicoliveCommentRecieverからコメント受信時にCreateTextを呼び出してあげればOK。
Randomとかで出現位置を変えてあげればバラバラに落ちていい感じになるんじゃないかな。

問題

  1. オブジェクトを掴んだ時に特定のイベントを呼び出したい
    ココとかココを参考にカスタマイズ。
CreateObject.cs
//文字オブジェクトを作成
commentObject = FlyingText.GetObjects(comment.Message);
if (commentObject == null)
{
        return;
}

//文字オブジェクトのTransformを取得
commentObjectTransform = commentObject.transform;
if (commentObjectTransform == null)
{
        return;
}

/*** 中略 ***/

//掴めるように Grabbable 有効化
VRTK_InteractableObject[] InteractableObject = commentObjectTransform.GetComponentsInChildren<VRTK_InteractableObject>();
foreach (VRTK_InteractableObject io in InteractableObject)
{
        io.isGrabbable = true;
        io.enabled = true;

        //Rigidbodyを取得
        Rigidbody rb = io.transform.GetComponent<Rigidbody>();

        //掴んだ時にイベントを呼び出すよう設定
        io.InteractableObjectGrabbed += (sender, args) =>
        {
            if (rb.useGravity)
            {
                rb.useGravity = false;
                rb.isKinematic = true;

                //オブジェクトをDestroyするタイマーを止める
                var timer = io.transform.GetComponent<ObjectTimer>();
                timer.TimerStop();
            }
        };
}
  1. Rigidbody の isKinematic を動的にTrueにできない
    上記のプログラムだと useGravity はFalseにできるけど、isKinematic をTrueにはできない。
    仕方ないのでFreezeAllとかで代用。
CreateObject.cs
        //Rigidbodyを取得
        Rigidbody rb = io.transform.GetComponent<Rigidbody>();

        //掴んだのをやめた時(手を離したとき)にイベントを呼び出すよう設定
        io.InteractableObjectUngrabbed += (sender, args) =>
        {
            //回転、位置を固定
            rb.constraints = RigidbodyConstraints.FreezeAll;
        };

        //掴んだ時にイベントを呼び出すよう設定
        io.InteractableObjectGrabbed += (sender, args) =>
        {
            if (rb.useGravity)
            {
                rb.useGravity = false;
                rb.isKinematic = true;

                //オブジェクトをDestroyするタイマーを止める
                var timer = io.transform.GetComponent<ObjectTimer>();
                timer.TimerStop();
            }

            //回転、位置をフリーに設定
            rb.constraints = RigidbodyConstraints.None;
        };

って思ってたけど、掴んでいる時にisKinematicをTrueにしてもVRTKに無効化されるよね・・・。
離した時にTrueにしないと意味ないよね。

CreateObject.cs
        //Rigidbodyを取得
        Rigidbody rb = io.transform.GetComponent<Rigidbody>();

        //掴んだのをやめた時(手を離したとき)にイベントを呼び出すよう設定
        io.InteractableObjectUngrabbed += (sender, args) =>
        {
            if (!rb.isKinematic)
            {
                //Kinematicを有効化
                rb.isKinematic = true;
            }
        };

        //掴んだ時にイベントを呼び出すよう設定
        io.InteractableObjectGrabbed += (sender, args) =>
        {
            if (rb.useGravity)
            {
                //Gravityを無効化
                rb.useGravity = false;

                //オブジェクトをDestroyするタイマーを止める                    
                var timer = io.transform.GetComponent<ObjectTimer>();
                timer.TimerStop();
            }
        };

isKinematicの動的な有効化できました(迫真)。
深夜2時の脳みそは信用できませんね。眠くなったら素直に寝ましょう。

  1. 文字オブジェクトがライティングの影響を受けて見えづらい
    後でシェーダーを動的に変更するプログラム追加します。

できたもの

モザイクで遊びたい

Unity AssetStoreまとめさんを見ていたらこんな記事を見つけたのでモザイクで遊んでみる。
記事の通りに行えば何も問題はない。
遠近感が失われるオブジェクトなのでどこまで手を伸ばしたら手に取れるのかわかりづらい。オブジェクトの厚さはそれなりにあった方がいいかも。

できたもの

ピースしたい

キャラクターの指の制御にはMMD4FingerControllerコレなどがありますが、個人的にわかり辛かった(ソースコード読む気がないだけ)のと大きくカスタマイズしたかったのでスクリプトを自作することにしました。
指の角度を変えるだけなのでそこまで難しいスクリプトにはなってないと思います。

HandController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HandController : MonoBehaviour
{
    //右手
    [Tooltip("右手 親指のTransform")]
    public GameObject rightThumbFinger;

    [Tooltip("右手 人差し指のTransform")]
    public GameObject rightIndexFinger;

    [Tooltip("右手 中指のTransform")]
    public GameObject rightMiddleFinger;

    [Tooltip("右手 薬指のTransform")]
    public GameObject rightRingFinger;

    [Tooltip("右手 小指のTransform")]
    public GameObject rightLittleFinger;

    //左手
    [Tooltip("左手 親指のTransform")]
    public GameObject leftThumbFinger;

    [Tooltip("左手 人差し指のTransform")]
    public GameObject leftIndexFinger;

    [Tooltip("左手 中指のTransform")]
    public GameObject leftMiddleFinger;

    [Tooltip("左手 薬指のTransform")]
    public GameObject leftRingFinger;

    [Tooltip("左手 小指のTransform")]
    public GameObject leftLittleFinger;

    //右手用リスト
    //List<Transform> right = new List<Transform>();
    private static List<List<GameObject>> rightHand = new List<List<GameObject>>();

    //左手用リスト
    //List<Transform> left = new List<Transform>();
    private static List<List<GameObject>> leftHand = new List<List<GameObject>>();

    // Use this for initialization
    void Start()
    {
        //右手を登録
        //0: 親指
        //1: 人差し指
        //2: 中指
        //3: 薬指
        //4: 小指
        rightHand.Add(GetAllChildren.GetAll(rightThumbFinger));
        rightHand.Add(GetAllChildren.GetAll(rightIndexFinger));
        rightHand.Add(GetAllChildren.GetAll(rightMiddleFinger));
        rightHand.Add(GetAllChildren.GetAll(rightRingFinger));
        rightHand.Add(GetAllChildren.GetAll(rightLittleFinger));

        //左手を登録
        //0: 親指
        //1: 人差し指
        //2: 中指
        //3: 薬指
        //4: 小指
        leftHand.Add(GetAllChildren.GetAll(leftThumbFinger));
        leftHand.Add(GetAllChildren.GetAll(leftIndexFinger));
        leftHand.Add(GetAllChildren.GetAll(leftMiddleFinger));
        leftHand.Add(GetAllChildren.GetAll(leftRingFinger));
        leftHand.Add(GetAllChildren.GetAll(leftLittleFinger));
    }

    public static int Rstate;
    public static bool RisGrab;
    public static int Lstate;
    public static bool LisGrab;
    private static int coef; 

    // Update is called once per frame
    void Update()
    {
        if (!RisGrab)
        {
            switch (Rstate)
            {
                case 0:
                    nomal(true);
                    break;
                case 1:
                    rock(true);
                    break;
                case 2:
                    peace(true);
                    break;
                case 3:
                    paper(true);
                    break;
                case 4:
                    cheat(true);
                    break;
                default:
                    Rstate = 0;
                    nomal(true);
                    break;
            }
        }

        if (!LisGrab)
        {
            switch (Lstate)
            {
                case 0:
                    nomal(false);
                    break;
                case 1:
                    rock(false);
                    break;
                case 2:
                    peace(false);
                    break;
                case 3:
                    paper(false);
                    break;
                case 4:
                    cheat(false);
                    break;
                default:
                    Lstate = 0;
                    nomal(false);
                    break;
            }
        }
    }

    //左右の手を判定して手のリストを返す関数
    private static List<List<GameObject>> getHand(bool isRight)
    {
        List<List<GameObject>> hand = new List<List<GameObject>>();
        if (isRight)
        {
            hand = rightHand;
            coef = 1;
        }
        else
        {
            hand = leftHand;
            coef = -1;
        }

        return hand;
    }

    //指を元に戻す
    public static List<List<GameObject>> nomal(bool isRight)
    {
        List<List<GameObject>> hand = getHand(isRight);

        //全ての指の回転をリセット
        for (int i = 0; i < 5; i++)
        {
            foreach (GameObject gameObject in hand[i])
            {
                gameObject.transform.localRotation = Quaternion.Euler(0, 0, 0);
            }
        }

        return hand;
    }

    //手をピースに
    public static void peace(bool isRight)
    {
        //指をリセット
        List<List<GameObject>> hand = nomal(isRight);

        //親指を回転
        hand[0][1].transform.localRotation = Quaternion.Euler(45, coef * 45, 0);
        hand[0][2].transform.localRotation = Quaternion.Euler(45, coef * 45, 0);

        //人差し指を回転
        hand[1][0].transform.localRotation = Quaternion.Euler(0, coef * -10, 0);

        //中指を回転
        hand[2][0].transform.localRotation = Quaternion.Euler(0, coef * 10, 0);

        //薬指と小指を回転
        for (int i = 3; i < 5; i++)
        {
            foreach (GameObject gameObject in hand[i])
            {
                gameObject.transform.localRotation = Quaternion.Euler(0, 0, coef * -75);
            }
        }
    }

    //手をグーに
    public static void rock(bool isRight)
    {
        //指をリセット
        List<List<GameObject>> hand = nomal(isRight);

        //親指を回転
        hand[0][1].transform.localRotation = Quaternion.Euler(45, coef * 45, 0);
        hand[0][2].transform.localRotation = Quaternion.Euler(45, coef * 45, 0);

        //その他の指を回転
        for (int i = 1; i < 5; i++)
        {
            foreach (GameObject gameObject in hand[i])
            {
                gameObject.transform.localRotation = Quaternion.Euler(0, 0, coef * -75);
            }
        }
    }

    //手をパーに
    public static void paper(bool isRight)
    {
        //指をリセット
        List<List<GameObject>> hand = nomal(isRight);

        //親指を回転
        hand[0][1].transform.localRotation = Quaternion.Euler(-10, coef * -10, 0);

        //人差し指を回転
        hand[1][0].transform.localRotation = Quaternion.Euler(0, coef * -10, 0);

        //薬指を回転
        hand[3][0].transform.localRotation = Quaternion.Euler(0, coef * 5, 0);

        //小指を回転
        hand[4][0].transform.localRotation = Quaternion.Euler(0, coef * 10, 0);
    }

    //手をグーチョキパー3つ混じったあの形に
    //http://www.irasutoya.com/2017/01/blog-post_2.html
    //フレミング左手の法則に似た形
    public static void cheat(bool isRight)
    {
        //指をリセット
        List<List<GameObject>> hand = nomal(isRight);

        //親指を回転
        hand[0][1].transform.localRotation = Quaternion.Euler(-10, coef * -10, 0);

        //人差し指を回転
        hand[1][0].transform.localRotation = Quaternion.Euler(0, coef * -10, 0);

        //中指を回転
        hand[2][0].transform.localRotation = Quaternion.Euler(0, coef * 10, 0);

        //薬指と小指を回転
        for (int i = 3; i < 5; i++)
        {
            foreach (GameObject gameObject in hand[i])
            {
                gameObject.transform.localRotation = Quaternion.Euler(0, 0, coef * -75);
            }
        }
    }

    //物を掴んている形に
    public static void grab(bool isRight)
    {
        //指をリセット
        List<List<GameObject>> hand = nomal(isRight);

        //親指を回転
        hand[0][0].transform.localRotation = Quaternion.Euler(0, 0, coef * -20);
        hand[0][1].transform.localRotation = Quaternion.Euler(20, coef * 20, 0);
        hand[0][2].transform.localRotation = Quaternion.Euler(20, coef * 20, 0);

        //その他の指を回転
        for (int i = 1; i < 5; i++)
        {
            foreach (GameObject gameObject in hand[i])
            {
                gameObject.transform.localRotation = Quaternion.Euler(0, 0, coef * -30);
            }
        }
    }
}

スクリプト中に出てくる GetAllChildren は、コチラのスクリプトをちょっと改造(親のオブジェクトもリストの中に含めるように)したものです。

GetAllChildren.cs
    public static List<GameObject> GetAll(this GameObject obj)
    {
        List<GameObject> allChildren = new List<GameObject>();
        allChildren.Add(obj);     //親オブジェクトもListに含めるようにAdd
        GetChildren(obj, ref allChildren);
        return allChildren;
    }

なんかすごい汚いプログラム書いてしまいました。Overrideとか駆使すればよかったですが今更面倒なのでこのままいきます。
指がアニメーションしないで一瞬で切り替わってしまいますが、とりあえず今はこれでいいでしょう。そのうちいい感じにします。
指のオブジェクトはそれぞれの根本にあるGameObjectを指定してください。ゆかりさんだとこんな感じ。
HandController.png

指の可動域

ここで例に挙げているのは右手の指です。左手の指は回転方向にマイナスをかけないとおかしくなります。
人差し指から小指までは Z軸を中心にマイナス方向へ回転 させれば自然な感じ。 最大90度。 Y軸中心は -10~+10度程度 なら回しても問題ない。X軸はダメ。
ただし注意が必要なのが親指で、 X軸とY軸を同時にプラス方向へ回転させないとおかしくなる。 回す角度は大体 45度、最大60度くらいまで にしないと回りすぎる。 Z軸は回転しすぎるとあり得ない方向に回る。
まとめると、

人差し指から小指 親指
x x -10? ~ 45+α
y -10 ~ 10 -10? ~ 45+α
z -90 ~ 0 -20? ~ 10?

DVcDx8-UQAEnkUC.jpg


できたもの

UnityNicoliveClient

とりすーぷさん作。
アセット内で使用されているSendWebRequestがUnity2017.2以上じゃないと実装されていないので、Unityのバージョンを再度変更。
Microphoneの問題もあるのでUnity2017.2.0b10をインストール。Fixed in future releaseってなってたけど、どのバージョンで修正してくれるんだろう。
無理にこれを使う必要はないので、Unity側で修正されたら移行する形でもよさそう。

2/12追記:とりすーぷさん側で2017.1以下でも動くように対応していただきました。ありがとうございます。
実装については別記事にて。

ゆかりさんとスプラトゥーン2 その2

2/10にニコ生で配信しました

【VR配信】ゆかりさんとスプラトゥーン2 その2【あーかいぶ】

浮かび上がった問題点

実装・解決したものは打消し線を入れてます。

既存の問題

  1. キャプチャ映像と音がズレている
  2. カメラを持つとブレる
  3. 生放送の管理ができない
  4. デスクトップの解像度が高すぎて文字が読めない
  5. VRIKの再セットアップしたい
  6. 特定のコメントを受信したら色々なモデルを出現させたい
    例えば「ゾウ」と打ったらUnity内にゾウが出現するとか
  7. ボタンを置いていろいろしたい
    カメラの切り替えとかオブジェクトの出現とか再配置とかゲーム画面をデスクトップ画面と置き換えるとか
  8. HTC VIVE と Nintendo Switch のコントローラを同時に持つことが難しい
    腕にマジックテープなどでVIVEのコントローラを固定すれば何とかできるかも?
  9. ユニティちゃん Candy Rock Star ライブステージ!にあるスピーカー実装したい

新たな問題

  1. ゲーム画面を見ている時にゆかりさんが後ろを向いている
    鏡の設置や配信設定を変えることで対応可能?
  2. やっぱりWebCamTexture重い
  3. コメントを落とす範囲が広すぎて取りに行けない
  4. テレポート時に姿勢がおかしくなってる
  5. PCゲーム配信したい
  6. 配信用のオブジェクト配置にするのが面倒
  7. Gameウィンドウからフォーカスが外れた時にフレームレートが落ちる
  8. マルチプレイしたい
  9. 車のモデル置いてレースクイーンごっことかしたい
  10. 表情を変化させたい

Gameウィンドウからフォーカスが外れた時にフレームレートが落ちる

以前からGameウィンドウを最前面に表示していないとフレームレートが落ちる問題を解決できずにいました。
そんな時に以下の情報を頂きました。ありがとうございます。

試しにOVRLipSync関連のスクリプトを無効化したところ問題が解決したため、focusedフラグを使用している部分を無効化することにしました。
focusedフラグを使用しているのは OVRLipSyncMicInput.cs で、処理内容は フォーカスが外れた時にマイク入力を破棄する 内容でした。
アプリケーションの終了時だけマイクを破棄してくれればいいので、容赦なくコメントアウトします。

マルチプレイしたい

Photon Unity Networkingを使ってマルチプレイしたい。
このシリーズを見れば基本的なことはわかる。
コールバックメソッドの一覧はこちら。

共通部

実装した方がいい機能

  1. マスターでログインするかゲストでログインするかで処理を変えた方がいい
  2. マスターからゲストを強制ログアウトさせる機能を実装した方がいい
  3. Roomを作れるのはマスターだけにした方がいい

基本的な枠組みはコチラから拝借。
このスクリプトを適当な空オブジェクトにアタッチしてあげればいい。

Login.cs
using UnityEngine;
using System.Collections;

public class Login : MonoBehaviour
{
    public bool isMaster;
    public static string userName;
    private string roomName = "ゆかりさんと○○";

    void Start()
    {
        if (!isMaster)
        {            
            //ゲストには無関係な機能を無効化
            GameObject.Find("VRTK").SetActive(false);
            GameObject yukariObject = GameObject.Find("結月ゆかり_純_ver1.0");
            yukariObject.GetComponent<RootMotion.FinalIK.VRIK>().enabled = false;
            yukariObject.GetComponent<OVRLipSyncContextMorphTarget>().enabled = false;
            yukariObject.GetComponent<OVRLipSyncContext>().enabled = false;
            yukariObject.GetComponent<OVRLipSyncMicInput>().enabled = false;
            yukariObject.GetComponent<UnityChan.AutoBlink>().enabled = false;
            yukariObject.GetComponent<UnityChan.SpringManager>().enabled = false;
            yukariObject.GetComponent<HandController>().enabled = false;
            GameObject.Find("uDD_Board").SetActive(false);
            GameObject.Find("FilmCamera/Camera").GetComponent<UnityCam>().enabled = false;
            GameObject.Find("NicoLiveComment").SetActive(false);
            GameObject.Find("CreateObject").SetActive(false);
        }

        // Photonに接続する(引数でゲームのバージョンを指定できる)
        PhotonNetwork.ConnectUsingSettings(null);
    }

    // ロビーに入ると呼ばれる
    void OnJoinedLobby()
    {
        Debug.Log("ロビーに入りました。");
        //YukariPhoton.setPhoton();

        if (isMaster)
        {
            //ルームを作成
            PhotonNetwork.CreateRoom(roomName, null, null);
        }
        else
        {
            // ルームに入室
            PhotonNetwork.JoinRoom(roomName);
        }


    }

    // ルームに入室すると呼ばれる
    void OnJoinedRoom()
    {
        Debug.Log("ルームへ入室しました。");

        if (isMaster)
        {
            //マスター側

            //ユーザ名を設定
            userName = "Master";
            //ユーザ名を通知
            PhotonNetwork.playerName = userName;
        }
        else
        {
            //ゲスト側

            //ユーザ名の設定
            if (userName != null)
            {
                //ユーザ名を通知
                PhotonNetwork.playerName = userName;
            }
            else
            {
                //プレイヤーの数を取得
                int num = PhotonNetwork.otherPlayers.Length;
                //ユーザ名を生成
                userName = "Guest" + num;
                //ユーザ名を通知
                PhotonNetwork.playerName = userName;
            }

            //ゲストのカメラを生成
            createCamera();
        }
    }

    // ルームの入室に失敗すると呼ばれる
    void OnPhotonJoinRoomFailed()
    {
        Debug.Log("ルームの入室に失敗しました。");

        //マスターだけがRoomを作る
        if (isMaster)
        {
            //ないとは思うけど、もう一度Roomを作る
            PhotonNetwork.CreateRoom(roomName);
        }
        else
        {
            Debug.Log("再接続");
            // ルームに入室する
            PhotonNetwork.JoinRoom(roomName);
        }

    }

    //ゲストのカメラを生成する関数
    private void createCamera()
    {
        Vector3 pos = new Vector3(0, (float)1.2, (float)-1.5);

        // 第1引数にResourcesフォルダの中にあるプレハブの名前(文字列)
        // 第2引数にposition
        // 第3引数にrotation
        // 第4引数にView ID(指定しない場合は0)
        GameObject photonObject = PhotonNetwork.Instantiate("CubeCamera", pos, Quaternion.identity, 0);

        //photonObjectの名前をユーザ名に設定
        photonObject.transform.name = userName;

        //カメラを有効化
        photonObject.GetComponentInChildren<Camera>().enabled = true;
        //オーディオリスナーを有効化
        photonObject.GetComponentInChildren<AudioListener>().enabled = true;
    }
}

マスター側

ゆかりさんの同期をするためには、VRIKの各IKに対してPhotonTransformViewを設定。SpringBoneにも付けないとフワフワしないけど、数が多いので断念。
もしやるならゆかりさんのIK全てに動的にスクリプトを紐づけた方がいい。
PhotonObjectのLoginスクリプトのIsMasterのチェックを付けてRun。
結月ゆかりモデルをプレハブ化していないけどいいの?と思うが、プレハブ化して~のくだりは動的にオブジェクトを生成する場合にのみ必要。最初から存在するオブジェクトはそのままでいい。
ココとか参考になるかも。

ゲスト側

ゲストユーザ向けにビルドするときの設定がややこしい。
ココを参考にゲーム起動時の解像度設定画面を表示するように。SteamVRが文句言ってくるけど無視。
PhotonObjectのLoginスクリプトのIsMasterのチェックを外して、Project SettingのQualityをGood以上に設定。これをしないと光源が一部削除される。

ゲストユーザの接続の前にワンクッション置きたかったため、簡単なログインページみたいなのを作ってみました。
スクリーンショット (93).png
タイトル画面の作り方はココを、InputFieldから情報を抜き出すのはココを参考にしました。
今はplayerNameに設定しているだけですが、ゲストの頭の上に名前とか表示出来たらよさそうですね。

GuestTitleScript.cs
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class GuestTitleScript : MonoBehaviour
{
    InputField inputField;

    void Start()
    {
        //InputFieldを取得
        GameObject go = GameObject.Find("InputField");
        inputField = go.GetComponent<InputField>();
    }

    //ボタンを押したら実行する
    public void GuestStart()
    {
        //Login.isMaster = false;
        Login.userName = inputField.text;
        SceneManager.LoadScene("VR Cosplay 201713");
    }
}

カメラの移動にはワールド座標系を用いれば難しくない。
ココココを参考に以下のスクリプトを作成。

ControllCamera.cs
using UnityEngine;

public class ControllCamera : MonoBehaviour
{
    GameObject go;

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (go == null)
        {
            go = GameObject.Find(Login.userName);
        }

        //スペースを押したときのイベント
        if (Input.GetKey(KeyCode.E))
        {
            Vector3 vector = go.transform.up * (float)0.025;
            go.transform.position += vector;
        }

        //Qを押したときのイベント
        if (Input.GetKey(KeyCode.Q))
        {
            Vector3 vector = go.transform.up * (float)0.025;
            go.transform.position -= vector;
        }

        //Wを押したときのイベント
        if (Input.GetKey(KeyCode.W))
        {
            //前へ進む
            Vector3 vector = go.transform.forward * (float)0.025;
            go.transform.position += vector;
        }

        //Aを押したときのイベント
        if (Input.GetKey(KeyCode.A))
        {
            //右へ進む
            Vector3 vector = go.transform.right * (float)0.025;
            go.transform.position -= vector;
        }

        //Sを押したときのイベント
        if (Input.GetKey(KeyCode.S))
        {
            //後ろへ進む
            Vector3 vector = go.transform.forward * (float)0.025;
            go.transform.position -= vector;
        }

        //Dを押したときのイベント
        if (Input.GetKey(KeyCode.D))
        {
            //左へ進む
            Vector3 vector = go.transform.right * (float)0.025;
            go.transform.position += vector;
        }

        //Rを押したときのイベント
        if (Input.GetKey(KeyCode.R))
        {
            //視点回転をリセット
            go.transform.rotation = Quaternion.Euler(0, 0, 0);
        }

        //↑を押したときのイベント
        if (Input.GetKey(KeyCode.UpArrow))
        {
            transform.Rotate(-1, 0, 0);
        }

        //↓を押したときのイベント
        if (Input.GetKey(KeyCode.DownArrow))
        {
            transform.Rotate(1, 0, 0);
        }

        //←を押したときのイベント
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            //左へ回転
            transform.Rotate(0, -1, 0);
        }

        //→を押したときのイベント
        if (Input.GetKey(KeyCode.RightArrow))
        {
            //右へ回転
            transform.Rotate(0, 1, 0);
        }
    }
}

ゲストを強制ログアウトさせる機能はココココを参考に、上記スクリプトに以下を追加

GuestController.cs
    //クライアント終了を通知する関数
    public static void SendEnd()
    {
        photonView.RPC("AppEnd", PhotonTargets.Others);
    }

    //クライアント終了処理
    [PunRPC]
    void AppEnd()
    {
        Application.Quit();
    }

あとはマスター側でSendEnd関数を呼べばいい。

問題

  1. StartでInstantiateしたら Failed to Instantiate prefab: CameraCube. Client should be in a room. Current connectionStateDetailed: PeerCreated って言われた
    RoomにJoinする前にInstantiateしようとしているのが問題。Joinした後に生成すれば問題ない。

  2. 別のユーザのカメラが表示されてしまう
    プレハブに登録したオブジェクトのCameraコンポーネントを無効化しておき、ログインしたタイミングでそのユーザが持っているカメラオブジェクトのCameraコンポーネントだけを有効化すれば問題ない。

Login.cs
    //ゲストのカメラを生成する関数
    private void createCamera()
    {
        Vector3 pos = new Vector3(0, (float)1.2, (float)-1.5);

        // 第1引数にResourcesフォルダの中にあるプレハブの名前(文字列)
        // 第2引数にposition
        // 第3引数にrotation
        // 第4引数にView ID(指定しない場合は0)
        GameObject photonObject = PhotonNetwork.Instantiate("CubeCamera", pos, Quaternion.identity, 0);

        //photonObjectの名前をユーザ名に設定
        photonObject.transform.name = userName;

        //カメラを有効化
        //ここが重要!!!
        photonObject.GetComponentInChildren<Camera>().enabled = true;
    }
  1. ゆかりさんに動的にPotonViewとかPhotonTransformViewとかAddComponentしてもPhotonViewに情報渡すときにnull reference exceptionと怒られる
    ゆかりさんのモデルにPhoton関連のスクリプトを何もアタッチせず、動的にすべてのIKに対してPhoton関連をアタッチしようと考えて以下のスクリプトを書きました。
YukariPhoton.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class YukariPhoton : MonoBehaviour
{
    // Use this for initialization
    void Start()
    {
        GameObject yukariObject = GameObject.Find("結月ゆかり_純_ver1.0/182.!Root");

        foreach (GameObject go in GetAllChildren.GetAll(yukariObject))
        {
            var photonView = go.GetComponent<PhotonView>();
            var photonTransformView = go.GetComponent<PhotonTransformView>();

            //位置を同期
            photonTransformView.m_PositionModel.SynchronizeEnabled = true;
            //回転を同期
            photonTransformView.m_RotationModel.SynchronizeEnabled = true;
            //Transformを同期するように設定
            photonView.ObservedComponents[0] = photonTransformView;
            //同期タイミングの設定
            photonView.synchronization = ViewSynchronization.UnreliableOnChange;
        }
    }
}

するとObservedComponents[0]に値を代入してるところでnull reference exceptionとエラーを吐かれてしまいました。
いろいろ調べたところ、ここに書いてあることが起きていたっぽい。
詳しく説明すると、変数の生成タイミングの違いによって起こっていたエラー。
FoldoutされているList型の変数が生成されるタイミングは、手動でAddComponentしたときは即座に、動的にAddComponentしたときはインスペクタで該当するGameObjectを見たら生成される。
Runした直後にアクセスしようとしてもインスペクタでGameObjectを見てないから変数が生成されていない。よってエラーが吐かれる。
正直この仕様考えたやつに対してぶん殴りたい。 小一時間問い詰めたい。 もしかしたら初期化すればいいのでは?ココとか参考になる。

対策としては事前に手動でコンポーネントをAddしておけばOK。右矢印とかShiftとか駆使してモデルのIKを全選択しよう。
ただし文字オブジェクトをどうやって同期させるかが非常に悩ましいことになった。
掴んだ時にイベントを発行してチャットでTransformの情報を送ってやる方法を考え中。

  1. マスター側でゲストのカメラを移動させても同期しない

    ゲストのカメラにVRTK関連を付けて持ち運べるようにしているのだが、移動がゲスト側に同期されない。対策案を検討中。

  2. なぜかゲストクライアントのCPU負荷がめちゃくちゃ高い
    ココとか参考になるかも。

できたもの

バーチャルアイドルちゃんねるの生主の方々にお手伝いいただきテストプレイをしました。






表情を変化させたい

ココとかAutoBlink.csとかを参考にして以下のスクリプトを書きました。
指を動かすスクリプトと似たようなものです。関数にPunRPCを付けて表情が変化したときにゲスト全員に変更通知が行くようにしています。

FaceController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FaceController : MonoBehaviour
{
    //表情モーフの参照
    public SkinnedMeshRenderer skinnedMeshRenderer;

    //表情ステータス保存用変数
    public static int state;
    //表情変化フラグ
    public static bool flag;

    private PhotonView photonView;
    private UnityChan.AutoBlink autoBlink;

    // Use this for initialization
    void Start()
    {
        photonView = GetComponent<PhotonView>();
        autoBlink = GetComponent<UnityChan.AutoBlink>();
    }

    // Update is called once per frame
    void Update()
    {
        if (flag)
        {
            switch (state)
            {
                case 0:
                    photonView.RPC("setNomal", PhotonTargets.All, true);
                    break;
                case 1:
                    photonView.RPC("setSmile", PhotonTargets.All);
                    break;
                case 2:
                    photonView.RPC("setTereSmile", PhotonTargets.All);
                    break;
                case 3:
                    photonView.RPC("setLeftWink", PhotonTargets.All);
                    break;
                case 4:
                    photonView.RPC("setRightWink", PhotonTargets.All);
                    break;
                default:
                    photonView.RPC("setNomal", PhotonTargets.All, true);
                    break;
            }
        }

        flag = false;
    }

    //無表情へ
    [PunRPC]
    void setNomal(bool blinkAcrive)
    {
        skinnedMeshRenderer.SetBlendShapeWeight(1, 0);
        skinnedMeshRenderer.SetBlendShapeWeight(2, 0);
        skinnedMeshRenderer.SetBlendShapeWeight(4, 0);
        skinnedMeshRenderer.SetBlendShapeWeight(41, 0);
        autoBlink.isActive = blinkAcrive;
    }

    //笑顔へ
    [PunRPC]
    void setSmile()
    {
        setNomal(false);
        skinnedMeshRenderer.SetBlendShapeWeight(1, 100);
    }

    //照れ笑顔へ
    [PunRPC]
    void setTereSmile()
    {
        setNomal(false);
        skinnedMeshRenderer.SetBlendShapeWeight(1, 100);
        skinnedMeshRenderer.SetBlendShapeWeight(41, 100);
    }

    //左目ウインクへ
    [PunRPC]
    void setLeftWink()
    {
        setNomal(false);
        skinnedMeshRenderer.SetBlendShapeWeight(4, 100);
    }

    //右目ウインクへ
    [PunRPC]
    void setRightWink()
    {
        setNomal(false);
        skinnedMeshRenderer.SetBlendShapeWeight(2, 100);
    }
}

これ以降の更新は別記事で行います

初期の段階からずっと同じ記事に書き続けていましたが、横の見出し一覧が4Kでもスクロール必須になったり、記事の保存が重かったり、編集したい箇所の把握が難しくなってきたりしているので、これ以降の更新は別記事に書きたいと思います。
新しい記事はコチラです。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.